Source code for httk.atomistic.structure

"""
The Simple structure representation for httk-atomistic.
"""

import fractions
from collections.abc import Sequence
from typing import TYPE_CHECKING

from httk.core import SurdVector, VectorLike

from .cell import Cell
from .cell_class_view import CellClassView
from .cell_like import CellLike
from .sites import Sites
from .sites_class_view import SitesClassView
from .sites_like import SitesLike
from .species import Species
from .species_class_view import SpeciesClassView
from .species_like import SpeciesLike

if TYPE_CHECKING:
    from .numeric_unitcell_structure_view import NumericUnitcellStructureView
    from .standardization import ConventionalCellResult


[docs] class Structure: """ A crystal structure in the Unitcell representation. A Structure holds a ``cell`` (a ``Cell`` of 3x3 cell vectors), ``sites`` (a ``Sites`` of Nx3 reduced coordinates), a list of ``species`` (each a ``Species``), and a length-N ``species_at_sites`` giving the species name occupying each site. Inputs are normalized on construction through the component families: the cell, sites, and each species are passed through their ``*Like`` unions, and every ``species_at_sites`` name must match one of the (uniquely named) species. The numeric model is exact and split by purpose. The fractional frame — reduced coordinates and symmetry — is rational and lives in ``sites`` as a :class:`~httk.core.FracVector`. The Cartesian frame — where radicals such as the hexagonal ``sqrt(3)`` appear — is exact in the squarefree-radical field: ``cell.basis`` is a :class:`~httk.core.SurdVector` and :meth:`cartesian_sites` returns the exact Cartesian positions. Pure magnitudes (bond-length comparisons) stay rational-exact via ``cell.metric()``. Floats appear only at the presentation and JSON boundaries. """ _cell: Cell _sites: Sites _species: tuple[Species, ...] _species_at_sites: tuple[str, ...] def __init__( self, cell: CellLike, sites: SitesLike, species: Sequence[SpeciesLike], species_at_sites: Sequence[str], ) -> None: norm_cell = cell if isinstance(cell, Cell) else CellClassView(cell) norm_sites = sites if isinstance(sites, Sites) else SitesClassView(sites) norm_species = tuple(s if isinstance(s, Species) else SpeciesClassView(s) for s in species) norm_species_at_sites = tuple(str(name) for name in species_at_sites) if len(norm_species_at_sites) != len(norm_sites): raise ValueError("Structure species_at_sites must have the same length as sites") names = [s.name for s in norm_species] if len(names) != len(set(names)): raise ValueError("Structure species names must be unique") known = set(names) for name in norm_species_at_sites: if name not in known: raise ValueError(f"Structure species_at_sites references unknown species name: {name!r}") self._cell = norm_cell self._sites = norm_sites self._species = norm_species self._species_at_sites = norm_species_at_sites @property
[docs] def cell(self) -> Cell: """The cell (3x3 cell vectors) as a ``Cell``.""" return self._cell
@property
[docs] def sites(self) -> Sites: """The site coordinates (Nx3 reduced coordinates) as a ``Sites``.""" return self._sites
@property
[docs] def species(self) -> tuple[Species, ...]: """The distinct species of this structure.""" return self._species
@property
[docs] def species_at_sites(self) -> tuple[str, ...]: """The species name occupying each site, in site order.""" return self._species_at_sites
@property
[docs] def coordinate_precision(self) -> fractions.Fraction | None: """How precisely the reduced coordinates were stated, in fractional units, or ``None``. Read through from :attr:`sites`. Dimensionless — see :meth:`cartesian_precision` for the corresponding length. """ return self._sites.precision
@property
[docs] def basis_precision(self) -> fractions.Fraction | None: """How precisely the cell basis was stated, as an absolute length, or ``None``. Read through from :attr:`cell`. """ return self._cell.precision
@property
[docs] def periodicity(self) -> tuple[bool, bool, bool]: """Which of the three basis rows is a genuine lattice translation. Read through from :attr:`cell`, where the full account lives. ``(True, True, True)`` for an ordinary crystal, which is what a structure built without saying otherwise is. """ return self._cell.periodicity
@property
[docs] def nperiodic_dimensions(self) -> int: """How many of the three directions are periodic, from 0 to 3.""" return self._cell.nperiodic_dimensions
[docs] def cartesian_precision(self) -> fractions.Fraction | None: """The coordinate precision as a length, or ``None`` if it is unknown. This is the number a real tolerance wants — an interatomic matching tolerance or an spglib ``symprec`` is a distance, and a fractional precision is not. A coordinate good to ``1e-4`` of a cell edge means something quite different in a 3 Å cell and a 30 Å one. Computed as the fractional precision times the *longest* cell edge, which is the conservative choice: it is the largest displacement that fractional uncertainty can produce along any axis. The cell's own precision is folded in as well, since a cell stated to ``1e-3`` cannot place an atom better than that however many digits the coordinates carry. """ fractional = self._sites.precision if fractional is None: return None longest = max(length.to_float() for length in self._cell.lengths) cartesian = fractional * fractions.Fraction(str(longest)) basis = self._cell.precision return cartesian if basis is None or basis < cartesian else basis
[docs] def cartesian_sites(self) -> SurdVector: """ The exact Cartesian site positions as an ``(N, 3)`` :class:`~httk.core.SurdVector`. Under the row-vector convention this is ``reduced_coords * cell.basis`` (each Cartesian position is the sum over lattice vectors ``sum_k reduced[k] * basis[k]``). The reduced coordinates are rational (a ``FracVector``), the cell basis carries the radicals (a ``SurdVector``), so the product is exact in the surd field — the hexagonal ``sqrt(3)`` survives into the Cartesian positions. """ return SurdVector.create(self._sites.reduced_coords) * self._cell.basis
[docs] def numeric(self) -> "NumericUnitcellStructureView": """A plain-numpy presentation of this structure (requires the ``httk-atomistic[numpy]`` extra).""" from .numeric_unitcell_structure_view import NumericUnitcellStructureView return NumericUnitcellStructureView(self)
[docs] def supercell( self, transformation: VectorLike, *, max_sites: int | None = 100_000, ) -> "SupercellResult": """Build an exact supercell from an integer 3x3 transformation.""" from .supercell import build_supercell return build_supercell(self, transformation, max_sites=max_sites)
[docs] def orthogonal_supercell( self, multiplier: int | None = None, *, tolerance: fractions.Fraction | str | float | None = None, max_multiplier: int | None = None, search_radius: int = 1, max_sites: int | None = 100_000, ) -> "SupercellResult": """Build a deterministically selected orthogonal supercell.""" from .supercell import orthogonal_supercell return orthogonal_supercell( self, multiplier, tolerance=tolerance, max_multiplier=max_multiplier, search_radius=search_radius, max_sites=max_sites, )
[docs] def cubic_supercell( self, multiplier: int | None = None, *, tolerance: fractions.Fraction | str | float | None = None, max_multiplier: int | None = None, search_radius: int = 1, max_sites: int | None = 100_000, ) -> "SupercellResult": """Build a deterministically selected cubic supercell.""" from .supercell import cubic_supercell return cubic_supercell( self, multiplier, tolerance=tolerance, max_multiplier=max_multiplier, search_radius=search_radius, max_sites=max_sites, )
[docs] def conventional_cell( self, *, tolerance: float | None = None, limit_denominator: int | None = None, ) -> "ConventionalCellResult": """Express this structure in its space group's IT standard-setting conventional cell.""" from .standardization import conventional_cell return conventional_cell( self, tolerance=tolerance, limit_denominator=limit_denominator, )
def __eq__(self, other: object) -> bool: """Equality of the quartet. Stated precision does not take part. Precision describes how the structure was *recorded*, not what it is, so the same atoms read from a more carefully written file are still the same structure. The component classes take the same view. """ if not isinstance(other, Structure): return NotImplemented return ( self._cell == other._cell and self._sites == other._sites and self._species == other._species and self._species_at_sites == other._species_at_sites ) def __repr__(self) -> str: return ( f"Structure(cell={self._cell!r}, sites={self._sites!r}, " f"species={self._species!r}, species_at_sites={self._species_at_sites!r})" )
if TYPE_CHECKING: from .supercell import SupercellResult