Skip to content

Commit

Permalink
Merge pull request #401 from anderscmathisen/align_vectors
Browse files Browse the repository at this point in the history
Add class method `from_align_vectors()` to Quaternion, Misorientation and Orientation
  • Loading branch information
hakonanes committed Dec 6, 2022
2 parents c8956ac + f31cd66 commit 0b82e2a
Show file tree
Hide file tree
Showing 6 changed files with 384 additions and 3 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -12,6 +12,12 @@ Unreleased

Added
-----
- Creation of ``Quaternion`` (s) (or instances of inheriting classes) from SciPy
``Rotation`` (s).
- Creation of one ``Quaternion`` or ``Rotation`` by aligning sets of vectors in two
reference frames, one ``Orientation`` by aligning sets of sample vectors and crystal
vectors, and one ``Misorientation`` by aligning two sets of crystal vectors in two
different crystals.
- ``row`` and ``col`` properties to ``CrystalMap`` giving the row and column coordinate
of each map point given by ``CrystalMap.shape``.
- ``Rotation`` class methods ``from_neo_euler()``, ``from_axes_angles()``,
Expand Down
172 changes: 171 additions & 1 deletion orix/quaternion/orientation.py
Expand Up @@ -33,7 +33,7 @@
from orix.quaternion.orientation_region import OrientationRegion
from orix.quaternion.rotation import Rotation
from orix.quaternion.symmetry import C1, Symmetry, _get_unique_symmetry_elements
from orix.vector import AxAngle, NeoEuler, Vector3d
from orix.vector import AxAngle, Miller, NeoEuler, Vector3d


