Source code for httk.atomistic.symmetry.standardization

"""Express an asymmetric-unit structure in its IT standard-setting cell.

The operation is exact after any optional recognition step. A setting-local ASU is mapped
to the standard Wyckoff table only here, because this operation explicitly requests the
standard conventional cell. An untabulated ASU instead uses its stored exact transform.
"""

import fractions
from dataclasses import dataclass

from httk.core import unwrap

from httk.atomistic.models.cell.cell import Cell
from httk.atomistic.models.structure.asu import ASUStructure
from httk.atomistic.models.structure.like import StructureLike
from httk.atomistic.models.structure.semantics import initialize_semantics
from httk.atomistic.models.structure.unitcell import UnitcellStructure
from httk.atomistic.models.structure.unitcell_view import UnitcellStructureView
from httk.atomistic.symmetry.recognition import recognize_asu
from httk.atomistic.symmetry.setting_transform import SettingTransform
from httk.atomistic.symmetry.spacegroup import Spacegroup

from ._standardization_common import (
    _as_existing_asu,
    _exact_site_bijection,
    _matrix_column_sum_factor,
    _matrix_row_sum_factor,
    _remap_assemblies,
    _scaled_composition,
    _scaled_precision,
    _semantic_value,
)

__all__ = ["ConventionalCellResult", "conventional_cell"]


