Skip to content

reflection_conditions

General reflection conditions (systematic absences) in International Tables form.

Given the symmetry operations of a space group, derive the reflection conditions grouped by reflection class (hkl, 0kl, h0l, ..., h00, 0k0, 00l, ...) as printed in the International Tables for Crystallography.

The derivation is exact and operation-based. The reflections left invariant by an operation (R, t) -- those with R^T h = h -- form an integer sublattice S of reciprocal space; these sublattices ARE the reflection classes (rank 3 = the general class hkl, rank 2 = a zone like 0kl, rank 1 = an axis like 00l). Every operation fixing such a class pointwise forces

t . h  in Z   for h in S

for the reflection to be present. Per class these congruences are collected, reduced to a minimal set, and expressed relative to the more general classes already listed -- so each printed line carries only the additional condition, as in the International Tables. Classes are derived from the operations, so hexagonal and cubic zones are handled without special-casing.

ReflectionCondition dataclass

A reflection condition for a single reflection class.

Attributes:

Name Type Description
reflection_class str

the class label, e.g. "0kl"

condition str

the additional human-readable condition, e.g. "k+l=4n"

basis tuple

(3, d) integer basis B; reflections in the class are h = B p

forms tuple

tuple of (coefficients, modulus) congruences on (h, k, l); a reflection in the class is present iff coefficients . h == 0 (mod modulus) for every form (the full condition on the class)

Source code in chmpy/crystal/reflection_conditions.py
@dataclass(frozen=True)
class ReflectionCondition:
    """A reflection condition for a single reflection class.

    Attributes:
        reflection_class: the class label, e.g. "0kl"
        condition: the additional human-readable condition, e.g. "k+l=4n"
        basis: (3, d) integer basis B; reflections in the class are h = B p
        forms: tuple of (coefficients, modulus) congruences on (h, k, l); a
            reflection in the class is present iff coefficients . h == 0
            (mod modulus) for every form (the full condition on the class)
    """

    reflection_class: str
    condition: str
    basis: tuple = field(repr=False)
    forms: tuple = field(repr=False)

    def __str__(self) -> str:
        return f"{self.reflection_class}: {self.condition}"

    def applies_to(self, hkl) -> bool:
        "Whether `hkl` belongs to this reflection class."
        return _in_lattice(np.asarray(hkl, dtype=int), np.array(self.basis))

    def is_present(self, hkl) -> bool:
        "Whether `hkl` satisfies the full condition on this class."
        h = np.asarray(hkl, dtype=int)
        return all(int(np.dot(coeffs, h)) % m == 0 for coeffs, m in self.forms)

applies_to(hkl)

Whether hkl belongs to this reflection class.

Source code in chmpy/crystal/reflection_conditions.py
def applies_to(self, hkl) -> bool:
    "Whether `hkl` belongs to this reflection class."
    return _in_lattice(np.asarray(hkl, dtype=int), np.array(self.basis))

is_present(hkl)

Whether hkl satisfies the full condition on this class.

Source code in chmpy/crystal/reflection_conditions.py
def is_present(self, hkl) -> bool:
    "Whether `hkl` satisfies the full condition on this class."
    h = np.asarray(hkl, dtype=int)
    return all(int(np.dot(coeffs, h)) % m == 0 for coeffs, m in self.forms)

reflection_conditions(symmetry_operations)

Derive the reflection conditions grouped by reflection class.

Parameters:

Name Type Description Default
symmetry_operations

the symmetry operations of a space group

required

Returns:

Type Description
list[ReflectionCondition]

list of ReflectionCondition ordered general -> zonal -> axial, excluding

list[ReflectionCondition]

classes with no condition or whose condition is already implied by a

list[ReflectionCondition]

more general class. An empty list means there are no reflection

list[ReflectionCondition]

conditions (every reflection is allowed).

Source code in chmpy/crystal/reflection_conditions.py
def reflection_conditions(symmetry_operations) -> list[ReflectionCondition]:
    """Derive the reflection conditions grouped by reflection class.

    Args:
        symmetry_operations: the symmetry operations of a space group

    Returns:
        list of ReflectionCondition ordered general -> zonal -> axial, excluding
        classes with no condition or whose condition is already implied by a
        more general class. An empty list means there are no reflection
        conditions (every reflection is allowed).
    """
    ops = [
        (
            np.round(op.rotation).astype(int),
            np.round(np.asarray(op.translation) * 12).astype(int),
        )
        for op in symmetry_operations
    ]

    classes: list[np.ndarray] = []
    for rotation, _ in ops:
        sublattice = _fixed_sublattice(rotation)
        if sublattice.shape[1] and not any(
            _same_lattice(sublattice, existing) for existing in classes
        ):
            classes.append(sublattice)
    classes.sort(key=lambda b: (-b.shape[1], int(np.count_nonzero(b)), _label(b)))

    results: list[ReflectionCondition] = []
    emitted: list[tuple[np.ndarray, list]] = []

    for basis in classes:
        d = basis.shape[1]
        # full condition on this class (hkl space) and projected to parameters
        hkl_forms, param_forms = set(), set()
        for rotation, translation in ops:
            if not np.array_equal(rotation.T @ basis, basis):
                continue
            hkl_form = _canonical(translation, 12)  # t . h == 0 (mod 1)
            if hkl_form is None:
                continue
            hkl_forms.add(hkl_form)
            projected = _canonical(basis.T @ np.array(hkl_form[0]), hkl_form[1])
            if projected is not None:
                param_forms.add(projected)
        if not param_forms:
            continue

        inherited = [
            projected
            for emitted_basis, emitted_forms in emitted
            if _is_sublattice(basis, emitted_basis)
            for projected in _project(emitted_basis, emitted_forms, basis)
        ]
        minimal = _minimal_conditions(sorted(param_forms), inherited, d)
        if not minimal:
            continue  # implied by a more general class

        symbols = _axis_symbols(basis)
        condition = ", ".join(
            sorted(_render(coeffs, symbols) + f"={m}n" for coeffs, m in minimal)
        )
        results.append(
            ReflectionCondition(
                reflection_class=_label(basis),
                condition=condition,
                basis=tuple(map(tuple, basis)),
                forms=tuple(sorted(hkl_forms)),
            )
        )
        emitted.append((basis, sorted(hkl_forms)))

    return results