"""The structure-to-protostructure recognition adapter."""
from functools import cached_property
from typing import Any, Self
from httk.core import unwrap
from httk.atomistic.models.formula.backend import ChemicalFormulaBackend
from httk.atomistic.models.formula.view_base import ChemicalFormulaViewBase
from httk.atomistic.models.protostructure.backend import ProtostructureBackend
from httk.atomistic.models.protostructure.occupation import WyckoffOccupation
from httk.atomistic.models.protostructure.protostructure import Protostructure
from httk.atomistic.models.prototype.backend import AnonymousStructureBackend
from httk.atomistic.models.prototype.view_base import AnonymousStructureViewBase
from httk.atomistic.models.structure.asu import FundamentalDomainStructure
from httk.atomistic.models.structure.backend import StructureBackend
from httk.atomistic.models.structure.view import StructureView
from httk.atomistic.symmetry.recognition import recognize_asu
[docs]
class RecognizedProtostructure(ProtostructureBackend):
r"""Project an ordinary structure lazily to a protostructure.
:param obj: The ordinary structure to recognize.
:param \*\*hints: Backend-selection hints.
"""
_structure: StructureBackend
_setting: Any
_standard: Any
_transform: Any
_tolerance: float | None
_limit_denominator: int | None
@staticmethod
def _source_hints(hints: dict[str, Any]) -> dict[str, Any]:
return {
name: value
for name, value in hints.items()
if name not in {"kind", "setting", "standard", "transform", "tolerance", "limit_denominator"}
}
@staticmethod
def _has_existing_asu(obj: Any) -> bool:
if isinstance(obj, FundamentalDomainStructure):
return True
if not isinstance(obj, StructureView):
return False
from httk.atomistic.models.structure.asu_view import ASUStructureView
return isinstance(obj, ASUStructureView) or isinstance(getattr(obj._backend, "_view", None), ASUStructureView)
@classmethod
def _backend_adopt(cls, obj: Any, **hints: Any) -> Self | None:
r"""Adopt and validate a recognized protostructure source.
:param obj: The source object to adopt.
:param \**hints: Backend-selection hints.
:return: An initialized backend, or ``None`` when this backend declines ``obj``.
"""
if hints and hints.get("kind", "structure") != "structure":
return None
setting = hints.get("setting")
standard = hints.get("standard")
transform = hints.get("transform")
if cls._has_existing_asu(obj) and any(
hints.get(name) is not None
for name in ("setting", "standard", "transform", "tolerance", "limit_denominator")
):
raise ValueError("ProtostructureView recognition arguments cannot be used with an existing ASU")
if setting is not None and (standard is not None or transform is not None):
raise TypeError("recognize_asu() takes either 'setting' or 'standard'/'transform', not both")
if setting is None and (standard is not None or transform is not None):
if standard is None or transform is None:
raise TypeError("recognize_asu() needs both 'standard' and 'transform' when either is given")
if not standard.is_standard_setting:
raise ValueError(f"'standard' must be an IT standard setting, got {standard.setting}")
if isinstance(obj, (AnonymousStructureBackend, AnonymousStructureViewBase)):
return None
if isinstance(obj, (ChemicalFormulaBackend, ChemicalFormulaViewBase)):
return None
if not isinstance(obj, (StructureView, StructureBackend)):
source_hints = cls._source_hints(hints)
try:
StructureBackend._select_backend(obj, **source_hints)
except TypeError as exc:
# Only StructureBackend.create's own no-match error means this probe declines;
# TypeErrors raised after a structure adapter matched are real input errors.
if str(exc) == f"Cannot represent {type(obj)} as StructureBackend":
return None
raise
return cls(obj, **hints)
def __init__(self, obj: Any, **hints: Any) -> None:
if isinstance(obj, StructureView):
self._structure = obj._backend
elif isinstance(obj, StructureBackend):
self._structure = obj
else:
self._structure = StructureBackend._select_backend(obj, **self._source_hints(hints))
self._setting = hints.get("setting")
self._standard = hints.get("standard")
self._transform = hints.get("transform")
self._tolerance = hints.get("tolerance")
self._limit_denominator = hints.get("limit_denominator")
def _effective_structure(self) -> Any:
resolver = getattr(self._structure, "resolve", None)
return resolver() if resolver is not None else self._structure
@staticmethod
def _validate_structure(structure: Any) -> None:
for species in structure.species:
if "X" in species.chemical_symbols or "X" in (species.attached or ()):
raise ValueError(
f"Protostructure cannot represent structure species {species.name!r} with unknown symbol 'X'"
)
if getattr(structure, "assemblies", None) is not None:
raise ValueError("Protostructure cannot represent assemblies")
if getattr(structure, "chemical_composition", None) is not None:
raise ValueError("Protostructure cannot represent chemical_composition")
if isinstance(structure, FundamentalDomainStructure):
has_site_moments = any(site.moment is not None for site in structure.wyckoff_sites)
else:
has_site_moments = getattr(structure, "site_moments", None) is not None
if has_site_moments:
raise ValueError("Protostructure cannot represent site_moments")
def _has_recognition_options(self) -> bool:
return any(
value is not None
for value in (self._setting, self._standard, self._transform, self._tolerance, self._limit_denominator)
)
@cached_property
def _derived(self) -> Protostructure:
structure = self._effective_structure()
asu = structure if isinstance(structure, FundamentalDomainStructure) else getattr(structure, "asu", None)
if asu is not None and self._has_recognition_options():
raise ValueError("ProtostructureView recognition arguments cannot be used with an existing ASU")
self._validate_structure(structure)
if asu is None:
asu = recognize_asu(
structure,
setting=self._setting,
standard=self._standard,
transform=self._transform,
tolerance=self._tolerance,
limit_denominator=self._limit_denominator,
)
self._validate_structure(asu)
standard, sites = asu._standard_wyckoff_sites()
species_by_name = {species.name: species for species in asu.species}
occupations = tuple((site.wyckoff, species_by_name[site.species]) for site in sites)
return Protostructure(standard, occupations)
[docs]
def resolve(self) -> Protostructure:
"""Return the complete recognized protostructure."""
return self._derived
@property
[docs]
def spacegroup(self):
"""Return the recognized standard-setting space group."""
return self._derived.spacegroup
@property
[docs]
def occupations(self) -> tuple[WyckoffOccupation, ...]:
"""Return the recognized occupied Wyckoff positions."""
return self._derived.occupations
[docs]
def unwrap(self) -> Any:
"""Return the original ordinary structure."""
return unwrap(self._structure)