Skip to content

diffcalc

Wrapper functions for DiffCalc

Requires: pip install diffcalc-core

Example: from mmg_toolbox.diffcalc import UB ub = UB() ub.latt(2.85, 2.85, 10.8, 90, 90, 120) ub.add_reflection('ref1', ...) ub.add_reflection('ref2', ...) ub.calc_ub('ref1', 'ref2') ub.con('gamma',0, 'mu',0, 'bisect')

angles = ub.hkl2angles((1,1,1), energy_kev=6)
hkl = ub.angles2hkl(phi, chi, eta, mu, delta, gamma, energy_kev)
solutions = ub.all_solutions((1, 1, 1), energy_kev=6)

UB

Wrapper class for DiffCalc functionality

Source code in mmg_toolbox/diffraction/diffcalc.py
class UB:
    """
    Wrapper class for DiffCalc functionality
    """
    ubcalc: UBCalculation
    constrains: Constraints
    limits: dict[str, tuple[float, float]]
    hklcalc: HklCalculation

    def __init__(self,
                 lattice: tuple[float, float, float, float, float, float] | None = None,
                 ref1: tuple[tuple[int, int, int], float, float, float, float, float, float, float] | None = None,
                 ref2: tuple[tuple[int, int, int], float, float, float, float, float, float, float] | None = None,
                 constraints: dict[str, float | None] | None = None,
                 limits: dict[str, tuple[float, float]] | None = None,
                 azir: tuple[float, float, float] = (1, 0, 0),
                 surface: tuple[float, float, float] = (0, 0, 1),
                 ):
        self.ubcalc = UBCalculation("sixcircle")
        if lattice is None:
            lattice = DEFAULT_LATTICE
        self.ubcalc.set_lattice("sample", *lattice)
        self.ubcalc.n_hkl = azir # azimuthal reference, hkl
        self.ubcalc.surf_nhkl = surface  # surface vector, hkl
        if ref1 is not None:
            self.add_reflection('ref1', *ref1)
        if ref2 is not None:
            self.add_reflection('ref2', *ref2)
        if ref1 is not None and ref2 is not None:
            self.ubcalc.calc_ub('ref1', 'ref2')

        if constraints is None:
            constraints = DEFAULT_CONSTRAINTS.copy()
        self.constraints = Constraints(self._names2diffcalc(**constraints))
        if limits is None:
            limits = DEFAULT_LIMITS.copy()
        self.limits = limits
        self.hklcalc = HklCalculation(self.ubcalc, self.constraints)
        self.lab_transformation = np.eye(3)

    def __repr__(self):
        return repr(self.ubcalc)

    def __str__(self):
        return f"{self.ubcalc}\n\nCONSTRAINTS\n{self.constraints}"

    def add_reflection(self, label: str, hkl: tuple[float, float, float],
                       phi: float = 0, chi: float = 0, eta: float = 0, mu: float = 0,
                       delta: float = 0, gamma: float = 0,
                       energy_kev: float | None = None, wavelength_a: float | None = None):
        """
        Add a reflection location to the UB calculation
        """
        if energy_kev is None:
            energy_kev = photon_energy(wavelength_a)
        self.ubcalc.add_reflection(
            tag=label,
            hkl=hkl,
            position=Position(
                indegrees=True,
                phi=phi,
                chi=chi,
                eta=eta,
                mu=mu,
                delta=delta,
                nu=gamma,
            ),
            energy=energy_kev,
        )

    def add_orientation(self, label: str, hkl: tuple[float, float, float], xyz: tuple[float, float, float]):
        """
        Add an orientation to the UB calculation
        """
        self.ubcalc.add_orientation(
            # Add orientation of crystal, can be used together with reflection
            hkl=hkl,
            xyz=xyz,  # xyz in diffractometer basis
            position=None,
            tag=label
        )

    def latt(self, a: float = 5.0, b: float | None = None, c: float | None = None,
             alpha: float | None = None, beta: float | None = None, gamma: float | None = None,
             name: str = 'xtl', system: str = None):
        """
        Set the crystal lattice
        """
        self.ubcalc.set_lattice(name, system, a, b, c, alpha, beta, gamma)

    def calcub(self, tag1: str = 'ref1', tag2: str = 'ref2'):
        self.ubcalc.calc_ub(tag1, tag2)

    def set_lab_transformation(self, transformation: np.ndarray):
        """Assign a 3x3 transformation matrix to appy to angles"""
        self.lab_transformation = transformation

    def ub_matrix(self):
        return np.dot(self.lab_transformation, self.ubcalc.UB)

    def orientation_matrix(self):
        return np.dot(self.lab_transformation, self.ubcalc.U)

    def lp(self):
        xtl = self.ubcalc.crystal
        return xtl.a1, xtl.a2, xtl.a3, xtl.alpha1, xtl.alpha2, xtl.alpha3

    def asdict(self):
        return self.hklcalc.asdict.copy()

    def load_from_diffcalc(self, hklcalc: HklCalculation | None = None,
                           ubcalc: UBCalculation | None = None,
                           constraints: Constraints | None = None):
        if hklcalc is not None:
            self.hklcalc = hklcalc
            self.ubcalc = hklcalc.ubcalc
            self.constraints = hklcalc.constraints
        else:
            if ubcalc is not None:
                self.ubcalc = ubcalc
            if constraints is not None:
                self.constraints = constraints

    def azir(self, hkl: tuple[float, float, float]):
        """Set azimuthal reference reflection"""
        self.ubcalc.n_hkl = hkl

    def _names2diffcalc(self, **kwargs):
        return {
            RENAME_AXES[name] if name in RENAME_AXES else name: value
            for name, value in kwargs.items()
        }

    def _diffcalc2names(self, **kwargs):
        RENAME = {value: name for name, value in RENAME_AXES.items()}
        return {
            RENAME[name] if name in RENAME else name: value
            for name, value in kwargs.items()
        }

    def set_limits(self, **limits: [str, tuple[float, float]]):
        """Set limits"""
        limits = self._names2diffcalc(**limits)
        self.limits.update(limits)

    def _set_constraints(self, **constraints):
        """Set constraints"""
        constraints = self._names2diffcalc(**constraints)
        self.constraints.clear()
        for name, value in constraints.items():
            setattr(self.constraints, name, value)

    def con(self,
            con1: str, value1: float | bool,
            con2: str, value2: float | bool,
            con3: str, value3: float | bool = True):
        """Apply contraints to the DiffCalc calculation"""
        self._set_constraints(**{con1: value1, con2: value2, con3: value3})

    def bisect_vertical(self):
        self.con('gamma', 0, 'mu', 0, 'bisect')

    def bisect_horizontal(self):
        self.con('delta', 0, 'eta', 0, 'bisect')

    def fixed_phi_vertical(self, phi: float = 0):
        self.con('gamma', 0, 'mu', 0, 'phi', phi)

    def fixed_phi_horizontal(self, phi: float = 0):
        self.con('delta', 0, 'eta', 0, 'phi', phi)

    def fixed_psi_vertical(self, psi: float = 0):
        self.con('gamma', 0, 'mu', 0, 'psi', psi)

    def fixed_psi_horizontal(self, psi: float = 0):
        self.con('delta', 0, 'eta', 0, 'psi', psi)

    def all_solutions(self, hkl: tuple[float, float, float],
                      energy_kev: float | None = None, wavelength_a: float | None = None) -> list[dict[str, float]]:
        """Calculate all angle solutions for the given HKL"""
        if wavelength_a is None:
            wavelength_a = photon_wavelength(energy_kev)
        h, k, l = hkl
        all_pos = self.hklcalc.get_position(h, k, l, wavelength_a)
        solutions = []
        for posn, virtual_angles in all_pos:
            pos = posn.asdict.copy()
            pos['gamma'] = pos['nu']
            ktheta, kappa, kphi = euler2kappa(pos['phi'], pos['chi'], pos['eta'])
            kap = {
                'ktheta': ktheta,
                'kappa': kappa,
                'kphi': kphi
            }
            solutions.append(self._diffcalc2names(**{**pos, **virtual_angles, **kap}))
        return solutions

    def hkl2angles(self, hkl: tuple[float, float, float],
                   energy_kev: float | None = None, wavelength_a: float | None = None) -> dict[str, float] | None:
        """Calculate the angles for the given HKL"""
        if wavelength_a is None:
            wavelength_a = photon_wavelength(energy_kev)
        h, k, l = hkl
        all_pos = self.hklcalc.get_position(h, k, l, wavelength_a)
        for posn, virtual_angles in all_pos:
            pos = posn.asdict.copy()
            pos['gamma'] = pos['nu']
            if all(
                ax_min < pos.get(axis) < ax_max for axis, (ax_min, ax_max) in self.limits.items()
            ):
                ktheta, kappa, kphi = euler2kappa(pos['phi'], pos['chi'], pos['eta'])
                kap = {
                    'ktheta': ktheta,
                    'kappa': kappa,
                    'kphi': kphi
                }
                return self._diffcalc2names(**{**pos, **virtual_angles, **kap})
        return None

    def angles2hkl(self, phi: float = 0, chi: float = 0, eta: float = 0, mu: float = 0,
                   delta: float = 0, gamma: float = 0,
                   energy_kev: float | None = None, wavelength_a: float | None = None) -> tuple[float, float, float]:
        """Calculate the HKL for the given angles"""
        if wavelength_a is None:
            wavelength_a = photon_wavelength(energy_kev)
        pos = Position(
            # Eulerean angles in You. et al diffractometer basis
            # (z-along phi when all angles 0, y along beam, x vertical)
            nu=gamma,  # detector rotation, positive about diffractometer x-axis (gamma)
            delta=delta,  # detector rotation, negative about diffractometer z-axis
            mu=mu,  # sample rotation, positive about diffractometer x-axis
            eta=eta,  # sample rotation, negative about diffractometer z-axis
            chi=chi,  # sample rotation, positive about diffractometer y-axis
            phi=phi  # sample rotation, negative about diffractometer z-axis
        )
        return self.hklcalc.get_hkl(pos, wavelength_a)

    def kappa2hkl(self, ktheta: float = 0, kappa: float = 0, kphi: float = 0, mu: float = 0,
                  delta: float = 0, gamma: float = 0,
                  energy_kev: float | None = None, wavelength_a: float | None = None) -> tuple[float, float, float]:
        """Calculate the HKL for the given Kappa-angles"""
        if wavelength_a is None:
            wavelength_a = photon_wavelength(energy_kev)
        phi, chi, eta = kappa2euler(ktheta, kappa, kphi)
        pos = Position(nu=gamma, delta=delta, mu=mu, eta=eta, chi=chi, phi=phi)
        return self.hklcalc.get_hkl(pos, wavelength_a)

    def euler2kappa(self, phi: float = 0, chi: float = 0, eta: float = 0) -> tuple[float, float, float]:
        """Calculate the Euler angles for the given Kappa-angles"""
        return euler2kappa(phi, chi, eta)

    def kappa2euler(self, ktheta: float = 0, kappa: float = 0, kphi: float = 0) -> tuple[float, float, float]:
        """Calculate the Euler angles for the given Kappa-angles"""
        return kappa2euler(ktheta, kappa, kphi)

