Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dc80085
fix loss of inversion during Rotation.__setitem__
argerlt Jul 8, 2026
a194e8b
Allow weighted quaternion averages and update docstring
argerlt Jul 8, 2026
855bd20
remove depreciated funcion
argerlt Jul 8, 2026
49ff3ee
Cover Orientation.from_symmetry() edge cases that were previously ign…
argerlt Jul 8, 2026
cf9178d
Update OrientationRegion docstrings with correct definitions
argerlt Jul 8, 2026
bc6751f
remove depreciated tests
argerlt Jul 8, 2026
b130216
speed up mis.reduce and ensure unique solution
argerlt Jul 8, 2026
2a74195
add clarification to the mis.reduce() docstring
argerlt Jul 8, 2026
8a74ffe
verbose clarification of from_symmetry logic
argerlt Jul 9, 2026
4fc1616
add symmetry-aware Misorientation.mean and update docstrings
argerlt Jul 9, 2026
8572852
docstring clarifications
argerlt Jul 9, 2026
ddf5413
add deprecations and tests
argerlt Jul 9, 2026
215b2f5
add misorientation.mean() tests
argerlt Jul 9, 2026
19e0ea9
fixing typos and adding coverage.
argerlt Jul 9, 2026
1c6ff0c
write unit test for uncovered lines in Stereographic plot
argerlt Jul 15, 2026
a671d5f
speed up orientation.dot and dot_outer and fix error in issue #673
argerlt Jul 15, 2026
d82953c
update tests for reduce, mean, and dot
argerlt Jul 15, 2026
596f969
Create reducing_and_averaging_misorientations.py
argerlt Jul 15, 2026
a85ddda
Update CHANGELOG.rst
argerlt Jul 15, 2026
e2cde11
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 15, 2026
0929218
fix broken link
argerlt Jul 15, 2026
5b7a131
Clarify vector_path example
argerlt Jul 15, 2026
dbf01b4
better logic for rotation setting that doesnt error out for single va…
argerlt Jul 15, 2026
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
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,34 @@ its best to adhere to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>

Added
-----
- ``Quaternion.mean(weight)`` allows the calculation of weighted averages.
- ``Misorientation.get_distance_matrix(lazy=True)``.
Currently opt-in, but will be the default in the next minor version.
- Non-lazy computation of dot products with
``Misorientation.get_distance_matrix(lazy=True)``.
Currently opt-in, but will be the default in the next minor version.

Changed
-------
- Misorientations and Orientations account for symmetry when calculating means.
- ``Miller.get_nearest()`` now raises a ``NotImplementedError`` rather than returning
``NotImplemented``.
- ``Mille.mean(use_symmetry=True)`` now raises a ``NotImplementedError`` rather than
returning ``NotImplemented``.
- Improved (faster and using less memory) non-lazy computation of misorientation angles
from ``Orientation.with_angle_outer()``.
- an OrientationRegion can now be calculated from symmetry for all 1024 possible combinations
of point groups (previously not implimented for 200 combinations).

Fixed
-----
- (Mis)orientation reduction to the fundamental zone via ``reduce()`` now correctly
applies the symmetries in the opposite order, from right to left,
`s_end * g * s_start`, where `g` is a (mis)orientation.
- Setting a Rotaion will now copy over the proper/improper marker if present.
- `Orientation.dot` and `Orientation.dot_outer` now correctly handle dot products for
multi-dimensional inputs.



