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 backendsviews:
UnitcellStructureView(presents any backend as aUnitcellStructure),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 arePlainCell/CellParams, and views areCellView/PlainCellView/CellParamsView, unionCellLike.Cellexposesbasisplus the derivedlengths,angles(the crystallographicalpha/beta/gammain degrees), andvolume. 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 aCellLikeis accepted (the basis is built with the standard orientation convention — first vector along x, second in the xy-plane), andCellParamsViewpresents any cell as its parameters, with the elements also available as the named propertiesa/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 isPlainSites, and views areSitesView/PlainSitesView, unionSitesLike.Sitesexposesreduced_coordsand is iterable, indexable, and sized over its rows.Species(one species; the OPTIMADEspeciesobject): the class itself is the class backend; the other backend isPlainSpecies, and views areSpeciesView/PlainSpeciesView, unionSpeciesLike. The class representation is the frozenSpecies; 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
UnitcellStructureis already the unit-cell backend and a length-3 triple usesPlainStructure. A malformed triple raisesTypeErrorfromcreate. Passkind="unitcell"orkind="plain"to force an interpretation.UnitcellStructureViewand the*Viewviews are lazy per component: each backend accessor is normalized on first access.PlainStructureView,PlainCellView,PlainSitesView,PlainSpeciesView, andCellParamsViewremain eager because they build tuple/dict payloads;SpeciesViewremains eager soSpeciesvalidation happens at construction. The plain views are genuine immutable-subclass views of their class (aCell, a tuple, …);PlainSpeciesViewis a genuine — but detached and mutable — OPTIMADEdict. Of the component presentations, onlyPlainStructureView,PlainSpeciesView,CellParamsView, andNumericUnitcellStructureVieware re-exported fromhttk.atomistic; the per-componentPlainCellView/PlainSitesView(and the numeric-view equivalents above) live in theirhttk.atomistic.models.*submodules or are reached by class conversion, sofrom httk.atomistic import PlainCellViewdoes not resolve.PlainStructureViewrequires 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 raiseTypeError. 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 (aUnitcellStructureor a triple, aCellor a raw basis matrix, aSpeciesor a dict).The numeric model is exact. A
Cellstores its lattice vectors as ahttk.core.SurdVector(the squarefree-radical field) factored as a positiveSurdScalarscaletimes anunscaled_basis, and itslengths/volumeare exactSurdScalars andanglesexactFractiondegrees. ASitesstores its reduced coordinates as an exact rationalhttk.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. AnASUStructureholds a structure as its asymmetric unit — a space group plus one site per orbit — and expands to aUnitcellStructureon demand; see Asymmetric units in detail. ACelland aSiteseach also carry an optionalprecisionrecording 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 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-floatlists (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 aNumericCell,NumericSites, orNumericUnitcellStructureViewthat mirrors the exact interface but returns true numpy: afloat64numpy.ndarrayfor every vector, a plainfloatfor 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