Skip to content
Open
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 .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
"name": "Alexander Clausen",
"orcid": "0000-0002-9555-7455",
"affiliation": "Jülich Research Centre, Ernst Ruska Centre"
},
{
"name": "Vincent Gao"
}
]
}
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`__, and this project
its best to adhere to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`__.


Unreleased
==========

Fixed
-----
- ``Rotation.to_euler()`` now returns correct Bunge Euler angles for 180 degree
rotations about an axis in the xy-plane not aligned with x or y. A sign error in
the singular branch of the quaternion-to-Euler conversion previously reflected the
reconstructed axis, so the angles did not round-trip back to the input rotation.


2026-06-10 - version 0.15.0
===========================

Expand Down
1 change: 1 addition & 0 deletions orix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@
"Dorian Depriester",
"Eric Prestat",
"Alexander Clausen",
"Vincent Gao",
]
2 changes: 1 addition & 1 deletion orix/quaternion/_conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,7 @@ def qu2eu_single(qu: np.ndarray) -> np.ndarray:
a = -2 * qu[0] * qu[3]
b = qu[0] * qu[0] - qu[3] * qu[3]
else:
a = -2 * qu[1] * qu[2]
a = 2 * qu[1] * qu[2]
b = qu[1] * qu[1] - qu[2] * qu[2]
eu[1] = np.pi
eu[0] = np.arctan2(a, b)
Expand Down
26 changes: 25 additions & 1 deletion orix/tests/test_quaternion/test_conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import numpy as np
import pytest

from orix.quaternion import Orientation
from orix.quaternion import Orientation, Rotation
from orix.quaternion._conversions import (
ax2qu,
ax2qu_2d,
Expand Down Expand Up @@ -105,6 +105,30 @@ def test_eu2qu2eu(self, euler_angles, quaternions_conversions):
assert ori.symmetry == Oh
assert np.allclose(ori.data[0], qu_64[-1], atol=1e-4)

def test_qu2eu_equatorial_twofold(self):
# 180 deg rotations about equatorial (xy-plane) axes hit the Phi = pi
# branch of qu2eu (Rowenhorst et al. (2015) Eq. A.14). Off-axis axes,
# with x and y both non-zero, must round-trip back to the rotation.
psi = np.linspace(0, np.pi, 19)
axes = np.column_stack([np.cos(psi), np.sin(psi), np.zeros_like(psi)])
qu = ax2qu(axes, np.full(psi.shape, np.pi))

# jit path: qu -> eu -> qu, compared as matrices to sidestep the
# quaternion double cover
assert np.allclose(qu2om(qu), qu2om(eu2qu(qu2eu(qu))), atol=1e-6)

# Pure-Python path
for q in qu:
q_rt = eu2qu_single.py_func(qu2eu_single.py_func(q))
assert np.allclose(
qu2om_single.py_func(q), qu2om_single.py_func(q_rt), atol=1e-6
)

# Public API round-trip
rot = Rotation.from_axes_angles(axes, np.pi)
rot_rt = Rotation.from_euler(rot.to_euler())
assert np.allclose(rot.to_matrix(), rot_rt.to_matrix(), atol=1e-6)

def test_get_pyramid(self, cubochoric_coordinates):
"""Cubochoric coordinates situated in expected pyramid."""
cu_64 = cubochoric_coordinates
Expand Down
9 changes: 5 additions & 4 deletions orix/tests/test_sampling/test_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,15 @@ def test_get_sample_reduced_fundamental(self):
v_Oh = R_Oh * vz
assert np.all(v_Oh <= Oh.fundamental_sector)

# Some rotations have a phi1 Euler angle of multiples of pi,
# presumably due to rounding errors
# A few sampled rotations are 180 degree rotations about an axis in
# the xy-plane (Phi = pi), where phi1 is degenerate and lands on a
# multiple of pi / 2
phi1_C1 = R_C1.to_euler()[:, 0].round(7)
assert np.allclose(np.unique(phi1_C1), 0, atol=1e-7)
phi1_C4 = R_C4.to_euler()[:, 0].round(7)
assert np.allclose(np.unique(phi1_C4), [0, np.pi / 2], atol=1e-7)
assert np.allclose(np.unique(phi1_C4), [0, np.pi / 2, 3 * np.pi / 2], atol=1e-7)
phi1_C6 = R_C6.to_euler()[:, 0].round(7)
assert np.allclose(np.unique(phi1_C6), [0, np.pi / 2], atol=1e-7)
assert np.allclose(np.unique(phi1_C6), [0, np.pi / 2, 3 * np.pi / 2], atol=1e-7)
phi1_Oh = R_Oh.to_euler()[:, 0].round(7)
assert np.allclose(np.unique(phi1_Oh), [0, np.pi / 2], atol=1e-7)

Expand Down