Skip to content

Commit b879afd

Browse files
authored
Merge pull request #445 from hakonanes/create-vectors-from-path-ends
Create vectors from path ends
2 parents dfba8aa + 486d670 commit b879afd

File tree

7 files changed

+183
-1
lines changed

7 files changed

+183
-1
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Unreleased
1111

1212
Added
1313
-----
14+
- ``Vector3d.from_path_ends()`` class method to get vectors between two vectors.
1415

1516
Changed
1617
-------
@@ -25,6 +26,8 @@ Removed
2526

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

2932
2023-03-14 - version 0.11.1
3033
===========================

examples/rotations/README.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Rotations
2+
=========
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
r"""
2+
===================
3+
Combining rotations
4+
===================
5+
6+
This example demonstrates how to combine two rotations :math:`g_A` and
7+
:math:`g_B`, i.e. from left to right like so
8+
9+
.. math::
10+
g_{AB} = g_A \cdot g_B.
11+
12+
This order follows from the convention of passive rotations chosen in
13+
orix which follows :cite:`rowenhorst2015consistent`.
14+
15+
To convince ourselves that this order is correct, we will reproduce the
16+
example given by Rowenhorst and co-workers in section 4.2.2 in the above
17+
mentioned paper. We want to rotate a vector :math:`(0, 0, z)` by two
18+
rotations: rotation :math:`A` by :math:`120^{\circ}` around
19+
:math:`[1 1 1]`, and rotation :math:`B` by :math:`180^{\circ}` around
20+
:math:`[1 1 0]`; rotation :math:`A` will be carried out first, followed
21+
by rotation :math:`B`.
22+
23+
Note that a negative angle when *defining* a rotation in the axis-angle
24+
representation is necessary for consistent transformations between
25+
rotation representations. The rotation still rotates a vector
26+
intuitively.
27+
"""
28+
29+
import matplotlib.pyplot as plt
30+
31+
from orix import plot
32+
from orix.quaternion import Rotation
33+
from orix.vector import Vector3d
34+
35+
plt.rcParams.update({"font.size": 12, "grid.alpha": 0.5})
36+
37+
gA = Rotation.from_axes_angles([1, 1, 1], -120, degrees=True)
38+
gB = Rotation.from_axes_angles([1, 1, 0], -180, degrees=True)
39+
gAB = gA * gB
40+
41+
# Compare with quaternions and orientation matrices from section 4.2.2
42+
# in Rowenhorst et al. (2015)
43+
g_all = Rotation.stack((gA, gB, gAB)).squeeze()
44+
print("gA, gB and gAB:\n* As quaternions:\n", g_all)
45+
print("* As orientation matrices:\n", g_all.to_matrix().squeeze().round(10))
46+
47+
v_start = Vector3d.zvector()
48+
v_end = gAB * v_start
49+
print(
50+
"Point rotated by gAB:\n",
51+
v_start.data.squeeze().tolist(),
52+
"->",
53+
v_end.data.squeeze().round(10).tolist(),
54+
)
55+
56+
# Illustrate the steps of the rotation by plotting the vector before
57+
# (red), during (green) and after (blue) the rotation and the rotation
58+
# paths (first: cyan; second: magenta)
59+
v_intermediate = gB * v_start
60+
61+
v_si_path = Vector3d.from_path_ends(Vector3d.stack((v_start, v_intermediate)))
62+
v_sie_path = Vector3d.from_path_ends(Vector3d.stack((v_intermediate, v_end)))
63+
64+
fig = plt.figure(layout="tight")
65+
ax0 = fig.add_subplot(121, projection="stereographic", hemisphere="upper")
66+
ax1 = fig.add_subplot(122, projection="stereographic", hemisphere="lower")
67+
ax0.stereographic_grid(), ax1.stereographic_grid()
68+
Vector3d.stack((v_start, v_intermediate, v_end)).scatter(
69+
figure=fig,
70+
s=50,
71+
c=["r", "g", "b"],
72+
axes_labels=["e1", "e2"],
73+
)
74+
ax0.plot(v_si_path, color="c"), ax1.plot(v_si_path, color="c")
75+
ax0.plot(v_sie_path, color="m"), ax1.plot(v_sie_path, color="m")
76+
gA.axis.scatter(figure=fig, c="orange")
77+
gB.axis.scatter(figure=fig, c="k")
78+
text_kw = dict(bbox=dict(alpha=0.5, fc="w", boxstyle="round,pad=0.1"), ha="right")
79+
ax0.text(v_start, s="Start", **text_kw)
80+
ax1.text(v_intermediate, s="Intermediate", **text_kw)
81+
ax1.text(v_end, s="End", **text_kw)
82+
ax1.text(gA.axis, s="Axis gA", **text_kw)
83+
ax0.text(gB.axis, s="Axis gB", **text_kw)

orix/plot/stereographic_plot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ def _polar_grid(self, resolution: Optional[float] = None):
746746
label=label,
747747
edgecolors=kwargs["ec"],
748748
facecolors=kwargs["fc"],
749+
alpha=kwargs["alpha"],
749750
)
750751
has_collection, index = self._has_collection(label, self.collections)
751752
if has_collection:

