Architecture
Publicly, atomref is still radii-first, with a small provisional X–H helper.
Internally, the package is built around four layers:
- elements — stable element metadata and symbol canonicalization,
- registry — curated quantity and dataset metadata plus packaged data loading,
- policy core — generic value selection with overrides, transfers, fallbacks, blocked keys, and provenance,
- quantity wrappers — convenience APIs such as
atomref.radiiandatomref.xh.
Core terminology
A few terms are deliberately separated in the design:
- quantity — the operational property family being requested,
- domain — the key space used to index that quantity,
- dataset — one curated source table inside the quantity,
- policy — the ordered rule set used to select a final value.
This separation is what allows the package to say, for example, that
rahm2016 belongs to the atomic_radius quantity but can still act as support
data in a van der Waals policy.
Domain support in the current runtime
The registry schema is domain-aware, but the current resolver intentionally implements only one domain:
element
That means:
- packaged built-in sets are currently element-indexed scalar tables,
ValuePolicyresolves element symbols,- transfer fitting is performed over element-wise overlap.
The metadata keeps domain explicit now so later versions can extend the data
model without having to reinterpret existing registry entries.
Policy resolution and transfer sources
The generic resolver works in a fixed order:
- blocked keys,
- overrides,
- base dataset,
- transfer models,
- fallback,
- missing.
Transfer sources can be:
- packaged datasets,
- custom
ElementScalarSetobjects, - generic
ValuePolicyobjects, - wrapper policies exposing
as_value_policy().
That last point is important. It means higher-level code can express "infer values from my chosen covalent-radii policy" instead of being forced to refer to one hard-coded predictor dataset.
Nested-policy safeguards and cycle detection
Policy-backed transfer sources are materialized with more than just raw numeric values. The resolver also tracks, per element:
- whether the value came from
base,override, substitution, linear transfer, or fallback, - the nested transfer depth that was required to produce it,
- placeholder status.
LinearTransfer uses that information twice:
- once when fitting the linear relation (
fit_sources/fit_max_depth), - again when deciding whether the predictor value for the requested element is
admissible (
prediction_sources/prediction_max_depth).
The default policy is intentionally conservative: fit only on direct nested predictor values, but allow one additional nested completion step when predicting the final requested element. This keeps the common two-stage use case possible without silently training on arbitrarily long inference chains.
Cycle detection is handled with a context-local activation stack. Both generic
ValuePolicy objects and wrapper policies are tracked, so recursion through a
freshly materialized wrapper policy is still detected reliably and safely.
Placeholder handling
Placeholder semantics stay attached to the value that was actually returned.
This means LookupResult.is_placeholder can be true for:
- a base lookup,
- a substitution transfer,
- a nested policy used as a transfer source.
A linear transfer normally returns a computed value and therefore does not carry
placeholder status itself. Instead, its provenance is carried by
resolved_from, explanatory notes, and transfer_depth.
Why the design stays small
The package deliberately avoids a large object graph or a chemistry-specific DSL.
A quantity wrapper is usually only a thin adapter over the generic policy core.
That keeps the internals easy to test and lets other scientific packages reuse
atomref without bringing in the rest of the Delone Commons stack.