Source code for httk.atomistic.integrations.ase.view

"""ASE ``Atoms`` view for the :mod:`httk.atomistic` structure family.

ASE is an optional dependency. This module intentionally imports it unconditionally,
so the package-level exports can guard the import while documentation tools can see
the public :class:`ASEAtomsView` definition.
"""

from typing import TYPE_CHECKING, Any, Self, cast

import ase
from httk.core import register_citation, unwrap

register_citation(
    applies_to="Structure interchange with the Atomic Simulation Environment (ASE)",
    references={
        "authors": (
            {"name": "Ask Hjorth Larsen"},
            {"name": "Jens Jørgen Mortensen"},
            {"name": "Jakob Blomqvist"},
            {"name": "Ivano E. Castelli"},
            {"name": "Rune Christensen"},
            {"name": "Marcin Dułak"},
            {"name": "Jesper Friis"},
            {"name": "Michael N. Groves"},
            {"name": "Bjørk Hammer"},
            {"name": "Cory Hargus"},
            {"name": "and others"},
        ),
        "title": "The atomic simulation environment—a Python library for working with atoms",
        "journal": "Journal of Physics: Condensed Matter",
        "volume": "29",
        "pages": "273002",
        "year": "2017",
        "doi": "10.1088/1361-648X/aa680e",
        "bib_type": "article",
    },
)

from httk.atomistic.elements import atomic_number
from httk.atomistic.models.moments.cartesian import CartesianSiteMoments
from httk.atomistic.models.moments.cartesian_view import CartesianSiteMomentsView
from httk.atomistic.models.moments.collinear import CollinearSiteMoments
from httk.atomistic.models.structure.backend import StructureBackend
from httk.atomistic.models.structure.unitcell_view import UnitcellStructureView
from httk.atomistic.models.structure.view import StructureView

if TYPE_CHECKING:
    from httk.atomistic.models.structure.like import StructureLike


[docs] class ASEAtomsView(StructureView, ase.Atoms): r"""Present a structure-like value as ASE ``Atoms``. The conversion carries the structure quartet: cell, reduced positions, atomic numbers, and periodicity. ASE must be installed to construct this view. Mixed or attached species cannot be represented by ASE atomic numbers and raise :class:`TypeError`. Site moments map to ``initial_magmoms`` and species charges map to ``initial_charges``. Structure charge, species spins, labels, masses, assemblies, declared chemical composition, and non-element species are rejected rather than discarded; :meth:`unwrap` recovers the original value behind the backend. :param obj: A structure-like value to present as ``Atoms``. :param \**hints: Backend-selection hints. """ _backend: StructureBackend def __new__(cls, obj: "StructureLike", **hints: Any) -> Self: if isinstance(obj, cls): return obj backend = cls._prepare_backend(obj, hints) structure = UnitcellStructureView(backend) if structure.assemblies is not None: raise TypeError("This structure cannot be represented as ASE Atoms because it has assemblies") if structure.chemical_composition is not None: raise TypeError( "This structure cannot be represented as ASE Atoms because it has a declared chemical composition" ) source = unwrap(backend) charge = getattr(source, "charge", getattr(backend, "charge", None)) if charge is not None: raise ValueError("This structure cannot be represented as ASE Atoms because it has a charge") species_values = getattr(source, "species", getattr(backend, "species", ())) for item in species_values: for field in ("spins", "labels", "mass"): if getattr(item, field, None) is not None: raise ValueError(f"This structure cannot be represented as ASE Atoms because species has {field}") instance = super().__new__(cls) species_by_name = {species.name: species for species in structure.species} numbers: list[int] = [] for name in structure.species_at_sites: species = species_by_name[name] if not species.is_single_element: raise TypeError( "This structure cannot be represented as ASE Atoms " f"(species {name!r} is not a single, unattached chemical element)" ) numbers.append(atomic_number(species.chemical_symbols[0])) ase.Atoms.__init__( instance, cell=structure.cell.basis.to_floats(), scaled_positions=structure.sites.reduced_coords.to_floats(), numbers=numbers, pbc=structure.cell.periodicity, ) moments = structure.site_moments if moments is not None: if isinstance(moments, CollinearSiteMoments): instance.set_initial_magnetic_moments([float(value) for value in moments.collinear_moments.to_floats()]) else: cartesian = moments if isinstance(moments, CartesianSiteMoments) else CartesianSiteMomentsView(moments) instance.set_initial_magnetic_moments( [[float(value) for value in row] for row in cartesian.cartesian_moments.to_floats()] ) if any(species.charges is not None for species in structure.species): initial_charges: list[float] = [] for name in structure.species_at_sites: charges = species_by_name[name].charges charge = None if charges is None else charges[0] initial_charges.append(0.0 if charge is None else float(cast(Any, charge))) cast(Any, instance).set_initial_charges(initial_charges) instance._backend = backend return instance def __init__(self, obj: "StructureLike", **hints: Any) -> None: pass
[docs] def unwrap(self) -> Any: """Return the original value represented by the underlying backend.""" return unwrap(self._backend)
[docs] def unview(self) -> ase.Atoms: """Return a base ``Atoms`` copy of this view. :return: A standalone ASE ``Atoms`` object. """ # ase.Atoms(other) builds a base-class copy of the presented atoms. return ase.Atoms(self)
__all__ = ["ASEAtomsView"]