Skip to content

ADR-0003 — Canonical species Y_extras layout at the kernel boundary

Status: Accepted

Context

A pipe carries a primary advected species (pipe.Y) plus a variable number of extra species columns (pipe.Y_extras, shape (N, n_extra)). Different cases carry different species in different column orders — e.g. the AVL Cruise-M importer drops some species and appends inert N2O/NH3, producing a non-canonical column order.

The stiff Brinkmeier @njit chemistry kernel, however, hardcodes a fixed canonical species set (N_EXTRAS = 10, every species in a fixed slot) — it must, because a Numba kernel can't introspect a per-pipe column map at compile time.

The original wrapper copied a pipe's extras into the kernel by position. When a pipe's column order didn't match the kernel's canonical order, species were silently mis-slotted: the chemistry consumed/produced the wrong species. This was the real root cause of an intermittent TWC validation flake (the species-misalignment bug class).

Decision

The wrapper that calls the chemistry kernel pads a pipe's sparse Y_extras into the kernel's full canonical layout by name, not by position: each present species is placed in its canonical slot (extras_canonical = SPECIES_NAMES[1:]), and absent species default to zero (src/aeolus1d/closures/twc_brinkmeier.py, the "Pad pipe.Y_extras into the kernel's full canonical layout" block). The result is unpacked back by name on return.

The invariant: at the kernel boundary, species are addressed by canonical name→slot, never by raw column index.

Update 2026-07: two refinements, invariant unchanged. (1) SPECIES_NAMES later gained an appended NH3 at index 11 (produced only by the opt-in passive-SCR reaction), so the kernel's canonical extras are now SPECIES_NAMES[1:1 + N_EXTRAS] (= SPECIES_NAMES[1:11], 10 slots, NH3 sliced off) rather than the whole SPECIES_NAMES[1:]; the BS kernel does not model NH3, and validate_case_pre_run refuses A_nh3_form_brnk > 0 with chem_solver='bs_brinkmeier'. (2) The pad/unpack is now performed by the ExtrasCanonicalMap helper (src/aeolus1d/closures/_extras_layout.py, build/to_dense/apply_back) rather than an inline loop, but the name→slot mapping is identical. N_EXTRAS = 10 is defined in src/aeolus1d/solver/chem_bs_brinkmeier.py.

Consequences

  • A case may carry any subset of species in any order without scrambling the chemistry — the misalignment bug class is closed by construction.
  • The kernel's canonical layout (N_EXTRAS, SPECIES_NAMES) is a contract: changing it requires updating the canonical list, not every call site.
  • A SpeciesContract handshake later generalised this name-resolution to all reaction kinds; this ADR records the underlying invariant.
  • tests/integration/test_species_conservation_network.py deliberately runs a pipe with a non-canonical extras order to pin the invariant.

References

  • src/aeolus1d/closures/twc_brinkmeier.py — the canonical pad/unpack.
  • tests/integration/test_species_conservation_network.py — the regression pin.