Skip to content

Commit 177c1dd

Browse files
authored
Merge pull request #4 from hakonanes/Add-Rotation_from_path_ends
Suggestions for changes to path ends PR
2 parents 68c246d + 5eb7bbb commit 177c1dd

File tree

7 files changed

+247
-231
lines changed

7 files changed

+247
-231
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Added
3434
An example of a custom projection is the :class:`~orix.plot.StereographicPlot`.
3535
This function replaces the previous behavior of relying on a side-effect of importing
3636
the :mod:`orix.plot` module, which also registered the projections.
37-
- :func:`~orix.quaternion.Rotation.from_path_ends` returns evenly spaced points
38-
mapping the shortest path betwen two or more rotations.
37+
- Method ``from_path_ends()`` to return quaternions, rotations, orientations, or
38+
misorientations along the shortest path between two or more points.
3939

4040
Changed
4141
-------

examples/plotting/visualizing_crystallographic_paths.py

Lines changed: 0 additions & 152 deletions
This file was deleted.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#
2+
# Copyright 2018-2025 the orix developers
3+
#
4+
# This file is part of orix.
5+
#
6+
# orix is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# orix is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with orix. If not, see <http://www.gnu.org/licenses/>.
18+
#
19+
20+
"""
21+
===============================================
22+
Visualizing paths between rotations and vectors
23+
===============================================
24+
25+
This example shows how define and plot paths through either rotation or vector space.
26+
This is akin to describing crystallographic fiber textures in metallurgy, or the
27+
shortest arcs connecting points on the surface of a unit sphere.
28+
29+
In both cases, "shortest" is defined as the route that minimizes the movement required
30+
to transform from point to point, which is typically not a stright line when plotted
31+
into a euclidean projection (axis-angle, stereographic, etc.).
32+
"""
33+
34+
import matplotlib as mpl
35+
import matplotlib.pyplot as plt
36+
import numpy as np
37+
38+
from orix.plot import register_projections
39+
from orix.plot.direction_color_keys import DirectionColorKeyTSL
40+
from orix.quaternion import Orientation, Rotation
41+
from orix.quaternion.symmetry import C1, Oh
42+
from orix.sampling import sample_S2
43+
from orix.vector import Vector3d
44+
45+
register_projections() # Register our custom Matplotlib projections
46+
np.random.seed(2319) # Reproducible random data
47+
48+
# Number of steps along each path
49+
n_steps = 30
50+
51+
########################################################################################
52+
# Example 1: Continuous path
53+
# ==========================
54+
#
55+
# This plot traces the path of an object rotated 90 degrees around the x-axis, then 90
56+
# degrees around the y-axis.
57+
58+
oris1 = Orientation.from_axes_angles(
59+
[[1, 0, 0], [1, 0, 0], [0, 1, 0]], [0, 90, 90], degrees=True
60+
)
61+
oris1[2] = oris1[1] * oris1[2]
62+
path = Orientation.from_path_ends(oris1, steps=n_steps)
63+
64+
# Create a list of RGBA color values for a gradient red line and blue line
65+
colors1 = np.vstack(
66+
[
67+
mpl.colormaps["Reds"](np.linspace(0.5, 1, n_steps)),
68+
mpl.colormaps["Blues"](np.linspace(0.5, 1, n_steps)),
69+
]
70+
)
71+
72+
# Here, we use the built-in plotting method from Orientation.scatter to auto-generate
73+
# the plot.
74+
# This is especially handy when plotting only a single set of orientations.
75+
path.scatter(marker=">", c=colors1)
76+
_ = plt.gca().set_title("Axis-angle space, two 90\N{DEGREE SIGN} rotations")
77+
78+
########################################################################################
79+
# Example 2: Multiple paths
80+
# =========================
81+
#
82+
# This plot shows several paths through the cubic (*m3m*) fundamental zone created by
83+
# rotating 20 randomly chosen points 30 degrees around the z-axis.
84+
# These paths are drawn in Rodrigues space, which is an equal-angle projection of
85+
# rotation space.
86+
# As such, notice how all lines tracing out axial rotations are straight, but lines
87+
# starting closer to the center of the fundamental zone appear shorter.
88+
#
89+
# The same paths are then also plotted in the inverse pole figure (IPF) for the sample
90+
# direction (0, 0, 1), IPF-Z.
91+
92+
# Random orientations with the cubic *m3m* crystal symmetry, located inside the
93+
# fundamental zone of the proper point group (*432*)
94+
oris2 = Orientation.random(10, symmetry=Oh).reduce()
95+
96+
# Rotation around the z-axis
97+
ori_shift = Orientation.from_axes_angles([0, 0, 1], -30, degrees=True)
98+
99+
# Plot path for the first orientation (to get a figure to add to)
100+
rot_end = ori_shift * oris2[0]
101+
points = Orientation.stack([oris2[0], rot_end])
102+
path = Orientation.from_path_ends(points, steps=n_steps)
103+
path.symmetry = Oh
104+
105+
colors2 = mpl.colormaps["inferno"](np.linspace(0, 1, n_steps))
106+
fig = path.scatter("rodrigues", position=121, return_figure=True, c=colors2)
107+
path.scatter("ipf", position=122, figure=fig, c=colors2)
108+
109+
# Plot the rest
110+
rod_ax, ipf_ax = fig.axes
111+
rod_ax.set_title("Orientation paths in Rodrigues space")
112+
ipf_ax.set_title("Vector paths in IPF-Z", pad=15)
113+
114+
for ori_start in oris2[1:]:
115+
rot_end = ori_shift * ori_start
116+
points = Orientation.stack([ori_start, rot_end])
117+
path = Orientation.from_path_ends(points, steps=n_steps)
118+
path.symmetry = Oh
119+
rod_ax.scatter(path, c=colors2)
120+
ipf_ax.scatter(path, c=colors2)
121+
122+
########################################################################################
123+
# Example 3: Multiple vector paths
124+
# ================================
125+
#
126+
# Rotate vectors around the (1, 1, 1) axis on a stereographic plot.
127+
128+
vec_ax = plt.subplot(projection="stereographic")
129+
vec_ax.set_title(r"Stereographic")
130+
vec_ax.set_labels("X", "Y")
131+
132+
ipf_colormap = DirectionColorKeyTSL(C1)
133+
134+
# Define a mesh of vectors with approximately 20 degree spacing, and within 80 degrees
135+
# of the z-axis
136+
vecs = sample_S2(20)
137+
vecs = vecs[vecs.polar < np.deg2rad(80)]
138+
139+
# Define a 15 degree rotation around (1, 1, 1)
140+
rot111 = Rotation.from_axes_angles([1, 1, 1], [0, 15], degrees=True)
141+
142+
for vec in vecs:
143+
path_ends = rot111 * vec
144+
145+
# Handle case where path start end end are the same vector
146+
if np.isclose(path_ends[0].dot(path_ends[1]), 1):
147+
vec_ax.scatter(path_ends[0], c=ipf_colormap.direction2color(path_ends[0]))
148+
continue
149+
150+
# Color each path using a gradient pased on the IPF coloring
151+
colors3 = ipf_colormap.direction2color(vec)
152+
path = Vector3d.from_path_ends(path_ends, steps=100)
153+
colors3_segment = colors3 * np.linspace(0.25, 1, path.size)[:, np.newaxis]
154+
vec_ax.scatter(path, c=colors3_segment)