class Misorientation(Rotation):
Expand Down Expand Up @@ -377,6 +377,92 @@ def get_distance_matrix(

return angles

@classmethod
def from_align_vectors(
cls,
other: Miller,
initial: Miller,
weights: Optional[np.ndarray] = None,
return_rmsd: bool = False,
return_sensitivity: bool = False,
) -> Misorientation:
"""Return an estimated misorientation to optimally align two
sets of vectors, one set in each crystal.
This method wraps :meth:`scipy.spatial.transform.Rotation.align_vectors`,
see that method for further explanations of parameters and
returns.
Parameters
----------
other
Directions of shape ``(N,)`` in the other crystal.
initial
Directions of shape ``(N,)`` in the initial crystal.
weights
The relative importance of the different vectors.
return_rmsd
Whether to return the root mean square distance (weighted)
between ``other`` and ``initial`` after alignment.
return_sensitivity
Whether to return the sensitivity matrix.
Returns
-------
estimated misorientation
Best estimate of the ``misorientation`` that transforms
``initial`` to ``other``. The symmetry of the
``misorientation`` is inferred from the phase of ``other``
and ``initial``, if given.
rmsd
Returned when ``return_rmsd=True``.
sensitivity
Returned when ``return_sensitivity=True``.
Raises
------
ValueError
If ``other`` and ``initial`` are not Miller instances.
Examples
--------
>>> from orix.quaternion import Misorientation
>>> from orix.vector import Miller
>>> from orix.crystal_map.phase_list import Phase
>>> uvw1 = Miller(
... uvw=[[1, 0, 0], [0, 1, 0]], phase=Phase(point_group="m-3m")
... )
>>> uvw2 = Miller(
... uvw=[[1, 0, 0], [0, 0, 1]], phase=Phase(point_group="m-3m")
... )
>>> mori12 = Misorientation.from_align_vectors(uvw2, uvw1)
>>> mori12 * uvw1
Miller (2,), point group m-3m, uvw
[[1. 0. 0.]
[0. 0. 1.]]
"""
if not isinstance(other, Miller) or not isinstance(initial, Miller):
raise ValueError(
"Arguments other and initial must both be of type Miller, "
f"but are of type {type(other)} and {type(initial)}."
)

out = super().from_align_vectors(
other=other,
initial=initial,
weights=weights,
return_rmsd=return_rmsd,
return_sensitivity=return_sensitivity,
)
out = list(out)

try:
out[0].symmetry = (initial.phase.point_group, other.phase.point_group)
except (AttributeError, ValueError):
pass

return out[0] if len(out) == 1 else out


class Orientation(Misorientation):
r"""Orientations represent misorientations away from a reference of
Expand Down Expand Up @@ -476,6 +562,90 @@ def from_euler(
ori.symmetry = symmetry
return ori

@classmethod
def from_align_vectors(
cls,
other: Miller,
initial: Vector3d,
weights: Optional[np.ndarray] = None,
return_rmsd: bool = False,
return_sensitivity: bool = False,
) -> Orientation:
"""Return an estimated orientation to optimally align vectors in
the crystal and sample reference frames.
This method wraps :meth:`scipy.spatial.transform.Rotation.align_vectors`,
see that method for further explanations of parameters and
returns.
Parameters
----------
other
Directions of shape ``(N,)`` in the other crystal.
initial
Directions of shape ``(N,)`` in the initial crystal.
weights
The relative importance of the different vectors.
return_rmsd
Whether to return the root mean square distance (weighted)
between ``other`` and ``initial`` after alignment.
return_sensitivity
Whether to return the sensitivity matrix.
Returns
-------
estimated orientation
Best estimate of the ``orientation`` that transforms
``initial`` to ``other``. The symmetry of the ``orientaiton``
is inferred form the point group of the phase of ``other``,
if given.
rmsd
Returned when ``return_rmsd=True``.
sensitivity
Returned when ``return_sensitivity=True``.
Raises
------
ValueError
If ``other`` is not a Miller instance.
Examples
--------
>>> from orix.quaternion import Orientation
>>> from orix.vector import Vector3d, Miller
>>> from orix.crystal_map.phase_list import Phase
>>> crystal_millers = Miller(
... uvw=[[0, 1 ,0], [1, 0, 0]], phase=Phase(point_group="m-3m")
... )
>>> sample_vectors = Vector3d([[0, -1, 0], [0, 0, 1]])
>>> ori = Orientation.from_align_vectors(crystal_millers, sample_vectors)
>>> ori * sample_vectors
Vector3d (2,)
[[0. 1. 0.]
[1. 0. 0.]]
"""
if not isinstance(other, Miller):
raise ValueError(
f"Argument other must be of type Miller, but has type {type(other)}"
)

out = Rotation.from_align_vectors(
other=other,
initial=initial,
weights=weights,
return_rmsd=return_rmsd,
return_sensitivity=return_sensitivity,
)
out = list(out)
out[0] = cls(out[0].data)

try:
out[0].symmetry = other.phase.point_group
except (AttributeError, ValueError):
pass

return out[0] if len(out) == 1 else out

@classmethod
def from_matrix(
cls, matrix: np.ndarray, symmetry: Optional[Symmetry] = None
Expand Down
110 changes: 109 additions & 1 deletion orix/quaternion/quaternion.py
Expand Up @@ -18,13 +18,14 @@

from __future__ import annotations

from typing import Union
from typing import Optional, Union
import warnings

import dask.array as da
from dask.diagnostics import ProgressBar
import numpy as np
import quaternion
from scipy.spatial.transform import Rotation as SciPyRotation

from orix._util import deprecated_argument
from orix.base import Object3d
Expand Down Expand Up @@ -850,3 +851,110 @@ def _outer_dask(
new_chunks = tuple(chunks1[:-1]) + tuple(chunks2[:-1]) + (-1,)

return out.rechunk(new_chunks)

@classmethod
def from_scipy_rotation(cls, rotation: SciPyRotation) -> Quaternion:
"""Return quaternion(s) from :class:`scipy.spatial.transform.Rotation`.
Parameters
----------
rotation
SciPy rotation(s).
Returns
-------
quaternion
Quaternion(s).
Notes
-----
The Scipy rotation is inverted to be consistent with the Orix framework of
passive rotations.
Examples
--------
>>> from orix.quaternion import Quaternion, Rotation
>>> from scipy.spatial.transform import Rotation as SciPyRotation
>>> euler = np.array([90, 0, 0]) * np.pi / 180
>>> scipy_rot = SciPyRotation.from_euler("ZXZ", euler)
>>> ori = Quaternion.from_scipy_rotation(scipy_rot)
>>> ori
Quaternion (1,)
[[ 0.7071 0. 0. -0.7071]]
>>> Rotation.from_euler(euler, direction="crystal2lab")
Rotation (1,)
[[0.7071 0. 0. 0.7071]]
"""
matrix = rotation.inv().as_matrix()

return cls.from_matrix(matrix=matrix)

@classmethod
def from_align_vectors(
cls,
other: Union[Vector3d, tuple, list],
initial: Union[Vector3d, tuple, list],
weights: Optional[np.ndarray] = None,
return_rmsd: bool = False,
return_sensitivity: bool = False,
) -> Quaternion:
"""Return an estimated quaternion to optimally align two sets of
vectors.
This method wraps :meth:`scipy.spatial.transform.Rotation.align_vectors`,
see that method for further explanations of parameters and
returns.
Parameters
----------
other
Vectors of shape ``(N,)`` in the other reference frame.
initial
Vectors of shape ``(N,)`` in the initial reference frame.
weights
The relative importance of the different vectors.
return_rmsd
Whether to return the root mean square distance (weighted)
between ``other`` and ``initial`` after alignment.
return_sensitivity
Whether to return the sensitivity matrix.
Returns
-------
estimated quaternion
Best estimate of the quaternion
that transforms ``initial`` to ``other``.
rmsd
Returned when ``return_rmsd=True``.
sensitivity
Returned when ``return_sensitivity=True``.
Examples
--------
>>> from orix.quaternion import Quaternion
>>> from orix.vector import Vector3d
>>> vecs1 = Vector3d([[1, 0, 0], [0, 1, 0]])
>>> vecs2 = Vector3d([[0, -1, 0], [0,0, 1]])
>>> quat12 = Quaternion.from_align_vectors(vecs2, vecs1)
>>> quat12 * vecs1
Vector3d (2,)
[[ 0. -1. 0.]
[ 0. 0. 1.]]
"""
if not isinstance(other, Vector3d):
other = Vector3d(other)
if not isinstance(initial, Vector3d):
initial = Vector3d(initial)
vec1 = initial.unit.data
vec2 = other.unit.data

out = SciPyRotation.align_vectors(
vec1, vec2, weights=weights, return_sensitivity=return_sensitivity
)
out = list(out)
out[0] = cls.from_scipy_rotation(out[0])

if not return_rmsd:
del out[1]

return out[0] if len(out) == 1 else out

0 comments on commit 0b82e2a

Please sign in to comment.