Skip to content
2 changes: 1 addition & 1 deletion orix/quaternion/quaternion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions orix/vector/miller.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from copy import deepcopy
from itertools import product
import logging
from typing import TYPE_CHECKING

from diffpy.structure import Lattice
Expand All @@ -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
Expand Down Expand Up @@ -366,6 +370,36 @@ 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):
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
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__(xyz=data, phase = self.phase)
out.coordinate_format = self.coordinate_format
return out

# ------------------------ Class methods ------------------------- #

@classmethod
Expand Down
99 changes: 61 additions & 38 deletions orix/vector/vector3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,6 +36,8 @@
if TYPE_CHECKING: # pragma: no cover
from orix.quaternion.symmetry import Symmetry

_logger = logging.getLogger(__name__)


class Vector3d(Object3d):
r"""Three-dimensional vectors.
Expand Down Expand Up @@ -246,52 +249,45 @@ def __add__(
self, other: int | float | list | tuple | np.ndarray | Vector3d
) -> Vector3d:
if isinstance(other, Vector3d):
return self.__class__(self.data + other.data)
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)
elif isinstance(other, (list, tuple)):
# convert lists and tuples into numpy arrays
other = np.array(other)

if isinstance(other, np.ndarray):
return self.__class__(self.data + other[..., np.newaxis])

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)

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

def __sub__(
self, other: int | float | list | tuple | np.ndarray | Vector3d
) -> Vector3d:
if isinstance(other, Vector3d):
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)
return self + (-other)

if isinstance(other, np.ndarray):
return self.__class__(self.data - other[..., np.newaxis])

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)

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

def __mul__(
Expand All @@ -312,15 +308,11 @@ 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)

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

def __truediv__(
Expand All @@ -343,6 +335,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
Expand Down Expand Up @@ -1726,3 +1719,33 @@ 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_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(
f"WARNING: Point group mismatch detected between crystal vectors ({pg1} versus {pg2})."
)
elif np.any(phase_checks):
_logger.warning(
"WARNING: Reference frame mismatch detected (Euclidean versus crystal vectors)."
)
crd_checks = [
hasattr(self, "coordinate_format"),
hasattr(other, "coordinate_format"),
]
if np.all(crd_checks):
cf1 = self.coordinate_format
cf2 = other.coordinate_format
if cf1 != cf2:
_logger.warning(
f"WARNING: Coordinates mismatch detected between crystal vectors ({cf1} versus {cf2})."
)
return None
Loading