Source code for httk.atomistic.vasp_structures

"""Build an exact :class:`~httk.atomistic.Structure` from a neutral POSCAR mapping.

:func:`structure_from_poscar` consumes the plain, string-preserving mapping
produced by ``httk.io.read_poscar`` (format tag ``"vasp-poscar"``) and turns it
into an exact :class:`~httk.atomistic.Structure`. It imports nothing from
*httk-io* — it only understands the neutral mapping shape — keeping the parsing
capability (*httk-io*) and the domain model (*httk-atomistic*) decoupled.

:func:`load_structure` is the convenience end-to-end entry point:
``httk.core.load`` picks the reader by file type, and a small adapter table maps
the payload's ``"format"`` tag to the matching structure builder.
"""

import fractions
from collections.abc import Mapping
from typing import Any, Callable

from httk.core import SurdScalar, SurdVector, load
from httk.core.vectors import exactmath

from .cell import Cell
from .species import Species
from .structure import Structure

# Cube roots leave the exact squarefree-radical field, so the volume-scaled cell's
# overall scale factor is a *deterministic rational approximation* at this precision
# (exactmath's default accuracy, 1e-10). Everything else — the basis rows and the
# reduced coordinates — remains exact.
_FALLBACK_PREC = exactmath.default_accuracy


def _to_fraction(scalar: SurdScalar) -> fractions.Fraction:
    """An exact :class:`~fractions.Fraction` for a rational scalar, else a deterministic approximation."""
    return fractions.Fraction(scalar.to_fractions_approx(_FALLBACK_PREC))


def _cube_root(value: fractions.Fraction) -> fractions.Fraction:
    """A deterministic rational approximation of the cube root of a positive rational."""
    ln = fractions.Fraction(exactmath.log(value, prec=_FALLBACK_PREC, limit=True))
    return fractions.Fraction(exactmath.exp(ln / 3, prec=_FALLBACK_PREC, limit=True))


[docs] def structure_from_poscar(data: Mapping[str, Any]) -> Structure: """Build an exact :class:`~httk.atomistic.Structure` from a neutral POSCAR mapping. ``data`` is the mapping returned by ``httk.io.read_poscar`` (its ``format`` must be ``"vasp-poscar"``). The cell basis is taken exactly from the file's string rows. For a positive universal scaling factor the ``scale`` string is used directly as the cell's exact scale; for a negative scaling factor (a target **volume** ``V``) the scale is the cube root of ``V / |det(basis)|`` — a value outside the exact surd field, so it is a deterministic rational approximation (the basis rows themselves stay exact). Direct coordinates become reduced coordinates directly (exact strings). Cartesian coordinates are converted exactly as ``cart * basis.inv()`` under the row-vector convention; because VASP scales *both* the lattice vectors and the Cartesian positions by the universal scaling factor, that factor cancels and the reduced coordinates are exact regardless of the scale/volume case. Species come from the VASP-5 species line (one single-element, unattached :class:`~httk.atomistic.Species` of concentration 1.0 per distinct symbol); a VASP-4 file (no species symbols) raises a :class:`ValueError`. Selective dynamics flags, if present, are ignored. """ fmt = data.get("format") if fmt != "vasp-poscar": raise ValueError(f"structure_from_poscar expected a 'vasp-poscar' mapping, got format={fmt!r}.") cell_rows = data["cell"] raw_basis = SurdVector.create(cell_rows) scale_str = data.get("scale") volume_str = data.get("volume") if scale_str is not None: cell = Cell(cell_rows, scale_str) elif volume_str is not None: abs_det = abs(_to_fraction(raw_basis.det())) if abs_det == 0: raise ValueError("Cannot volume-scale a degenerate cell (zero determinant).") target_volume = _to_fraction(SurdVector.create(volume_str)._as_scalar()) cell = Cell(cell_rows, _cube_root(target_volume / abs_det)) else: cell = Cell(cell_rows) if data["cartesian"]: # reduced = cart * basis^-1 (row-vector convention); the universal scale cancels. reduced: Any = SurdVector.create(data["coords"]) * raw_basis.inv() else: reduced = data["coords"] symbols = data.get("symbols") if symbols is None: raise ValueError( "structure_from_poscar cannot build species for a VASP-4 POSCAR (no species symbols); " "provide a VASP-5 file with an explicit species line." ) counts = data["counts"] species: list[Species] = [] seen: set[str] = set() species_at_sites: list[str] = [] for symbol, count in zip(symbols, counts): species_at_sites.extend([symbol] * count) if symbol not in seen: seen.add(symbol) species.append(Species(name=symbol, chemical_symbols=(symbol,), concentration=(1.0,))) return Structure(cell, reduced, species, species_at_sites)
#: Maps a loaded payload's ``"format"`` tag to the structure builder for it. _STRUCTURE_ADAPTERS: dict[str, Callable[[Mapping[str, Any]], Structure]] = { "vasp-poscar": structure_from_poscar, }
[docs] def load_structure(path: str) -> Structure: """Load a file and build a :class:`~httk.atomistic.Structure` from it. ``httk.core.load(path)`` selects the reader by file type (transparently decompressing ``.bz2`` / ``.gz`` files); the payload's ``"format"`` tag then selects the matching structure builder. A payload without a recognized ``"format"`` tag (for example a CIF, which returns a different shape) raises a clear :class:`ValueError`. """ payload = load(path) fmt = payload.get("format") if isinstance(payload, Mapping) else None adapter = _STRUCTURE_ADAPTERS.get(fmt) if isinstance(fmt, str) else None if adapter is None: raise ValueError( f"Cannot build a Structure from {path!r}: unrecognized payload format tag {fmt!r}. " f"Known structure formats: {', '.join(sorted(_STRUCTURE_ADAPTERS))}." ) return adapter(payload)