@dataclass(frozen=True, slots=True)
[docs] class ConventionalCellResult: """Store a structure in its space group's IT standard-setting conventional cell. ``asu`` is the new standard-setting ASU that was expanded to make ``structure``. ``transform`` is the standard-to-own transform from the ASU that was supplied to, or recognized from, the operation; its orientation is :math:`f_own = f_std M^T + v`, so this operation undoes it for the cell basis. ``multiplier`` is the exact ratio of conventional-cell site count to input-cell site count. For the 527 vendored settings it is at least one; an untabulated, caller-supplied supercell transform may still produce a ratio below one. :param structure: The resulting full conventional-cell structure. :param asu: The resulting asymmetric-unit structure in the standard setting. :param spacegroup: The space group represented by the result. :param transform: The standard-to-own transform used for the input structure. :param multiplier: The exact ratio of result site count to input site count. """
[docs] structure: UnitcellStructure
[docs] asu: ASUStructure
[docs] spacegroup: Spacegroup
[docs] transform: SettingTransform
[docs] multiplier: fractions.Fraction
[docs] def conventional_cell( structure: StructureLike | ASUStructure, *, tolerance: float | None = None, limit_denominator: int | None = None, ) -> ConventionalCellResult: """Return ``structure`` in its space group's IT standard-setting conventional cell. An existing :class:`~httk.atomistic.ASUStructure` (including an :class:`~httk.atomistic.ASUStructureView`, an ASU backend, or a full-cell view backed by one) is used exactly as stored. Supplying ``tolerance`` or ``limit_denominator`` for that path raises :class:`ValueError`, because those arguments belong to recognition. Any other :class:`~httk.atomistic.StructureLike` is first passed to :func:`~httk.atomistic.recognize_asu`. That tolerant step may snap measured coordinates onto symmetry positions and chooses the transform recorded in the result; it does not preserve an unstated input transform or promise a :func:`~httk.atomistic.same_crystal` match to noisy input coordinates. The optional tolerance is a Cartesian matching distance and the optional denominator limit idealises free parameters. The returned ``transform`` is the existing ASU's transform, or the transform chosen by recognition for a plain input; the returned ``asu`` has an identity transform. Construction and expansion are exact, including the rhombohedral case where the standard hexagonal cell contains three primitive cells. Basis precision is multiplied by ``M.T()`` and coordinate precision by the maximum absolute column sum of ``inv(M.T())``; unknown precision remains unknown. Requires a fully 3D-periodic structure. :param structure: The structure or asymmetric-unit structure to standardize. :param tolerance: The Cartesian recognition tolerance, or ``None`` to derive it. :param limit_denominator: The maximum denominator for idealised free parameters, or ``None`` to retain their exact stated values. :return: The standardized structure and transform metadata. :raises ImportError: If recognition is needed and the optional spglib dependency is unavailable. :raises ValueError: If recognition arguments are supplied for an existing ASU, the structure is not fully periodic, or unsupported site moments are present. """ original = UnitcellStructureView(structure) asu = _as_existing_asu(structure) had_existing_asu = asu is not None source: object = original if asu is not None else unwrap(original) source_molecular = bool(_semantic_value(source, original, "molecular", False)) source_assemblies = _semantic_value(source, original, "assemblies") if source_assemblies is not None: source_assemblies = tuple(source_assemblies) source_composition = _semantic_value(source, original, "chemical_composition") source_descriptive = _semantic_value(source, original, "chemical_formula_descriptive") source_hill = _semantic_value(source, original, "chemical_formula_hill") source_optimization = _semantic_value(source, original, "optimization_type") if asu is not None: if tolerance is not None or limit_denominator is not None: raise ValueError("conventional_cell() tolerance and limit_denominator cannot be used with an existing ASU") else: asu = recognize_asu( original, tolerance=tolerance, limit_denominator=limit_denominator, ) assert asu is not None if any(site.moment is not None for site in asu.wyckoff_sites): # ponytail: refusal; carry exact cartesian moments through once setting-change frame invariance is pinned by a test raise ValueError( "conventional_cell does not yet support structures with site moments; keep the original setting" ) transform = asu.transform_from_standard standard, standard_sites = asu._standard_wyckoff_sites() basis_matrix = transform.matrix.T() coordinate_matrix = basis_matrix.inv() new_cell_precision = _scaled_precision( asu.cell.precision, _matrix_row_sum_factor(basis_matrix), ) new_coordinate_precision = _scaled_precision( asu.coordinate_precision, _matrix_column_sum_factor(coordinate_matrix), ) new_cell = Cell( transform.basis_to_standard(asu.cell.basis), precision=new_cell_precision, periodicity=asu.cell.periodicity, ) def _standard_asu(charge: fractions.Fraction | None = None) -> ASUStructure: return ASUStructure( new_cell, standard, standard_sites, asu.species, transform=SettingTransform.identity(), coordinate_precision=new_coordinate_precision, molecular=source_molecular, # An existing reduced representation indexes assemblies against its domain. # Plain input assemblies index the full unit cell and are remapped below. assemblies=asu.assemblies if had_existing_asu else None, charge=charge, ) standard_asu = _standard_asu() result_structure = UnitcellStructureView(standard_asu) original_count = len(original.sites) if original_count == 0: raise ValueError("conventional_cell() cannot determine a site-count multiplier for an empty structure") multiplier = fractions.Fraction(len(result_structure.sites), original_count) if original.charge is not None: # The multiplier is only known after the first expansion, so rebuild with scaled charge. standard_asu = _standard_asu(original.charge * multiplier) result_structure = UnitcellStructureView(standard_asu) # All tabulated transforms have determinant 1 or 3. Keep this invariant explicit while # allowing a caller-supplied untabulated transform to describe a larger own cell. if asu.setting() is not None: assert multiplier >= 1 if had_existing_asu: result_assemblies = result_structure.assemblies else: mapping = _exact_site_bijection(original, result_structure, transform) result_assemblies = _remap_assemblies(source_assemblies, mapping) scaled_composition = _scaled_composition(source_composition, multiplier) if source_composition is not None else None # The returned ASU is part of the public transform result, so retain every annotation # it can express directly. Full-cell assemblies from a plain input remain on the # expanded structure because their indices do not name domain sites. initialize_semantics( standard_asu, nsites=len(standard_asu.wyckoff_sites), molecular=source_molecular, assemblies=asu.assemblies if had_existing_asu else None, symmetry=None, chemical_composition=scaled_composition, chemical_formula_descriptive=source_descriptive, chemical_formula_hill=source_hill, optimization_type=source_optimization, ) initialize_semantics( result_structure, nsites=len(result_structure.sites), molecular=source_molecular, assemblies=result_assemblies, symmetry=None, chemical_composition=scaled_composition, chemical_formula_descriptive=source_descriptive, chemical_formula_hill=source_hill, optimization_type=source_optimization, ) return ConventionalCellResult( result_structure, standard_asu, standard, transform, multiplier, )