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.

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

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.

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 SurdScalars 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 Asymmetric units in detail. 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 Data precision in detail.

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.

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 renderingcell.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 layerCell.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.)

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.

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 Reading VASP POSCAR / CONTCAR files for the neutral, string-preserving reader mapping. For VASP plane-wave coefficients, see Plane-wave 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.

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 Build exact general, orthogonal, and cubic supercells 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:

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*Views 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.