From f0488d0085371e36f4718e77d2c047ece34dbc9c Mon Sep 17 00:00:00 2001 From: Austin Gerlt <83073845+argerlt@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:51:03 -0600 Subject: [PATCH 1/8] add Miller __neg__, __add__, and __sub__ --- orix/vector/miller.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/orix/vector/miller.py b/orix/vector/miller.py index b6a59c54..f53ff577 100644 --- a/orix/vector/miller.py +++ b/orix/vector/miller.py @@ -366,6 +366,51 @@ def __getitem__(self, key) -> Miller: m.coordinate_format = self.coordinate_format return m + def __neg__(self) -> Vector3d: + out = self.__class__(-self.data,phase=self.phase) + out.coordinate_format = self.coordinate_format + return out + + def __add__( + self, other: int | float | list | tuple | np.ndarray | Vector3d | Miller + ) -> Miller: + data = None + if isinstance(other, Vector3d): + data =self.data + other.data + elif isinstance(other, (int, float)): + data =self.data + other + elif isinstance(other, (list, tuple)): + other = np.array(other) + if isinstance(other, np.ndarray): + data = self.data - other[..., np.newaxis] + + if data is None: + return NotImplemented + + out = self.__class__(data, self.phase) + out.coordinate_format = self.coordinate_format + return out + + def __sub__( + self, other: int | float | list | tuple | np.ndarray | Vector3d | Miller + ) -> Miller: + data = None + if isinstance(other, Vector3d): + data =self.data - other.data + elif isinstance(other, (int, float)): + data =self.data - other + elif isinstance(other, (list, tuple)): + other = np.array(other) + if isinstance(other, np.ndarray): + data = self.data - other[..., np.newaxis] + + if data is None: + return NotImplemented + + out = self.__class__(data, self.phase) + out.coordinate_format = self.coordinate_format + return out + # ------------------------ Class methods ------------------------- # @classmethod From 45f4fcb9a071840582fe8488d73a857dfca8a8a9 Mon Sep 17 00:00:00 2001 From: Austin Gerlt <83073845+argerlt@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:52:07 -0600 Subject: [PATCH 2/8] Simplify __r_add__ and __r_sub__ and make them work correctly with new Miller changes --- orix/vector/vector3d.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py index c4d4caa3..8db52356 100644 --- a/orix/vector/vector3d.py +++ b/orix/vector/vector3d.py @@ -258,14 +258,8 @@ def __add__( return NotImplemented def __radd__(self, other: int | float | list | tuple | np.ndarray) -> Vector3d: - if isinstance(other, (int, float)): - return self.__class__(other + self.data) - elif isinstance(other, (list, tuple)): - other = np.array(other) - - if isinstance(other, np.ndarray): - return self.__class__(other[..., np.newaxis] + self.data) - + if isinstance(other, (int, float, list, tuple, np.ndarray, Vector3d)): + return self + other return NotImplemented def __sub__( @@ -284,14 +278,8 @@ def __sub__( return NotImplemented def __rsub__(self, other: int | float | list | tuple | np.ndarray) -> Vector3d: - if isinstance(other, (int, float)): - return self.__class__(other - self.data) - elif isinstance(other, (list, tuple)): - other = np.array(other) - - if isinstance(other, np.ndarray): - return self.__class__(other[..., np.newaxis] - self.data) - + if isinstance(other, (int, float, list, tuple, np.ndarray, Vector3d)): + return -self + other return NotImplemented def __mul__( From 33320289292923ecde6b901b2e363c29bbcbed30 Mon Sep 17 00:00:00 2001 From: Austin Gerlt <83073845+argerlt@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:53:33 -0600 Subject: [PATCH 3/8] add logic to Vector3D that makes vector-miller and miller-vector combinations both return millers --- orix/vector/vector3d.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py index 8db52356..f329f276 100644 --- a/orix/vector/vector3d.py +++ b/orix/vector/vector3d.py @@ -246,7 +246,11 @@ def __add__( self, other: int | float | list | tuple | np.ndarray | Vector3d ) -> Vector3d: if isinstance(other, Vector3d): - return self.__class__(self.data + other.data) + # Vector + Miller = Miller + if hasattr(other,'phase'): + return other + self + else: + return self.__class__(self.data + other.data) elif isinstance(other, (int, float)): return self.__class__(self.data + other) elif isinstance(other, (list, tuple)): @@ -266,7 +270,11 @@ def __sub__( self, other: int | float | list | tuple | np.ndarray | Vector3d ) -> Vector3d: if isinstance(other, Vector3d): - return self.__class__(self.data - other.data) + # Vector - Miller = Miller + if hasattr(other,'phase'): + return - other + self + else: + return self.__class__(self.data - other.data) elif isinstance(other, (int, float)): return self.__class__(self.data - other) elif isinstance(other, (list, tuple)): @@ -301,14 +309,8 @@ def __mul__( return NotImplemented def __rmul__(self, other: int | float | list | tuple | np.ndarray) -> Vector3d: - if isinstance(other, (int, float)): - return self.__class__(other * self.data) - elif isinstance(other, (list, tuple)): - other = np.array(other) - - if isinstance(other, np.ndarray): - return self.__class__(other[..., np.newaxis] * self.data) - + if isinstance(other, (int, float, list, tuple, np.ndarray, Vector3d)): + return self * other return NotImplemented def __truediv__( From 13055148b050f505c0598caa95f3717bf2778095 Mon Sep 17 00:00:00 2001 From: Austin Gerlt <83073845+argerlt@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:54:00 -0600 Subject: [PATCH 4/8] add warnings for allowed but ill-advised symmetry combos --- orix/vector/vector3d.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py index f329f276..3ff902e1 100644 --- a/orix/vector/vector3d.py +++ b/orix/vector/vector3d.py @@ -20,6 +20,7 @@ from __future__ import annotations from copy import deepcopy +import logging from typing import TYPE_CHECKING, Any import dask.array as da @@ -35,6 +36,7 @@ if TYPE_CHECKING: # pragma: no cover from orix.quaternion.symmetry import Symmetry +_logger = logging.getLogger(__name__) class Vector3d(Object3d): r"""Three-dimensional vectors. @@ -247,6 +249,7 @@ def __add__( ) -> Vector3d: if isinstance(other, Vector3d): # Vector + Miller = Miller + self._check_for_symmetry_mismatch(other) if hasattr(other,'phase'): return other + self else: @@ -271,6 +274,7 @@ def __sub__( ) -> Vector3d: if isinstance(other, Vector3d): # Vector - Miller = Miller + self._check_for_symmetry_mismatch(other) if hasattr(other,'phase'): return - other + self else: @@ -333,6 +337,7 @@ def __rtruediv__(self, other: Any) -> None: def __eq__(self, other: Any) -> np.ndarray: if isinstance(other, Vector3d): + self._check_for_symmetry_mismatch(other) return np.all(self.data == other.data, axis=-1) else: return self.data == other @@ -340,6 +345,7 @@ def __eq__(self, other: Any) -> np.ndarray: def __hash__(self) -> int: return id(self) + # ------------------------ Class methods ------------------------- # @classmethod @@ -1716,3 +1722,16 @@ def _dot_outer_dask(self, other: Vector3d, chunk_size: int = 20) -> da.Array: v2 = da.from_array(other.data, chunks=chunks2) return da.tensordot(v1, v2, axes=(v1.ndim - 1, v2.ndim - 1)) + + def _check_for_symmetry_mismatch(self, other) -> None: + # NOTE: Vector3D does not include phase information, but some of its + # subclasses (most importantly, Miller) do, and adding the checks + # in Vector3D allows for checking regardless of order ie, (v+m vs m+v) + # as well as identical messages in all subclasses. + phase_checks = [hasattr(self,'phase'), hasattr(other,'phase')] + if np.all(phase_checks): + if self.phase.point_group != other.phase.point_group: + _logger.warning("WARNING: Point group mismatch detected between crystal vectors.") + elif np.any(phase_checks): + _logger.warning("WARNING: This operation is between a crystal and real space vector.") + return None From 85900c5058202e59c0ac053ca8ebc4457236367a Mon Sep 17 00:00:00 2001 From: Austin Gerlt <83073845+argerlt@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:33:36 -0600 Subject: [PATCH 5/8] allow quaternion multiplication of ReciprocalLatticeVectors from DiffSims --- orix/quaternion/quaternion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 8b69a7f2..b89df394 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -222,7 +222,7 @@ def __mul__(self, other: Quaternion | Vector3d) -> Quaternion | Vector3d: ) else: # pragma: no cover v = qu_rotate_vec(self.unit.data, other.data) - if isinstance(other, Miller): + if hasattr(object, 'phase'): m = other.__class__(xyz=v, phase=other.phase) m.coordinate_format = other.coordinate_format return m From a73522a3e9bbaf2add75d4436bf9de0e9cf3eeeb Mon Sep 17 00:00:00 2001 From: Austin Gerlt <83073845+argerlt@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:34:29 -0600 Subject: [PATCH 6/8] Formatting --- orix/vector/miller.py | 10 +++++----- orix/vector/vector3d.py | 18 +++++++++++------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/orix/vector/miller.py b/orix/vector/miller.py index f53ff577..1fe9e87a 100644 --- a/orix/vector/miller.py +++ b/orix/vector/miller.py @@ -367,7 +367,7 @@ def __getitem__(self, key) -> Miller: return m def __neg__(self) -> Vector3d: - out = self.__class__(-self.data,phase=self.phase) + out = self.__class__(-self.data, phase=self.phase) out.coordinate_format = self.coordinate_format return out @@ -376,9 +376,9 @@ def __add__( ) -> Miller: data = None if isinstance(other, Vector3d): - data =self.data + other.data + data = self.data + other.data elif isinstance(other, (int, float)): - data =self.data + other + data = self.data + other elif isinstance(other, (list, tuple)): other = np.array(other) if isinstance(other, np.ndarray): @@ -396,9 +396,9 @@ def __sub__( ) -> Miller: data = None if isinstance(other, Vector3d): - data =self.data - other.data + data = self.data - other.data elif isinstance(other, (int, float)): - data =self.data - other + data = self.data - other elif isinstance(other, (list, tuple)): other = np.array(other) if isinstance(other, np.ndarray): diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py index 3ff902e1..cbf27415 100644 --- a/orix/vector/vector3d.py +++ b/orix/vector/vector3d.py @@ -38,6 +38,7 @@ _logger = logging.getLogger(__name__) + class Vector3d(Object3d): r"""Three-dimensional vectors. @@ -250,7 +251,7 @@ def __add__( if isinstance(other, Vector3d): # Vector + Miller = Miller self._check_for_symmetry_mismatch(other) - if hasattr(other,'phase'): + if hasattr(other, "phase"): return other + self else: return self.__class__(self.data + other.data) @@ -275,8 +276,8 @@ def __sub__( if isinstance(other, Vector3d): # Vector - Miller = Miller self._check_for_symmetry_mismatch(other) - if hasattr(other,'phase'): - return - other + self + if hasattr(other, "phase"): + return -other + self else: return self.__class__(self.data - other.data) elif isinstance(other, (int, float)): @@ -345,7 +346,6 @@ def __eq__(self, other: Any) -> np.ndarray: def __hash__(self) -> int: return id(self) - # ------------------------ Class methods ------------------------- # @classmethod @@ -1728,10 +1728,14 @@ def _check_for_symmetry_mismatch(self, other) -> None: # subclasses (most importantly, Miller) do, and adding the checks # in Vector3D allows for checking regardless of order ie, (v+m vs m+v) # as well as identical messages in all subclasses. - phase_checks = [hasattr(self,'phase'), hasattr(other,'phase')] + phase_checks = [hasattr(self, "phase"), hasattr(other, "phase")] if np.all(phase_checks): if self.phase.point_group != other.phase.point_group: - _logger.warning("WARNING: Point group mismatch detected between crystal vectors.") + _logger.warning( + "WARNING: Point group mismatch detected between crystal vectors." + ) elif np.any(phase_checks): - _logger.warning("WARNING: This operation is between a crystal and real space vector.") + _logger.warning( + "WARNING: This operation is between a crystal and real space vector." + ) return None From 454c94b937eaa5ce6e2fdb0952f590917e8fd419 Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Fri, 16 Oct 2026 16:57:00 -0600 Subject: [PATCH 7/8] account for coordinate choice in addition/subtraction of crystal and euclidean vectors. --- orix/vector/miller.py | 29 ++++++++----------------- orix/vector/vector3d.py | 48 +++++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 43 deletions(-) diff --git a/orix/vector/miller.py b/orix/vector/miller.py index 1fe9e87a..da4bcfea 100644 --- a/orix/vector/miller.py +++ b/orix/vector/miller.py @@ -21,6 +21,7 @@ from copy import deepcopy from itertools import product +import logging from typing import TYPE_CHECKING from diffpy.structure import Lattice @@ -34,6 +35,9 @@ from orix.quaternion.symmetry import Symmetry +_logger = logging.getLogger(__name__) + + class Miller(Vector3d): r"""Direct crystal lattice vectors (uvw or UVTW) and reciprocal crystal lattice vectors (hkl or hkil), the latter known as Miller @@ -376,6 +380,11 @@ def __add__( ) -> Miller: data = None if isinstance(other, Vector3d): + self._check_for_crystal_mismatch(other) + if not hasattr(other, "phase"): + # assume the other vector is being added in the same + # coordinate frame as the crystal vector + other = self.__class__(*{self.coordinate_format: other.data}, phase=self.phase) data = self.data + other.data elif isinstance(other, (int, float)): data = self.data + other @@ -391,26 +400,6 @@ def __add__( out.coordinate_format = self.coordinate_format return out - def __sub__( - self, other: int | float | list | tuple | np.ndarray | Vector3d | Miller - ) -> Miller: - data = None - if isinstance(other, Vector3d): - data = self.data - other.data - elif isinstance(other, (int, float)): - data = self.data - other - elif isinstance(other, (list, tuple)): - other = np.array(other) - if isinstance(other, np.ndarray): - data = self.data - other[..., np.newaxis] - - if data is None: - return NotImplemented - - out = self.__class__(data, self.phase) - out.coordinate_format = self.coordinate_format - return out - # ------------------------ Class methods ------------------------- # @classmethod diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py index cbf27415..591e7f4c 100644 --- a/orix/vector/vector3d.py +++ b/orix/vector/vector3d.py @@ -249,15 +249,18 @@ def __add__( self, other: int | float | list | tuple | np.ndarray | Vector3d ) -> Vector3d: if isinstance(other, Vector3d): - # Vector + Miller = Miller - self._check_for_symmetry_mismatch(other) + # Check for symmetry and reference fram mismatches + self._check_for_crystal_mismatch(other) if hasattr(other, "phase"): + # If 'other' has a phase attribute (e.g, 'Miller'), + # use its addition function instead. return other + self else: return self.__class__(self.data + other.data) elif isinstance(other, (int, float)): return self.__class__(self.data + other) elif isinstance(other, (list, tuple)): + # convert lists and tuples into numpy arrays other = np.array(other) if isinstance(other, np.ndarray): @@ -273,22 +276,10 @@ def __radd__(self, other: int | float | list | tuple | np.ndarray) -> Vector3d: def __sub__( self, other: int | float | list | tuple | np.ndarray | Vector3d ) -> Vector3d: - if isinstance(other, Vector3d): - # Vector - Miller = Miller - self._check_for_symmetry_mismatch(other) - if hasattr(other, "phase"): - return -other + self - else: - return self.__class__(self.data - other.data) - elif isinstance(other, (int, float)): - return self.__class__(self.data - other) - elif isinstance(other, (list, tuple)): + # Convert to `self + (-other)` to avoid code repetition + if isinstance(other, (list, tuple)): other = np.array(other) - - if isinstance(other, np.ndarray): - return self.__class__(self.data - other[..., np.newaxis]) - - return NotImplemented + return self + (-other) def __rsub__(self, other: int | float | list | tuple | np.ndarray) -> Vector3d: if isinstance(other, (int, float, list, tuple, np.ndarray, Vector3d)): @@ -1723,19 +1714,30 @@ def _dot_outer_dask(self, other: Vector3d, chunk_size: int = 20) -> da.Array: return da.tensordot(v1, v2, axes=(v1.ndim - 1, v2.ndim - 1)) - def _check_for_symmetry_mismatch(self, other) -> None: - # NOTE: Vector3D does not include phase information, but some of its - # subclasses (most importantly, Miller) do, and adding the checks - # in Vector3D allows for checking regardless of order ie, (v+m vs m+v) - # as well as identical messages in all subclasses. + def _check_for_crystal_mismatch(self, other) -> None: + # NOTE: Vector3D does not include phase or coordinate_format + # information, but some of it's subclasses (most importantly, Miller) + # do. Adding the checker function to Vector3D allows logging + # consistent warnings regardless of operation order (ie, v+m or m+v). phase_checks = [hasattr(self, "phase"), hasattr(other, "phase")] if np.all(phase_checks): if self.phase.point_group != other.phase.point_group: + pg1 = self.phase.point_group.name + pg2 = other.phase.point_group.name _logger.warning( - "WARNING: Point group mismatch detected between crystal vectors." + "WARNING: Point group mismatch ({pg1} versus {pg2}) detected between crystal vectors." ) elif np.any(phase_checks): _logger.warning( "WARNING: This operation is between a crystal and real space vector." ) + crd_checks = [ + hasattr(self,'coordinate_format'), hasattr(other,'coordinate_format') + ] + if np.all(crd_checks): + cf1 = self.coordinate_format + cf2 = other.coordinate_format + _logger.warning( + "WARNING: Coordinates mismatch ({cf1} versus {cf2}) detected between crystal vectors." + ) return None From 3ae5e8dd357afc9d1b12968e8e623b9e414fe7b0 Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Fri, 16 Oct 2026 17:16:12 -0600 Subject: [PATCH 8/8] improved error language and proper coordinate format copying --- orix/vector/miller.py | 4 ++-- orix/vector/vector3d.py | 30 +++++++++++++++++++----------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/orix/vector/miller.py b/orix/vector/miller.py index da4bcfea..381b403e 100644 --- a/orix/vector/miller.py +++ b/orix/vector/miller.py @@ -384,7 +384,7 @@ def __add__( if not hasattr(other, "phase"): # assume the other vector is being added in the same # coordinate frame as the crystal vector - other = self.__class__(*{self.coordinate_format: other.data}, phase=self.phase) + other = self.__class__(**{self.coordinate_format: other.data}, phase=self.phase) data = self.data + other.data elif isinstance(other, (int, float)): data = self.data + other @@ -396,7 +396,7 @@ def __add__( if data is None: return NotImplemented - out = self.__class__(data, self.phase) + out = self.__class__(xyz=data, phase = self.phase) out.coordinate_format = self.coordinate_format return out diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py index 591e7f4c..d354ad89 100644 --- a/orix/vector/vector3d.py +++ b/orix/vector/vector3d.py @@ -249,13 +249,13 @@ def __add__( self, other: int | float | list | tuple | np.ndarray | Vector3d ) -> Vector3d: if isinstance(other, Vector3d): - # Check for symmetry and reference fram mismatches - self._check_for_crystal_mismatch(other) if hasattr(other, "phase"): # If 'other' has a phase attribute (e.g, 'Miller'), # use its addition function instead. return other + self else: + # Check for symmetry and reference fram mismatches + self._check_for_crystal_mismatch(other) return self.__class__(self.data + other.data) elif isinstance(other, (int, float)): return self.__class__(self.data + other) @@ -268,7 +268,9 @@ def __add__( return NotImplemented - def __radd__(self, other: int | float | list | tuple | np.ndarray) -> Vector3d: + def __radd__( + self, other: int | float | list | tuple | np.ndarray + ) -> Vector3d: if isinstance(other, (int, float, list, tuple, np.ndarray, Vector3d)): return self + other return NotImplemented @@ -281,7 +283,9 @@ def __sub__( other = np.array(other) return self + (-other) - def __rsub__(self, other: int | float | list | tuple | np.ndarray) -> Vector3d: + def __rsub__( + self, other: int | float | list | tuple | np.ndarray + ) -> Vector3d: if isinstance(other, (int, float, list, tuple, np.ndarray, Vector3d)): return -self + other return NotImplemented @@ -304,7 +308,9 @@ def __mul__( return NotImplemented - def __rmul__(self, other: int | float | list | tuple | np.ndarray) -> Vector3d: + def __rmul__( + self, other: int | float | list | tuple | np.ndarray + ) -> Vector3d: if isinstance(other, (int, float, list, tuple, np.ndarray, Vector3d)): return self * other return NotImplemented @@ -1725,19 +1731,21 @@ def _check_for_crystal_mismatch(self, other) -> None: pg1 = self.phase.point_group.name pg2 = other.phase.point_group.name _logger.warning( - "WARNING: Point group mismatch ({pg1} versus {pg2}) detected between crystal vectors." + f"WARNING: Point group mismatch detected between crystal vectors ({pg1} versus {pg2})." ) elif np.any(phase_checks): _logger.warning( - "WARNING: This operation is between a crystal and real space vector." + "WARNING: Reference frame mismatch detected (Euclidean versus crystal vectors)." ) crd_checks = [ - hasattr(self,'coordinate_format'), hasattr(other,'coordinate_format') - ] + hasattr(self, "coordinate_format"), + hasattr(other, "coordinate_format"), + ] if np.all(crd_checks): cf1 = self.coordinate_format cf2 = other.coordinate_format - _logger.warning( - "WARNING: Coordinates mismatch ({cf1} versus {cf2}) detected between crystal vectors." + if cf1 != cf2: + _logger.warning( + f"WARNING: Coordinates mismatch detected between crystal vectors ({cf1} versus {cf2})." ) return None