"""Frozen storage records for complete atomistic structures."""
import fractions
import math
from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, Any, NoReturn, cast
import httk.core.storage_markers
from httk.core import FracVector, SurdScalar, SurdVector
from .species import Species
if TYPE_CHECKING:
from .structure import Structure
from .structure_like import StructureLike
class _FrozenList(list[bool]):
"""A list-shaped storage value that rejects every mutation."""
def _reject(self, *args: Any, **kwargs: Any) -> NoReturn:
raise TypeError("StructureRecord.periodicity is immutable")
def __setitem__(self, key: Any, value: Any) -> NoReturn:
self._reject()
def __delitem__(self, key: Any) -> NoReturn:
self._reject()
def append(self, value: bool) -> NoReturn:
self._reject()
def clear(self) -> NoReturn:
self._reject()
def extend(self, values: Any) -> NoReturn:
self._reject()
def insert(self, index: Any, value: bool) -> NoReturn:
self._reject()
def pop(self, index: Any = -1) -> NoReturn:
self._reject()
def remove(self, value: bool) -> NoReturn:
self._reject()
def reverse(self) -> NoReturn:
self._reject()
def sort(self, *args: Any, **kwargs: Any) -> NoReturn:
self._reject()
def __iadd__(self, values: Any) -> NoReturn: # type: ignore[misc]
self._reject()
def __imul__(self, value: Any) -> NoReturn: # type: ignore[misc]
self._reject()
def _extract_surd_scalar(vector: SurdVector, index: tuple[int, int]) -> SurdScalar:
"""Extract one exact scalar through SurdVector's public coefficient API."""
components = {radicand: vector.coefficient(radicand)[index].to_fraction() for radicand in vector.radicands}
return cast(SurdScalar, SurdVector.from_radicand_map(components))
@dataclass(frozen=True)
[docs]
class SpeciesRecord:
"""The storable frozen snapshot of an atomistic :class:`~httk.atomistic.Species`.
The presence flags preserve ``None`` for optional tuple fields because a relational child
table cannot distinguish a missing optional value from an empty tuple when reconstructing.
"""
[docs]
chemical_symbols: tuple[str, ...]
[docs]
concentration: tuple[float, ...]
[docs]
mass: tuple[float, ...] | None = None
[docs]
original_name: str | None = None
[docs]
attached: tuple[str, ...] | None = None
[docs]
nattached: tuple[int, ...] | None = None
[docs]
mass_present: bool = False
[docs]
attached_present: bool = False
def __post_init__(self) -> None:
object.__setattr__(self, "chemical_symbols", tuple(self.chemical_symbols))
object.__setattr__(self, "concentration", tuple(self.concentration))
if self.mass is not None:
object.__setattr__(self, "mass", tuple(self.mass))
if self.attached is not None:
object.__setattr__(self, "attached", tuple(self.attached))
if self.nattached is not None:
object.__setattr__(self, "nattached", tuple(self.nattached))
if not isinstance(self.mass_present, bool) or not isinstance(self.attached_present, bool):
raise TypeError("SpeciesRecord presence flags must be bool values")
if self.mass_present and self.mass is None:
raise ValueError("SpeciesRecord mass_present=True requires a mass tuple")
if not self.mass_present and self.mass:
raise ValueError("SpeciesRecord mass_present=False cannot have non-empty mass")
if (self.attached is None) != (self.nattached is None):
raise ValueError("SpeciesRecord attached and nattached must be provided together")
if self.attached_present and self.attached is None:
raise ValueError("SpeciesRecord attached_present=True requires attached and nattached tuples")
if not self.attached_present and (self.attached or self.nattached):
raise ValueError("SpeciesRecord attached_present=False cannot have non-empty attachments")
for field, values in (
("concentration", self.concentration),
("mass", self.mass or ()),
):
if any(not math.isfinite(value) for value in values):
raise ValueError(f"SpeciesRecord {field} values must be finite floats")
mass = self.mass if self.mass_present else None
attached = self.attached if self.attached_present else None
nattached = self.nattached if self.attached_present else None
try:
Species(
name=self.name,
chemical_symbols=self.chemical_symbols,
concentration=self.concentration,
mass=mass,
original_name=self.original_name,
attached=attached,
nattached=nattached,
)
except (TypeError, ValueError) as error:
raise ValueError("SpeciesRecord fields do not describe a valid Species") from error
object.__setattr__(self, "mass", mass)
object.__setattr__(self, "attached", attached)
object.__setattr__(self, "nattached", nattached)
@classmethod
[docs]
def from_species(cls, species: Species) -> "SpeciesRecord":
"""Create a storage record containing the exact species metadata."""
return cls(
name=species.name,
chemical_symbols=species.chemical_symbols,
concentration=species.concentration,
mass=species.mass,
original_name=species.original_name,
attached=species.attached,
nattached=species.nattached,
mass_present=species.mass is not None,
attached_present=species.attached is not None,
)
[docs]
def to_species(self) -> Species:
"""Reconstruct the domain :class:`~httk.atomistic.Species` from this snapshot."""
return Species(
name=self.name,
chemical_symbols=self.chemical_symbols,
concentration=self.concentration,
mass=self.mass if self.mass_present else None,
original_name=self.original_name,
attached=self.attached if self.attached_present else None,
nattached=self.nattached if self.attached_present else None,
)
@dataclass(frozen=True)
[docs]
class StructureRecord:
"""A storable snapshot of a complete :class:`~httk.atomistic.Structure`.
This record is a snapshot, not a live proxy: changing a source ``Structure`` does not
update it, and changing a record does not update a previously reconstructed structure.
Rationals, surd bases, precisions, and periodicity remain exact. Species float fields
round-trip at IEEE-double fidelity, with the SQL layer's documented caveat that ``-0.0``
may return as ``+0.0``.
``periodicity`` is annotated as a list because the storage schema supports homogeneous
variable-length sequences, not fixed-length tuples. It is copied and validated here so
callers cannot mutate the input list through the record. Construction also builds and
discards the domain ``Structure`` once, so invalid domain combinations cannot exist.
"""
[docs]
basis: tuple[SurdScalar, ...]
[docs]
reduced_coords: Annotated[FracVector, httk.core.storage_markers.Shape(0, 3)]
[docs]
species: tuple[SpeciesRecord, ...]
[docs]
species_at_sites: tuple[str, ...]
[docs]
periodicity: list[bool]
[docs]
basis_precision: fractions.Fraction | None
[docs]
coordinate_precision: fractions.Fraction | None
def __post_init__(self) -> None:
basis = tuple(self.basis)
if len(basis) != 9 or not all(isinstance(value, SurdScalar) for value in basis):
raise ValueError("StructureRecord basis must contain exactly 9 SurdScalar values")
if not isinstance(self.reduced_coords, FracVector):
raise TypeError("StructureRecord reduced_coords must be a FracVector")
species = tuple(self.species)
if not all(isinstance(value, SpeciesRecord) for value in species):
raise TypeError("StructureRecord species must contain SpeciesRecord values")
species_at_sites = tuple(self.species_at_sites)
periodicity = list(self.periodicity)
if len(periodicity) != 3 or not all(isinstance(value, bool) for value in periodicity):
raise ValueError("StructureRecord periodicity must contain exactly 3 bool values")
object.__setattr__(self, "basis", basis)
object.__setattr__(self, "reduced_coords", FracVector.use(self.reduced_coords))
object.__setattr__(self, "species", species)
object.__setattr__(self, "species_at_sites", species_at_sites)
object.__setattr__(self, "periodicity", _FrozenList(periodicity))
self.to_structure()
@classmethod
[docs]
def from_structure(cls, structure: "StructureLike") -> "StructureRecord":
"""Take a canonical snapshot of any :data:`~httk.atomistic.StructureLike` input."""
from .unitcell_structure_view import UnitcellStructureView
canonical = UnitcellStructureView(structure)
basis = canonical.cell.basis
basis_values = tuple(_extract_surd_scalar(basis, (row, column)) for row in range(3) for column in range(3))
return cls(
basis=basis_values,
reduced_coords=canonical.sites.reduced_coords,
species=tuple(SpeciesRecord.from_species(value) for value in canonical.species),
species_at_sites=canonical.species_at_sites,
periodicity=list(canonical.periodicity),
basis_precision=canonical.basis_precision,
coordinate_precision=canonical.coordinate_precision,
)
[docs]
def to_structure(self) -> "Structure":
"""Rebuild the exact :class:`~httk.atomistic.Structure` represented by this snapshot."""
from .cell import Cell
from .sites import Sites
from .structure import Structure
radicands = sorted({radicand for value in self.basis for radicand in value.radicands})
components = {
radicand: [
[self.basis[3 * row + column].coefficient(radicand).to_fraction() for column in range(3)]
for row in range(3)
]
for radicand in radicands
}
basis = SurdVector.from_radicand_map(components)
return Structure(
cell=Cell(basis, precision=self.basis_precision, periodicity=self.periodicity),
sites=Sites(self.reduced_coords, precision=self.coordinate_precision),
species=tuple(value.to_species() for value in self.species),
species_at_sites=self.species_at_sites,
)