add_orientation(label, hkl, xyz)

Add an orientation to the UB calculation

Source code in mmg_toolbox/diffraction/diffcalc.py
def add_orientation(self, label: str, hkl: tuple[float, float, float], xyz: tuple[float, float, float]):
    """
    Add an orientation to the UB calculation
    """
    self.ubcalc.add_orientation(
        # Add orientation of crystal, can be used together with reflection
        hkl=hkl,
        xyz=xyz,  # xyz in diffractometer basis
        position=None,
        tag=label
    )

add_reflection(label, hkl, phi=0, chi=0, eta=0, mu=0, delta=0, gamma=0, energy_kev=None, wavelength_a=None)

Add a reflection location to the UB calculation

Source code in mmg_toolbox/diffraction/diffcalc.py
def add_reflection(self, label: str, hkl: tuple[float, float, float],
                   phi: float = 0, chi: float = 0, eta: float = 0, mu: float = 0,
                   delta: float = 0, gamma: float = 0,
                   energy_kev: float | None = None, wavelength_a: float | None = None):
    """
    Add a reflection location to the UB calculation
    """
    if energy_kev is None:
        energy_kev = photon_energy(wavelength_a)
    self.ubcalc.add_reflection(
        tag=label,
        hkl=hkl,
        position=Position(
            indegrees=True,
            phi=phi,
            chi=chi,
            eta=eta,
            mu=mu,
            delta=delta,
            nu=gamma,
        ),
        energy=energy_kev,
    )

