Data precision

A data file states its numbers to a definite number of digits, and that is a claim. 0.3333 says a coordinate is known to about 1e-4 of a cell edge; 0.33 says only 1e-2. httk records that claim and uses it, so a matching tolerance or an spglib symprec follows the data instead of being a constant somebody guessed.

from httk.atomistic import load_asu_structure, structure_tolerance

asu = load_asu_structure("measured.cif")
asu.coordinate_precision      # Fraction(1, 10000) — from the file's own digits
asu.cell.precision            # Fraction(3, 10000) — widened by a stated esd
structure_tolerance(asu)      # a matching tolerance derived from both

What is recorded

Two values, on the objects that own the numbers they describe.

Where

Units

coordinate precision

Sites

fractional (dimensionless)

basis precision

Cell

the cell’s length units

Both are exact rationals — 1e-4 is Fraction(1, 10000) — so nothing picks up binary noise on the way to becoming a tolerance. None means unknown, which is a real answer and not the same as claiming exactness. A non-positive value is rejected: zero would assert an exactness no measurement has.

Structure reads both through, and adds the value a tolerance actually wants:

structure.coordinate_precision     # fractional
structure.basis_precision          # a length
structure.cartesian_precision()    # the coordinate precision as a length

cartesian_precision() multiplies by the longest cell edge — the largest displacement a fractional uncertainty can produce — and takes the cell’s own precision as a floor, since a cell known only to 1e-3 cannot place an atom better than that however many digits the coordinates carry. The same 1e-4 coordinates therefore mean 5e-4 in a 5 Å cell and 3e-3 in a 30 Å one, which is the entire reason a fractional precision has to be converted before it is used as a distance.

Precision is not part of identity

It takes no part in ==, on Cell, Sites, Structure, or in same_crystal(). The same atoms read from a more carefully written file are still the same structure. (Cell.__eq__ already ignored the scale/unscaled factoring for the same reason.)

Where it comes from

Any source that writes its numbers to a definite number of digits infers a precision from them.

CIF states both a number of digits and, often, an explicit standard uncertainty. Where both are present the coarser wins, that being the weaker and therefore safer claim — so 5.6402(3) is precise to 3e-4, not to the 1e-4 its four decimals alone would suggest.

POSCAR has no uncertainty concept, so the digits are all there is. Two conversions happen when the structure is built, because they need the assembled cell:

  • Cartesian coordinates are a length, so they are divided by the shortest cell edge to become fractional.

  • The cell vectors are multiplied by the scaling factor, so their precision scales with them. The scaling factor’s own digits are deliberately not charged as uncertainty: it defines units rather than being measured apart from the rows it scales, and charging it would declare a 5.64 Å cell precise to only half an ångström because the file wrote 1.0.

Across a set of values the coarsest wins: a structure is only as precisely stated as its least precisely stated number, so one sloppy 0.5 in a table of six-decimal coordinates really does mean the table is good to 1e-1.

A value written as an exact rational — 1/3 — states a value rather than a measurement and contributes nothing at all, rather than dragging the table down to the width of its last digit.

The underlying primitive is in httk-core and is usable directly:

from httk.core import combined_precision, decimal_precision

decimal_precision("0.3333")      # Fraction(1, 10000)
decimal_precision("1.2e-3")      # Fraction(1, 10000)
decimal_precision("1/3")         # None — exact, not measured
combined_precision(["0.123456", "0.5"])   # Fraction(1, 10) — the coarsest

What it is used for

structure_tolerance() turns a recorded precision into a matching tolerance, and it is the default for every tolerant operation in the package: recognize_asu(), ASUStructureView, asu_structure_from_cif(), and spglib’s symprec. Passing an explicit tolerance= still overrides it.

The tolerance is the Cartesian precision times two — two independently rounded values can differ by twice their precision, so anything tighter would reject data that is in fact consistent — capped so that it can never reach half the closest approach between two sites, which is what would let genuinely distinct atoms merge.

As a calibration, coordinates written to four decimals in a 5 Å cell derive exactly 1e-3, matching the fallback used when precision is unknown. Recorded precision lets other data derive an appropriate value.

Why it matters

Consider a CIF written to three decimals whose site is meant to sit on Wyckoff 4e of space group 15 (0, y, 1/4), rounded to 0.001 0.333 0.251 — 0.005 Å off in x and 0.007 Å off in z:

tolerance

outcome

fixed 1e-3

misses the special position, lands on the general position 8f, generates 8 atoms

derived 0.014 Å

recognised as 4e, generates 4 atoms

Using the recorded precision avoids silently producing a structure with twice the correct number of atoms in this case.

Minimum separation is a cap, not a precision

The minimum separation bounds a derived tolerance from above. Treating it as part of the recorded precision would be incorrect: two accidentally close atoms would make a structure look far more precisely stated than it is.

Serving it

Two provider-specific OPTIMADE properties, described by published definitions from schemas.httk.org rather than paraphrased locally:

Property

Definition

Unit

_httk_coordinate_precision

core/fractional_coordinate_precision

dimensionless

_httk_basis_precision

core/length_precision

ångström

Both serve null for a structure that states no precision. OPTIMADE standardises nothing here — the only standard_uncertainty in its structures definition is CODATA metadata inside a unit definition — so these carry the _httk_ prefix OPTIMADE requires of a database-specific property name, while the definitions keep their own $id.

Storing precision needs nothing special: float and Fraction are already first-class scalar kinds in httk-data.

See also