icon_sc.core.state — state layer

State layer: names/units registries, boundary DataArrays, and the S05 vault.

Canonical-name registry (architecture §2.5).

One table maps each canonical quantity name to its canonical units, CF standard name (if any), ICON Fortran short name (if any) and GRIB2 triplet (ingestion only).

Namespacing (§2.5): CF standard names are the canonical names and carry no prefix (the cf: namespace is implicit — spelling it out is rejected). Solver-internal quantities — those without a CF standard name — live in the explicit icon: namespace (icon:exner_function, icon:normal_wind, …). The registry enforces that split both ways:

  • an unprefixed registration claims CF identity (cf_name defaults to the name);

  • an unprefixed registration that disclaims CF identity (cf_name=NO_CF) is the “unnamespaced icon” error: it must be registered as icon:<name> instead;

  • an icon: registration passing a cf_name is contradictory: quantities with a CF standard name are registered under it, unprefixed;

  • any namespace prefix other than icon: is rejected.

The _on_interface_levels suffix (sympl convention) marks the height_interface dim variant of a quantity; unit lookups for a suffixed name fall back to its base quantity, since the physical quantity — hence its canonical units — is the same.

Seed rows: icon4py v0.2.0 model/common/states/data.py (see REFERENCES.lock).

icon_sc.core.state.names.NO_CF: Final[str] = '<no-cf-standard-name>'

Sentinel for “this quantity has no CF standard name” (distinct from the default None = “the canonical name is the CF standard name”).

exception icon_sc.core.state.names.NamesRegistryError

Invalid or conflicting canonical-name registration/lookup.

class icon_sc.core.state.names.QuantityDef(name, units, cf_name, icon_name, grib2)

One row of the canonical registry: name ↔ units ↔ CF ↔ ICON ↔ GRIB2.

Parameters:
icon_sc.core.state.names.base_name(name)

Strip the _on_interface_levels suffix (identity when absent).

Parameters:

name (str)

Return type:

str

icon_sc.core.state.names.is_on_interface_levels(name)

True if name carries the _on_interface_levels suffix.

Parameters:

name (str)

Return type:

bool

icon_sc.core.state.names.known_quantities()

Sorted canonical names currently registered.

Return type:

tuple[str, …]

icon_sc.core.state.names.lookup_quantity(name)

Return the registered quantity, resolving the interface-levels fallback.

Parameters:

name (str)

Return type:

QuantityDef

icon_sc.core.state.names.on_interface_levels(name)

The interface-level variant of a canonical name (idempotent).

Parameters:

name (str)

Return type:

str

icon_sc.core.state.names.register_quantity(name, units, cf_name=None, icon_name=None, grib2=None)

Register a canonical quantity (frozen interface, SPEC S02).

units is the canonical unit string for the quantity; component contracts are verified against it by icon_sc.core.state.units.verify_noop().

Parameters:
Return type:

QuantityDef

Canonical-units table access and the no-op-conversion verifier (architecture §2.4).

Canonical units are stored as plain strings in the names registry; Pint is consulted only at negotiation time (registration/verification), through a cached identity check (pattern from stubbiali/sympl oop: lru_cache on the registry call). Nothing on the execution path — in particular IngressPlan.apply() — may import Pint; the import happens lazily inside units_identical(), and only when two unit strings are not literally equal.

Unit-string cleanup (% → percent, ° → degree) and the extra degrees_north/degrees_east/percent definitions are ported from upstream sympl _core/units.py (see REFERENCES.lock).

exception icon_sc.core.state.units.UnitsError

A component’s declared units are not the canonical units (no-op violation).

icon_sc.core.state.units.canonical_units(name)

Canonical unit string of a registered quantity (frozen interface, SPEC S02).

*_on_interface_levels variants fall back to their base quantity.

Parameters:

name (str)

Return type:

str

icon_sc.core.state.units.convert_array(values, source, target)

Convert an array from source to target units via Pint (allocating).

Negotiation-time only (non-strict ingress executing a ConversionPlan, S03); strict mode forbids the call sites, and nothing on the apply path may reach this.

Raises:

UnitsError – When Pint cannot convert (undefined/incompatible units).

Parameters:
Return type:

Any

icon_sc.core.state.units.units_identical(units_a, units_b)

True iff Pint deems the two unit strings the same unit (identity, not mere dimensional compatibility): conversion between them is a no-op.

Parameters:
Return type:

bool

icon_sc.core.state.units.verify_noop(component_units, canonical)

Verify a component’s declared units equal the canonical units (§2.4).

sympl’s Pint conversion path must compile to a no-op in production; any pair Pint deems non-identity (K vs degC, m s-1 vs km h-1, 1 vs g/kg, …) is rejected. Called at negotiation time only.

Parameters:
  • component_units (str)

  • canonical (str)

