Skip to content

Commit 247b947

Browse files
committed
add example and fix tests for from_path
1 parent 3336a78 commit 247b947

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
r"""
2+
=========================================
3+
Plot Paths In Rotation and Vector Space
4+
=========================================
5+
This example shows how paths though either rotation or vector space
6+
can be plotted using ORIX. These are the shortest paths through their
7+
respective spaces, and thus not always straight lines in euclidean
8+
projections (axis-angle, stereographic, etc.).
9+
This functionality is available in :class:`~orix.vector.Vector3d`,
10+
:class:`~orix.quaternions.Rotation`,
11+
:class:`~orix.quaternions.Orientation`,
12+
and :class:`~orix.quaternions.Misorientation`.
13+
"""
14+
15+
from matplotlib import cm
16+
import matplotlib.pyplot as plt
17+
import numpy as np
18+
19+
from orix.plot import register_projections
20+
from orix.plot.direction_color_keys import DirectionColorKeyTSL
21+
from orix.quaternion import Orientation, OrientationRegion, Quaternion
22+
from orix.quaternion.symmetry import C1, Oh
23+
from orix.sampling import sample_S2
24+
from orix.vector import Vector3d
25+
26+
plt.close("all")
27+
register_projections() # Register our custom Matplotlib projections
28+
np.random.seed(2319) # Create reproducible random data
29+
30+
31+
fig = plt.figure(figsize=(6, 6))
32+
n_steps = 30
33+
34+
# ========= #
35+
# Example 1: Plotting multiple paths into a user defined axis
36+
# ========= #
37+
# This subplot shows several paths through the cubic (m3m) fundamental zone
38+
# created by rotating 20 randomly chosen points 30 degrees around the z axis.
39+
# these paths are drawn in rodrigues space, which is an equal-angle projection
40+
# of rotation space. As such, notice how all lines tracing out axial rotations
41+
# are straight, but lines starting closer to the center of the fundamental zone
42+
# appear shorter.
43+
# the sampe paths are then also plotted on an Inverse Pole Figure (IPF) plot.
44+
rod_ax = fig.add_subplot(2, 2, 1, projection="rodrigues", proj_type="ortho")
45+
ipf_ax = fig.add_subplot(2, 2, 2, projection="ipf", symmetry=Oh)
46+
47+
# 10 random orientations with the cubic m3m ('Oh' in the schoenflies notation)
48+
# crystal symmetry.
49+
oris = Orientation(
50+
data=np.array(
51+
[
52+
[0.69, 0.24, 0.68, 0.01],
53+
[0.26, 0.59, 0.32, 0.7],
54+
[0.07, 0.17, 0.93, 0.31],
55+
[0.6, 0.03, 0.61, 0.52],
56+
[0.51, 0.38, 0.34, 0.69],
57+
[0.31, 0.86, 0.22, 0.35],
58+
[0.68, 0.67, 0.06, 0.31],
59+
[0.01, 0.12, 0.05, 0.99],
60+
[0.39, 0.45, 0.34, 0.72],
61+
[0.65, 0.59, 0.46, 0.15],
62+
]
63+
),
64+
symmetry=Oh,
65+
)
66+
# reduce them to their crystallographically identical representations
67+
oris = oris.reduce()
68+
# define a 20 degree rotation around the z axis
69+
shift = Orientation.from_axes_angles([0, 0, 1], 30, degrees=True)
70+
# for each orientation, calculate and plot the path they would take during a
71+
# 45 degree shift.
72+
segment_colors = cm.inferno(np.linspace(0, 1, n_steps))
73+
for ori in oris:
74+
points = Orientation.stack([ori, (shift * ori)]).reduce()
75+
points.symmetry = Oh
76+
path = Orientation.from_path_ends(points, steps=n_steps)
77+
rod_ax.scatter(path, c=segment_colors)
78+
ipf_ax.scatter(path, c=segment_colors)
79+
80+
# add the wireframe and clean up the plot.
81+
fz = OrientationRegion.from_symmetry(path.symmetry)
82+
rod_ax.plot_wireframe(fz)
83+
rod_ax._correct_aspect_ratio(fz)
84+
rod_ax.axis("off")
85+
rod_ax.set_title(r"Rodrigues, multiple paths")
86+
ipf_ax.set_title(r"IPF, multiple paths ")
87+
88+
89+
# %%
90+
# ========= #
91+
# Example 2: Plotting a path using `Rotation.scatter'
92+
# ========= #
93+
# This subplot traces the path of an object rotated 90 degrees around the
94+
# X axis, then 90 degrees around the Y axis.
95+
rots = Orientation.from_axes_angles(
96+
[[1, 0, 0], [1, 0, 0], [0, 1, 0]], [0, 90, 90], degrees=True, symmetry=C1
97+
)
98+
rots[2] = rots[1] * rots[2]
99+
path = Orientation.from_path_ends(rots, steps=n_steps)
100+
# create a list of RGBA color values for a gradient red line and blue line
101+
path_colors = np.vstack(
102+
[cm.Reds(np.linspace(0.5, 1, n_steps)), cm.Blues(np.linspace(0.5, 1, n_steps))]
103+
)
104+
105+
# Here, we instead use the in-built plotting tool from
106+
# Orientation.scatter to auto-generate the subplot. This is especially handy when
107+
# plotting only a single Orientation object.
108+
path.scatter(figure=fig, position=[2, 2, 3], marker=">", c=path_colors)
109+
fig.axes[2].set_title(r"Axis-Angle, two $90^\circ$ rotations")
110+
111+
# %%
112+
113+
# ========= #
114+
# Example 3: paths in stereographic plots
115+
# ========= #
116+
# This is similar to the second example, but now vectors are being rotated
117+
# 30 degrees around the [1,1,1] axis on a stereographic plot.
118+
119+
vec_ax = plt.subplot(2, 2, 4, projection="stereographic", hemisphere="upper")
120+
ipf_colormap = DirectionColorKeyTSL(C1)
121+
122+
# define a mesh of vectors with approximately 20 degree spacing, and
123+
# within 80 degrees of the Z axis
124+
vecs = sample_S2(20)
125+
vecs = vecs[vecs.polar < (80 * np.pi / 180)]
126+
127+
# define a 15 degree rotation around [1,1,1]
128+
rots = Quaternion.from_axes_angles([1, 1, 1], [0, 15], degrees=True)
129+
130+
for vec in vecs:
131+
path_ends = rots * vec
132+
# color each path using a gradient pased on the IPF coloring.
133+
c = ipf_colormap.direction2color(vec)
134+
if np.abs(path_ends.cross(path_ends[::-1])[0].norm) > 1e-12:
135+
path = Vector3d.from_path_ends(path_ends, steps=100)
136+
segment_c = c * np.linspace(0.25, 1, path.size)[:, np.newaxis]
137+
vec_ax.scatter(path, c=segment_c)
138+
else:
139+
vec_ax.scatter(path_ends[0], c=c)
140+
141+
vec_ax.set_title(r"Stereographic")
142+
vec_ax.set_labels("X", "Y")
143+
plt.tight_layout()

