Source code for httk.atomistic.models.crystalpattern.anonymize
"""Validation and canonical labelling helpers for anonymized structures."""
from collections.abc import Sequence
from fractions import Fraction
from typing import Any
from httk.atomistic.models.formula.notation import anonymous_symbol
from httk.atomistic.models.species.species import Species
[docs]
def dummy_species(label: str) -> Species:
"""Return the exact dummy species used by anonymous structures.
:param label: The anonymous symbol to use as the species name.
:return: The canonical dummy species.
"""
return Species(label, ("X",), (1,), labels=(label,))
[docs]
def is_dummy_species(species: Species) -> bool:
"""Return whether ``species`` has precisely the sanctioned dummy-species shape.
:param species: The species to inspect.
:return: Whether the species is a canonical dummy species.
"""
return (
species.name == (species.labels[0] if species.labels is not None and len(species.labels) == 1 else None)
and species.chemical_symbols == ("X",)
and species.concentration == (Fraction(1),)
and species.labels == (species.name,)
and species.mass is None
and species.attached is None
and species.nattached is None
and species.concentration_precision in (None, (None,))
and species.charges is None
and species.spins is None
and species.original_name is None
)
[docs]
def canonical_dummy_assignment(amounts: Sequence[tuple[str, Fraction | int]]) -> dict[str, str]:
"""Map element-like keys to anonymous symbols by descending amount.
:param amounts: The keys and represented site counts.
:return: The deterministic key-to-anonymous-symbol assignment.
"""
ordered = sorted(amounts, key=lambda item: (-item[1], item[0]))
return {element: anonymous_symbol(index) for index, (element, _) in enumerate(ordered)}
[docs]
def require_anonymizable(structure: Any) -> None:
"""Reject structure features outside this phase's deliberate scope.
Only fully occupied, single-real-element site species are anonymized. Future phases may
add merge or label-preserving modes for decorated species; this mode intentionally refuses
those cases, as well as assemblies, stated compositions, and site moments.
:param structure: The structure whose representation is being anonymized.
:raises ValueError: If the structure contains unsupported composition features.
"""
names = tuple(structure.species_at_sites)
by_name = {species.name: species for species in structure.species}
unused = sorted(set(by_name) - set(names))
if unused:
raise ValueError(f"cannot anonymize structure: species {unused[0]!r} is unused")
used: list[Species] = []
for name in names:
try:
species = by_name[name]
except KeyError as exc:
raise ValueError(f"cannot anonymize structure: unknown species {name!r}") from exc
if not species.is_single_element:
raise ValueError(f"cannot anonymize structure: species {name!r} is not a single real element")
used.append(species)
elements: dict[str, str] = {}
for species in used:
element = species.chemical_symbols[0]
previous = elements.get(element)
if previous is not None and previous != species.name:
raise ValueError(
f"cannot anonymize structure: distinct species {previous!r} and {species.name!r} share element {element!r}"
)
elements[element] = species.name
if getattr(structure, "assemblies", None) is not None:
raise ValueError("cannot anonymize structure: assemblies are not supported")
if getattr(structure, "chemical_composition", None) is not None:
raise ValueError("cannot anonymize structure: chemical_composition is not supported")
if getattr(structure, "site_moments", None) is not None:
raise ValueError("cannot anonymize structure: site_moments are not supported")