"""
A view presenting any structure backend as a UnitcellStructure (the Unitcell representation).
"""
import fractions
from functools import cached_property
from typing import Any, Self
from httk.core import unwrap
from httk.core.optimade import IncompleteOptimadeResourceError
from httk.atomistic.composition import Assembly
from httk.atomistic.models.cell.cell import Cell
from httk.atomistic.models.moments.backend import SiteMomentsBackend
from httk.atomistic.models.sites.sites import Sites
from httk.atomistic.models.species.species import Species
from httk.atomistic.models.structure.asu import FundamentalDomainStructure
from httk.atomistic.models.structure.backend import StructureBackend
from httk.atomistic.models.structure.like import StructureLike
from httk.atomistic.models.structure.semantics import _METADATA_UNSET, _resolve_view_metadata, _semantic_value
from httk.atomistic.models.structure.unitcell import (
UnitcellStructure,
_check_site_moments,
_check_sites_length,
_check_species_at_sites,
_check_species_names,
_norm_cell,
_norm_site_moments,
_norm_sites,
_norm_species,
_norm_species_at_sites,
)
from httk.atomistic.models.structure.view import StructureView
[docs]
class UnitcellStructureView(StructureView, UnitcellStructure):
r"""
A view presenting an underlying structure backend as a ``UnitcellStructure``.
This view is a genuine ``UnitcellStructure``, so it can be passed anywhere a UnitcellStructure
is accepted. Each component is normalized lazily on first access. For an
ASU-backed view, accessing ``cell`` or ``species`` never triggers expansion.
An ASU backend is expanded when unit-cell sites, species-at-sites, moments, or
assemblies are requested. Assembly correlations that cannot be mapped from the
fundamental domain to the full cell raise an error.
:param obj: The structure backend or source to present.
:param \**hints: Backend-selection and metadata options passed to construction.
:raises httk.core.optimade.entries.IncompleteOptimadeResourceError: If the source
declares a coordinate span that cannot be projected as a native unit cell.
"""
_backend: StructureBackend
_deferred_immutable_id: str | None | object
_deferred_last_modified: Any
_effective_backend_cache: StructureBackend | None
def __new__(
cls,
obj: StructureLike,
*,
immutable_id: str | None | object = _METADATA_UNSET,
last_modified: Any = _METADATA_UNSET,
**hints: Any,
) -> Self:
if isinstance(obj, cls):
if immutable_id is _METADATA_UNSET and last_modified is _METADATA_UNSET:
return obj
backend = obj._backend
resolver = getattr(backend, "resolve", None)
if resolver is None:
resolved_immutable_id, resolved_last_modified = _resolve_view_metadata(
obj,
immutable_id=immutable_id,
last_modified=last_modified,
)
if (resolved_immutable_id, resolved_last_modified) == (obj.immutable_id, obj.last_modified):
return obj
cls._validate_span(backend)
else:
resolved_immutable_id, resolved_last_modified = None, None
immutable_id = (
getattr(obj, "_deferred_immutable_id", _METADATA_UNSET)
if immutable_id is _METADATA_UNSET
else immutable_id
)
last_modified = (
getattr(obj, "_deferred_last_modified", _METADATA_UNSET)
if last_modified is _METADATA_UNSET
else last_modified
)
else:
backend = cls._prepare_backend(obj, hints)
resolver = getattr(backend, "resolve", None)
if resolver is not None:
resolved_immutable_id, resolved_last_modified = None, None
else:
resolved_immutable_id, resolved_last_modified = _resolve_view_metadata(
obj,
immutable_id=immutable_id,
last_modified=last_modified,
)
cls._validate_span(backend)
instance = super().__new__(cls)
instance._backend = backend
instance._immutable_id = resolved_immutable_id
instance._last_modified = resolved_last_modified
instance._effective_backend_cache = None
if resolver is not None:
instance._deferred_immutable_id = immutable_id
instance._deferred_last_modified = last_modified
return instance
@staticmethod
def _validate_span(backend: StructureBackend) -> None:
span = getattr(backend, "site_coordinate_span", None)
if span in {
"fundamental_domain",
"asymmetric_unit",
"molecular_fundamental_domain",
"molecular_asymmetric_unit",
"molecular_entities",
"other",
} and not isinstance(backend, FundamentalDomainStructure):
raise IncompleteOptimadeResourceError(
f"site_coordinate_span={span!r} cannot be projected as a native unit-cell UnitcellStructure view"
)
def _effective_backend(self) -> StructureBackend:
cached = self._effective_backend_cache
if cached is not None:
return cached
resolver = getattr(self._backend, "resolve", None)
if resolver is None:
return self._backend
backend = resolver()
immutable_id, last_modified = _resolve_view_metadata(
backend,
immutable_id=self._deferred_immutable_id,
last_modified=self._deferred_last_modified,
)
self._validate_span(backend)
object.__setattr__(self, "_immutable_id", immutable_id)
object.__setattr__(self, "_last_modified", last_modified)
self._effective_backend_cache = backend
return backend
def _metadata(self, name: str, default: Any = None) -> Any:
return _semantic_value(self._effective_backend(), name, default)
def __init__(self, obj: StructureLike, **hints: Any) -> None:
pass
def _fill_cell(self) -> None:
object.__setattr__(self, "_cell", _norm_cell(self._effective_backend().cell))
def _fill_species(self) -> None:
species = _norm_species(self._effective_backend().species)
_check_species_names(species)
object.__setattr__(self, "_species", species)
def _fill_species_at_sites(self) -> None:
# Exception to the no-shadowed-read rule: this cheap dependency is acyclic because
# species never reads species_at_sites.
species_at_sites = _norm_species_at_sites(self._effective_backend().species_at_sites)
_check_species_at_sites(species_at_sites, self._species)
object.__setattr__(self, "_species_at_sites", species_at_sites)
def _fill_sites(self) -> None:
sites = _norm_sites(self._effective_backend().sites)
_check_sites_length(sites, self._species_at_sites)
object.__setattr__(self, "_sites", sites)
def _fill_site_moments(self) -> None:
value = _norm_site_moments(self._effective_backend().site_moments)
_check_site_moments(value, self.sites, self.cell)
object.__setattr__(self, "_site_moments", value)
@cached_property
def _cell(self) -> Cell: # type: ignore[override] # pyright: ignore[reportIncompatibleVariableOverride]
self._fill_cell()
return self.__dict__["_cell"]
@cached_property
def _sites(self) -> Sites: # type: ignore[override] # pyright: ignore[reportIncompatibleVariableOverride]
self._fill_sites()
return self.__dict__["_sites"]
@cached_property
def _species(self) -> tuple[Species, ...]: # type: ignore[override] # pyright: ignore[reportIncompatibleVariableOverride]
self._fill_species()
return self.__dict__["_species"]
@cached_property
def _species_at_sites(self) -> tuple[str, ...]: # type: ignore[override] # pyright: ignore[reportIncompatibleVariableOverride]
self._fill_species_at_sites()
return self.__dict__["_species_at_sites"]
@cached_property
def _site_moments(self) -> SiteMomentsBackend | None: # type: ignore[override] # pyright: ignore[reportIncompatibleVariableOverride]
self._fill_site_moments()
return self.__dict__["_site_moments"]
@property
[docs]
def site_moments(self) -> SiteMomentsBackend | None:
"""Expose the site's magnetic moments."""
return self._site_moments
@property
[docs]
def charge(self) -> fractions.Fraction | None:
"""Expose the explicitly assigned charge."""
return self._effective_backend().charge
[docs]
def unwrap(self) -> Any:
"""Return the raw value wrapped by the backend.
:return: The original source value.
"""
return unwrap(self._backend)
[docs]
def unview(self) -> UnitcellStructure:
"""Materialize this presentation as a standalone unit-cell structure.
:return: The exact unit-cell structure represented by this view.
"""
# A genuine UnitcellStructure backend carrying the same metadata is exactly the presented
# value: reuse it. Otherwise (other backends, ASU expansion, or view-level metadata)
# materialize a plain UnitcellStructure from the presented components.
backend = self._effective_backend()
if type(backend) is UnitcellStructure and (self.immutable_id, self.last_modified) == (
backend.immutable_id,
backend.last_modified,
):
return backend
return UnitcellStructure(
self.cell,
self.sites,
self.species,
self.species_at_sites,
site_moments=self.site_moments,
molecular=self.molecular,
assemblies=self.assemblies,
symmetry=self.symmetry,
chemical_composition=self.chemical_composition,
chemical_formula_descriptive=self.chemical_formula_descriptive,
chemical_formula_hill=self.chemical_formula_hill,
optimization_type=self.optimization_type,
immutable_id=self.immutable_id,
last_modified=self.last_modified,
charge=self.charge,
)
@property
[docs]
def immutable_id(self) -> str | None:
"""Expose the immutable source identifier."""
self._effective_backend()
return _semantic_value(self, "immutable_id", private_name="_immutable_id")
@property
[docs]
def last_modified(self) -> Any:
"""Expose the source modification timestamp."""
self._effective_backend()
return _semantic_value(self, "last_modified", private_name="_last_modified")
@property
[docs]
def molecular(self) -> bool:
"""Expose whether the presented structure is molecular."""
return bool(self._metadata("molecular", False))
@property
[docs]
def site_coordinate_span(self) -> str:
"""Expose the unit-cell coordinate span."""
self._effective_backend()
return "molecular_unit_cell" if self.molecular else "unit_cell"
@property
[docs]
def symmetry(self) -> Any:
"""Expose the optional symmetry metadata."""
return self._metadata("symmetry")
@property
[docs]
def assemblies(self) -> tuple[Assembly, ...] | None:
"""Expose site correlations in the presented unit cell."""
backend = self._effective_backend()
if "_assemblies" in self.__dict__:
return _semantic_value(self, "assemblies", private_name="_assemblies")
if isinstance(backend, FundamentalDomainStructure) and "_assemblies" not in self.__dict__:
return backend._expanded_assemblies()
return self._metadata("assemblies")
@property
[docs]
def chemical_composition(self) -> Any:
"""Expose the optional chemical composition metadata."""
return self._metadata("chemical_composition")
@property
@property
@property
[docs]
def optimization_type(self) -> str | None:
"""Expose the optional optimization provenance."""
return self._metadata("optimization_type")
@property
[docs]
def site_coordinate_span_description(self) -> str | None:
"""Expose the optional coordinate-span description."""
return self._metadata("site_coordinate_span_description")
@property
[docs]
def space_group_it_number(self) -> int | None:
"""Expose the optional space-group number."""
return self._space_group_metadata("space_group_it_number")
@property
[docs]
def space_group_symbol_hall(self) -> str | None:
"""Expose the optional Hall symbol."""
return self._space_group_metadata("space_group_symbol_hall")
@property
[docs]
def space_group_symbol_hermann_mauguin(self) -> str | None:
"""Expose the optional Hermann–Mauguin symbol."""
return self._space_group_metadata("space_group_symbol_hermann_mauguin")
@property
[docs]
def space_group_symbol_hermann_mauguin_extended(self) -> str | None:
"""Expose the optional extended Hermann–Mauguin symbol."""
return self._space_group_metadata("space_group_symbol_hermann_mauguin_extended")
@property
[docs]
def space_group_symmetry_operations_xyz(self) -> tuple[str, ...] | None:
"""Expose the optional symmetry operations in ``xyz`` notation."""
value = self._space_group_metadata("space_group_symmetry_operations_xyz")
return value if value is not None else (("x,y,z",) if self.nperiodic_dimensions else None)
@property
[docs]
def wyckoff_positions(self) -> tuple[str, ...] | None:
"""Expose the optional Wyckoff positions."""
return self._space_group_metadata("wyckoff_positions")
def _space_group_metadata(self, name: str) -> Any:
if getattr(self._backend, "resolve", None) is None:
symmetry = _semantic_value(self, "symmetry", private_name="_symmetry")
else:
backend = self._effective_backend()
value = getattr(backend, name, None)
if value is not None:
return value
symmetry = getattr(backend, "symmetry", None)
return None if symmetry is None else getattr(symmetry, name, None)