Source code for httk.atomistic.models.trajectory.trajectory
"""Store the native immutable trajectory representation."""
from collections.abc import Iterator, Mapping, Sequence
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, ClassVar
from httk.atomistic.models.species.species import Species
from httk.atomistic.models.structure.unitcell import UnitcellStructure
from httk.atomistic.models.trajectory.backend import TrajectoryBackend
if TYPE_CHECKING:
from httk.atomistic.models.structure.like import StructureLike
[docs]
class Trajectory(TrajectoryBackend):
"""Store an immutable trajectory in the native backend.
A trajectory requires at least one frame and keeps one constant composition
across all frames.
:param frames: Unit-cell structures to coerce and store.
:param observables: Optional per-frame observable values.
:param reference_frames: Optional indexes of bounded reference frames.
"""
[docs]
kind: ClassVar[str] = "native"
__httk_storage_record__: ClassVar[type[Any]]
_frames: tuple[UnitcellStructure, ...]
_observables: Mapping[str, tuple[Any, ...]]
_reference_frames: tuple[int, ...] | None
_species: tuple[Species, ...]
_species_at_sites: tuple[str, ...]
def __init__(
self,
frames: Sequence["StructureLike"],
observables: Mapping[str, Sequence[Any]] | None = None,
reference_frames: Sequence[int] | None = None,
) -> None:
if not frames:
raise ValueError("Trajectory requires at least one frame")
from httk.atomistic.models.structure.unitcell_view import UnitcellStructureView
coerced = tuple(UnitcellStructureView(frame).unview() for frame in frames)
species = coerced[0].species
species_at_sites = coerced[0].species_at_sites
for index, frame in enumerate(coerced[1:], 1):
if frame.species != species or frame.species_at_sites != species_at_sites:
raise ValueError(f"Trajectory frame {index} has a varying composition")
values = {} if observables is None else {name: tuple(value) for name, value in observables.items()}
for name, value in values.items():
if len(value) != len(coerced):
raise ValueError(f"Trajectory observable {name!r} has length {len(value)}, expected {len(coerced)}")
references: tuple[int, ...] | None = None
if reference_frames is not None:
checked: list[int] = []
for reference in reference_frames:
if not isinstance(reference, int) or isinstance(reference, bool):
raise ValueError(f"Trajectory reference frame {reference!r} is not an integer")
if not 0 <= reference < len(coerced):
raise ValueError(f"Trajectory reference frame {reference!r} is out of bounds")
checked.append(reference)
references = tuple(sorted(set(checked)))
self._frames = coerced
self._observables = MappingProxyType(values)
self._reference_frames = references
self._species = species
self._species_at_sites = species_at_sites
@property
[docs]
def nframes(self) -> int:
"""Return the number of stored frames."""
return len(self._frames)
[docs]
def frame(self, i: int) -> UnitcellStructure:
"""Return one stored frame by index.
:param i: Frame index.
:return: The requested unit-cell structure.
:raises IndexError: If the index is out of range.
"""
return self._frames[i]
def __repr__(self) -> str:
parts = [f"frames=(... {len(self._frames)} frame(s) ...)"]
if self._observables:
parts.append(f"observables={tuple(self._observables)!r}")
if self._reference_frames is not None:
parts.append(f"reference_frames={self._reference_frames!r}")
return f"Trajectory({', '.join(parts)})"
[docs]
def frames(self) -> Iterator[UnitcellStructure]:
"""Iterate over the stored frames.
:return: An iterator of unit-cell structures.
"""
return iter(self._frames)
@property
[docs]
def reference_frames(self) -> tuple[int, ...] | None:
"""Return the bounded reference-frame indexes, or ``None``."""
return self._reference_frames
@property
[docs]
def species(self) -> tuple[Species, ...]:
"""Return the constant distinct species."""
return self._species
@property
[docs]
def species_at_sites(self) -> tuple[str, ...]:
"""Return the constant species name at each site."""
return self._species_at_sites
@property
[docs]
def observable_names(self) -> tuple[str, ...]:
"""Return the names of stored observables."""
return tuple(self._observables)
[docs]
def observable(self, name: str) -> tuple[Any, ...]:
"""Return one observable's values in frame order.
:param name: Observable name.
:return: The observable values.
:raises KeyError: If the observable is unavailable.
"""
try:
return self._observables[name]
except KeyError:
raise KeyError(name) from None