Return type:

None

Boundary DataArray construction (architecture §2.2).

State values are xarray.DataArray s over FieldBuffer buffers; construction here is the only sanctioned way to stamp the attrs schema (units / location / halo / grid_uuid). The buffer is wrapped, never copied and never coerced (no .values anywhere on core paths — the duck-array lesson of §4.2).

icon_sc.core.state.dataarray.make_dataarray(buffer, *, name, dims, units, location, grid_uuid=None)

Wrap buffer in a boundary DataArray (frozen interface, SPEC S02).

Stamps attrs['units'], attrs['location'], attrs['halo'] (HaloState.VALID — a freshly constructed field has no stale ghost points by definition; the halo validator pass flips it) and, when given, attrs['grid_uuid'] (provenance: refuse to mix grids, §2.2).

Raises:
  • TypeError – If buffer does not satisfy the FieldBuffer protocol.

  • ValueError – On a rank mismatch, multiple horizontal dims, or a horizontal dim contradicting location.

Parameters:
  • buffer (FieldBuffer)

  • name (str)

  • dims (Sequence[str])

  • units (str)

  • location (Location | str)

  • grid_uuid (str | None)

Return type:

DataArray

StateVault — the state’s execution-phase form (architecture §8.2, SPEC S05).

A dense, slotted container: a flat list of raw buffers plus an interned name → index map that is consulted only at bind time. The public dict-of-DataArrays view survives as a lazily materialized façade (icon_sc.core.state.facade), so monitors, interactive inspection and the interpreted tier keep unmodified sympl semantics while nothing xarray-shaped executes on the step path.

Two counters govern coherence:

  • epoch — bumped by any out-of-band state mutation through the façade (rebinding or deleting a field). A plan materialized against the vault records the epoch and raises StalePlanError on the next run_step after a bump (§8.2 staleness guard).

  • generation — bumped by plan-internal buffer swaps (Swap). It invalidates the façade’s cached DataArray wrappers only; the plan stays valid (swaps are its own doing).

class icon_sc.core.state.vault.SlotMeta(name, dims, units, location, dtype, attrs)

Boundary metadata of one vault slot (what the façade needs to rewrap it).

Parameters:
class icon_sc.core.state.vault.StateVault

Slotted execution-phase state (frozen interface, SPEC S05).

StateVault.from_state(state) adopts the state’s raw buffers (zero-copy: .data of every boundary DataArray, never .values); vault.facade() returns the lazy dict-of-DataArrays view. buffers/names/ schema_hash/epoch are the public §8.2 surface; the compiler may append slots for published outputs at materialization (add_slot), which refreshes schema_hash.

classmethod from_state(state)

Build a vault over a dict-of-DataArrays state (frozen interface).

Buffers are adopted, not copied; the time entry is carried alongside the slots (it is not a field). Non-DataArray entries other than time are rejected — the vault is the execution form of a boundary state.

Parameters:

state (Mapping[str, Any])

Return type:

StateVault

add_slot(name, buffer, meta)

Append a published-output slot (compiler use, bind/materialize time).

Refreshes schema_hash (the slot set is part of the schema) and bumps generation so a live façade picks the new field up.

Parameters:
Return type:

int

meta(index)

The boundary metadata of slot index.

Parameters:

index (int)

Return type:

SlotMeta

dim_sizes()

Dimension-name → length map over all slots (consistency-checked).

Return type:

dict[str, int]

note_out_of_band_mutation()

Record an out-of-band mutation (façade rebind/delete): plans go stale.

Return type:

None

note_swap()

Record a plan-internal buffer swap: façade caches invalidate, plans stay valid.

Return type:

None

facade()

The lazy dict-of-DataArrays view (frozen interface, SPEC S05).

DataArray wrappers are cached per slot and reconstructed only when the vault generation moved (slot swaps, added slots) — sympl semantics for monitors and inspection, zero cost for the step path. The same façade object is returned on every call. Rebinding or deleting a field through the façade bumps epoch and stales any bound plan.

Return type:

Mapping[str, DataArray]

Lazy DataArray façade over a StateVault (§8.2).

The public dict-of-DataArrays state view of the execution tier: DataArray wrappers are materialized on access, cached per slot, and invalidated by the vault’s generation counter (bumped by plan-internal swaps and slot additions). Rebinding or deleting a field through the façade is an out-of-band mutation: it bumps the vault epoch, and any plan bound to the vault raises StalePlanError on its next run_step (SPEC S05 guard). In-place writes to a field’s buffer (facade[name].data[...] = ...) preserve buffer identity and do not stale plans — values are the user’s business, identities are the plan’s.

class icon_sc.core.state.facade.VaultFacade(vault)

Mapping view of a vault: slot fields as boundary DataArrays plus time.

Parameters:

vault (StateVault)