From 0bfc682070391d7d2c3527da073082c19b02cd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 8 Sep 2024 18:45:23 +0200 Subject: [PATCH 01/28] Add private constants module for checking installed packages once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/constants.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 orix/constants.py diff --git a/orix/constants.py b/orix/constants.py new file mode 100644 index 00000000..c8ceb017 --- /dev/null +++ b/orix/constants.py @@ -0,0 +1,22 @@ +"""Constants and such useful across modules.""" + +from importlib.metadata import version +from pathlib import Path +import tomllib + +# Dependencies +with open(Path(__file__).parent.parent / "pyproject.toml", "rb") as f: + d = tomllib.load(f) + deps = d["project"]["dependencies"] + optional_deps = d["project"]["optional-dependencies"]["all"] + deps += optional_deps + +installed = {} +for pkg in deps: + try: + _ = version(pkg) + installed[pkg] = True + except ImportError: + installed[pkg] = False + +del version, Path, tomllib, d, deps, optional_deps From 1e7a357c52707918cece4fbf85558b594fac18f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 8 Sep 2024 18:45:59 +0200 Subject: [PATCH 02/28] Make numpy-quaternion an optional dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd543f7c..38672e25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ dependencies = [ "matplotlib-scalebar", "numba", "numpy", - "numpy-quaternion", "pooch >= 0.13", # TODO: Remove once https://github.com/diffpy/diffpy.structure/issues/97 is fixed "pycifrw", @@ -41,6 +40,9 @@ dependencies = [ ] [project.optional-dependencies] +all = [ + "numpy-quaternion", +] doc = [ "ipykernel", # Used by nbsphinx to execute notebooks "memory_profiler", From 6638f12c7af844eeab6c5f21eb27460b2067ec4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 8 Sep 2024 18:46:26 +0200 Subject: [PATCH 03/28] Return of Numba accelerated quaternion-quaternion and quaternion-vector, but using generalized universal functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/quaternion.py | 171 ++++++++++++++++++++-------------- 1 file changed, 103 insertions(+), 68 deletions(-) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 5b77a39c..43cf4aee 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -23,11 +23,12 @@ import dask.array as da from dask.diagnostics import ProgressBar +import numba as nb import numpy as np -import quaternion from scipy.spatial.transform import Rotation as SciPyRotation from orix._base import Object3d +from orix.constants import installed from orix.quaternion import _conversions from orix.vector import AxAngle, Homochoric, Miller, Rodrigues, Vector3d @@ -48,9 +49,8 @@ class Quaternion(Object3d): .. math:: - i^2 = j^2 = k^2 = -1; - - ij = -ji = k; jk = -kj = i; ki = -ik = j. + i^2 &= j^2 = k^2 = -1;\\ + ij &= -ji = k; jk = -kj = i; ki = -ik = j. In orix, quaternions are stored with the scalar part first followed by the vector part, denoted :math:`Q = (a, b, c, d)`. @@ -61,12 +61,9 @@ class Quaternion(Object3d): .. math:: - a_3 = a_1 \cdot a_2 - b_1 \cdot b_2 - c_1 \cdot c_2 - d_1 \cdot d_2 - - b_3 = a_1 \cdot b_2 + b_1 \cdot a_2 + c_1 \cdot d_2 - d_1 \cdot c_2 - - c_3 = a_1 \cdot c_2 - b_1 \cdot d_2 + c_1 \cdot a_2 + d_1 \cdot b_2 - + a_3 = a_1 \cdot a_2 - b_1 \cdot b_2 - c_1 \cdot c_2 - d_1 \cdot d_2\\ + b_3 = a_1 \cdot b_2 + b_1 \cdot a_2 + c_1 \cdot d_2 - d_1 \cdot c_2\\ + c_3 = a_1 \cdot c_2 - b_1 \cdot d_2 + c_1 \cdot a_2 + d_1 \cdot b_2\\ d_3 = a_1 \cdot d_2 + b_1 \cdot c_2 - c_1 \cdot b_2 + d_1 \cdot a_2 Rotation of a 3D vector :math:`v = (x, y, z)` by a quaternion is @@ -74,11 +71,9 @@ class Quaternion(Object3d): .. math:: - v'_x = x(a^2 + b^2 - c^2 - d^2) + 2[z(a \cdot c + b \cdot d) + y(b \cdot c - a \cdot d)] - - v'_y = y(a^2 - b^2 + c^2 - d^2) + 2[x(a \cdot d + b \cdot c) + z(c \cdot d - a \cdot b)] - - v'_z = z(a^2 - b^2 - c^2 + d^2) + 2[y(a \cdot b + c \cdot d) + x(b \cdot d - a \cdot c)] + v'_x = x + 2a(cz - dy) - 2d(dx - bz) + 2c(by - cx)\\ + v'_y = y + 2d(cz - dy) + 2a(dx - bz) - 2b(by - cx)\\ + v'_z = z - 2c(cz - dy) + 2b(dx - bz) + 2a(by - cx) The norm of a quaternion is defined as @@ -120,66 +115,38 @@ class Quaternion(Object3d): @property def a(self) -> np.ndarray: - """Return or set the scalar quaternion component. - - Parameters - ---------- - value : numpy.ndarray - Scalar quaternion component. - """ + """Return or set the scalar quaternion component.""" return self.data[..., 0] @a.setter - def a(self, value: np.ndarray): - """Set the scalar quaternion component.""" + def a(self, value: np.ndarray) -> None: self.data[..., 0] = value @property def b(self) -> np.ndarray: - """Return or set the first vector quaternion component. - - Parameters - ---------- - value : numpy.ndarray - First vector quaternion component. - """ + """Return or set the first vector quaternion component.""" return self.data[..., 1] @b.setter - def b(self, value: np.ndarray): - """Set the first vector quaternion component.""" + def b(self, value: np.ndarray) -> None: self.data[..., 1] = value @property def c(self) -> np.ndarray: - """Return or set the second vector quaternion component. - - Parameters - ---------- - value : numpy.ndarray - Second vector quaternion component. - """ + """Return or set the second vector quaternion component.""" return self.data[..., 2] @c.setter - def c(self, value: np.ndarray): - """Set the second vector quaternion component.""" + def c(self, value: np.ndarray) -> None: self.data[..., 2] = value @property def d(self) -> np.ndarray: - """Return or set the third vector quaternion component. - - Parameters - ---------- - value : numpy.ndarray - Third vector quaternion component. - """ + """Return or set the third vector quaternion component.""" return self.data[..., 3] @d.setter - def d(self, value: np.ndarray): - """Set the third vector quaternion component.""" + def d(self, value: np.ndarray) -> None: self.data[..., 3] = value @property @@ -208,29 +175,51 @@ def antipodal(self) -> Quaternion: @property def conj(self) -> Quaternion: r"""Return the conjugate of the quaternion - :math:`Q^* = a - bi - cj - dk`. + :math:`Q^{*} = a - bi - cj - dk`. """ - Q = quaternion.from_float_array(self.data).conj() - return self.__class__(quaternion.as_float_array(Q)) + if installed["numpy-quaternion"]: + import quaternion + + qu2 = quaternion.from_float_array(self.data).conj() + qu2 = quaternion.as_float_array(qu2) + else: + qu2 = np.empty_like(self.data) + qu_conj_gufunc(self.data, qu2) + Q = self.__class__(qu2) + return Q # ------------------------ Dunder methods ------------------------ # def __invert__(self) -> Quaternion: return self.__class__(self.conj.data / (self.norm**2)[..., np.newaxis]) - def __mul__(self, other: Union[Quaternion, Vector3d]): + def __mul__( + self, other: Union[Quaternion, Vector3d] + ) -> Union[Quaternion, Vector3d]: if isinstance(other, Quaternion): - Q1 = quaternion.from_float_array(self.data) - Q2 = quaternion.from_float_array(other.data) - return other.__class__(quaternion.as_float_array(Q1 * Q2)) + if installed["numpy-quaternion"]: + import quaternion + + Q1 = quaternion.from_float_array(self.data) + Q2 = quaternion.from_float_array(other.data) + qu2 = quaternion.as_float_array(Q1 * Q2) + else: + qu2 = qu_multiply(self.data, other.data) + Q = self.__class__(qu2) + return Q elif isinstance(other, Vector3d): - # check broadcast shape is correct before calculation, as - # quaternion.rotat_vectors will perform outer product - # this keeps current __mul__ broadcast behaviour - Q1 = quaternion.from_float_array(self.data) - v = quaternion.as_vector_part( - (Q1 * quaternion.from_vector_part(other.data)) * ~Q1 - ) + if installed["numpy-quaternion"]: + import quaternion + + # Don't use rotate_vectors as it may perform an outer + # product. The following keeps current __mul__ broadcast + # behavior. + Q1 = quaternion.from_float_array(self.data) + v = quaternion.as_vector_part( + (Q1 * quaternion.from_vector_part(other.data)) * ~Q1 + ) + else: + v = qu_rotate_vec(self.unit.data, other.data) if isinstance(other, Miller): m = other.__class__(xyz=v, phase=other.phase) m.coordinate_format = other.coordinate_format @@ -243,7 +232,7 @@ def __neg__(self) -> Quaternion: return self.__class__(-self.data) def __eq__(self, other: Union[Any, Quaternion]) -> bool: - """Check if quaternions have equal shapes and values.""" + """Check if quaternions have equal shapes and components.""" if ( isinstance(other, Quaternion) and self.shape == other.shape @@ -302,8 +291,8 @@ def from_axes_angles( if degrees: angles = np.deg2rad(angles) - Q = _conversions.ax2qu(axes, angles) - Q = cls(Q) + qu = _conversions.ax2qu(axes, angles) + Q = cls(qu) Q = Q.unit return Q @@ -1237,3 +1226,49 @@ def _outer_dask( new_chunks = tuple(chunks1[:-1]) + tuple(chunks2[:-1]) + (-1,) return out.rechunk(new_chunks) + + +@nb.guvectorize("(n)->(n)", cache=True) +def qu_conj_gufunc(qu: np.ndarray, qu2: np.ndarray) -> None: # pragma: no cover + qu2[0] = qu[0] + for i in range(3): + qu2[i + 1] = -1.0 * qu[i + 1] + + +@nb.guvectorize("(n),(n)->(n)", cache=True) +def qu_multiply_gufunc( + qu1: np.ndarray, qu2: np.ndarray, qu12: np.ndarray +) -> None: # pragma: no cover + qu12[0] = qu1[0] * qu2[0] - qu1[1] * qu2[1] - qu1[2] * qu2[2] - qu1[3] * qu2[3] + qu12[1] = qu1[1] * qu2[0] + qu1[0] * qu2[1] - qu1[3] * qu2[2] + qu1[2] * qu2[3] + qu12[2] = qu1[2] * qu2[0] + qu1[3] * qu2[1] + qu1[0] * qu2[2] - qu1[1] * qu2[3] + qu12[3] = qu1[3] * qu2[0] - qu1[2] * qu2[1] + qu1[1] * qu2[2] + qu1[0] * qu2[3] + + +def qu_multiply(qu1: np.ndarray, qu2: np.ndarray) -> np.ndarray: + shape = np.broadcast_shapes(qu1.shape, qu2.shape) + qu12 = np.empty(shape, dtype=np.float64) + qu_multiply_gufunc(qu1, qu2, qu12) + return qu12 + + +@nb.guvectorize("(n),(m)->(m)", cache=True) +def qu_rotate_vec_gufunc( + qu: np.ndarray, v1: np.ndarray, v2: np.ndarray +) -> None: # pragma: no cover + a, b, c, d = qu + x, y, z = v1 + tx = 2 * (c * z - d * y) + ty = 2 * (d * x - b * z) + tz = 2 * (b * y - c * x) + v2[0] = x + a * tx - d * ty + c * tz + v2[1] = y + d * tx + a * ty - b * tz + v2[2] = z - c * tx + b * ty + a * tz + + +def qu_rotate_vec(qu: np.ndarray, v: np.ndarray) -> np.ndarray: + qu, v = np.atleast_2d(qu, v) + shape = np.broadcast_shapes(qu.shape[:-1], v.shape[:-1]) + (3,) + v2 = np.empty(shape, dtype=np.float64) + qu_rotate_vec_gufunc(qu, v, v2) + return v2 From e2a6ad4254c22a984c44a9adcde91b0ee7bc008b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 8 Sep 2024 18:47:22 +0200 Subject: [PATCH 04/28] Use Numba njit instead of setting nopython param, remove conventions listed elsewhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/_conversions.py | 69 +++++++++++++-------------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/orix/quaternion/_conversions.py b/orix/quaternion/_conversions.py index 6fa2c1b6..4a4e4069 100644 --- a/orix/quaternion/_conversions.py +++ b/orix/quaternion/_conversions.py @@ -18,19 +18,6 @@ """Conversions of rotations between many common representations from :cite:`rowenhorst2015consistent`, accelerated with Numba. - -Conventions: - -1. Right-handed Cartesian reference frames -2. Rotation angles are taken to be positive for a counter-clockwise - rotation when viewing from the end point of the rotation axis unit - vector towards the origin. -3. Rotations are *interpreted* in the passive sense. This means that we - rotate reference frames with vectors fixed in space. Rotations are - basis transformations rather than coordinate transformations. -4. Euler angle triplets are implemented using the Bunge convention, with - angular ranges as [0, 2pi], [0, pi], and [0, 2pi]. -5. Rotation angles are limited to [0, pi]. """ from typing import Tuple @@ -41,7 +28,7 @@ FLOAT_EPS = np.finfo(float).eps -@nb.jit("int64(float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("int64(float64[:])", cache=True, fastmath=True, nogil=True) def get_pyramid_single(xyz: np.ndarray) -> int: """Determine to which out of six pyramids in the cube a (x, y, z) coordinate belongs. @@ -77,7 +64,7 @@ def get_pyramid_single(xyz: np.ndarray) -> int: return 6 -@nb.jit("int64[:](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("int64[:](float64[:, :])", cache=True, fastmath=True, nogil=True) def get_pyramid_2d(xyz: np.ndarray) -> np.ndarray: """Determine to which out of six pyramids in the cube a 2D array of (x, y, z) coordinates belongs. @@ -116,7 +103,7 @@ def get_pyramid(xyz: np.ndarray) -> np.ndarray: return pyramids -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def cu2ho_single(cu: np.ndarray) -> np.ndarray: """Convert a single set of cubochoric coordinates to un-normalized homochoric coordinates :cite:`singh2016orientation`. @@ -195,7 +182,7 @@ def cu2ho_single(cu: np.ndarray) -> np.ndarray: return np.roll(ho, -1) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def cu2ho_2d(cu: np.ndarray) -> np.ndarray: """Convert multiple cubochoric coordinates to un-normalized homochoric coordinates :cite:`singh2016orientation`. @@ -234,7 +221,7 @@ def cu2ho(cu: np.ndarray) -> np.ndarray: return ho -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ho2ax_single(ho: np.ndarray) -> np.ndarray: """Convert a single set of homochoric coordinates to an un-normalized axis-angle pair :cite:`rowenhorst2015consistent`. @@ -285,7 +272,7 @@ def ho2ax_single(ho: np.ndarray) -> np.ndarray: return ax -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ho2ax_2d(ho: np.ndarray) -> np.ndarray: """Convert multiple homochoric coordinates to un-normalized axis-angle pairs :cite:`rowenhorst2015consistent`. @@ -325,7 +312,7 @@ def ho2ax(ho: np.ndarray) -> np.ndarray: return ax -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ax2ro_single(ax: np.ndarray) -> np.ndarray: """Convert a single angle-axis pair to an un-normalized Rodrigues vector :cite:`rowenhorst2015consistent`. @@ -365,7 +352,7 @@ def ax2ro_single(ax: np.ndarray) -> np.ndarray: return ro -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ax2ro_2d(ax: np.ndarray) -> np.ndarray: """Convert multiple axis-angle pairs to un-normalized Rodrigues vectors :cite:`rowenhorst2015consistent`. @@ -405,7 +392,7 @@ def ax2ro(ax: np.ndarray) -> np.ndarray: return ro -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ro2ax_single(ro: np.ndarray) -> np.ndarray: """Convert a single Rodrigues vector to an un-normalized axis-angle pair :cite:`rowenhorst2015consistent`. @@ -434,7 +421,7 @@ def ro2ax_single(ro: np.ndarray) -> np.ndarray: return np.append(ro[:3] / norm, 2 * np.arctan(ro[3])) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ro2ax_2d(ro: np.ndarray) -> np.ndarray: """Convert multiple Rodrigues vectors to un-normalized axis-angle pairs :cite:`rowenhorst2015consistent`. @@ -474,7 +461,7 @@ def ro2ax(ro: np.ndarray) -> np.ndarray: return ax -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ax2qu_single(ax: np.ndarray) -> np.ndarray: """Convert a single axis-angle pair to a unit quaternion :cite:`rowenhorst2015consistent`. @@ -505,7 +492,7 @@ def ax2qu_single(ax: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ax2qu_2d(ax: np.ndarray) -> np.ndarray: """Convert multiple axis-angle pairs to unit quaternions :cite:`rowenhorst2015consistent`. @@ -582,7 +569,7 @@ def ax2qu(axes: np.ndarray, angles: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def qu2ax_single(qu: np.ndarray) -> np.ndarray: """Convert a single (un)normalized quaternion to a normalized axis-angle pair :cite:`rowenhorst2015consistent`. @@ -622,7 +609,7 @@ def qu2ax_single(qu: np.ndarray) -> np.ndarray: return ax -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def qu2ax_2d(qu: np.ndarray) -> np.ndarray: """Convert multiple (un)normalized quaternions to normalized axis-angle pairs :cite:`rowenhorst2015consistent`. @@ -685,7 +672,7 @@ def qu2ax(qu: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: return axes, angles -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ho2ro_single(ho: np.ndarray) -> np.ndarray: """Convert a single set of homochoric coordinates to an un-normalized Rodrigues vector :cite:`rowenhorst2015consistent`. @@ -708,7 +695,7 @@ def ho2ro_single(ho: np.ndarray) -> np.ndarray: return ax2ro_single(ho2ax_single(ho)) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ho2ro_2d(ho: np.ndarray) -> np.ndarray: """Convert multiple homochoric coordinates to un-normalized Rodrigues vectors :cite:`rowenhorst2015consistent`. @@ -748,7 +735,7 @@ def ho2ro(ho: np.ndarray) -> np.ndarray: return ro -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def cu2ro_single(cu: np.ndarray) -> np.ndarray: """Convert a single set of cubochoric coordinates to an un-normalized Rodrigues vector :cite:`rowenhorst2015consistent`. @@ -774,7 +761,7 @@ def cu2ro_single(cu: np.ndarray) -> np.ndarray: return ho2ro_single(cu2ho_single(cu)) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def cu2ro_2d(cu: np.ndarray) -> np.ndarray: """Convert multiple cubochoric coordinates to un-normalized Rodrigues vectors :cite:`rowenhorst2015consistent`. @@ -814,7 +801,7 @@ def cu2ro(cu: np.ndarray) -> np.ndarray: return ro -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def eu2qu_single(eu: np.ndarray) -> np.ndarray: """Convert three Euler angles (alpha, beta, gamma) to a unit quaternion :cite:`rowenhorst2015consistent`. @@ -854,7 +841,7 @@ def eu2qu_single(eu: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def eu2qu_2d(eu: np.ndarray) -> np.ndarray: """Convert multiple Euler angles (alpha, beta, gamma) to unit quaternions. @@ -894,7 +881,7 @@ def eu2qu(eu: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:, :])", cache=True, fastmath=True, nogil=True) def om2qu_single(om: np.ndarray) -> np.ndarray: """Convert a single (3, 3) rotation matrix to a unit quaternion :cite:`rowenhorst2015consistent`. @@ -955,7 +942,7 @@ def om2qu_single(om: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:, :](float64[:, :, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :, :])", cache=True, fastmath=True, nogil=True) def om2qu_3d(om: np.ndarray) -> np.ndarray: """Convert multiple rotation matrices to unit quaternions :cite:`rowenhorst2015consistent`. @@ -995,7 +982,7 @@ def om2qu(om: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def qu2eu_single(qu: np.ndarray) -> np.ndarray: """Convert a unit quaternion to three Euler angles :cite:`rowenhorst2015consistent`. @@ -1050,7 +1037,7 @@ def qu2eu_single(qu: np.ndarray) -> np.ndarray: return np.mod(eu, np.pi * 2) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def qu2eu_2d(qu: np.ndarray) -> np.ndarray: """Convert multiple unit quaternions to Euler angles. @@ -1089,7 +1076,7 @@ def qu2eu(qu: np.ndarray) -> np.ndarray: return eu -@nb.jit("float64[:, :](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:])", cache=True, fastmath=True, nogil=True) def qu2om_single(qu: np.ndarray) -> np.ndarray: """Convert a unit quaternion to an orthogonal rotation matrix :cite:`rowenhorst2015consistent`. @@ -1137,7 +1124,7 @@ def qu2om_single(qu: np.ndarray) -> np.ndarray: return om -@nb.jit("float64[:, :, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def qu2om_2d(qu: np.ndarray) -> np.ndarray: """Convert multiple unit quaternions to orthogonal rotation matrices. @@ -1177,7 +1164,7 @@ def qu2om(qu: np.ndarray) -> np.ndarray: return om -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def qu2ho_single(qu: np.ndarray) -> np.ndarray: """Convert a single (un)normalized quaternion to a normalized homochoric vector :cite:`rowenhorst2015consistent`. @@ -1212,7 +1199,7 @@ def qu2ho_single(qu: np.ndarray) -> np.ndarray: return ho -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def qu2ho_2d(qu: np.ndarray) -> np.ndarray: """Convert multiple (un)normalized quaternions to normalized homochoric vectors :cite:`rowenhorst2015consistent`. From 3671e7f4003afcca4ae69c8b88b17774092d10f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 8 Sep 2024 19:04:04 +0200 Subject: [PATCH 05/28] Fix numba doc link, add orix[all] to installation guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- doc/conf.py | 2 ++ doc/user/installation.rst | 49 +++++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 25676091..1d0e59ee 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -66,8 +66,10 @@ "matplotlib": ("https://matplotlib.org/stable", None), "nbsphinx": ("https://nbsphinx.readthedocs.io/en/latest", None), "nbval": ("https://nbval.readthedocs.io/en/latest", None), + "numba": ("https://numba.readthedocs.io/en/latest", None), "numpy": ("https://numpy.org/doc/stable", None), "numpydoc": ("https://numpydoc.readthedocs.io/en/latest", None), + "pooch": ("https://www.fatiando.org/pooch/latest", None), "pytest": ("https://docs.pytest.org/en/stable", None), "python": ("https://docs.python.org/3", None), "pyxem": ("https://pyxem.readthedocs.io/en/latest", None), diff --git a/doc/user/installation.rst b/doc/user/installation.rst index 509a43d3..17864d4e 100644 --- a/doc/user/installation.rst +++ b/doc/user/installation.rst @@ -13,9 +13,12 @@ With pip ======== orix is availabe from the Python Package Index (PyPI), and can therefore be installed -with `pip `__. To install, run the following:: +with `pip `__. +The software has optional dependencies for some functionality. +See the tables below for the core and optional dependencies. +To install all dependencies, do:: - pip install orix + pip install orix[all] To update orix to the latest release:: @@ -25,7 +28,7 @@ To install a specific version of orix (say version 0.8.1):: pip install orix==0.8.1 -.. _optional-dependencies: +.. _install-with-anaconda: With Anaconda ============= @@ -35,7 +38,7 @@ To install with Anaconda, we recommend you install it in a `conda environment with the `Miniconda distribution `__. To create an environment and activate it, run the following:: - conda create --name orix-env python=3.9 + conda create --name orix-env python=3.11 conda activate orix-env If you prefer a graphical interface to manage packages and environments, you can install @@ -74,4 +77,40 @@ exchanged with ``zip``. See the :ref:`contributing guide ` for how to set up a development installation and keep it up to date. -.. _https://github.com/pyxem/orix/archive/v/orix-.tar.gz: https://github.com/pyxem/orix/archive/v/orix-.tar.gz \ No newline at end of file +.. _https://github.com/pyxem/orix/archive/v/orix-.tar.gz: https://github.com/pyxem/orix/archive/v/orix-.tar.gz + + +.. _dependencies: + +Dependencies +============ + +orix builds on the great work and effort of many people. +This is a list of core package dependencies: + +==================================================== ============================================================ +Package Purpose +==================================================== ============================================================ +:doc:`dask` Out-of-memory processing of data larger than RAM +:doc:`diffpy.structure ` Handling of crystal structures +:doc:`h5py ` Read/write of HDF5 files +:doc:`matplotlib ` Visualization +`matplotlib-scalebar`_ Scale bar for crystal map plots +:doc:`numba ` CPU acceleration +:doc:`numpy ` Handling of N-dimensional arrays +:doc:`pooch ` Downloading and caching of datasets +:doc:`scipy ` Optimization algorithms, filtering and more +`tqdm `__ Progressbars +==================================================== ============================================================ + +.. _matplotlib-scalebar: https://github.com/ppinard/matplotlib-scalebar + +Some functionality requires optional dependencies: + +=================== =========================================== ======================= +Package Purpose Required in module +=================== =========================================== ======================= +`numpy-quaternion`_ Faster quaternion and vector multiplication :mod:`~orix.quaternion` +=================== =========================================== ======================= + +.. _numpy-quaternion: https://quaternion.readthedocs.io/en/stable/ From 67474274777d0096d9bb217473b0eb1f6eb45031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 8 Sep 2024 19:04:29 +0200 Subject: [PATCH 06/28] Remember to check for numpy-quaternion in outer with Dask MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/quaternion.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 43cf4aee..f5ca903f 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -1073,11 +1073,16 @@ def outer( else: da.store(darr, arr) else: - Q1 = quaternion.from_float_array(self.data) - Q2 = quaternion.from_float_array(other.data) - # np.outer works with flattened array - Q = np.outer(Q1, Q2).reshape(Q1.shape + Q2.shape) - arr = quaternion.as_float_array(Q) + if installed["numpy-quaternion"]: + import quaternion + + Q1 = quaternion.from_float_array(self.data) + Q2 = quaternion.from_float_array(other.data) + # np.outer works with flattened array + Q = np.outer(Q1, Q2).reshape(Q1.shape + Q2.shape) + arr = quaternion.as_float_array(Q) + else: + pass return other.__class__(arr) elif isinstance(other, Vector3d): if lazy: @@ -1089,8 +1094,13 @@ def outer( else: da.store(darr, arr) else: - Q = quaternion.from_float_array(self.data) - arr = quaternion.rotate_vectors(Q, other.data) + if installed["numpy-quaternion"]: + import quaternion + + Q = quaternion.from_float_array(self.data) + arr = quaternion.rotate_vectors(Q, other.data) + else: + pass if isinstance(other, Miller): m = other.__class__(xyz=arr, phase=other.phase) m.coordinate_format = other.coordinate_format From 3ced005368e7ff42a1ec002447b6998a08834cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 8 Sep 2024 19:18:27 +0200 Subject: [PATCH 07/28] Allow -1 in shape passed to Object3d.reshape() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orix/_base.py b/orix/_base.py index 508d172e..2a0cdc9a 100644 --- a/orix/_base.py +++ b/orix/_base.py @@ -271,7 +271,7 @@ def reshape(self, *shape: Union[int, tuple]) -> Object3d: if len(shape) == 1 and isinstance(shape[0], tuple): shape = shape[0] obj = self.__class__(self.data.reshape(*shape, self.dim)) - obj._data = self._data.reshape(*shape, -1) + obj._data = self._data.reshape(*shape, self._data.shape[-1]) return obj def transpose(self, *axes: Optional[int]) -> Object3d: From 277d6016ad32840d103975b2aae6e262cec3297b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 8 Sep 2024 19:23:40 +0200 Subject: [PATCH 08/28] Quaternion/quaternion and vector outer product w/o numpy-quaternion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/quaternion.py | 36 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index f5ca903f..a68fd0dd 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -1066,47 +1066,49 @@ def outer( if isinstance(other, Quaternion): if lazy: darr = self._outer_dask(other, chunk_size=chunk_size) - arr = np.empty(darr.shape) + qu = np.empty(darr.shape) if progressbar: with ProgressBar(): - da.store(darr, arr) + da.store(darr, qu) else: - da.store(darr, arr) + da.store(darr, qu) else: if installed["numpy-quaternion"]: import quaternion - Q1 = quaternion.from_float_array(self.data) - Q2 = quaternion.from_float_array(other.data) + qu1 = quaternion.from_float_array(self.data) + qu2 = quaternion.from_float_array(other.data) # np.outer works with flattened array - Q = np.outer(Q1, Q2).reshape(Q1.shape + Q2.shape) - arr = quaternion.as_float_array(Q) + qu12 = np.outer(qu1, qu2).reshape(qu1.shape + qu2.shape) + qu = quaternion.as_float_array(qu12) else: - pass - return other.__class__(arr) + Q12 = self.reshape(-1, 1) * other.reshape(1, -1) + qu = Q12.data.reshape(self.shape + other.shape + (4,)) + return other.__class__(qu) elif isinstance(other, Vector3d): if lazy: darr = self._outer_dask(other, chunk_size=chunk_size) - arr = np.empty(darr.shape) + v_arr = np.empty(darr.shape) if progressbar: with ProgressBar(): - da.store(darr, arr) + da.store(darr, v_arr) else: - da.store(darr, arr) + da.store(darr, v_arr) else: if installed["numpy-quaternion"]: import quaternion - Q = quaternion.from_float_array(self.data) - arr = quaternion.rotate_vectors(Q, other.data) + qu12 = quaternion.from_float_array(self.data) + v_arr = quaternion.rotate_vectors(qu12, other.data) else: - pass + v = self.reshape(-1, 1) * other.reshape(1, -1) + v_arr = v.data.reshape(self.shape + other.shape + (3,)) if isinstance(other, Miller): - m = other.__class__(xyz=arr, phase=other.phase) + m = other.__class__(xyz=v_arr, phase=other.phase) m.coordinate_format = other.coordinate_format return m else: - return other.__class__(arr) + return other.__class__(v_arr) else: raise NotImplementedError( "This operation is currently not avaliable in orix, please use outer " From 4a589c1844ea7959eec415d7bb3996acd4b77999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 8 Sep 2024 19:32:01 +0200 Subject: [PATCH 09/28] Add test run with minimal requirements (no numpy-quaternion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- .github/workflows/build.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 150240bd..5efd4734 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,9 @@ jobs: python-version: 3.8 DEPENDENCIES: diffpy.structure==3.0.2 matplotlib==3.5 LABEL: -oldest + - os: ubuntu-latest + python-version: 3.11 + LABEL: -minimum_requirement steps: - uses: actions/checkout@v4 @@ -58,11 +61,17 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install depedencies and package + - name: Install core depedencies and package shell: bash run: | pip install -U -e .'[doc,tests,coverage]' + - name: Install optional dependencies + if: ${{ !contains(matrix.LABEL, 'minimum_requirement') }} + shell: bash + run: | + pip install -e .'[all]' + - name: Install oldest supported version if: ${{ contains(matrix.LABEL, 'oldest') }} run: | From c3bebd2a0cd2a24f46d80fec31220c70faff70f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 9 Sep 2024 18:03:04 +0200 Subject: [PATCH 10/28] Improve description of optional dependencies in installation guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- doc/user/installation.rst | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/doc/user/installation.rst b/doc/user/installation.rst index 17864d4e..1b234248 100644 --- a/doc/user/installation.rst +++ b/doc/user/installation.rst @@ -14,12 +14,17 @@ With pip orix is availabe from the Python Package Index (PyPI), and can therefore be installed with `pip `__. -The software has optional dependencies for some functionality. -See the tables below for the core and optional dependencies. -To install all dependencies, do:: +To install all of orix' functionality, do:: pip install orix[all] +To install only the strictly required dependencies with limited functionality, do:: + + pip install orix + +See :ref:`dependencies` for the base and optional dependencies and alternatives for how +to install these. + To update orix to the latest release:: pip install --upgrade orix @@ -44,10 +49,17 @@ To create an environment and activate it, run the following:: If you prefer a graphical interface to manage packages and environments, you can install the `Anaconda distribution `__ instead. -To install:: +To install all of orix' functionality, do:: conda install orix --channel conda-forge +To install only the strictly required dependencies with limited functionality, do:: + + conda install orix-base -c conda-forge + +See :ref:`dependencies` for the base and optional dependencies and alternatives for how +to install these. + To update orix to the latest release:: conda update orix @@ -114,3 +126,6 @@ Package Purpose Required in modu =================== =========================================== ======================= .. _numpy-quaternion: https://quaternion.readthedocs.io/en/stable/ + +Optional dependencies can be installed either with `pip install orix[all]` or by +installing each dependency separately, such as `pip install orix numpy-quaternion`. From 21e86bca05152edc82f42b5e5e7802776783d9b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 9 Sep 2024 18:03:21 +0200 Subject: [PATCH 11/28] Add tomllib (Python >= 3.11) as doc dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 38672e25..aa2b2f6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ ] [project.optional-dependencies] -all = [ +all = [ # NB! Update constants.py if this list is updated! "numpy-quaternion", ] doc = [ @@ -58,6 +58,7 @@ doc = [ "sphinx-design", "sphinx-gallery", "sphinxcontrib-bibtex >= 1.0", + "tomllib", ] tests = [ "numpydoc", From 20fc367467f4b5807b9ff191f409a43b4911a752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 9 Sep 2024 18:03:55 +0200 Subject: [PATCH 12/28] Remove use of tomllib for checking availability of optional dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/constants.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/orix/constants.py b/orix/constants.py index c8ceb017..f291e51f 100644 --- a/orix/constants.py +++ b/orix/constants.py @@ -1,22 +1,15 @@ """Constants and such useful across modules.""" from importlib.metadata import version -from pathlib import Path -import tomllib - -# Dependencies -with open(Path(__file__).parent.parent / "pyproject.toml", "rb") as f: - d = tomllib.load(f) - deps = d["project"]["dependencies"] - optional_deps = d["project"]["optional-dependencies"]["all"] - deps += optional_deps +# NB! Update project config file if this list is updated! +optional_deps = ["numpy-quaternion"] installed = {} -for pkg in deps: +for pkg in optional_deps: try: _ = version(pkg) installed[pkg] = True except ImportError: installed[pkg] = False -del version, Path, tomllib, d, deps, optional_deps +del optional_deps From a35d522abac240e88ff65df473be795842a5238c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 9 Sep 2024 18:06:13 +0200 Subject: [PATCH 13/28] Don't install doc dependencies in CI test runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5efd4734..10ce9592 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,7 @@ jobs: - name: Install core depedencies and package shell: bash run: | - pip install -U -e .'[doc,tests,coverage]' + pip install -U -e .'[tests,coverage]' - name: Install optional dependencies if: ${{ !contains(matrix.LABEL, 'minimum_requirement') }} From cd23271ccfc84eecff7893aaee407e48c9270cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 9 Sep 2024 18:22:48 +0200 Subject: [PATCH 14/28] Remove unnecessary tomllib dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- doc/conf.py | 11 ++--------- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 1d0e59ee..b60cc358 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -6,10 +6,8 @@ import inspect import os from os.path import dirname, relpath -from pathlib import Path import re import sys -import tomllib from numpydoc.docscrape_sphinx import SphinxDocString @@ -22,13 +20,8 @@ # sys.path.insert(0, os.path.abspath(".")) sys.path.append("../") -top_dir = Path(__file__).parent.parent - -with open(top_dir / "pyproject.toml", "rb") as f: - metadata = tomllib.load(f) - -project = metadata["project"]["name"] -author = metadata["project"]["authors"][0]["name"] +project = "orix" +author = "orix developers" copyright = f"2018-{str(datetime.now().year)}, {author}" release = orix.__version__ diff --git a/pyproject.toml b/pyproject.toml index aa2b2f6e..28bd6d2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,6 @@ doc = [ "sphinx-design", "sphinx-gallery", "sphinxcontrib-bibtex >= 1.0", - "tomllib", ] tests = [ "numpydoc", From e22bb93515f665840dc8f6a798bb8a26d758d685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 9 Sep 2024 19:17:33 +0200 Subject: [PATCH 15/28] Ensure float64 in Numba functions, wrap Quaternion(self) in outer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/quaternion.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index a68fd0dd..25f1921b 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -183,8 +183,9 @@ def conj(self) -> Quaternion: qu2 = quaternion.from_float_array(self.data).conj() qu2 = quaternion.as_float_array(qu2) else: - qu2 = np.empty_like(self.data) - qu_conj_gufunc(self.data, qu2) + qu1 = self.data.astype(np.float64) + qu2 = np.empty_like(qu1) + qu_conj_gufunc(qu1, qu2) Q = self.__class__(qu2) return Q @@ -1079,11 +1080,11 @@ def outer( qu1 = quaternion.from_float_array(self.data) qu2 = quaternion.from_float_array(other.data) # np.outer works with flattened array - qu12 = np.outer(qu1, qu2).reshape(qu1.shape + qu2.shape) + qu12 = np.outer(qu1, qu2).reshape(*qu1.shape, *qu2.shape) qu = quaternion.as_float_array(qu12) else: - Q12 = self.reshape(-1, 1) * other.reshape(1, -1) - qu = Q12.data.reshape(self.shape + other.shape + (4,)) + Q12 = Quaternion(self).reshape(-1, 1) * other.reshape(1, -1) + qu = Q12.data.reshape(*self.shape, *other.shape, 4) return other.__class__(qu) elif isinstance(other, Vector3d): if lazy: @@ -1098,11 +1099,11 @@ def outer( if installed["numpy-quaternion"]: import quaternion - qu12 = quaternion.from_float_array(self.data) - v_arr = quaternion.rotate_vectors(qu12, other.data) + qu = quaternion.from_float_array(self.data) + v_arr = quaternion.rotate_vectors(qu, other.data) else: - v = self.reshape(-1, 1) * other.reshape(1, -1) - v_arr = v.data.reshape(self.shape + other.shape + (3,)) + v = Quaternion(self).reshape(-1, 1) * other.reshape(1, -1) + v_arr = v.reshape(*self.shape, *other.shape).data if isinstance(other, Miller): m = other.__class__(xyz=v_arr, phase=other.phase) m.coordinate_format = other.coordinate_format @@ -1259,6 +1260,10 @@ def qu_multiply_gufunc( def qu_multiply(qu1: np.ndarray, qu2: np.ndarray) -> np.ndarray: shape = np.broadcast_shapes(qu1.shape, qu2.shape) + if not np.issubdtype(qu1.dtype, np.float64): + qu1 = qu1.astype(np.float64) + if not np.issubdtype(qu2.dtype, np.float64): + qu2 = qu2.astype(np.float64) qu12 = np.empty(shape, dtype=np.float64) qu_multiply_gufunc(qu1, qu2, qu12) return qu12 @@ -1279,8 +1284,13 @@ def qu_rotate_vec_gufunc( def qu_rotate_vec(qu: np.ndarray, v: np.ndarray) -> np.ndarray: - qu, v = np.atleast_2d(qu, v) + qu = np.atleast_2d(qu) + v = np.atleast_2d(v) shape = np.broadcast_shapes(qu.shape[:-1], v.shape[:-1]) + (3,) + if not np.issubdtype(qu.dtype, np.float64): + qu = qu.astype(np.float64) + if not np.issubdtype(v.dtype, np.float64): + v = v.astype(np.float64) v2 = np.empty(shape, dtype=np.float64) qu_rotate_vec_gufunc(qu, v, v2) return v2 From b23968e3f33cf93c2ccaf6bfc52d8cf0b1fe681d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 9 Sep 2024 19:18:52 +0200 Subject: [PATCH 16/28] Dependency on typing_extensions (remove once >=3.11 is min.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 28bd6d2f..c91bfd4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,8 @@ dependencies = [ "pycifrw", "scipy", "tqdm", + # TODO: Remove once Python 3.11 is the minimal version + "typing_extensions", ] [project.optional-dependencies] From ce736d864397d7dd62fa07e9437072a88876d300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 9 Sep 2024 19:27:41 +0200 Subject: [PATCH 17/28] Use old version of quaternion-vector multiplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/quaternion.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 25f1921b..c2cd7bcb 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -71,9 +71,9 @@ class Quaternion(Object3d): .. math:: - v'_x = x + 2a(cz - dy) - 2d(dx - bz) + 2c(by - cx)\\ - v'_y = y + 2d(cz - dy) + 2a(dx - bz) - 2b(by - cx)\\ - v'_z = z - 2c(cz - dy) + 2b(dx - bz) + 2a(by - cx) + v'_x = x(a^2 + b^2 - c^2 - d^2) + 2z(a * c + b * d) + y(b * c - a * d)\\ + v'_y = y(a^2 - b^2 + c^2 - d^2) + 2x(a * d + b * c) + z(c * d - a * b)\\ + v'_z = z(a^2 - b^2 - c^2 + d^2) + 2y(a * b + c * d) + x(b * d - a * c) The norm of a quaternion is defined as @@ -1273,14 +1273,19 @@ def qu_multiply(qu1: np.ndarray, qu2: np.ndarray) -> np.ndarray: def qu_rotate_vec_gufunc( qu: np.ndarray, v1: np.ndarray, v2: np.ndarray ) -> None: # pragma: no cover - a, b, c, d = qu - x, y, z = v1 - tx = 2 * (c * z - d * y) - ty = 2 * (d * x - b * z) - tz = 2 * (b * y - c * x) - v2[0] = x + a * tx - d * ty + c * tz - v2[1] = y + d * tx + a * ty - b * tz - v2[2] = z - c * tx + b * ty + a * tz + aa = qu[0] * qu[0] + ab = qu[0] * qu[1] + ac = qu[0] * qu[2] + ad = qu[0] * qu[3] + bb = qu[1] * qu[1] + bc = qu[1] * qu[2] + bd = qu[1] * qu[3] + cc = qu[2] * qu[2] + cd = qu[2] * qu[3] + dd = qu[3] * qu[3] + v2[0] = v1[0] * (aa + bb - cc - dd) + 2 * v1[2] * (ac + bd) + v1[1] * (bc - ad) + v2[1] = v1[1] * (aa - bb + cc - dd) + 2 * v1[0] * (ad + bc) + v1[2] * (cd - ab) + v2[2] = v1[2] * (aa - bb - cc + dd) + 2 * v1[1] * (ab + cd) + v1[0] * (bd - ac) def qu_rotate_vec(qu: np.ndarray, v: np.ndarray) -> np.ndarray: From df5793682eedbcb0280498dc6f8ffd2fffef78e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 9 Sep 2024 19:37:13 +0200 Subject: [PATCH 18/28] Use new formula for quaternion-vector multiplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/quaternion.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index c2cd7bcb..25f1921b 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -71,9 +71,9 @@ class Quaternion(Object3d): .. math:: - v'_x = x(a^2 + b^2 - c^2 - d^2) + 2z(a * c + b * d) + y(b * c - a * d)\\ - v'_y = y(a^2 - b^2 + c^2 - d^2) + 2x(a * d + b * c) + z(c * d - a * b)\\ - v'_z = z(a^2 - b^2 - c^2 + d^2) + 2y(a * b + c * d) + x(b * d - a * c) + v'_x = x + 2a(cz - dy) - 2d(dx - bz) + 2c(by - cx)\\ + v'_y = y + 2d(cz - dy) + 2a(dx - bz) - 2b(by - cx)\\ + v'_z = z - 2c(cz - dy) + 2b(dx - bz) + 2a(by - cx) The norm of a quaternion is defined as @@ -1273,19 +1273,14 @@ def qu_multiply(qu1: np.ndarray, qu2: np.ndarray) -> np.ndarray: def qu_rotate_vec_gufunc( qu: np.ndarray, v1: np.ndarray, v2: np.ndarray ) -> None: # pragma: no cover - aa = qu[0] * qu[0] - ab = qu[0] * qu[1] - ac = qu[0] * qu[2] - ad = qu[0] * qu[3] - bb = qu[1] * qu[1] - bc = qu[1] * qu[2] - bd = qu[1] * qu[3] - cc = qu[2] * qu[2] - cd = qu[2] * qu[3] - dd = qu[3] * qu[3] - v2[0] = v1[0] * (aa + bb - cc - dd) + 2 * v1[2] * (ac + bd) + v1[1] * (bc - ad) - v2[1] = v1[1] * (aa - bb + cc - dd) + 2 * v1[0] * (ad + bc) + v1[2] * (cd - ab) - v2[2] = v1[2] * (aa - bb - cc + dd) + 2 * v1[1] * (ab + cd) + v1[0] * (bd - ac) + a, b, c, d = qu + x, y, z = v1 + tx = 2 * (c * z - d * y) + ty = 2 * (d * x - b * z) + tz = 2 * (b * y - c * x) + v2[0] = x + a * tx - d * ty + c * tz + v2[1] = y + d * tx + a * ty - b * tz + v2[2] = z - c * tx + b * ty + a * tz def qu_rotate_vec(qu: np.ndarray, v: np.ndarray) -> np.ndarray: From f0ea1f3ea748b828198af83ef3dcbf87d6cad6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Wed, 11 Sep 2024 20:14:47 +0200 Subject: [PATCH 19/28] Improve tests for 0 in Vector3d.perpendicular MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/vector/vector3d.py | 70 ++++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py index ea62b36d..fa2e9836 100644 --- a/orix/vector/vector3d.py +++ b/orix/vector/vector3d.py @@ -27,6 +27,7 @@ import matplotlib.pyplot as plt import numpy as np +from orix import constants from orix._base import Object3d @@ -159,9 +160,21 @@ def _tuples(self) -> Set: @property def perpendicular(self) -> Vector3d: - """Return the perpendicular vectors.""" - if np.any(self.x == 0) and np.any(self.y == 0): - if np.any(self.z == 0): + r"""Return the perpendicular vectors. + + Notes + ----- + The following convention is used: + + .. math:: + + (x, y, z) &\rightarrow (-y, x, 0),\\ + (0, 0, z) &\rightarrow (1, 0, 0). + """ + if np.any(abs(self.x) < constants.eps12) and np.any( + abs(self.y) < constants.eps12 + ): + if np.any(abs(self.z) < constants.eps12): raise ValueError("No vectors are perpendicular") return Vector3d.xvector() x = -self.y @@ -432,7 +445,7 @@ def from_path_ends( satisfy these two conditions .. math:: - (v_1 \times v_i) \cdot (v_1 \times v_2) \geq 0, + (v_1 \times v_i) \cdot (v_1 \times v_2) \geq 0,\\ (v_2 \times v_i) \cdot (v_2 \times v_1) \geq 0. """ v = Vector3d(vectors).flatten() @@ -448,8 +461,8 @@ def from_path_ends( v_normal = v1.cross(v2) v_circle = v_normal.get_circle(steps=steps) - cond1 = v1.cross(v_circle).dot(v1.cross(v2)) >= 0 - cond2 = v2.cross(v_circle).dot(v2.cross(v1)) >= 0 + cond1 = v1.cross(v_circle).dot(v_normal) >= 0 + cond2 = v2.cross(v_circle).dot(-v_normal) >= 0 v_path = v_circle[cond1 & cond2] @@ -466,31 +479,43 @@ def from_path_ends( # --------------------- Other public methods --------------------- # def dot(self, other: Vector3d) -> np.ndarray: - """Return the dot products of the vectors and the other vectors. + r"""Return the dot products :math:`D` of the vectors. Parameters ---------- other - Other vectors with a compatible shape. + Other vectors. Shapes must be broadcastable. Returns ------- - dot_products + D Dot products. + Notes + ----- + The dot product :math:`D` between :math:`\mathbf{v_1}` and + :math:`\mathbf{v_2}` is given by + + .. math:: + + D = \mathbf{v_1}\cdot\mathbf{v_2} = |\mathbf{v_1}|\:|\mathbf{v_2}|\cos{\omega}, + + where :math:`\omega` is the angle between the two vectors. + Examples -------- >>> from orix.vector import Vector3d - >>> v = Vector3d((0, 0, 1.0)) - >>> w = Vector3d(((0, 0, 0.5), (0.4, 0.6, 0))) - >>> v.dot(w) + >>> v1 = Vector3d([0, 0, 1]) + >>> v2 = Vector3d([[0, 0, 0.5], [0.4, 0.6, 0]]) + >>> v1.dot(v2) array([0.5, 0. ]) - >>> w.dot(v) + >>> v2.dot(v1) array([0.5, 0. ]) """ if not isinstance(other, Vector3d): - raise ValueError("{} is not a vector!".format(other)) - return np.sum(self.data * other.data, axis=-1) + raise ValueError(f"{other} is not a vector") + D = np.sum(self.data * other.data, axis=-1) + return D def dot_outer( self, @@ -596,8 +621,8 @@ def angle_with(self, other: Vector3d, degrees: bool = False) -> np.ndarray: def rotate( self, - axis: Union[np.ndarray, Vector3d] = None, - angle: Union[List[float], float, np.np.ndarray] = 0, + axis: Union[np.ndarray, Vector3d, None] = None, + angle: Union[List[float], float, np.ndarray] = 0, ) -> Vector3d: """Convenience function for rotating this vector. @@ -824,11 +849,18 @@ def get_circle( vector to the current vector at ``opening_angle`` and (2) about the current vector in a full circle. """ + from orix.quaternion.rotation import Rotation + circles = self.zero((self.size, steps)) full_circle = np.linspace(0, 2 * np.pi, num=steps) opening_angles = np.ones(self.size) * opening_angle - for i, (v, oa) in enumerate(zip(self.flatten(), opening_angles)): - circles[i] = v.rotate(v.perpendicular, oa).rotate(v, full_circle) + v = self.flatten() + for i in range(v.size): + vi = v[i] + R1 = Rotation.from_axes_angles(vi.perpendicular, opening_angles[i]) + R2 = Rotation.from_axes_angles(vi, full_circle) + circles[i] = R2 * R1 * vi + return circles def inverse_pole_density_function( From 1beb3d4adf2ebcef86716799c726e36f5288dccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Wed, 11 Sep 2024 20:15:20 +0200 Subject: [PATCH 20/28] Improve syntax in Quaternion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/quaternion.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 25f1921b..26259626 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -201,12 +201,12 @@ def __mul__( if installed["numpy-quaternion"]: import quaternion - Q1 = quaternion.from_float_array(self.data) - Q2 = quaternion.from_float_array(other.data) - qu2 = quaternion.as_float_array(Q1 * Q2) + qu1 = quaternion.from_float_array(self.data) + qu2 = quaternion.from_float_array(other.data) + qu12 = quaternion.as_float_array(qu1 * qu2) else: - qu2 = qu_multiply(self.data, other.data) - Q = self.__class__(qu2) + qu12 = qu_multiply(self.data, other.data) + Q = self.__class__(qu12) return Q elif isinstance(other, Vector3d): if installed["numpy-quaternion"]: @@ -215,9 +215,9 @@ def __mul__( # Don't use rotate_vectors as it may perform an outer # product. The following keeps current __mul__ broadcast # behavior. - Q1 = quaternion.from_float_array(self.data) + qu = quaternion.from_float_array(self.data) v = quaternion.as_vector_part( - (Q1 * quaternion.from_vector_part(other.data)) * ~Q1 + (qu * quaternion.from_vector_part(other.data)) * ~qu ) else: v = qu_rotate_vec(self.unit.data, other.data) @@ -1244,8 +1244,9 @@ def _outer_dask( @nb.guvectorize("(n)->(n)", cache=True) def qu_conj_gufunc(qu: np.ndarray, qu2: np.ndarray) -> None: # pragma: no cover qu2[0] = qu[0] - for i in range(3): - qu2[i + 1] = -1.0 * qu[i + 1] + qu2[1] = -qu[1] + qu2[2] = -qu[2] + qu2[3] = -qu[3] @nb.guvectorize("(n),(n)->(n)", cache=True) From 3ab493238c33b0fd60ff7eb0402e63fee219467b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Wed, 11 Sep 2024 20:15:41 +0200 Subject: [PATCH 21/28] Use small numbers from new constants module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/constants.py | 8 ++++++-- orix/quaternion/_conversions.py | 22 +++++++++++----------- orix/quaternion/orientation_region.py | 11 +++++------ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/orix/constants.py b/orix/constants.py index f291e51f..7b861c40 100644 --- a/orix/constants.py +++ b/orix/constants.py @@ -3,8 +3,8 @@ from importlib.metadata import version # NB! Update project config file if this list is updated! -optional_deps = ["numpy-quaternion"] -installed = {} +optional_deps: list[str] = ["numpy-quaternion"] +installed: dict[str, bool] = {} for pkg in optional_deps: try: _ = version(pkg) @@ -12,4 +12,8 @@ except ImportError: installed[pkg] = False +# Tolerances +eps9 = 1e-9 +eps12 = 1e-12 + del optional_deps diff --git a/orix/quaternion/_conversions.py b/orix/quaternion/_conversions.py index 4a4e4069..7d891255 100644 --- a/orix/quaternion/_conversions.py +++ b/orix/quaternion/_conversions.py @@ -25,7 +25,7 @@ import numba as nb import numpy as np -FLOAT_EPS = np.finfo(float).eps +from orix import constants @nb.njit("int64(float64[:])", cache=True, fastmath=True, nogil=True) @@ -593,10 +593,10 @@ def qu2ax_single(qu: np.ndarray) -> np.ndarray: """ omega = 2 * np.arccos(qu[0]) - if omega < FLOAT_EPS: + if omega < constants.eps9: return np.array([0, 0, 1, 0], dtype=np.float64) - if np.abs(qu[0]) < FLOAT_EPS: + if np.abs(qu[0]) < constants.eps9: return np.array([qu[1], qu[2], qu[3], np.pi], dtype=np.float64) s = np.sqrt(np.sum(np.square(qu[1:]))) @@ -910,26 +910,26 @@ def om2qu_single(om: np.ndarray) -> np.ndarray: qu = np.zeros(4, dtype=np.float64) - if a_almost < FLOAT_EPS: + if a_almost < constants.eps9: qu[0] = 0 else: qu[0] = 0.5 * np.sqrt(a_almost) - if b_almost < FLOAT_EPS: + if b_almost < constants.eps9: qu[1] = 0 elif om[2, 1] < om[1, 2]: qu[1] = -0.5 * np.sqrt(b_almost) else: qu[1] = 0.5 * np.sqrt(b_almost) - if c_almost < FLOAT_EPS: + if c_almost < constants.eps9: qu[2] = 0 elif om[0, 2] < om[2, 0]: qu[2] = -0.5 * np.sqrt(c_almost) else: qu[2] = 0.5 * np.sqrt(c_almost) - if d_almost < FLOAT_EPS: + if d_almost < constants.eps9: qu[3] = 0 elif om[1, 0] < om[0, 1]: qu[3] = -0.5 * np.sqrt(d_almost) @@ -1012,8 +1012,8 @@ def qu2eu_single(qu: np.ndarray) -> np.ndarray: q_bc = (qu[1] * qu[1]) + (qu[2] * qu[2]) chi = np.sqrt(q_ad * q_bc) - if chi < FLOAT_EPS: - if q_bc < FLOAT_EPS: + if chi < constants.eps9: + if q_bc < constants.eps9: a = -2 * qu[0] * qu[3] b = qu[0] * qu[0] - qu[3] * qu[3] else: @@ -1032,7 +1032,7 @@ def qu2eu_single(qu: np.ndarray) -> np.ndarray: eu[1] = np.arctan2(2 * chi, q_ad - q_bc) eu[2] = np.arctan2(eu_2a, eu_2b) - eu[np.abs(eu) < FLOAT_EPS] = 0 + eu[np.abs(eu) < constants.eps9] = 0 return np.mod(eu, np.pi * 2) @@ -1188,7 +1188,7 @@ def qu2ho_single(qu: np.ndarray) -> np.ndarray: """ omega = 2 * np.arccos(qu[0]) - if omega < FLOAT_EPS: + if omega < constants.eps9: return np.zeros(3, dtype=np.float64) s = np.sqrt(np.sum(np.square(qu[1:]))) diff --git a/orix/quaternion/orientation_region.py b/orix/quaternion/orientation_region.py index 988105d4..0cde8341 100644 --- a/orix/quaternion/orientation_region.py +++ b/orix/quaternion/orientation_region.py @@ -23,13 +23,12 @@ import numpy as np +from orix import constants from orix.quaternion import Quaternion from orix.quaternion.rotation import Rotation from orix.quaternion.symmetry import C1, Symmetry, get_distinguished_points from orix.vector import Rodrigues -_FLOAT_EPS = 1e-9 # Small number to avoid round off problems - def _get_large_cell_normals(s1, s2): dp = get_distinguished_points(s1, s2) @@ -133,8 +132,8 @@ def __gt__(self, other: OrientationRegion) -> np.ndarray: """ c = Quaternion(self).dot_outer(Quaternion(other)) inside = np.logical_or( - np.all(np.greater_equal(c, -_FLOAT_EPS), axis=0), - np.all(np.less_equal(c, +_FLOAT_EPS), axis=0), + np.all(np.greater_equal(c, -constants.eps9), axis=0), + np.all(np.less_equal(c, constants.eps9), axis=0), ) return inside @@ -204,8 +203,8 @@ def get_plot_data(self) -> Rotation: from orix.vector import Vector3d # Get a grid of vector directions - theta = np.linspace(0, 2 * np.pi - _FLOAT_EPS, 361) - rho = np.linspace(0, np.pi - _FLOAT_EPS, 181) + theta = np.linspace(0, 2 * np.pi - constants.eps9, 361) + rho = np.linspace(0, np.pi - constants.eps9, 181) theta, rho = np.meshgrid(theta, rho) g = Vector3d.from_polar(rho, theta) From dfae2a5578752657e93a1170ed1a160348ffa45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Wed, 11 Sep 2024 20:17:01 +0200 Subject: [PATCH 22/28] Don't re-run stale examples, change doc dir from build to _build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- .gitignore | 2 +- doc/Makefile | 2 +- doc/conf.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 244b1526..0f6d691e 100644 --- a/.gitignore +++ b/.gitignore @@ -63,7 +63,7 @@ instance/ .scrapy # Sphinx documentation -doc/build/ +_build/ doc/examples/ doc/reference/generated/ doc/source/_autosummary/ diff --git a/doc/Makefile b/doc/Makefile index ab377f87..78a7cba8 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -6,7 +6,7 @@ SPHINXOPTS = SPHINXBUILD = PYDEVD_DISABLE_FILE_VALIDATION=1 python -Xfrozen_modules=off -m sphinx SOURCEDIR = . -BUILDDIR = build +BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: diff --git a/doc/conf.py b/doc/conf.py index b60cc358..2b81c2d3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -80,7 +80,7 @@ # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ - "build", + "_build", "Thumbs.db", ".DS_Store", # Suppress warnings from Sphinx regarding "duplicate source files": @@ -331,7 +331,7 @@ def _str_examples(self): "filename_pattern": "^((?!sgskip).)*$", "gallery_dirs": "examples", "reference_url": {"orix": None}, - "run_stale_examples": True, + "run_stale_examples": False, "show_memory": True, } autosummary_generate = True From e079b89d461173eaa61b7e0bd518810b9e6a5e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Wed, 11 Sep 2024 20:17:20 +0200 Subject: [PATCH 23/28] Touch up reST syntax in installation guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- doc/user/installation.rst | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/user/installation.rst b/doc/user/installation.rst index 1b234248..04477174 100644 --- a/doc/user/installation.rst +++ b/doc/user/installation.rst @@ -100,32 +100,32 @@ Dependencies orix builds on the great work and effort of many people. This is a list of core package dependencies: -==================================================== ============================================================ -Package Purpose -==================================================== ============================================================ -:doc:`dask` Out-of-memory processing of data larger than RAM -:doc:`diffpy.structure ` Handling of crystal structures -:doc:`h5py ` Read/write of HDF5 files -:doc:`matplotlib ` Visualization -`matplotlib-scalebar`_ Scale bar for crystal map plots -:doc:`numba ` CPU acceleration -:doc:`numpy ` Handling of N-dimensional arrays -:doc:`pooch ` Downloading and caching of datasets -:doc:`scipy ` Optimization algorithms, filtering and more -`tqdm `__ Progressbars -==================================================== ============================================================ +================================================ ================================================ +Package Purpose +================================================ ================================================ +:doc:`dask` Out-of-memory processing of data larger than RAM +:doc:`diffpy.structure ` Handling of crystal structures +:doc:`h5py ` Read/write of HDF5 files +:doc:`matplotlib ` Visualization +`matplotlib-scalebar`_ Scale bar for crystal map plots +:doc:`numba ` CPU acceleration +:doc:`numpy ` Handling of N-dimensional arrays +:doc:`pooch ` Downloading and caching of datasets +:doc:`scipy ` Optimization algorithms, filtering and more +`tqdm `__ Progressbars +================================================ ================================================ .. _matplotlib-scalebar: https://github.com/ppinard/matplotlib-scalebar Some functionality requires optional dependencies: -=================== =========================================== ======================= -Package Purpose Required in module -=================== =========================================== ======================= -`numpy-quaternion`_ Faster quaternion and vector multiplication :mod:`~orix.quaternion` -=================== =========================================== ======================= +=================== =========================================== +Package Purpose +=================== =========================================== +`numpy-quaternion`_ Faster quaternion and vector multiplication +=================== =========================================== .. _numpy-quaternion: https://quaternion.readthedocs.io/en/stable/ -Optional dependencies can be installed either with `pip install orix[all]` or by -installing each dependency separately, such as `pip install orix numpy-quaternion`. +Optional dependencies can be installed either with ``pip install orix[all]`` or by +installing each dependency separately, such as ``pip install orix numpy-quaternion``. From 803b2a1aa6936c115a02385275a32d9c1d6c86a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Wed, 11 Sep 2024 20:28:47 +0200 Subject: [PATCH 24/28] Update test affected by improved Vector3d.perpendicular MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/tests/sampling/test_sampling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orix/tests/sampling/test_sampling.py b/orix/tests/sampling/test_sampling.py index 770827a5..9189c619 100644 --- a/orix/tests/sampling/test_sampling.py +++ b/orix/tests/sampling/test_sampling.py @@ -204,7 +204,7 @@ def test_get_sample_reduced_fundamental(self): # Some rotations have a phi1 Euler angle of multiples of pi, # presumably due to rounding errors phi1_C1 = R_C1.to_euler()[:, 0].round(7) - assert np.allclose(np.unique(phi1_C1), [0, 2 * np.pi], atol=1e-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) phi1_C6 = R_C6.to_euler()[:, 0].round(7) From d247b22f5d6c39425eecdd70fbb02fc2715b2415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 15 Sep 2024 19:18:09 +0200 Subject: [PATCH 25/28] In-line comments on Numba and generalized universal functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/quaternion.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 26259626..b818d538 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -1241,6 +1241,20 @@ def _outer_dask( return out.rechunk(new_chunks) +# ------------------- Numba accelerated functions -------------------- # +# Functions with Numba decorators are compiled to machine code at run +# time (just-in-time) and cached for later calls. +# +# Some functions are generalized universal functions (gufuncs), +# https://numba.readthedocs.io/en/stable/user/vectorize.html. +# Array shapes are determined from signatures such as (n)->(n), meaning +# the input and output arrays both have single dimensions of size n. +# The final input parameter (array) is overwritten inside the function, +# with no return. +# Ensure float64 to avoid surprising errors (some occured during +# testing). + + @nb.guvectorize("(n)->(n)", cache=True) def qu_conj_gufunc(qu: np.ndarray, qu2: np.ndarray) -> None: # pragma: no cover qu2[0] = qu[0] From e7dec7d5c901c7264528a7115370938b6d6017aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 15 Sep 2024 19:48:20 +0200 Subject: [PATCH 26/28] Correct spelling orix' -> orix's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- RELEASE.rst | 2 +- doc/user/installation.rst | 4 ++-- examples/plotting/subplots.py | 2 +- orix/io/plugins/orix_hdf5.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/RELEASE.rst b/RELEASE.rst index 81760ff6..b02b5b66 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,7 +1,7 @@ How to make a new release of ``orix`` ===================================== -After version 0.9.0, orix' branching model changed to one similar to the Gitflow +After version 0.9.0, orix's branching model changed to one similar to the Gitflow Workflow (`original blog post `__). diff --git a/doc/user/installation.rst b/doc/user/installation.rst index 04477174..c82f846f 100644 --- a/doc/user/installation.rst +++ b/doc/user/installation.rst @@ -14,7 +14,7 @@ With pip orix is availabe from the Python Package Index (PyPI), and can therefore be installed with `pip `__. -To install all of orix' functionality, do:: +To install all of orix's functionality, do:: pip install orix[all] @@ -49,7 +49,7 @@ To create an environment and activate it, run the following:: If you prefer a graphical interface to manage packages and environments, you can install the `Anaconda distribution `__ instead. -To install all of orix' functionality, do:: +To install all of orix's functionality, do:: conda install orix --channel conda-forge diff --git a/examples/plotting/subplots.py b/examples/plotting/subplots.py index af64dddf..96a0bd4b 100644 --- a/examples/plotting/subplots.py +++ b/examples/plotting/subplots.py @@ -3,7 +3,7 @@ Subplots ======== -This example shows how to place different plots in the same figure using orix' various +This example shows how to place different plots in the same figure using orix's various :mod:`plot types `, which extend Matplotlib's plot types. By first creating a blank figure and then using diff --git a/orix/io/plugins/orix_hdf5.py b/orix/io/plugins/orix_hdf5.py index b2877112..90fca855 100644 --- a/orix/io/plugins/orix_hdf5.py +++ b/orix/io/plugins/orix_hdf5.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . -"""Reader and writer of a crystal map to and from orix' own HDF5 file +"""Reader and writer of a crystal map to and from orix's own HDF5 file format. """ @@ -44,7 +44,7 @@ def file_reader(filename: str, **kwargs) -> CrystalMap: - """Return a crystal map from a file in orix' HDF5 file format. + """Return a crystal map from a file in orix's HDF5 file format. Parameters ---------- From c30e891942a0ea7af2fb3308b8be3dc7302299eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 15 Sep 2024 19:51:13 +0200 Subject: [PATCH 27/28] Describe use of precision tolerances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/constants.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/orix/constants.py b/orix/constants.py index 7b861c40..40c0b1a7 100644 --- a/orix/constants.py +++ b/orix/constants.py @@ -1,3 +1,20 @@ +# Copyright 2018-2024 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 . + """Constants and such useful across modules.""" from importlib.metadata import version @@ -12,7 +29,9 @@ except ImportError: installed[pkg] = False -# Tolerances +# Typical tolerances for comparisons in need of a precision. We +# generally use the highest precision possible (allowed by testing on +# different OS and Python versions). eps9 = 1e-9 eps12 = 1e-12 From bc23f087a8768ab02e912181b3f2fbfccd629f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 15 Sep 2024 19:52:53 +0200 Subject: [PATCH 28/28] Improve quaternion tests, add custom skip decorators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/tests/conftest.py | 32 ++++++++++++++++++------ orix/tests/quaternion/test_quaternion.py | 28 +++++++++++++-------- orix/tests/test_constants.py | 30 ++++++++++++++++++++++ 3 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 orix/tests/test_constants.py diff --git a/orix/tests/conftest.py b/orix/tests/conftest.py index da5b7a40..489bb7fb 100644 --- a/orix/tests/conftest.py +++ b/orix/tests/conftest.py @@ -25,23 +25,26 @@ import numpy as np import pytest +from orix import constants from orix.crystal_map import CrystalMap, PhaseList, create_coordinate_arrays from orix.quaternion import Rotation +# --------------------------- pytest hooks --------------------------- # + def pytest_sessionstart(session): # pragma: no cover plt.rcParams["backend"] = "agg" -@pytest.fixture -def rotations(): - return Rotation([(2, 4, 6, 8), (-1, -3, -5, -7)]) - +# -------------------- Control of test selection --------------------- # -@pytest.fixture() -def eu(): - return np.random.rand(10, 3) +skipif_numpy_quaternion_present = pytest.mark.skipif( + constants.installed["numpy-quaternion"], reason="numpy-quaternion installed" +) +skipif_numpy_quaternion_missing = pytest.mark.skipif( + not constants.installed["numpy-quaternion"], reason="numpy-quaternion not installed" +) # ---------------------------- IO fixtures --------------------------- # @@ -1164,7 +1167,7 @@ def crystal_map(crystal_map_input): # ---------- Rotation representations for conversion tests ----------- # -# NOTE to future test writers on unittest data: +# NOTE: to future test writers on unittest data: # All the data below can be recreated using 3Drotations, which is # available at # https://github.com/marcdegraef/3Drotations/blob/master/src/python. @@ -1372,6 +1375,19 @@ def euler_angles(): # ------- End of rotation representations for conversion tests ------- # +# ------------------------ Geometry fixtures ------------------------- # + + +@pytest.fixture +def rotations(): + return Rotation([(2, 4, 6, 8), (-1, -3, -5, -7)]) + + +@pytest.fixture() +def eu(): + return np.random.rand(10, 3) + + @pytest.fixture(autouse=True) def import_to_namespace(doctest_namespace): """Make :mod:`numpy` and :mod:`matplotlib.pyplot` available in diff --git a/orix/tests/quaternion/test_quaternion.py b/orix/tests/quaternion/test_quaternion.py index e3959164..5137b374 100644 --- a/orix/tests/quaternion/test_quaternion.py +++ b/orix/tests/quaternion/test_quaternion.py @@ -48,11 +48,6 @@ def quaternion(request): return Quaternion(request.param) -@pytest.fixture -def identity(): - return Quaternion((1, 0, 0, 0)) - - @pytest.fixture( params=[ (0.881, 0.665, 0.123, 0.517), @@ -105,8 +100,8 @@ def test_mul(self, quaternion, something): assert np.allclose(q1.c, qa * sc - qb * sd + qc * sa + qd * sb) assert np.allclose(q1.d, qa * sd + qb * sc - qc * sb + qd * sa) - def test_mul_identity(self, quaternion, identity): - assert np.allclose((quaternion * identity).data, quaternion.data) + def test_mul_identity(self, quaternion): + assert np.allclose((quaternion * Quaternion.identity()).data, quaternion.data) def test_no_multiplicative_inverse(self, quaternion, something): q1 = quaternion * something @@ -162,10 +157,21 @@ def test_dot_outer(self, quaternion, something): ], ) def test_multiply_vector(self, quaternion, vector, expected): - q = Quaternion(quaternion) - v = Vector3d(vector) - v_new = q * v - assert np.allclose(v_new.data, expected) + Q1 = Quaternion(quaternion) + v1 = Vector3d(vector) + v2 = Q1 * v1 + assert np.allclose(v2.data, expected) + + def test_multiply_vector_float32(self): + Q1 = Quaternion.random() + v1 = Vector3d.random() + + Q2 = Quaternion(Q1) + Q2._data = Q2._data.astype(np.float32) + + v2 = Q1 * v1 + v3 = Q2 * v1 + assert np.allclose(v3.data, v2.data, atol=1e-6) def test_abcd_properties(self): quat = Quaternion([2, 2, 2, 2]) diff --git a/orix/tests/test_constants.py b/orix/tests/test_constants.py new file mode 100644 index 00000000..aa171ecc --- /dev/null +++ b/orix/tests/test_constants.py @@ -0,0 +1,30 @@ +# Copyright 2018-2024 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 . + +from orix import constants + +from .conftest import skipif_numpy_quaternion_missing, skipif_numpy_quaternion_present + + +class TestConstants: + @skipif_numpy_quaternion_present + def test_numpy_quaternion_not_installed(self): + assert not constants.installed["numpy-quaternion"] + + @skipif_numpy_quaternion_missing + def test_numpy_quaternion_installed(self): + assert constants.installed["numpy-quaternion"]