all_solutions(hkl, energy_kev=None, wavelength_a=None)

Calculate all angle solutions for the given HKL

Source code in mmg_toolbox/diffraction/diffcalc.py
def all_solutions(self, hkl: tuple[float, float, float],
                  energy_kev: float | None = None, wavelength_a: float | None = None) -> list[dict[str, float]]:
    """Calculate all angle solutions for the given HKL"""
    if wavelength_a is None:
        wavelength_a = photon_wavelength(energy_kev)
    h, k, l = hkl
    all_pos = self.hklcalc.get_position(h, k, l, wavelength_a)
    solutions = []
    for posn, virtual_angles in all_pos:
        pos = posn.asdict.copy()
        pos['gamma'] = pos['nu']
        ktheta, kappa, kphi = euler2kappa(pos['phi'], pos['chi'], pos['eta'])
        kap = {
            'ktheta': ktheta,
            'kappa': kappa,
            'kphi': kphi
        }
        solutions.append(self._diffcalc2names(**{**pos, **virtual_angles, **kap}))
    return solutions

angles2hkl(phi=0, chi=0, eta=0, mu=0, delta=0, gamma=0, energy_kev=None, wavelength_a=None)

Calculate the HKL for the given angles

Source code in mmg_toolbox/diffraction/diffcalc.py
def angles2hkl(self, phi: float = 0, chi: float = 0, eta: float = 0, mu: float = 0,
               delta: float = 0, gamma: float = 0,
               energy_kev: float | None = None, wavelength_a: float | None = None) -> tuple[float, float, float]:
    """Calculate the HKL for the given angles"""
    if wavelength_a is None:
        wavelength_a = photon_wavelength(energy_kev)
    pos = Position(
        # Eulerean angles in You. et al diffractometer basis
        # (z-along phi when all angles 0, y along beam, x vertical)
        nu=gamma,  # detector rotation, positive about diffractometer x-axis (gamma)
        delta=delta,  # detector rotation, negative about diffractometer z-axis
        mu=mu,  # sample rotation, positive about diffractometer x-axis
        eta=eta,  # sample rotation, negative about diffractometer z-axis
        chi=chi,  # sample rotation, positive about diffractometer y-axis
        phi=phi  # sample rotation, negative about diffractometer z-axis
    )
    return self.hklcalc.get_hkl(pos, wavelength_a)

