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
SpeciesContracthandshake later generalised this name-resolution to all reaction kinds; this ADR records the underlying invariant. tests/integration/test_species_conservation_network.pydeliberately 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.