Source code for httk.atomistic.integrations.vasp.structure
"""Lazy VASP POSCAR structures backed by neutral httk payloads."""
import os
from collections.abc import Mapping
from pathlib import Path
from typing import Any, ClassVar, Self
import httk.core
from httk.core.register import format_serializers
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.backend import StructureBackend
from httk.atomistic.models.structure.unitcell import UnitcellStructure
from httk.atomistic.models.structure.view import StructureView
[docs]
class VASPStructure(StructureBackend):
r"""Load a VASP POSCAR structure lazily.
This backend is explicitly constructed because a generic structure source should not
silently claim every POSCAR path.
It is not registered in ``backend_classes``. Constructing it from a view whose
unwrapped value is already a ``VASPStructure`` returns that backend by identity.
The payload's ``raw`` channel preserves the source representation for byte-exact
saving.
:param obj: A POSCAR path, neutral payload, or serializer-supported source.
:param \**hints: Backend-selection hints.
"""
[docs]
kind: ClassVar[str] = "vasp"
_source: Any
_payload: Mapping[str, Any] | None
_resolved: UnitcellStructure | None
_vasp_initialized: bool
def __new__(cls, obj: Any, **hints: Any) -> Self:
if isinstance(obj, cls):
return obj
if isinstance(obj, StructureView):
unwrapped = httk.core.unwrap(obj)
if isinstance(unwrapped, cls):
return unwrapped
backend = getattr(obj, "_backend", None)
if isinstance(backend, cls):
return backend
return super().__new__(cls)
@classmethod
def _backend_adopt(cls, obj: Any, **hints: Any) -> Self | None:
r"""Adopt a VASP structure 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.get("kind", cls.kind) != cls.kind:
return None
if isinstance(obj, cls):
return obj
if isinstance(obj, StructureView):
unwrapped = httk.core.unwrap(obj)
if isinstance(unwrapped, cls):
return unwrapped
backend = getattr(obj, "_backend", None)
if isinstance(backend, cls):
return backend
return cls(obj, **hints)
def __init__(self, obj: Any, **hints: Any) -> None:
if getattr(self, "_vasp_initialized", False):
return
self._source = obj
self._payload = None
self._resolved = None
if isinstance(obj, (str, os.PathLike)):
name = os.fsdecode(os.fspath(obj))
if not Path(name).exists():
raise FileNotFoundError(f"VASP structure source does not exist: {name!r}")
elif isinstance(obj, Mapping):
if obj.get("format") != "vasp-poscar":
raise ValueError("VASPStructure payload must have format 'vasp-poscar'.")
self._payload = obj
self._vasp_initialized = True
@property
[docs]
def payload(self) -> Mapping[str, Any]:
"""Return the original, loaded, or synthesized neutral POSCAR payload."""
if self._payload is None:
if isinstance(self._source, (str, os.PathLike)):
self._payload = httk.core.load(os.fsdecode(os.fspath(self._source)), raw=True)
else:
payload = format_serializers.dispatch("vasp-poscar", self._source)
if payload is None:
raise TypeError("VASP POSCAR serializer returned no payload")
self._payload = payload
assert self._payload is not None
return self._payload
@property
@property
[docs]
def selective_dynamics(self) -> Any:
"""Return selective-dynamics flags, if present."""
return self.payload.get("selective_dynamics")
[docs]
def resolve(self) -> UnitcellStructure:
"""Build and memoize the canonical structure from the POSCAR payload.
:return: The resolved unit-cell structure.
"""
if self._resolved is None:
from httk.atomistic._loading import _structure_from_poscar
self._resolved = _structure_from_poscar(self.payload)
return self._resolved
@property
[docs]
def cell(self) -> Cell:
"""Return the resolved cell."""
return self.resolve().cell
@property
[docs]
def sites(self) -> Sites:
"""Return the resolved reduced coordinates."""
return self.resolve().sites
@property
[docs]
def species(self) -> tuple[Species, ...]:
"""Return the resolved distinct species."""
return self.resolve().species
@property
[docs]
def species_at_sites(self) -> tuple[str, ...]:
"""Return the resolved species name at each site."""
return self.resolve().species_at_sites
@property
[docs]
def site_moments(self) -> SiteMomentsBackend | None:
"""Return resolved site moments, or ``None``."""
return self.resolve().site_moments
@property
[docs]
def charge(self) -> Any:
"""Return the resolved structure charge, if present."""
return self.resolve().charge
def __getattr__(self, name: str) -> Any:
if name.startswith("_"):
raise AttributeError(name)
return getattr(self.resolve(), name)
[docs]
def unwrap(self) -> Any:
"""Return the original POSCAR source."""
return self._source