orix/tests/test_quaternion/test_quaternion.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,27 +336,43 @@ def test_equality(self):
336336
assert Q1 != Q2
337337
assert Q1 == Q2.inv()
338338

339+
def test_from_path_ends(self):
340+
# choose a path with repeats and 180 tilts to test edge cases
341+
waypoints = Quaternion(
342+
data=np.array(
339343
[
340344
[1, 0, 0, 0],
341345
[1, 0, 0, 0],
342346
[1, 1, 0, 0],
343347
[1, 0, 1, 0],
344348
[1, 0, 0, 1],
345349
[1, 0, 0, 1],
350+
[1, 0, -1, 0],
351+
[1, 0, 1, 0],
352+
[-1, 0, 1, 0],
353+
[1, 0, 0, -1],
346354
[-1, 0, 0, -1],
347355
[-1, 0, 0, 1],
348356
]
349357
)
350358
)
351359
path = Quaternion.from_path_ends(waypoints)
352360
loop = Quaternion.from_path_ends(waypoints, closed=True, steps=11)
361+
# check the sizes are as expected
362+
assert path.shape == (1100,)
363+
assert loop.shape == (132,)
364+
# check the spacing between points is homogenous
353365
path_spacing = [(x[1:]).dot(x[:-1]) for x in path.reshape(11, 100)]
354366
loop_spacing = [(x[1:]).dot(x[:-1]) for x in loop.reshape(12, 11)]
355367
assert np.all(np.std(path_spacing, axis=1) < 1e-12)
356368
assert np.all(np.std(loop_spacing, axis=1) < 1e-12)
357369

358370
def test_from_path_ends_fiber(self):
359371
# check that a linear path in quaternion space follows an explicitly defined
372+
# fiber along the same path.
373+
# this ensures the path is the shortest distance in SO3 space, (as opposed
374+
# to rodrigues or quaternion space) and also evenly spaced along the path.
375+
Q1 = Quaternion.identity()
360376
Q2 = Quaternion.from_axes_angles([1, 1, 1], 60, degrees=True)
361377
Q12 = Quaternion.stack((Q1, Q2))
362378
Q_path1 = Quaternion.from_axes_angles([1, 1, 1], np.arange(59), degrees=True)

0 commit comments

Comments
 (0)