Source code for httk.atomistic.compat
"""ASE interoperability for the :mod:`httk.atomistic` structure family.
The protocol deliberately describes only the four ASE ``Atoms`` methods needed for
conversion. ASE is optional: duck-typed objects can be converted without installing
ASE, while :class:`~httk.atomistic.ase_atoms_view.ASEAtomsView` is available only when
ASE itself is installed.
"""
from typing import Any, Protocol, runtime_checkable
from .cell import Cell
from .elements import symbol_of
from .sites import Sites
from .species import Species
from .structure import Structure
from .structure_backend import StructureBackend
from .structure_view import StructureView
@runtime_checkable
[docs]
class ASEAtomsProtocol(Protocol):
"""The minimal method surface needed to treat an object as ASE ``Atoms``.
This is a runtime-checkable, duck-typed protocol. ASE is not required: any object
providing these four methods qualifies for :class:`ASEAtomsBackend`.
"""
[docs]
def get_cell(self) -> Any:
"""Return the cell vectors as rows."""
[docs]
def get_scaled_positions(self) -> Any:
"""Return the reduced positions."""
[docs]
def get_atomic_numbers(self) -> Any:
"""Return one atomic number per site."""
[docs]
def get_pbc(self) -> Any:
"""Return one periodicity flag per cell row."""
def _float_rows(values: Any) -> list[list[float]]:
"""Render an iterable of vector rows without importing a numeric dependency."""
return [[float(value) for value in row] for row in values]
[docs]
class ASEAtomsBackend(StructureBackend):
"""Backend for ASE ``Atoms`` and compatible duck-typed objects.
Conversion is eager because reading the four methods and normalizing their values is
real work. The original object remains available through :meth:`unwrap`.
"""
_raw: Any
_structure: Structure
def __new__(cls, obj: Any, **hints: Any) -> Any:
if hints.get("kind", "ase") != "ase":
return None
# ASEAtomsView is itself an Atoms subclass and therefore satisfies the protocol;
# existing structure-family objects must remain represented by their own backend.
if isinstance(obj, (Structure, StructureBackend, StructureView)):
return None
if not isinstance(obj, ASEAtomsProtocol):
return None
return super().__new__(cls)
def __init__(self, obj: ASEAtomsProtocol, **hints: Any) -> None:
symbols = tuple(symbol_of(int(number)) for number in obj.get_atomic_numbers())
distinct_symbols = tuple(dict.fromkeys(symbols))
self._raw = obj
self._structure = Structure(
Cell(
_float_rows(obj.get_cell()),
periodicity=tuple(bool(flag) for flag in obj.get_pbc()),
),
Sites(_float_rows(obj.get_scaled_positions())),
tuple(Species.create(symbol) for symbol in distinct_symbols),
symbols,
)
@property
[docs]
def cell(self) -> Cell:
"""The exact cell converted from the native cell rows."""
return self._structure.cell
@property
[docs]
def sites(self) -> Sites:
"""The exact reduced coordinates converted from the native positions."""
return self._structure.sites
@property
[docs]
def species(self) -> tuple[Species, ...]:
"""The distinct single-element species in first-appearance order."""
return self._structure.species
@property
[docs]
def species_at_sites(self) -> tuple[str, ...]:
"""The species name occupying each site."""
return self._structure.species_at_sites
[docs]
def unwrap(self) -> Any:
"""Return the original Atoms-like object."""
return self._raw
try:
from .ase_atoms_view import ASEAtomsView # noqa: F401
except ImportError:
_ase_available = False
else:
_ase_available = True
def __getattr__(name: str) -> Any:
if name == "ASEAtomsView" and not _ase_available:
raise ImportError("ASEAtomsView requires ASE; install the optional 'ase' package")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = ["ASEAtomsBackend", "ASEAtomsProtocol"]
if _ase_available:
__all__.append("ASEAtomsView")