Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create vectors from path ends #445

Merged
merged 3 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Unreleased

Added
-----
- ``Vector3d.from_path_ends()`` class method to get vectors between two vectors.

Changed
-------
Expand All @@ -23,6 +24,8 @@ Removed

Fixed
-----
- Transparency of polar stereographic grid lines can now be controlled by Matplotlib's
``grid.alpha``, just like the azimuth grid lines.

2023-03-14 - version 0.11.1
===========================
Expand Down
2 changes: 2 additions & 0 deletions examples/rotations/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Rotations
=========
83 changes: 83 additions & 0 deletions examples/rotations/combine_rotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
r"""
===================
Combining rotations
===================

This example demonstrates how to combine two rotations :math:`g_A` and
:math:`g_B`, i.e. from left to right like so

.. math::
g_{AB} = g_A \cdot g_B.

This order follows from the convention of passive rotations chosen in
orix which follows :cite:`rowenhorst2015consistent`.

To convince ourselves that this order is correct, we will reproduce the
example given by Rowenhorst and co-workers in section 4.2.2 in the above
mentioned paper. We want to rotate a vector :math:`(0, 0, z)` by two
rotations: rotation :math:`A` by :math:`120^{\circ}` around
:math:`[1 1 1]`, and rotation :math:`B` by :math:`180^{\circ}` around
:math:`[1 1 0]`; rotation :math:`A` will be carried out first, followed
by rotation :math:`B`.

Note that a negative angle when *defining* a rotation in the axis-angle
representation is necessary for consistent transformations between
rotation representations. The rotation still rotates a vector
intuitively.
"""

import matplotlib.pyplot as plt

from orix import plot
from orix.quaternion import Rotation
from orix.vector import Vector3d

plt.rcParams.update({"font.size": 12, "grid.alpha": 0.5})

gA = Rotation.from_axes_angles([1, 1, 1], -120, degrees=True)
gB = Rotation.from_axes_angles([1, 1, 0], -180, degrees=True)
gAB = gA * gB

# Compare with quaternions and orientation matrices from section 4.2.2
# in Rowenhorst et al. (2015)
g_all = Rotation.stack((gA, gB, gAB)).squeeze()
print("gA, gB and gAB:\n* As quaternions:\n", g_all)
print("* As orientation matrices:\n", g_all.to_matrix().squeeze().round(10))

v_start = Vector3d.zvector()
v_end = gAB * v_start
print(
"Point rotated by gAB:\n",
v_start.data.squeeze().tolist(),
"->",
v_end.data.squeeze().round(10).tolist(),
)

# Illustrate the steps of the rotation by plotting the vector before
# (red), during (green) and after (blue) the rotation and the rotation
# paths (first: cyan; second: magenta)
v_intermediate = gB * v_start

v_si_path = Vector3d.from_path_ends(Vector3d.stack((v_start, v_intermediate)))
v_sie_path = Vector3d.from_path_ends(Vector3d.stack((v_intermediate, v_end)))

fig = plt.figure(layout="tight")
ax0 = fig.add_subplot(121, projection="stereographic", hemisphere="upper")
ax1 = fig.add_subplot(122, projection="stereographic", hemisphere="lower")
ax0.stereographic_grid(), ax1.stereographic_grid()
Vector3d.stack((v_start, v_intermediate, v_end)).scatter(
figure=fig,
s=50,
c=["r", "g", "b"],
axes_labels=["e1", "e2"],
)
ax0.plot(v_si_path, color="c"), ax1.plot(v_si_path, color="c")
ax0.plot(v_sie_path, color="m"), ax1.plot(v_sie_path, color="m")
gA.axis.scatter(figure=fig, c="orange")
gB.axis.scatter(figure=fig, c="k")
text_kw = dict(bbox=dict(alpha=0.5, fc="w", boxstyle="round,pad=0.1"), ha="right")
ax0.text(v_start, s="Start", **text_kw)
ax1.text(v_intermediate, s="Intermediate", **text_kw)
ax1.text(v_end, s="End", **text_kw)
ax1.text(gA.axis, s="Axis gA", **text_kw)
ax0.text(gB.axis, s="Axis gB", **text_kw)
1 change: 1 addition & 0 deletions orix/plot/stereographic_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ def _polar_grid(self, resolution: Optional[float] = None):
label=label,
edgecolors=kwargs["ec"],
facecolors=kwargs["fc"],
alpha=kwargs["alpha"],
)
has_collection, index = self._has_collection(label, self.collections)
if has_collection:
Expand Down
7 changes: 6 additions & 1 deletion orix/tests/plot/test_stereographic_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,15 @@ def test_grids(self):
assert ax._azimuth_resolution == azimuth_res
assert ax._polar_resolution == polar_res