orix/tests/plot/test_stereographic_plot.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,15 @@ def test_grids(self):
102102
assert ax._azimuth_resolution == azimuth_res
103103
assert ax._polar_resolution == polar_res
104104

105-
ax.stereographic_grid(azimuth_resolution=30, polar_resolution=45)
105+
alpha = 0.5
106+
with plt.rc_context({"grid.alpha": alpha}):
107+
ax.stereographic_grid(azimuth_resolution=30, polar_resolution=45)
106108
assert ax._azimuth_resolution == 30
107109
assert ax._polar_resolution == 45
108110

111+
assert len(ax.collections) == 2
112+
assert all([coll.get_alpha() for coll in ax.collections])
113+
109114
plt.close("all")
110115

111116
def test_set_labels(self):

orix/tests/test_vector3d.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,24 @@ def test_get_circle(self):
642642
assert np.allclose(c.mean().data, [0, 0, 0], atol=1e-2)
643643
assert np.allclose(v.cross(c[0, 0]).data, [1, 0, 0])
644644

645+
def test_from_path_ends(self):
646+
vx = Vector3d.xvector()
647+
vy = Vector3d.yvector()
648+
649+
v_xy = Vector3d.from_path_ends(Vector3d.stack((vx, vy)))
650+
assert v_xy.size == 27
651+
assert np.allclose(v_xy.polar, np.pi / 2)
652+
assert np.allclose(v_xy[-1].data, vy.data)
653+
654+
v_xyz = Vector3d.from_path_ends(
655+
[[1, 0, 0], [0, 1, 0], [0, 0, 1]], steps=150, close=True
656+
)
657+
assert v_xyz.size == 115
658+
assert np.allclose(v_xyz[-1].data, vx.data)
659+
660+
with pytest.raises(ValueError, match="No vectors are perpendicular"):
661+
_ = Vector3d.from_path_ends(Vector3d.stack((vx, -vx)))
662+
645663

646664
class TestPlotting:
647665
v = Vector3d(

orix/vector/vector3d.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,76 @@ def zvector(cls) -> Vector3d:
476476
"""Return a unit vector in the z-direction."""
477477
return cls((0, 0, 1))
478478

479+
@classmethod
480+
def from_path_ends(
481+
cls,
482+
vectors: Union[list, tuple, Vector3d],
483+
close: bool = False,
484+
steps: int = 100,
485+
) -> Vector3d:
486+
r"""Return vectors along the shortest path on the sphere between
487+
two or more consectutive vectors.
488+
489+
Parameters
490+
----------
491+
vectors
492+
Two or more vectors to get paths between.
493+
close
494+
Whether to append the first to the end of ``vectors`` in
495+
order to close the paths when more than two vectors are
496+
passed. Default is False.
497+
steps
498+
Number of vectors in the great circle about the normal
499+
vector between each two vectors *before* restricting the
500+
circle to the path between the two. Default is 100. More
501+
steps give a smoother path on the sphere.
502+
503+
Returns
504+
-------
505+
paths
506+
Vectors along the shortest path(s) between given vectors.
507+
508+
Notes
509+
-----
510+
The vectors along the shortest path on the sphere between two
511+
vectors :math:`v_1` and :math:`v_2` are found by first getting
512+
the vectors :math:`v_i` along the great circle about the vector
513+
normal to these two vectors, and then only keeping the part of
514+
the circle between the two vectors. Vectors within this part
515+
satisfy these two conditions
516+
517+
.. math::
518+
(v_1 \times v_i) \cdot (v_1 \times v_2) \geq 0,
519+
(v_2 \times v_i) \cdot (v_2 \times v_1) \geq 0.
520+
"""
521+
v = Vector3d(vectors).flatten()
522+
523+
if close:
524+
v = Vector3d(np.concatenate((v.data, v[0].data)))
525+
526+
paths_list = []
527+
528+
n = v.size - 1
529+
for i in range(n):
530+
v1, v2 = v[i : i + 2]
531+
v_normal = v1.cross(v2)
532+
v_circle = v_normal.get_circle(steps=steps)
533+
534+
cond1 = v1.cross(v_circle).dot(v1.cross(v2)) >= 0
535+
cond2 = v2.cross(v_circle).dot(v2.cross(v1)) >= 0
536+
537+
v_path = v_circle[cond1 & cond2]
538+
539+
to_concatenate = (v1.data, v_path.data)
540+
if i == n - 1:
541+
to_concatenate += (v2.data,)
542+
paths_list.append(np.concatenate(to_concatenate, axis=0))
543+
544+
paths_data = np.concatenate(paths_list, axis=0)
545+
paths = Vector3d(paths_data)
546+
547+
return paths
548+
479549
def angle_with(self, other: Vector3d, degrees: bool = False) -> np.ndarray:
480550
"""Return the angles between these vectors in other vectors.
481551

0 commit comments

Comments
 (0)