Source code for diffcalc.hkl.geometry

"""Module providing diffractometer position definition and rotation matrices.

Diffractometer axes and rotation matrix definitions are following conventions
described in [1]_.

References
----------
.. [1] H. You. "Angle calculations for a '4S+2D' six-circle diffractometer"
       J. Appl. Cryst. (1999). 32, 614-623.
"""
from math import degrees, radians
from typing import Dict, Tuple, Union

import numpy as np
from diffcalc.util import I, x_rotation, y_rotation, z_rotation
from numpy.linalg import inv


[docs]class Position: """Class representing diffractometer orientation. Diffractometer orientation corresponding to (4+2) geometry defined in H. You paper (add reference) Attributes ---------- fields: Tuple[str, str, str, str, str, str] Tuple with angle names mu: float, default = 0.0 mu angle value delta: float, default = 0.0 delta angle value nu: float, default = 0.0 nu angle value eta: float, default = 0.0 eta angle value chi: float, default = 0.0 chi angle value phi: float, default = 0.0 phi angle value indegrees: bool, default = True If True, arguments are angles in degrees. """ fields: Tuple[str, str, str, str, str, str] = ( "mu", "delta", "nu", "eta", "chi", "phi", ) def __init__( self, mu: float = 0.0, delta: float = 0.0, nu: float = 0.0, eta: float = 0.0, chi: float = 0.0, phi: float = 0.0, indegrees: bool = True, ): self._mu: float = radians(mu) if indegrees else mu self._delta: float = radians(delta) if indegrees else delta self._nu: float = radians(nu) if indegrees else nu self._eta: float = radians(eta) if indegrees else eta self._chi: float = radians(chi) if indegrees else chi self._phi: float = radians(phi) if indegrees else phi self.indegrees: bool = indegrees def __str__(self): """Represent Position object information as a string. Returns ------- str Position object string representation. """ if self.indegrees: return f"Position({', '.join((f'{k}: {v:.4f}' for k, v in self.asdict.items()))})" return f"Position({', '.join((f'{k}: {degrees(v):.4f}' for k, v in self.asdict.items()))})"
[docs] @classmethod def asdegrees(cls, pos: "Position") -> "Position": """Create new Position object with angles in degrees. Parameters ---------- pos: Position Input Position object Returns ------- Position New Position object with angles in degrees. """ res = cls(**pos.asdict, indegrees=pos.indegrees) res.indegrees = True return res
[docs] @classmethod def asradians(cls, pos: "Position") -> "Position": """Create new Position object with angles in radians. Parameters ---------- pos: Position Input Position object Returns ------- Position New Position object with angles in radians. """ res = cls(**pos.asdict, indegrees=pos.indegrees) res.indegrees = False return res
@property def mu(self) -> Union[float, None]: """Value of of mu angle.""" if self.indegrees: return degrees(self._mu) else: return self._mu @mu.setter def mu(self, val): if self.indegrees: self._mu = radians(val) else: self._mu = val @mu.deleter def mu(self): self._mu = None @property def delta(self) -> Union[float, None]: """Value of of delta angle.""" if self.indegrees: return degrees(self._delta) else: return self._delta @delta.setter def delta(self, val): if self.indegrees: self._delta = radians(val) else: self._delta = val @delta.deleter def delta(self): self._delta = None @property def nu(self) -> Union[float, None]: """Value of of nu angle.""" if self.indegrees: return degrees(self._nu) else: return self._nu @nu.setter def nu(self, val): if self.indegrees: self._nu = radians(val) else: self._nu = val @nu.deleter def nu(self): self._nu = None @property def eta(self) -> Union[float, None]: """Value of of eta angle.""" if self.indegrees: return degrees(self._eta) else: return self._eta @eta.setter def eta(self, val): if self.indegrees: self._eta = radians(val) else: self._eta = val @eta.deleter def eta(self): self._eta = None @property def chi(self) -> Union[float, None]: """Value of of chi angle.""" if self.indegrees: return degrees(self._chi) else: return self._chi @chi.setter def chi(self, val): if self.indegrees: self._chi = radians(val) else: self._chi = val @chi.deleter def chi(self): self._chi = None @property def phi(self) -> Union[float, None]: """Value of of phi angle.""" if self.indegrees: return degrees(self._phi) else: return self._phi @phi.setter def phi(self, val): if self.indegrees: self._phi = radians(val) else: self._phi = val @phi.deleter def phi(self): self._phi = None @property def asdict(self) -> Dict[str, float]: """Return dictionary of diffractometer angles. Returns ------- Dict[str, float] Dictionary of axis names and angle values. """ return {field: getattr(self, field) for field in self.fields} @property def astuple(self) -> Tuple[float, float, float, float, float, float]: """Return tuple of diffractometer angles. Returns ------- Tuple[float, float, float, float, float, float] Tuple of angle values. """ mu, delta, nu, eta, chi, phi = tuple( getattr(self, field) for field in self.fields ) return mu, delta, nu, eta, chi, phi
[docs]def get_rotation_matrices( pos: Position, ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray,]: """Create rotation matrices corresponding to the diffractometer axes. Parameters ---------- pos: Position Position object containing set of diffractometer angles Returns ------- Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray] Tuple containing set of rotation matrices corresponding to input diffractometer angle values. """ pos_in_rad = Position.asradians(pos) mu, delta, nu, eta, chi, phi = pos_in_rad.astuple return ( rot_MU(mu), rot_DELTA(delta), rot_NU(nu), rot_ETA(eta), rot_CHI(chi), rot_PHI(phi), )
[docs]def rot_NU(nu: float) -> np.ndarray: """Return rotation matrix corresponding to nu axis. Parameters ---------- nu: float nu axis angle Returns ------- np.ndarray Rotation matrix as a NumPy array. """ return x_rotation(nu)
[docs]def rot_DELTA(delta: float) -> np.ndarray: """Return rotation matrix corresponding to delta axis. Parameters ---------- delta: float delta axis angle Returns ------- np.ndarray Rotation matrix as a NumPy array. """ return z_rotation(-delta)
[docs]def rot_MU(mu_or_alpha: float) -> np.ndarray: """Return rotation matrix corresponding to mu axis. Parameters ---------- mu: float mu axis angle Returns ------- np.ndarray Rotation matrix as a NumPy array. """ return x_rotation(mu_or_alpha)
[docs]def rot_ETA(eta: float) -> np.ndarray: """Return rotation matrix corresponding to eta axis. Parameters ---------- eta: float eta axis angle Returns ------- np.ndarray Rotation matrix as a NumPy array. """ return z_rotation(-eta)
[docs]def rot_CHI(chi: float) -> np.ndarray: """Return rotation matrix corresponding to chi axis. Parameters ---------- chi: float chi axis angle Returns ------- np.ndarray Rotation matrix as a NumPy array. """ return y_rotation(chi)
[docs]def rot_PHI(phi: float) -> np.ndarray: """Return rotation matrix corresponding to phi axis. Parameters ---------- phi: float phi axis angle Returns ------- np.ndarray Rotation matrix as a NumPy array. """ return z_rotation(-phi)
[docs]def get_q_phi(pos: Position) -> np.ndarray: """Calculate scattering vector in laboratory frame. Calculate hkl in the phi frame in units of 2 * pi / lambda. Parameters ---------- pos: object Diffractometer angles in radians. Returns ------- matrix: Scattering vector coordinates corresponding to the input position. """ pos_in_rad = Position.asradians(pos) [MU, DELTA, NU, ETA, CHI, PHI] = get_rotation_matrices(pos_in_rad) # Equation 12: Compute the momentum transfer vector in the lab frame y = np.array([[0], [1], [0]]) q_lab = (NU @ DELTA - I) @ y # Transform this into the phi frame. return np.array(inv(PHI) @ inv(CHI) @ inv(ETA) @ inv(MU) @ q_lab)