# Structures in detail This page documents practical usage of the structure classes in `httk.atomistic`. It follows the same view/backend pattern as the datastream classes in `httk.core`. The implementation is organized by role: component and structure models live under `models/`, symmetry operations under `symmetry/`, persisted records and projections under `storage/`, and entry definitions and providers under `entries/`. ## Overview A crystal structure is available through one family of backends and views: - backends: `UnitcellStructure` (the unit-cell representation), `FundamentalDomainStructure`, `PlainStructure` (an spglib-like triple), and the optional numeric and record backends - views: `UnitcellStructureView` (presents any backend as a `UnitcellStructure`), `PlainStructureView` (presents any backend as a `(lattice, positions, numbers)` tuple) - accepted union: `StructureLike` Every backend produces the same canonical Unitcell quartet declared by `StructureAPI`: `cell` (a `Cell` of 3x3 cell vectors), `sites` (a `Sites` of Nx3 reduced coordinates), `species` (a tuple of `Species`), and `species_at_sites` (the species name at each site). Views build their presentation from that quartet; there is no pairwise conversion between representations. In normal user code, you usually accept `StructureLike` and normalize immediately to one view. Record objects are backends too: use class conversion, such as `UnitcellStructureView(record)`, instead of a pair of conversion methods. ## DatastreamStructure `DatastreamStructure` accepts a local path, a named open stream, a `httk.core.DatastreamURL`, or a `urllib.request.Request`. It checks the source configuration eagerly (including the path and loader name, and network consent), then parses lazily on first access to the file's native representation. Its `unwrap()` returns the original source object. ```python from httk.atomistic import ASUStructureView, DatastreamStructure, UnitcellStructureView from httk.core import DatastreamURL unitcell = UnitcellStructureView("example.cif") asu = ASUStructureView("example.cif") remote = DatastreamStructure(DatastreamURL("https://example.com/example.cif")) ``` `ASUStructureView(path)` adopts declared CIF symmetry exactly; it does not run tolerant symmetry recognition for a native asymmetric-unit result. ## Common Calling Patterns ```python from httk.atomistic import UnitcellStructure, UnitcellStructureView, PlainStructureView # A UnitcellStructure (the Unitcell representation) structure = UnitcellStructure( cell=[[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]], sites=[[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]], species=[ {"name": "Na", "chemical_symbols": ["Na"], "concentration": [1.0]}, {"name": "Cl", "chemical_symbols": ["Cl"], "concentration": [1.0]}, ], species_at_sites=["Na", "Cl"], ) # UnitcellStructure in -> primitive triple out lattice, positions, numbers = PlainStructureView(structure) # spglib-like triple in -> UnitcellStructure out triple = ( [[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]], [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]], [11, 17], ) as_structure = UnitcellStructureView(triple) ``` ## Component families The `cell`, `sites`, and `species` components each get the same view/backend treatment as `UnitcellStructure` itself: - `Cell`: the class itself is the class backend; other backends are `PlainCell` / `CellParams`, and views are `CellView` / `PlainCellView` / `CellParamsView`, union `CellLike`. `Cell` exposes `basis` plus the derived `lengths`, `angles` (the crystallographic `alpha`/`beta`/`gamma` in degrees), and `volume`. The params representation is a flat `(a, b, c, alpha, beta, gamma)` 6-tuple (angles in degrees): a cell can be constructed from parameters anywhere a `CellLike` is accepted (the basis is built with the standard orientation convention — first vector along x, second in the xy-plane), and `CellParamsView` presents any cell as its parameters, with the elements also available as the named properties `a`/`b`/`c`/ `alpha`/`beta`/`gamma`. Note that parameters carry no orientation, so cell → params → cell reproduces lengths, angles, and volume but not the original orientation. - `Sites`: the class itself is the class backend; the other backend is `PlainSites`, and views are `SitesView` / `PlainSitesView`, union `SitesLike`. `Sites` exposes `reduced_coords` and is iterable, indexable, and sized over its rows. - `Species` (one species; the OPTIMADE `species` object): the class itself is the class backend; the other backend is `PlainSpecies`, and views are `SpeciesView` / `PlainSpeciesView`, union `SpeciesLike`. The class representation is the frozen `Species`; the primitive representation is an OPTIMADE species dict. ```python from httk.atomistic import Cell, CellParamsView, CellView, PlainSpeciesView, UnitcellStructure cell = CellView(structure.cell) # class conversion; this is a Cell cell.lengths # a triple of exact SurdScalar norms cell.angles # (alpha, beta, gamma) as exact Fraction degrees cell.volume # an exact SurdScalar raw_basis = CellView(cell).basis.to_floats() # render the exact basis as plain floats params = CellParamsView(cell) # (a, b, c, alpha, beta, gamma) as floats params.a, params.gamma optimade = dict(PlainSpeciesView(structure.species[0])) # a species as an OPTIMADE dict # Construct from parameters (standard orientation convention): structure_from_params = UnitcellStructure( cell=(4.0, 4.0, 4.0, 90.0, 90.0, 90.0), sites=[[0.0, 0.0, 0.0]], species=[{"name": "Fe", "chemical_symbols": ["Fe"], "concentration": [1.0]}], species_at_sites=["Fe"], ) ``` The kinds dispatch by type and shape: a `Cell`/`Sites`/`Species` is already its class backend, a raw basis matrix / dict uses its `*Plain` backend, and a flat 6-sequence uses the `CellParams` backend. Pass `kind="plain"` or `kind="params"` to force an interpretation; class instances are selected by their type. ## Notes - A `UnitcellStructure` is already the unit-cell backend and a length-3 triple uses `PlainStructure`. A malformed triple raises `TypeError` from `create`. Pass `kind="unitcell"` or `kind="plain"` to force an interpretation. - `UnitcellStructureView` and the `*View` views are lazy per component: each backend accessor is normalized on first access. `PlainStructureView`, `PlainCellView`, `PlainSitesView`, `PlainSpeciesView`, and `CellParamsView` remain eager because they build tuple/dict payloads; `SpeciesView` remains eager so `Species` validation happens at construction. The plain views are genuine immutable-subclass views of their class (a `Cell`, a tuple, ...); `PlainSpeciesView` is a genuine — but detached and mutable — OPTIMADE `dict`. Of the component presentations, only `PlainStructureView`, `PlainSpeciesView`, `CellParamsView`, and `NumericUnitcellStructureView` are re-exported from `httk.atomistic`; the per-component `PlainCellView` / `PlainSitesView` (and the numeric-view equivalents above) live in their `httk.atomistic.models.*` submodules or are reached by class conversion, so `from httk.atomistic import PlainCellView` does not resolve. - `PlainStructureView` requires every site's species to be a single, unattached chemical element; alloy, vacancy, and attached species cannot be represented as a bare atomic number and raise `TypeError`. Such species survive in the Unitcell representation. - Rewrapping a view returns the same object, and views built from the same backend share it. `unwrap(view)` returns the native raw object (a `UnitcellStructure` or a triple, a `Cell` or a raw basis matrix, a `Species` or a dict). - The numeric model is **exact**. A `Cell` stores its lattice vectors as a `httk.core.SurdVector` (the squarefree-radical field) factored as a positive `SurdScalar` `scale` times an `unscaled_basis`, and its `lengths`/`volume` are exact `SurdScalar`s and `angles` exact `Fraction` degrees. A `Sites` stores its reduced coordinates as an exact rational `httk.core.FracVector`. The rule for leaving the exact model is uniform: **exact accessors return vector objects; render them** — `.to_floats()` on any of them gives nested plain-float lists (numpy-free, JSON-ready), `float(...)` works on every exact scalar, the plain views give immutable float tuples, and the numpy-backed numeric layer (`.numeric()`, see below) gives true numpy arrays. An `ASUStructure` holds a structure as its asymmetric unit — a space group plus one site per orbit — and expands to a `UnitcellStructure` on demand; see {doc}`asu`. A `Cell` and a `Sites` each also carry an optional `precision` recording how precisely their numbers were stated by the source they came from; see {doc}`precision`. ## Exact geometry: scale, surd matrices, and Cartesian positions The numeric layer is exact and split by purpose. The **fractional** frame (reduced coordinates, symmetry) is rational and lives in `Sites` as a `httk.core.FracVector`; the **Cartesian** frame — where radicals such as the hexagonal $\sqrt3$ appear — is exact in the squarefree-radical field (`httk.core.SurdVector`). Magnitudes (bond-length comparisons) stay rational-exact via the metric. ```python import fractions from httk.core import FracVector, SurdVector from httk.atomistic import Cell, CellParams, UnitcellStructure F = fractions.Fraction # Cell parameters -> an EXACT basis: hexagonal a=b=3, c=5, gamma=120 carries a real sqrt(3). cell = Cell(CellParams((3, 3, 5, 90, 90, 120)).basis) assert 3 in cell.basis.radicands # the sqrt(3) is exact, not a float # Angles come back exactly through the reverse-Niven table; volume is (45/2)*sqrt(3): assert cell.angles == (F(90), F(90), F(120)) assert cell.volume == SurdVector.from_radicand_map({3: F(45, 2)}) # The scale carries an overall length factor: unscaled rows scaled by 4 == the absolute basis, # and the volume scales as scale**3. Angles are scale-independent. scaled = Cell([[1, 0, 0], [0, 1, 0], [0, 0, 1]], scale=4) assert scaled.basis == Cell([[4, 0, 0], [0, 4, 0], [0, 0, 4]]).basis assert scaled.volume == SurdVector(64) # Exact Cartesian positions: reduced (rational) coordinates times the surd cell basis. structure = UnitcellStructure( cell=cell, sites=[[F(0), F(0), F(0)], [F(1, 3), F(1, 3), F(0)]], species=[{"name": "Mg", "chemical_symbols": ["Mg"], "concentration": [1.0]}], species_at_sites=["Mg", "Mg"], ) cartesian = structure.cartesian_sites() # an exact (N, 3) SurdVector assert 3 in cartesian.radicands # the sqrt(3) survives into Cartesian space # A bond squared-length is rational-exact; the exact-Cartesian and rational-metric routes agree. diff = FracVector([F(1, 3), F(1, 3), F(0)]) bond_sqr_cartesian = (SurdVector(diff) * cell.basis).lengthsqr() bond_sqr_metric = (SurdVector(diff) * cell.metric()).dot(SurdVector(diff)) assert bond_sqr_cartesian == bond_sqr_metric assert bond_sqr_cartesian.is_rational # Rendering is compositional: every exact vector object renders itself (numpy-free)... assert cell.basis.to_floats()[0] == [3.0, 0.0, 0.0] assert structure.cartesian_sites().to_floats()[0] == [0.0, 0.0, 0.0] assert structure.sites.reduced_coords.to_floats()[1] == [1.0 / 3.0, 1.0 / 3.0, 0.0] assert float(cell.volume) == cell.volume.to_float() # scalars support float(...) ``` ## The numeric layer: plain floats and numpy There are two ways to leave the exact model for plain floats, and they serve different needs: - The **compositional rendering** — `cell.basis.to_floats()`, `structure.cartesian_sites().to_floats()`, `sites.reduced_coords.to_floats()`, `float(cell.volume)` — every exact vector object renders itself as nested plain-`float` lists (a family-level guarantee: `to_floats()`/`to_float()` are part of the vector contract). It needs **no numpy**, works everywhere, and (with the plain views and the OPTIMADE records) is the numpy-free JSON/presentation boundary. - The **numeric layer** — `Cell.numeric()`, `Sites.numeric()`, `UnitcellStructure.numeric()` — returns a `NumericCell`, `NumericSites`, or `NumericUnitcellStructureView` that mirrors the exact interface but returns true numpy: a `float64` `numpy.ndarray` for every vector, a plain `float` for every scalar (`scale`, `volume`). Reach for it when you want numpy arrays — plotting, a numerical routine, quick inspection. The exact object is always one hop back via `.exact`. The numeric layer is numpy-backed, so it **requires the `httk-atomistic[numpy]` extra** and raises `ImportError` eagerly at construction when numpy is not installed. (The `.to_floats()` renderings, the plain views and the OPTIMADE records stay numpy-free, so numpy is optional for everything except this numpy presentation.) ```python import numpy import fractions from httk.atomistic import Cell, CellParams, UnitcellStructure F = fractions.Fraction cell = Cell(CellParams((3, 3, 5, 90, 90, 120)).basis) # hexagonal: a real sqrt(3) structure = UnitcellStructure( cell=cell, sites=[[F(0), F(0), F(0)], [F(1, 3), F(1, 3), F(0)]], species=[{"name": "Mg", "chemical_symbols": ["Mg"], "concentration": [1.0]}], species_at_sites=["Mg", "Mg"], ) numeric = structure.numeric() # Vectors are plain float64 ndarrays; the sqrt(3)/2 entry appears as a float: basis = numeric.cell.basis assert isinstance(basis, numpy.ndarray) and basis.dtype == numpy.float64 assert basis[1].tolist() == [-1.5, 3.0 * numpy.sqrt(3.0) / 2.0, 0.0] # Angles are a (3,) float64 ndarray in degrees; scalars are plain floats: assert numeric.cell.angles.tolist() == [90.0, 90.0, 120.0] assert isinstance(numeric.cell.volume, float) # Cartesian positions as a plain (N, 3) ndarray: cartesian = numeric.cartesian_sites() assert isinstance(cartesian, numpy.ndarray) and cartesian.shape == (2, 3) # .exact is the escape hatch back to the exact object: assert numeric.exact == structure ``` The same presentation is also available as views over any backend — the whole-structure `NumericUnitcellStructureView` (re-exported from `httk.atomistic`), and the per-component `CellNumericView` / `SitesNumericView` (in `httk.atomistic.models.cell.numeric_view` / `httk.atomistic.models.sites.numeric_view`, or reached by class conversion) — mirroring the `*View` pattern (rewrap-idempotent, `unwrap` returns the raw original), and likewise requiring numpy. ## Loading a POSCAR `httk.core.load` returns a `UnitcellStructure` for POSCAR and CONTCAR files. It uses the neutral, string-preserving reader payload internally, so cell rows and coordinates remain exact; compressed `.bz2` and `.gz` files work too. ```python from httk.core import load structure = load("POSCAR") assert [species.name for species in structure.species] == ["Na", "Cl"] assert structure.cell.basis.to_floats() == [[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]] assert structure.species_at_sites == ("Na", "Cl") ``` A negative scale line encodes a target cell **volume**; the resulting overall scale is the cube root of `V / |det(basis)|`, which leaves the exact surd field, so it is a deterministic rational approximation (the basis rows stay exact). The POSCAR reader ships in-tree and is registered when `httk.core` is imported; see {doc}`/poscar` for the neutral, string-preserving reader mapping. For VASP plane-wave coefficients, see {doc}`/wavefunctions`. ## Building supercells `UnitcellStructure.supercell(A)` applies an exact integer transformation to the cell rows. If `A` has determinant $d$, the result contains exactly $|d|$ periodic copies and has basis `A * structure.cell.basis`. Coordinates and translation cosets stay rational-exact; no geometric tolerance is used. ```python result = structure.supercell([[2, 0, 0], [0, 2, 0], [0, 0, 1]]) supercell = result.structure assert result.multiplier == 4 assert len(supercell.sites) == 4 * len(structure.sites) ``` When the transformation is not known, `orthogonal_supercell(multiplier=N)` and `cubic_supercell(multiplier=N)` search a deterministic bounded set of integer matrices with determinant `N`. The multiplier is explicit, so atom count and memory use are predictable before materialization. Their exact, dimensionless `orthogonality_score` and `cubicity_score` are zero precisely when the target shape is achieved. The default `max_sites=100_000` guard fails before allocation; pass a larger value, or `None`, deliberately when constructing a larger result. See the complete {doc}`/examples/build_a_supercell` example for the skewed cell from the httk v1 Step 2 tutorial and the reason its old `tolerance` search knob became an exact multiplier in v2. ## Serving structures as OPTIMADE `StructureEntryProvider` maps `{id: UnitcellStructure}` onto the neutral `httk.core.EntryProvider` contract for a serving module such as *httk-serve*. Besides the core structural fields it auto-derives the standard composition fields for a fully ordered structure (every species a single, unattached element): `nperiodic_dimensions`, `dimension_types`, `elements_ratios`, and the `chemical_formula_reduced` / `_anonymous` / `_descriptive` strings. The `structure.formula` convenience is an eager reduced-formula `str` view and raises for incomplete compositions; use `chemical_formula_reduced` when the documented `str | None` result is required. It also accepts `None` for a known entry that has no structure (structural properties then serve null), and can serve custom database-specific properties via an extended definition: ```python from httk.atomistic import UnitcellStructure, StructureEntryProvider from httk.atomistic import Species from httk.core import PropertyDefinition cell = [[5.6, 0.0, 0.0], [0.0, 7.6, 0.0], [0.0, 0.0, 5.3]] sites = [[0.01 * i, 0.0, 0.0] for i in range(20)] species = [ Species(name="Fe", chemical_symbols=("Fe",), concentration=(1.0,)), Species(name="O", chemical_symbols=("O",), concentration=(1.0,)), Species(name="Sm", chemical_symbols=("Sm",), concentration=(1.0,)), ] smfeo3 = UnitcellStructure(cell, sites, species, ["Fe"] * 4 + ["O"] * 12 + ["Sm"] * 4) energy = PropertyDefinition.from_simple("_httk_total_energy", description="Total energy.", fulltype="float") provider = StructureEntryProvider( {"smfeo3": smfeo3, "known-but-empty": None}, extra_definitions={"_httk_total_energy": energy}, properties={"smfeo3": {"_httk_total_energy": -12.5}}, ) records = {record["__id"]: record for record in provider.records("structures")} # gcd(4, 12, 4) = 4 -> Fe O3 Sm (alphabetical); anonymous orders amounts descending. assert records["smfeo3"]["chemical_formula_reduced"] == "FeO3Sm" assert records["smfeo3"]["chemical_formula_anonymous"] == "A3BC" assert records["smfeo3"]["_httk_total_energy"] == -12.5 # The structure-less entry serves null for structural and derived fields: assert records["known-but-empty"]["lattice_vectors"] is None assert records["known-but-empty"]["chemical_formula_reduced"] is None ``` ## Shared Behavior, `unwrap`, and `unview` `unwrap(obj)` returns the most raw representation available: - for `UnitcellStructure` / `UnitcellStructureView` this is the `UnitcellStructure` backend - for `PlainStructure` / `PlainStructureView` this is the `(lattice, positions, numbers)` triple - for non-view/backend objects it returns the object unchanged `unview(obj)` (from `httk.core`) sheds the httk view wrapper sideways instead, returning a plain instance of the *presented* type: a `CellView` becomes a `Cell` (reusing the backend object when it already is exactly the presented value), the `Plain*View`s become plain tuples/dicts, `UnitcellStructureView`/`ASUStructureView` become plain structures (view-level `immutable_id`/`last_modified` metadata is carried onto the materialized structure), and `ASEAtomsView` a base-class `ase.Atoms`. `NumericUnitcellStructureView` is interface-only and raises `TypeError` — use `.exact` or `UnitcellStructureView(...)` instead. Non-view inputs pass through unchanged. See the four-verb table in the *httk-core* backend/view guide.