"""UnitcellStructure backend for the three exact native storage records."""
from functools import cached_property
from typing import Any, Self, cast
from httk.atomistic.composition import Assembly
from httk.atomistic.models.cell.cell import Cell
from httk.atomistic.models.cell.view import CellView
from httk.atomistic.models.formula.composition_view import CompositionView
from httk.atomistic.models.formula.record import RecordComposition
from httk.atomistic.models.sites.sites import Sites
from httk.atomistic.models.sites.view import SitesView
from httk.atomistic.models.species.species import Species
from httk.atomistic.models.species.view import SpeciesView
from httk.atomistic.models.structure.asu import ASUStructure, FundamentalDomainStructure
from httk.atomistic.models.structure.backend import StructureBackend
from httk.atomistic.models.structure.semantics import StructureSymmetry
from httk.atomistic.storage.records import (
ASUStructureRecord,
FundamentalDomainStructureRecord,
UnitcellStructureRecord,
_assembly_from_record,
_chemical_composition_from_record,
_domain_structure_from_record,
_moment_from_record,
_symmetry_from_record,
)
[docs]
class RecordStructure(StructureBackend):
"""Expose a storage record through the existing structure view family.
Unit-cell records expose their stored components directly; fundamental-domain and
asymmetric-unit records expand through the native domain structure when a unit-cell
view is requested.
:param obj: The unit-cell, fundamental-domain, or asymmetric-unit record.
:param \\*\\*hints: Backend-selection hints.
"""
_record: UnitcellStructureRecord | FundamentalDomainStructureRecord | ASUStructureRecord
@classmethod
def _backend_adopt(cls, obj: Any, **hints: Any) -> Self | None:
r"""Adopt a structure record.
: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", "record") != "record":
return None
if not isinstance(obj, (UnitcellStructureRecord, FundamentalDomainStructureRecord, ASUStructureRecord)):
return None
return cls(obj, **hints)
def __init__(
self,
obj: UnitcellStructureRecord | FundamentalDomainStructureRecord | ASUStructureRecord,
**hints: Any,
) -> None:
self._record = obj
@cached_property
[docs]
def composition(self) -> CompositionView:
"""Expose the record's authoritative normalized composition."""
return CompositionView(RecordComposition(self._record.normalized_composition))
@cached_property
def _native(self) -> Any:
record = self._record
assert isinstance(record, (FundamentalDomainStructureRecord, ASUStructureRecord))
return _domain_structure_from_record(record)
@cached_property
def _expanded(self) -> Any:
from httk.atomistic.models.structure.unitcell_view import UnitcellStructureView
return UnitcellStructureView(self._native)
@property
def _is_unitcell(self) -> bool:
return isinstance(self._record, UnitcellStructureRecord)
@cached_property
[docs]
def cell(self) -> Cell: # pyright: ignore[reportIncompatibleMethodOverride]
"""Expose the record-backed cell.
:return: The cell, directly from a unit-cell record or from the expanded domain.
"""
# These wrap sites hold record components by construction, so the kind
# hint selects the record backend without probing the raw-input ones.
return CellView(cast(Any, self._record.cell), kind="record") if self._is_unitcell else self._native.cell
@cached_property
[docs]
def sites(self) -> Sites: # pyright: ignore[reportIncompatibleMethodOverride]
"""Expose the record-backed sites.
:return: The sites, directly from a unit-cell record or from the expanded domain.
"""
return (
SitesView(cast(Any, self._record.sites), kind="record")
if isinstance(self._record, UnitcellStructureRecord)
else self._expanded.sites
)
@cached_property
[docs]
def species(self) -> tuple[Species, ...]: # pyright: ignore[reportIncompatibleMethodOverride]
"""Expose the record-backed distinct species.
:return: The species referenced by the structure.
"""
return (
tuple(SpeciesView(cast(Any, value), kind="record") for value in self._record.species)
if self._is_unitcell
else self._native.species
)
@cached_property
[docs]
def species_at_sites(self) -> tuple[str, ...]: # pyright: ignore[reportIncompatibleMethodOverride]
"""Expose the species occupying each record-backed site.
:return: Site species names in site order.
"""
return (
self._record.species_at_sites
if isinstance(self._record, UnitcellStructureRecord)
else self._expanded.species_at_sites
)
@cached_property
[docs]
def site_moments(self) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
"""Expose optional record-backed site moments.
:return: Site moments, or ``None`` when they are unstated.
"""
if isinstance(self._record, UnitcellStructureRecord):
return _moment_from_record(
self._record.site_moments_kind,
self._record.site_moments,
self._record.site_moments_precision,
self.cell,
)
return self._expanded.site_moments
@property
[docs]
def charge(self) -> Any:
"""Expose the record's explicitly assigned charge.
:return: The assigned charge, or ``None`` when it is unstated.
"""
return getattr(self._record, "charge", None)
@property
[docs]
def molecular(self) -> bool:
"""Expose whether the record describes a molecular unit cell.
:return: Whether molecular semantics are enabled.
"""
return self._record.molecular
@property
[docs]
def assemblies(self) -> tuple[Assembly, ...] | None:
"""Expose the record's site assemblies.
:return: Assemblies, or ``None`` when they are unstated.
"""
if self._is_unitcell:
return (
None
if self._record.assemblies is None
else tuple(_assembly_from_record(value) for value in self._record.assemblies)
)
return self._expanded.assemblies
@property
[docs]
def symmetry(self) -> StructureSymmetry | None:
"""Expose the record's symmetry metadata.
:return: Symmetry metadata, or ``None`` when it is absent.
"""
if isinstance(self._record, UnitcellStructureRecord):
return None if self._record.symmetry is None else _symmetry_from_record(self._record.symmetry)
native = self._native
assert isinstance(native, FundamentalDomainStructure)
positions = native.wyckoff_positions
expanded_positions = (
None
if positions is None
else tuple(letter for letter, count in zip(positions, native.multiplicities()) for _ in range(count))
)
return StructureSymmetry(
native.space_group_it_number,
native.space_group_symbol_hall,
native.space_group_symbol_hermann_mauguin,
native.space_group_symbol_hermann_mauguin_extended,
native.space_group_symmetry_operations_xyz,
expanded_positions,
)
@property
[docs]
def chemical_composition(self) -> Any:
"""Expose the record's chemical composition.
:return: Chemical composition, or ``None`` when it is absent.
"""
return (
None
if self._record.chemical_composition is None
else _chemical_composition_from_record(self._record.chemical_composition)
)
@property
@property
@property
[docs]
def optimization_type(self) -> str | None:
"""Expose the record's optimization provenance.
:return: The optimization type, or ``None`` when it is absent.
"""
return self._record.optimization_type
@property
[docs]
def immutable_id(self) -> str | None:
"""Expose the record's immutable source identifier.
:return: The identifier, or ``None`` when it is absent.
"""
return self._record.immutable_id
@property
[docs]
def last_modified(self) -> Any:
"""Expose the record's modification timestamp.
:return: The timestamp, or ``None`` when it is absent.
"""
return self._record.last_modified
@property
[docs]
def asu(self) -> ASUStructure | FundamentalDomainStructure | None:
"""Expose the native domain structure when the record stores one.
:return: The native asymmetric or fundamental domain, or ``None`` for unit-cell records.
"""
if isinstance(self._record, (ASUStructureRecord, FundamentalDomainStructureRecord)):
return self._native
return None
[docs]
def unwrap(self) -> UnitcellStructureRecord | FundamentalDomainStructureRecord | ASUStructureRecord:
"""Return the exact fetched record rather than a reconstructed structure.
:return: The original storage record.
"""
return self._record