orix/quaternion/misorientation.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -277,40 +277,43 @@ def from_scipy_rotation(
277277
def from_path_ends(
278278
cls, points: Misorientation, closed: bool = False, steps: int = 100
279279
) -> Misorientation:
280-
"""Return misorientations tracing the shortest path between
281-
two or more consecutive points.
280+
"""Return misorientations tracing the shortest path between two
281+
or more consecutive points.
282282
283283
Parameters
284284
----------
285285
points
286286
Two or more misorientations that define points along the
287287
path.
288288
closed
289-
Option to add a final trip from the last point back to
290-
the first, thus closing the loop. The default is False.
289+
Add a final trip from the last point back to the first, thus
290+
closing the loop. Default is False.
291291
steps
292-
Number of misorientations to return between each point
293-
along the path defined by `points`. The default is 100.
292+
Number of misorientations to return between each point along
293+
the path given by *points*. Default is 100.
294294
295295
Returns
296296
-------
297297
path
298-
regularly spaced misorientations following the shortest
299-
path.
298+
Regularly spaced misorientations along the path.
299+
300+
See Also
301+
--------
302+
:class:`~orix.quaternion.Quaternion.from_path_ends`,
303+
:class:`~orix.quaternion.Orientation.from_path_ends`
300304
301305
Notes
302306
-----
303307
This function traces the shortest path between points without
304-
any regard to symmetry. Concept of "shortest path" is not
308+
considering symmetry. The concept of "shortest path" is not
305309
well-defined for misorientations, which can define multiple
306310
symmetrically equivalent points with non-equivalent paths.
307311
"""
308-
# Confirm `points` are misorientations.
309-
if type(points) is not cls:
312+
points_type = type(points)
313+
if points_type is not cls:
310314
raise TypeError(
311-
f"Points must be a Misorientation, not of type {type(points)}"
315+
f"Points must be misorientations, not of type {points_type}"
312316
)
313-
# Create a path through Quaternion space, then reapply the symmetry.
314317
out = Rotation.from_path_ends(points=points, closed=closed, steps=steps)
315318
return cls(out.data, symmetry=points.symmetry)
316319

0 commit comments

Comments
 (0)