ax.stereographic_grid(azimuth_resolution=30, polar_resolution=45)
alpha = 0.5
with plt.rc_context({"grid.alpha": alpha}):
ax.stereographic_grid(azimuth_resolution=30, polar_resolution=45)
assert ax._azimuth_resolution == 30
assert ax._polar_resolution == 45

assert len(ax.collections) == 2
assert all([coll.get_alpha() for coll in ax.collections])

plt.close("all")

def test_set_labels(self):
Expand Down
18 changes: 18 additions & 0 deletions orix/tests/test_vector3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,24 @@ def test_get_circle(self):
assert np.allclose(c.mean().data, [0, 0, 0], atol=1e-2)
assert np.allclose(v.cross(c[0, 0]).data, [1, 0, 0])

def test_from_path_ends(self):
vx = Vector3d.xvector()
vy = Vector3d.yvector()

v_xy = Vector3d.from_path_ends(Vector3d.stack((vx, vy)))
assert v_xy.size == 27
assert np.allclose(v_xy.polar, np.pi / 2)
assert np.allclose(v_xy[-1].data, vy.data)

v_xyz = Vector3d.from_path_ends(
[[1, 0, 0], [0, 1, 0], [0, 0, 1]], steps=150, close=True
)
assert v_xyz.size == 115
assert np.allclose(v_xyz[-1].data, vx.data)

with pytest.raises(ValueError, match="No vectors are perpendicular"):
_ = Vector3d.from_path_ends(Vector3d.stack((vx, -vx)))


class TestPlotting:
v = Vector3d(
Expand Down
70 changes: 70 additions & 0 deletions orix/vector/vector3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,76 @@ def zvector(cls) -> Vector3d:
"""Return a unit vector in the z-direction."""
return cls((0, 0, 1))

@classmethod
def from_path_ends(
cls,
vectors: Union[list, tuple, Vector3d],
close: bool = False,
steps: int = 100,
) -> Vector3d:
r"""Return vectors along the shortest path on the sphere between
two or more consectutive vectors.

Parameters
----------
vectors
Two or more vectors to get paths between.
close
Whether to append the first to the end of ``vectors`` in
order to close the paths when more than two vectors are
passed. Default is False.
steps
Number of vectors in the great circle about the normal
vector between each two vectors *before* restricting the
circle to the path between the two. Default is 100. More
steps give a smoother path on the sphere.

Returns
-------
paths
Vectors along the shortest path(s) between given vectors.

Notes
-----
The vectors along the shortest path on the sphere between two
vectors :math:`v_1` and :math:`v_2` are found by first getting
the vectors :math:`v_i` along the great circle about the vector
normal to these two vectors, and then only keeping the part of
the circle between the two vectors. Vectors within this part
satisfy these two conditions

.. math::
(v_1 \times v_i) \cdot (v_1 \times v_2) \geq 0,
(v_2 \times v_i) \cdot (v_2 \times v_1) \geq 0.
"""
v = Vector3d(vectors).flatten()

if close:
v = Vector3d(np.concatenate((v.data, v[0].data)))

paths_list = []

n = v.size - 1
for i in range(n):
v1, v2 = v[i : i + 2]
v_normal = v1.cross(v2)
v_circle = v_normal.get_circle(steps=steps)

cond1 = v1.cross(v_circle).dot(v1.cross(v2)) >= 0
cond2 = v2.cross(v_circle).dot(v2.cross(v1)) >= 0

v_path = v_circle[cond1 & cond2]

to_concatenate = (v1.data, v_path.data)
if i == n - 1:
to_concatenate += (v2.data,)
paths_list.append(np.concatenate(to_concatenate, axis=0))

paths_data = np.concatenate(paths_list, axis=0)
paths = Vector3d(paths_data)

return paths

def angle_with(self, other: Vector3d, degrees: bool = False) -> np.ndarray:
"""Return the angles between these vectors in other vectors.

Expand Down