azir(hkl)

Set azimuthal reference reflection

Source code in mmg_toolbox/diffraction/diffcalc.py
def azir(self, hkl: tuple[float, float, float]):
    """Set azimuthal reference reflection"""
    self.ubcalc.n_hkl = hkl

con(con1, value1, con2, value2, con3, value3=True)

Apply contraints to the DiffCalc calculation

Source code in mmg_toolbox/diffraction/diffcalc.py
def con(self,
        con1: str, value1: float | bool,
        con2: str, value2: float | bool,
        con3: str, value3: float | bool = True):
    """Apply contraints to the DiffCalc calculation"""
    self._set_constraints(**{con1: value1, con2: value2, con3: value3})

euler2kappa(phi=0, chi=0, eta=0)

Calculate the Euler angles for the given Kappa-angles

Source code in mmg_toolbox/diffraction/diffcalc.py
def euler2kappa(self, phi: float = 0, chi: float = 0, eta: float = 0) -> tuple[float, float, float]:
    """Calculate the Euler angles for the given Kappa-angles"""
    return euler2kappa(phi, chi, eta)

hkl2angles(hkl, energy_kev=None, wavelength_a=None)

Calculate the angles for the given HKL

Source code in mmg_toolbox/diffraction/diffcalc.py
def hkl2angles(self, hkl: tuple[float, float, float],
               energy_kev: float | None = None, wavelength_a: float | None = None) -> dict[str, float] | None:
    """Calculate the angles for the given HKL"""
    if wavelength_a is None:
        wavelength_a = photon_wavelength(energy_kev)
    h, k, l = hkl
    all_pos = self.hklcalc.get_position(h, k, l, wavelength_a)
    for posn, virtual_angles in all_pos:
        pos = posn.asdict.copy()
        pos['gamma'] = pos['nu']
        if all(
            ax_min < pos.get(axis) < ax_max for axis, (ax_min, ax_max) in self.limits.items()
        ):
            ktheta, kappa, kphi = euler2kappa(pos['phi'], pos['chi'], pos['eta'])
            kap = {
                'ktheta': ktheta,
                'kappa': kappa,
                'kphi': kphi
            }
            return self._diffcalc2names(**{**pos, **virtual_angles, **kap})
    return None