2026-06-06 - version 0.14.3
Expand Down
12 changes: 12 additions & 0 deletions doc/user/bibliography.bib
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ @article{johnstone2020density
volume = {53},
year = {2020}
}
@article{markley_averaging_2007,
author = {Markley, F. Landis and Cheng, Yang and Crassidis, John L. and Oshman, Yaakov},
doi = {10.2514/1.28949},
journal = {Journal of Guidance, Control, and Dynamics},
number = {4},
title = {Averaging {Quaternions}},
volume = {30},
pages = {1193--1197},
year = {2007},
issn = {0731-5090, 1533-3884},
url = {https://arc.aiaa.org/doi/10.2514/1.28949},
}
@PhdThesis{martineau2020multivariate,
author = {Martineau, Benjamin Helks},
title = {Multivariate Analysis for Scanning (Transmission) Electron Diffraction},
Expand Down
230 changes: 230 additions & 0 deletions examples/misorientations/reducing_and_averaging_misorientations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Copyright 2018-2026 the orix developers
#
# This file is part of orix.
#
# orix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# orix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with orix. If not, see <http://www.gnu.org/licenses/>.
#
r"""
=======================================================
Reducing and Averaging Misorientations and Orientations
=======================================================

This example introduces the concept of reducing an orientation or
misorientation with respect to symmetry, as well as the related
concept of averaging a misorientation.
"""

import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import norm

from orix.plot.rotation_plot import _setup_rotation_plot
import orix.quaternion as oqu
import orix.quaternion.symmetry as osm

# Reproducible random data.
np.random.seed(2319)


# convenience function for plotting.
def setup_plot(fz):
fig, ax = _setup_rotation_plot(projection="homochoric")
fig.set_figwidth(6)
fig.set_figheight(5)
ax.axis("off")
ax._correct_aspect_ratio(fz)
return (fig, ax)


#########################################################################################
# Basic Example
# -------------
# `Orientation.reduce` and `Misorientation.reduce` will convert a transform
# with symmetry to a unique equivalent representation with the smallest
# possible angle of rotation. This is what is meant by "reducing" a
# transform.

g_random = oqu.Orientation.random(shape=1, symmetry=osm.Oh)
g_reduced = g_random.reduce()

g_fz = oqu.OrientationRegion.from_symmetry(osm.C1, osm.Oh)
g_all = osm.Oh.outer(g_random)
g_equiv = g_all[oqu.Rotation(g_all).dot(g_reduced) < 0.999]

fig_1, ax_1 = setup_plot(g_fz)
ax_1.set_xlim([-1, 1])
ax_1.set_aspect("equal")
ax_1.scatter(g_equiv, color="black")
ax_1.scatter(g_reduced, color="red")
ax_1.plot_wireframe(g_fz, color="grey")
fig_1.suptitle(
"Reduced (red) and equivalent (black) \nrepresentations in point group Oh (m-3m)"
)

#########################################################################################
# The inclusion of symmetry combined with the periodic nature of rotations
# can make the definition of a mean or average ambiguous (more on this below),
# so the first step when an average is calculated is to reduce the transforms
# and then calculate the average. A side effect of this is the mean
# (mis)orientation returned by ORIX will also be a reduced representation.

qu_data = np.stack([norm.rvs(i, 0.06, 20) for i in [0.39, 0.28, -0.39, 0.78]]).T
m_sym = [osm.C3, osm.D2]

m_clustered = oqu.Misorientation(qu_data, symmetry=m_sym)
m_reduced = m_clustered.reduce()
m_mean, m_neighbors = m_clustered.mean(return_neighbors=True)

fz_cluster = oqu.OrientationRegion.from_symmetry(*m_sym)

fig2, ax2 = setup_plot(fz_cluster)
ax2.scatter(oqu.Rotation(m_clustered), c="k")
ax2.scatter(oqu.Rotation(m_reduced), c="r")
ax2.scatter(oqu.Rotation(m_mean), color="blue", marker="X", s=100)
ax2.plot_wireframe(fz_cluster, color="grey")
fig2.suptitle(
"Reduced (red), Original (black), and symmetry-aware \nMean (blue) for {C3(3)-> D2(222)} system"
)

#########################################################################################
# The Fundamental Zone
# --------------------
#
# In the plots above, wireframes were included that defined bounded volumes
# within which all reduced representations fell. This is known as a
# Fundamental Zone (FZ), and contains a single unique (aka, fundamental)
# representation of every transformation with respect to a given symmetry.
# representations that fall within a fundamental zone are also garunteed to
# have the smallest possible angular component.
#
# There are several ways in which a fundamental zone can be defined, with most
# discrepencies stemming from how improper transforms
# (ie, inversions and rotoinversions) should be handled. ORIX uses the rules
# presented in :cite:`krakow2017onthree`, but expanded to all 1024
# misorientation groups. This can be verified by comparing the following plots
# to Figure 5 of the same paper.

name2group = {x.name: x for x in osm._groups}

fig5 = plt.figure(figsize=[5, 8])

base_pairs = [
["432", "3"],
["23", "3"],
["432", "1"],
["23", "1"],
["3", "422"],
["622", "1"],
["4", "211"],
["32", "1"],
["222", "1"],
["3", "4"],
["6", "1"],
["4", "1"],
["3", "1"],
["211", "1"],
["1", "1"],
]
for i, pair in enumerate(base_pairs):
s1 = name2group[pair[0]]
s2 = name2group[pair[1]]
fz = oqu.OrientationRegion.from_symmetry(s1, s2)
ax = fig5.add_subplot(5, 3, i + 1, projection="homochoric")
ax.axis("off")
ax._correct_aspect_ratio(fz)
ax.plot_wireframe(fz)
ax.set_title("{}: {} -> {}".format("abcdefghijklmno"[i], s1.name, s2.name))

plt.tight_layout()

#########################################################################################
# If users are unfamiliar with how Rodrigues, Homochoric, or NeoEulerian plots
# are used to plot rotations in 3D space, the same paper also contains a
# concise overview.
#
# It is not ran as part of this example since calculating and plotting 66
# fundamental zones is time consuming, but the following plot matches Table
# 3 of the same paper, showing the subdivisions of the above 15 zones caused
# by shared rotation elements between the point groups. Users may plot it
# themselves by running this code locally with `plot_me=True`


plot_me = False

names = ["432", "23", "622", "6", "32", "3", "422", "4", "222", "211", "1"]
if plot_me is True:
table_fig = plt.figure()
for i in range(len(names)):
n1 = names[i]
s1 = name2group[n1]
for j in range(len(names) - i):
n2 = names[-j - 1]
s2 = name2group[n2]
fz = oqu.OrientationRegion.from_symmetry(s1, s2)
ij = j * 11 + i + 1
ax = table_fig.add_subplot(11, 11, ij, projection="homochoric")
ax.axis("off")
ax._correct_aspect_ratio(fz)
ax.plot_wireframe(fz)

#########################################################################################
# Defining a Mean in Rotation Space
# ---------------------------------
#
# Up until now, we have not defined what is meant by the mean of a group of
# transforms. Because rotation space is periodic, the concept of a Euclidean
# norm does not apply, and instead a Frobenius norm is used, which in this
# context can be thought of as the magnitude of the angular rotation necessary
# to align two transforms. Noteably, this is NOT simply the normalized average
# of two transform's quaternion representations, as is sometimes done in other
# software to get a fast approximation for clustered transforms. See the
# docstring for `Quaternion.mean` for details on this topic.
#
# The extension of this is a mean transform is defined as the transform whose
# total angular deviation from all transforms in the queried group is the
# minimum possible value.
#
# However, there is not a convenient algorithm to calculate this correctly,
# so instead ORIX does the following:
#
# 1) transforms are reduced to the appropriate fundamental zone.
# 2) A rough mean is calculated.
# 3) transforms with equivalents closer to the rough mean are updated to the closer value
# 4) A precise mean is recalculated.
#
# The plot below is provided to help visualize this process.

np.random.seed(2319)
qu_data = np.stack([norm.rvs(i, 0.1, 20) for i in [0.1, 0.1, 0.2, 0.3]]).T

o_cluster = oqu.Orientation(qu_data, symmetry=osm.D2)
o_reduced = o_cluster.reduce()
o_mean, o_neighbors = o_cluster.mean(return_neighbors=True)
o_flipped = o_neighbors[np.abs(o_neighbors.angle - o_reduced.angle) > 1e-3]


fig_ave, ax_ave = setup_plot(fz)
ax_ave.scatter(oqu.Rotation(o_cluster), color="black")
ax_ave.scatter(oqu.Rotation(o_reduced), color="red")
ax_ave.scatter(oqu.Rotation(o_flipped), color="green")
ax_ave.scatter(oqu.Rotation(o_mean), color="blue", marker="X", s=100)

fz = oqu.OrientationRegion.from_symmetry(end=osm.D2)
ax_ave.plot_wireframe(fz, color="grey")
ax_ave.set_xlim([-1, 1])
ax_ave.set_ylim([-1, 1])
ax_ave.set_zlim([-1, 1])
fig_ave.suptitle(
"Original (black), Reduced (red), and flipped(green) \nrepresentations for point group D2 (222)"
)
10 changes: 6 additions & 4 deletions examples/plotting/visualizing_rotation_vector_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@
path.symmetry = Oh

colors2 = mpl.colormaps["inferno"](np.linspace(0, 1, n_steps))
fig = path.scatter("rodrigues", position=121, return_figure=True, c=colors2)
path.scatter("ipf", position=122, figure=fig, c=colors2)
fig2 = path.scatter("rodrigues", position=121, return_figure=True, c=colors2)
path.scatter("ipf", position=122, figure=fig2, c=colors2)

# Plot the rest
rod_ax, ipf_ax = fig.axes
rod_ax, ipf_ax = fig2.axes
rod_ax.set_title("Orientation paths in Rodrigues space")
ipf_ax.set_title("Vector paths in IPF-Z", pad=15)

Expand All @@ -125,7 +125,9 @@
#
# Rotate vectors around the (1, 1, 1) axis on a stereographic plot.

vec_ax = plt.subplot(projection="stereographic")

fig3 = plt.figure()
vec_ax = fig3.add_subplot(111, projection="stereographic")
vec_ax.set_title(r"Stereographic")
vec_ax.set_labels("X", "Y")

Expand Down
Loading
Loading