Source code for diffBloch.core.crystal

"""Pure unit-cell and lattice-centering helpers."""

from __future__ import annotations

import numpy as np
from numpy.typing import NDArray

type FloatArray = NDArray[np.float64]
type IntArray = NDArray[np.int64]


[docs] def cell_matrix_from_parameters(parameters: FloatArray) -> FloatArray: """Return the fractional-to-Cartesian cell matrix from six cell parameters. ``parameters`` are ordered as ``(a, b, c, alpha, beta, gamma)``, with lengths in Angstrom and angles in degrees. Rows of the returned matrix are the real-space basis vectors. """ values = np.asarray(parameters, dtype=np.float64) if values.shape != (6,): raise ValueError("cell parameters must have shape (6,)") a, b, c, alpha_deg, beta_deg, gamma_deg = values alpha = np.deg2rad(alpha_deg) beta = np.deg2rad(beta_deg) gamma = np.deg2rad(gamma_deg) cos_alpha = np.cos(alpha) cos_beta = np.cos(beta) cos_gamma = np.cos(gamma) sin_gamma = np.sin(gamma) volume_factor = np.sqrt( 1.0 - cos_alpha**2 - cos_beta**2 - cos_gamma**2 + 2.0 * cos_alpha * cos_beta * cos_gamma ) return np.asarray( [ [a, 0.0, 0.0], [b * cos_gamma, b * sin_gamma, 0.0], [ c * cos_beta, c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma, c * volume_factor / sin_gamma, ], ], dtype=np.float64, )
[docs] def reciprocal_cell(cell: FloatArray) -> FloatArray: """Return reciprocal lattice vectors as rows in Angstrom^-1. Uses the ASE-compatible convention ``reciprocal_cell = pinv(cell).T``. """ matrix = _cell_array(cell) return np.linalg.pinv(matrix).T
[docs] def cell_volume(cell: FloatArray) -> float: """Return the positive unit-cell volume in Angstrom^3.""" matrix = _cell_array(cell) return float(abs(np.linalg.det(matrix)))
[docs] def orientation_basis(cell: FloatArray, orientation: FloatArray) -> FloatArray: """Lab-frame reciprocal basis for one orientation: ``reciprocal_cell(cell @ orientation.T)``. The single home for the orientation convention. ``orientation`` is not guaranteed exactly orthonormal -- it carries PETS's own UB-vs-cell-parameters fit residual (see ``preprocess.orientation``) -- so this is NOT ``reciprocal_basis @ orientation.T``. ``cell`` rows are the real-space basis; returns rows ``a*, b*, c*`` in inverse Angstrom. ``orientation = I`` reproduces ``reciprocal_cell(cell)``. """ matrix = _cell_array(cell) rotation = np.asarray(orientation, dtype=np.float64) if rotation.shape != (3, 3): raise ValueError("orientation must have shape (3, 3)") return reciprocal_cell(matrix @ rotation.T)
[docs] def reflection_condition(hkl: IntArray, centering: str) -> NDArray[np.bool_]: """Return the mask of reflections allowed by a lattice-centering rule.""" miller = np.asarray(hkl, dtype=np.int64) if miller.ndim != 2 or miller.shape[1] != 3: raise ValueError("hkl must have shape (N, 3)") match centering.upper(): case "P": return np.ones(miller.shape[0], dtype=np.bool_) case "I": return np.asarray(miller.sum(axis=1) % 2 == 0, dtype=np.bool_) case "F": even = (miller % 2 == 0).all(axis=1) odd = (miller % 2 != 0).all(axis=1) return np.asarray(even | odd, dtype=np.bool_) case "A": return np.asarray((miller[:, 1] + miller[:, 2]) % 2 == 0, dtype=np.bool_) case "B": return np.asarray((miller[:, 0] + miller[:, 2]) % 2 == 0, dtype=np.bool_) case "C": return np.asarray((miller[:, 0] + miller[:, 1]) % 2 == 0, dtype=np.bool_) case _: raise ValueError(f"unsupported lattice centering: {centering!r}")
def _cell_array(cell: FloatArray) -> FloatArray: matrix = np.asarray(cell, dtype=np.float64) if matrix.shape != (3, 3): raise ValueError("cell must have shape (3, 3)") return matrix