Asymmetric units¶
A crystal is mostly repetition. ASUStructure represents the part that is not:
a space group, one site per symmetry orbit, and the values of that site’s free
parameters. Expanding it produces the full unit cell; recognizing a full cell
produces it back.
from httk.atomistic import UnitcellStructureView, load_asu_structure
asu = load_asu_structure("nacl.cif")
structure = UnitcellStructureView(asu) # the full cell, exactly
ASUStructure is part of StructureLike, so it can be passed anywhere a
structure is accepted — the OPTIMADE provider, the numeric layer, everything.
The expansion is lazy: building one, or reading its space group, never generates
the cell.
What it holds¶
|
the cell, in the structure’s own setting |
|
the space group, as its International Tables standard setting |
|
a |
|
one |
|
the species the sites name |
An ASUSite carries a bare Wyckoff letter ("e", not "4e") and one exact
value per degree of freedom — none at all for a fixed position such as an
inversion centre. Partial occupancy needs nothing special: it lives in the
referenced Species, which already carries a
composition.
Any setting, including untabulated ones¶
A space group can be written in many settings, and they are genuinely
different coordinate systems. Wyckoff letter e of space group 15 reads
0,y,1/4 in the standard setting 15:b1 and 1/4,0,z in 15:c1.
httk records the Wyckoff data against the standard setting always, and carries a transform to whatever setting the structure is actually in. Because that transform is stored rather than looked up, a setting that appears in no table is representable exactly as well as a tabulated one:
from httk.atomistic import ASUSite, ASUStructure, SettingTransform, Spacegroup
from httk.core import FracVector
# A tabulated setting: look the transform up.
tabulated = Spacegroup.for_setting("15:c1").transform_from_standard
# A setting in no table: state the transform yourself.
shifted = SettingTransform(FracVector.eye((3, 3)), ["1/8", "1/8", "1/8"])
asu.setting() returns the tabulated Spacegroup when there is one and None
when there is not — an untabulated setting simply has no name, which does not
stop it working.
The transform is never derived¶
httk will not search for a transform that maps one group onto another. Such a
search is massively underdetermined: for space group 15 alone there are 192
valid candidates using only integer entries in {-1,0,1} and translations on a
1/24 grid, and the full solution set — the affine normalizer coset — is
infinite. Crucially, those candidates describe different crystals. Picking one
arbitrarily would silently produce the wrong structure, so a setting that cannot
be identified is refused with an explanation instead.
Direction¶
The transform maps standard coordinates into the structure’s own setting. Under httk’s row-vector convention:
f_own = f_std * M.T() + v
f_std = (f_own - v) * inv(M).T()
B_own = inv(M).T() * B_std # cell basis rows
Applying it backwards yields a structurally valid but systematically wrong crystal.
det M is 1 for 520 of those settings and 3 for the seven
rhombohedral-axes ones (IT numbers 146, 148, 155, 160, 161, 166, 167), where the
standard hexagonal cell holds three primitive rhombohedral cells. Expansion
handles that: the standard orbit is three times too large for the smaller cell
and the surplus points coincide exactly under wrapping.
Expansion is exact¶
There is no coordinate grid, no snapping, no neighbour search, and no tolerance
anywhere in the path from an ASUStructure to a full cell. Reduced coordinates,
symmetry operations, Wyckoff parameters, and the setting transform are all exact
rationals, and the vendored orbit tables are complete and pre-deduplicated, so
expansion is affine arithmetic over ℚ followed by an exact equality test.
structure = UnitcellStructureView(asu)
structure.sites.reduced_coords.to_fractions()
# [[Fraction(0, 1), Fraction(0, 1), Fraction(0, 1)], ...]
The cell survives untouched, so a hexagonal cell keeps its exact sqrt(3).
For an existing ASU — including a full-cell view that is still backed by one —
conventional_cell() returns the same crystal re-expressed in the space group’s IT
standard-setting conventional cell. It keeps the ASU’s standard-to-own transform in the
result for provenance, while the result ASU has an identity transform; the basis change and
expansion remain exact. For a plain Structure, the operation first runs tolerant
recognize_asu(): measured coordinates may be snapped, and the result records the transform
chosen by recognition rather than an unstated transform from the input. Consequently a noisy
plain structure need not be same_crystal()-equal to the result. The returned multiplier
records the conventional/original site-count ratio, including the factor of three for a
rhombohedral setting.
Recognition is where tolerance lives¶
Going the other way — from a full cell to an asymmetric unit — means deciding which Wyckoff position each site is meant to occupy. A measured structure’s coordinates lie near a symmetric arrangement rather than on one, so this step is tolerant, and it is the only one that is.
from httk.atomistic import recognize_asu
asu = recognize_asu(structure, setting=Spacegroup.standard(225))
The space group can be supplied three ways, in decreasing order of preference:
setting=the structure’s own tabulated setting, as when a CIF names its group. Nothing is searched for and spglib is not involved.
standard=andtransform=for a structure in a setting that appears in no table. Also spglib-free.
- neither
the symmetry is found with spglib, which must be installed —
pip install httk-atomistic[default]. This is the only path that needs it.
tolerance is a Cartesian distance measured in the real cell, so it means
the same thing along a short axis and a long one; a fractional tolerance would
not.
Left unspecified it is derived from how precisely the structure was stated —
see Data precision. A file written to four decimals in a 5 Å cell derives
1e-3; the same file written to three decimals derives ten times that, and is
matched correctly where a fixed value would silently put its atoms on the wrong
Wyckoff position. A structure that states no precision falls back to
DEFAULT_TOLERANCE.
The exactness contract¶
A file’s decimals embed as the rational the file literally wrote.
0.3333is3333/10000— never silently1/3, and neverfloat("0.3333"), whose exact value is6004199023210345/18014398509481984and claims a precision nobody stated.Choosing a position is the only tolerant step, and it is a discrete decision.
The chosen position then supplies exact values for its fixed components.
0.2499does not travel onward; the position says that component is1/4. Only the free parameters keep the measured value. Idealising those too is available throughlimit_denominator=, but it is opt-in, because turning0.3333into1/3is a claim about the data that only you can make.After that the object is exact and closed.
Expansion is lossless; recognition is not
expand → recognize → expand is idempotent. recognize → expand is not the
identity on the input coordinates, and must not be relied on as though it were —
recognition deliberately snaps a measured structure onto an idealised symmetric
one.
Round-tripping¶
Structure → ASUStructure → Structure reproduces the same crystal exactly, with
no numerical drift, and for input that was already exact the coordinates come
back identical:
from httk.atomistic import same_crystal
rebuilt = UnitcellStructureView(recognize_asu(structure, setting=setting))
assert same_crystal(structure, rebuilt)
same_crystal() is the predicate that guarantee is
stated in: same cell and same multiset of (species, wrapped coordinate),
ignoring site order and which lattice translate was written down. Site ordering
is not preserved — the rebuilt structure follows canonical expansion order — so
Structure.__eq__, which compares site lists element by element, is stricter
than crystallographic identity and is the wrong tool here.
Reading CIFs¶
A CIF is the natural source for an asymmetric unit: it lists one site per orbit and states the operations that generate the rest, so no symmetry search is needed.
from httk.atomistic import asu_structures_from_cif, load_asu_structure, load_structure
from httk.core import load
asu = load_asu_structure("structure.cif") # keeps the ASU form
structure = load_structure("structure.cif") # expands to the full cell
everything = asu_structures_from_cif(load("multi.cif", raw=True)) # one per data block
The setting is identified by comparing the file’s symmetry operations against the tabulated ones exactly, so a file written in a non-standard setting is recognized as such rather than reinterpreted.
What the file declares — a Hall symbol, or an International Tables number — is
treated as a claim to be checked, not a hint. A declaration naming no known
setting, or naming one whose operations are not the file’s, is a genuine
inconsistency and raises: the two halves of the file disagree, and quietly
believing one of them is how a wrong structure gets built. Hall symbols are
matched in the conventional spelling a CIF writes (-C 2yc), normalized to the
spelling the tables index.
When the operations are the trustworthy half — a file whose symbols are known to
be wrong — pass trust_declared_symmetry=False to ignore the declaration and
identify the setting from the operations alone:
asu = load_asu_structure("misdeclared.cif", trust_declared_symmetry=False)
The Hermann-Mauguin symbol is deliberately not checked: it has too many legitimate spellings, and OPTIMADE itself notes that it does not unambiguously communicate the axis, cell, or origin choice.
The cell is built exactly from a, b, c, α, β, γ rather than from a
pre-multiplied floating-point basis, so a cubic cell keeps exact right angles.
Occupancies become the composition of the corresponding Species.
Reading is tolerant of blocks that are not structures — a CIF may hold
bibliographic entries or an incomplete draft — so httk.core.load never fails
on their account; it records why each was skipped under unparsed, and asking
for structures raises with those reasons rather than returning an empty list.
Serving symmetry over OPTIMADE¶
StructureEntryProvider serves an ASUStructure’s symmetry automatically. The standard
OPTIMADE properties — space_group_it_number, the Hall and Hermann-Mauguin symbols,
space_group_symmetry_operations_xyz, wyckoff_positions, fractional_site_positions,
and site_coordinate_span — come straight from the ASU. A plain Structure carries no
symmetry, so they serve null; inferring a space group there would mean running a
symmetry search behind the caller’s back, with a tolerance nobody chose, on every record.
The symbols and Wyckoff letters describe the setting the structure is written in, as
OPTIMADE requires, not the standard setting the ASU stores them against. That distinction
is not cosmetic: setting 224:1 permutes Wyckoff letters i and j, and the rhombohedral
settings change multiplicities, so a 3a of the standard hexagonal cell is served as the 1a
it is in the smaller cell.
For what OPTIMADE does not standardise — which setting, and the change of basis to it — httk serves six provider-specific properties whose definitions are taken verbatim from schemas.httk.org rather than paraphrased locally:
Property |
Describes |
|---|---|
|
the setting code, e.g. |
|
the normalized Hall symbol naming the setting |
|
whether it is the IT standard setting |
|
the crystal system |
|
the lattice centring letter |
|
the standard-to-own change of basis, as exact rationals |
The _httk_ prefix is what OPTIMADE requires of a database-specific property name; the
definition keeps its own $id, so a client that follows it reaches the published schema.
A structure in an untabulated setting serves its space-group number and its transform —
enough to reconstruct it — but null for the symbols and Wyckoff letters, which name a
setting that in that case has no name.
The symmetry tables¶
The Wyckoff positions, symmetry operations, and setting transforms come from two
CC BY 4.0 datasets vendored in httk.atomistic.data: 527 space-group settings
(230 of them standard settings) and the change of basis from each to its
standard setting. They are read lazily — nothing is loaded until symmetry is
actually used. See the package’s README.md for provenance and attribution.
See also¶
Structures — the
Structuremodel these expand intoData precision — where the default tolerance comes from
Asymmetric units: reading a CIF, expanding it, and going back again — a runnable walkthrough of everything above