Source code for httk.atomistic.symmetry.xyz

"""Exact parsers for crystallographic coordinate-operation notation."""

import re
from collections.abc import Sequence
from decimal import Decimal
from fractions import Fraction

from .affine_operation import AffineOperation

__all__ = ["operation_from_xyz", "operation_from_xyzt", "parse_linear_expression"]

_NUMBER = r"(?:\d+/\d+|\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?"
_SUPERSPACE_VARS = ("x1", "x2", "x3", "x4", "x5", "x6")


def _fraction(text: str) -> Fraction:
    return Fraction(text) if "/" in text else Fraction(Decimal(text))


def _parse_linear_expression(
    expression: str, variables: Sequence[str], *, integer_coefficients: bool = False
) -> tuple[tuple[Fraction, ...], Fraction]:
    text = expression.replace(" ", "")
    if not text:
        raise ValueError("Empty symmetry expression")
    if text[0] not in "+-":
        text = "+" + text

    names = "|".join(re.escape(name) for name in sorted(variables, key=len, reverse=True))
    token = re.compile(rf"([+-])(?:(?:(?P<coefficient>{_NUMBER})\*?)?(?P<variable>{names})|(?P<constant>{_NUMBER}))")
    coefficients = {name: Fraction(0) for name in variables}
    constant = Fraction(0)
    position = 0
    for match in token.finditer(text):
        if match.start() != position:
            raise ValueError(f"Unparsed tail in {expression!r} near {text[position:]!r}")
        position = match.end()
        sign = 1 if match.group(1) == "+" else -1
        variable = match.group("variable")
        if variable is None:
            constant += sign * _fraction(match.group("constant"))
            continue
        coefficient = _fraction(match.group("coefficient")) if match.group("coefficient") else Fraction(1)
        if integer_coefficients and coefficient.denominator != 1:
            raise ValueError(f"Non-integer coefficient on {variable} in {expression!r}")
        coefficients[variable] += sign * coefficient

    if position != len(text):
        raise ValueError(f"Unparsed tail in {expression!r} near {text[position:]!r}")
    return tuple(coefficients[name] for name in variables), constant


[docs] def parse_linear_expression( expression: str, allowed_vars: Sequence[str] = _SUPERSPACE_VARS ) -> tuple[tuple[int, ...], Fraction]: """Parse one superspace linear expression into integer coefficients and a translation. The parser accepts the six superspace variables ``x1`` through ``x6`` by default and preserves an exact rational translation. :param expression: The linear expression to parse. :param allowed_vars: The variable names accepted in ``expression``. :return: The integer coefficients in ``allowed_vars`` and the exact translation. :raises ValueError: If the expression is empty, malformed, or has a non-integer variable coefficient. """ coefficients, constant = _parse_linear_expression(expression, allowed_vars, integer_coefficients=True) return tuple(coefficient.numerator for coefficient in coefficients), constant
def _operation(parts: Sequence[str]) -> AffineOperation: rows = [] translations = [] for part in parts: row, translation = _parse_linear_expression(part, ("x", "y", "z")) rows.append(row) translations.append(translation) return AffineOperation(rows, translations)
[docs] def operation_from_xyz(operation: str) -> AffineOperation: """Parse an exact three-coordinate crystallographic operation. :param operation: The comma-separated ``x,y,z`` operation string. :return: The exact affine operation. :raises ValueError: If the operation does not contain three valid coordinate expressions. """ parts = tuple(part.strip() for part in operation.split(",")) if len(parts) != 3: raise ValueError(f"Expected three coordinates in symmetry operation: {operation!r}") return _operation(parts)
[docs] def operation_from_xyzt(operation: str) -> tuple[AffineOperation, int]: """Parse an exact magnetic operation and return its time-reversal flag. The first three comma-separated fields use crystallographic ``x,y,z`` grammar; the fourth field must be ``+1`` or ``-1``. :param operation: The comma-separated ``x,y,z,time-reversal`` operation string. :return: The affine operation and its time-reversal sign. :raises ValueError: If the operation does not contain three coordinate expressions and a valid time-reversal flag. """ parts = tuple(part.strip() for part in operation.split(",")) if len(parts) != 4: raise ValueError(f"Expected three coordinates and a time-reversal flag: {operation!r}") try: time_reversal = int(parts[3]) except ValueError as error: raise ValueError(f"Invalid time-reversal flag in {operation!r}") from error if time_reversal not in (-1, 1): raise ValueError(f"Time-reversal flag must be +1 or -1 in {operation!r}") return _operation(parts[:3]), time_reversal