kappa2euler(ktheta=0, kappa=0, kphi=0)

Calculate the Euler angles for the given Kappa-angles

Source code in mmg_toolbox/diffraction/diffcalc.py
def kappa2euler(self, ktheta: float = 0, kappa: float = 0, kphi: float = 0) -> tuple[float, float, float]:
    """Calculate the Euler angles for the given Kappa-angles"""
    return kappa2euler(ktheta, kappa, kphi)

kappa2hkl(ktheta=0, kappa=0, kphi=0, mu=0, delta=0, gamma=0, energy_kev=None, wavelength_a=None)

Calculate the HKL for the given Kappa-angles

Source code in mmg_toolbox/diffraction/diffcalc.py
def kappa2hkl(self, ktheta: float = 0, kappa: float = 0, kphi: float = 0, mu: float = 0,
              delta: float = 0, gamma: float = 0,
              energy_kev: float | None = None, wavelength_a: float | None = None) -> tuple[float, float, float]:
    """Calculate the HKL for the given Kappa-angles"""
    if wavelength_a is None:
        wavelength_a = photon_wavelength(energy_kev)
    phi, chi, eta = kappa2euler(ktheta, kappa, kphi)
    pos = Position(nu=gamma, delta=delta, mu=mu, eta=eta, chi=chi, phi=phi)
    return self.hklcalc.get_hkl(pos, wavelength_a)

latt(a=5.0, b=None, c=None, alpha=None, beta=None, gamma=None, name='xtl', system=None)

Set the crystal lattice

Source code in mmg_toolbox/diffraction/diffcalc.py
def latt(self, a: float = 5.0, b: float | None = None, c: float | None = None,
         alpha: float | None = None, beta: float | None = None, gamma: float | None = None,
         name: str = 'xtl', system: str = None):
    """
    Set the crystal lattice
    """
    self.ubcalc.set_lattice(name, system, a, b, c, alpha, beta, gamma)

set_lab_transformation(transformation)

Assign a 3x3 transformation matrix to appy to angles

Source code in mmg_toolbox/diffraction/diffcalc.py
def set_lab_transformation(self, transformation: np.ndarray):
    """Assign a 3x3 transformation matrix to appy to angles"""
    self.lab_transformation = transformation

set_limits(**limits)

Set limits

Source code in mmg_toolbox/diffraction/diffcalc.py
def set_limits(self, **limits: [str, tuple[float, float]]):
    """Set limits"""
    limits = self._names2diffcalc(**limits)
    self.limits.update(limits)