Skip to content

ReactionNetworkBenchmark with packaged FAIR cards (#65) - #74

Merged
tjkessler merged 7 commits into
mainfrom
feature/reaction-benchmark
Oct 15, 2026
Merged

tjkessler merged 7 commits into
mainfrom
feature/reaction-benchmark

Conversation

@tjkessler

Copy link
Copy Markdown
Owner

Refs #65. Seventh PR for the reaction-network datasets. Stacked on #73 (→ #72 → #71 → #70 → #69). Merge in order #69 → … → #73 → this one. Each PR retargets to main when the one below it merges, and CI starts then.

Changes

FAIR cards (the source of truth). Six JSON cards under koopman_graph/datasets/reaction/data/ are loaded with importlib.resources. I checked that they ship in the built wheel.

dataset_id Generator Simulator Notes
reaction_chain_v1 first_order_chain(4) exact expm closed-form spectrum {−0.25, −0.5, −1} + 1 conservation zero
reaction_michaelis_menten_v1 catalog RK4 2 conservation laws; boundary equilibrium; hyperedge encoding
reaction_brusselator_v1 catalog RK4 unstable focus / limit cycle (negative timescales)
reaction_random_network_v1 sample_random_network(seed=0) RK4 element-balanced; drains onto a boundary steady state
reaction_van_de_vusse_v1 CSTR RK4 + inputs seeded piecewise-constant (T, F/V) stored as global control_inputs
reaction_cantera_h2o2_v1 Cantera h2o2.yaml Cantera ReactorNet [cantera]; isothermal const-volume at 1200 K; SHA-256 recorded

Each card records:

  • the generator and its parameters, the simulator and substeps, the time grid and units, and the default count, length, and seed;
  • per-species initial ranges (plus control ranges and hold steps for the CSTR), the log floor, and the graph encoding;
  • scope, format, provenance, license, and limitations, plus a Cantera section for the h2o2 card (version, SHA-256, reference state, the illustration-only note, the citation DOI);
  • an oracle section: reference state, conservation laws, dynamic eigenvalues (slowest first), and timescales (null = infinite).

ReactionNetworkBenchmark is exported from koopman_graph.datasets, like the other *Benchmark classes; the root __all__ is unchanged.

  • dataset_ids() and load_card(id) list and read the packaged cards.
  • generate(id, *, num_trajectories, num_timesteps, seed, simulator="card"|"ssa", volume) draws seeded initial conditions (and control schedules) and integrates them. It returns a ReactionDataset with raw float64 concentrations (N, T, S), optional controls (N, T, C), and the card. simulator="ssa" swaps in Gillespie paths for the uncontrolled, non-Cantera cards.
  • ReactionDataset.sequence(i) returns a GraphSnapshotSequence with one node per species. Features are the (log-)concentration as a (S, 1) tensor, edge_index / edge_weight come from species_influence_graph (plus hyperedge_index for reaction_hyperedge), and it carries timestamps and global control_inputs.
  • split does a deterministic trajectory-level 70/10/20 split by index (trajectories are i.i.d.).
  • save / load write and read versioned torch.save payloads (format_version = 1), loaded with weights_only=True.

Keeping cards honest.

  • compute_card_oracle(card) computes the oracle section. It uses equilibrium and oracle_spectrum for catalog and random cards, the steady state and J − D·I at the control midpoints for the CSTR, and Cantera's equilibrate("TV") for h2o2.
  • scripts/build_reaction_cards.py writes the cards, and --check exits 1 if a committed card is stale.
  • The tests recompute every oracle and run --check.

Fixes found while building the cards (separate commit e51a327, plus a follow-up)

  • equilibrium could return a false equilibrium. On the converted Cantera h2o2 system, the clamped Newton solve stopped at a near-boundary point (H, O ≈ 0). The absolute residual there was about 5e-11, but net production equalled gross turnover, so flux was one-way and it wasn't a steady state. Cantera's equilibrium has net/gross ≈ 4e-15.
    • equilibrium now requires |dc_s/dt| ≤ 1e-6·gross_s + 1e-12·(peak gross turnover at initial) and raises otherwise.
    • The absolute term still accepts true boundary steady states with vanishing leftovers, such as the random network, which drains species to about 1e-31.
    • Exact boundary equilibria (Michaelis-Menten) are unaffected.
  • eigenvalue_timescales (new, used by OracleSpectrum.timescales) maps |Re λ| ≤ 1e-12·max|λ| to inf. Before, the random network's drained mode showed up as an 8e16 timescale instead of infinite.

Tests (tests/datasets/test_reaction_benchmark.py)

  • Every card has the required FAIR fields, and an unknown id is rejected.
  • Card oracles match recomputation for all 5 non-Cantera cards. The Cantera card matches within 1e-6 and its SHA matches the bundled file.
  • The chain oracle is closed-form, and the random network's drained mode is infinite.
  • Seeds reproduce data exactly and different seeds differ. Defaults come from the card.
  • Closed datasets conserve elements to 1e-9.
  • Initial conditions stay inside the card ranges, trajectories equal a rerun of the card's simulator, and a long Michaelis-Menten run reaches the boundary equilibrium.
  • Sequence layout, transform, hyperedge encoding, and timestamps are correct.
  • CSTR controls are piecewise-constant over hold_steps, in range, and exposed as control_inputs.
  • SSA output is integer counts / V and seed-reproducible; invalid uses are rejected.
  • The split is deterministic and covers every trajectory; edge cases are rejected.
  • Payloads round-trip, including controls, and wrong format or version is rejected.
  • build_reaction_cards.py --check is clean (skipped without Cantera).

Verification (macOS arm64, Python 3.13, Cantera 3.2.0)

  • ruff check and ruff format --check on src/ tests/ scripts/ pass.
  • pytest -n auto -m "not slow and not distributed and not ray" --cov=koopman_graph --cov-fail-under=90: 3516 passed, 26 skipped, 97.30%. benchmark.py is at 100% and oracles.py at 99%.
  • sphinx-build -W --keep-going passes. The class is documented once, under koopman_graph.datasets, because the reaction automodule excludes it to avoid a duplicate-object error.
  • python -m build --wheel includes all 6 cards.

Only the example notebook remains for #65 after this.

🤖 Generated with Claude Code

tjkessler and others added 3 commits October 15, 2026 15:57
equilibrium() clamps Newton iterates to c >= 0. On the converted Cantera
h2o2 system at 1200 K it stopped at a near-boundary point (H, O ~ 0)
where every flux is tiny and the absolute residual (~5e-11) looked
converged, but net production equalled gross turnover: flux ran one way,
so it was not a steady state. Cantera's thermochemical equilibrium has
net/gross ~ 4e-15 there.

After convergence, require max_s |dc_s/dt| / gross turnover_s <= 1e-6
and raise RuntimeError otherwise (suggesting a better guess). Exact
boundary equilibria (e.g. Michaelis-Menten, zero flux) still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #65. Fills in koopman_graph.datasets.reaction.benchmark:

- Six FAIR JSON cards under reaction/data/ define the frozen datasets
  (chain, Michaelis-Menten, Brusselator, random network, Van de Vusse
  CSTR with controls, Cantera h2o2). Cards record generator, simulator,
  grid, seed, initial ranges, log floor, graph encoding, oracle
  references, units, license, limitations, and Cantera provenance.
- ReactionNetworkBenchmark.generate -> ReactionDataset (float64
  concentrations, controls, per-trajectory GraphSnapshotSequence with
  species nodes, timestamps, control_inputs), optional SSA simulator,
  deterministic trajectory split, versioned save/load (weights_only).
- compute_card_oracle + scripts/build_reaction_cards.py (--check) write
  the oracle sections; tests recompute them.
- eigenvalue_timescales maps numerically zero Re(λ) to inf; the
  stationarity check gains a flux-scaled absolute tolerance so boundary
  steady states with vanishing leftovers are accepted.
- Exported from koopman_graph.datasets (root __all__ unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- build_reaction_cards.py --check compares card numbers with the test
  tolerances instead of byte-exact text, so platform/BLAS last-digit
  differences no longer fail CI, and flags reaction_*.json files that no
  spec produces.
- system_from_card (and so generate / compute_card_oracle) refuses a
  Cantera card whose recorded mechanism SHA-256 differs from the
  installed file.
- eigenvalue_timescales treats only round-off-level real parts
  (100 * eps * max|lambda|) as zero, so slow modes in stiff spectra stay
  finite.
- split rejects zero ratios instead of silently assigning one trajectory.
- SSA path seeds are drawn from the dataset generator (no aliasing across
  dataset seeds).
- ReactionDataset.features bounds-checks its index like sequence.
- compute_card_oracle builds the CSTR reactor once.
- CHANGELOG: move the extras / marker / CI / import-hygiene entries back
  under Added.
- Tests: equilibrium raises on a forced Newton stall, stiff-spectrum
  timescales, tolerant card comparison, orphan cards, SHA mismatch, index
  checks, split zero ratios, SSA seed independence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Carries the reviewed #69-#73 fixes up to #74. Conflict in
datasets/reaction/oracles.py resolved by taking #71's hardened version
(exact projector linear equilibrium, residual-checked Newton with
line-search stall detection, OracleSpectrum.zero_tolerance, one SVD for
rank / range / conservation laws) and re-applying #74's additions:

- _require_stationary flux check on both Newton return paths, so a
  clamped one-way boundary stall is still refused;
- eigenvalue_timescales as the single timescale implementation, now with
  an optional absolute zero_tolerance (OracleSpectrum.timescales passes
  its own; 0 compares exactly) and the round-off default otherwise.

test_equilibrium_rejects_one_way_boundary_point accepts either guard's
message, since both refuse the stalled point. Committed cards are
unchanged (build_reaction_cards.py --check passes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tjkessler added a commit that referenced this pull request Oct 15, 2026
Carries the reviewed #69-#74 fixes up to #75 and clears its conflict.
benchmark.py: keep the graph_encoding validation in
ReactionDataset.sequence and use #74's _check_index helper. CHANGELOG:
keep the notebook / graph_encoding entries and #74's eigenvalue_timescales
and Cantera SHA-256 entry (updated for the optional zero_tolerance).

The notebook re-executed on the merged code prints the same numbers as
its committed outputs, so the outputs are left unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from feature/reaction-cantera-io to main October 15, 2026 21:05
tjkessler and others added 3 commits October 15, 2026 17:07
Ubuntu CI failed test_card_oracle_matches_recomputation (Michaelis-Menten,
random network), test_cantera_card_oracle_and_provenance, and the card
builder --check: oracle['conservation_laws'] drifted. With two or more
conservation laws any rotation of an orthonormal basis is valid, and
torch.linalg.svd returns a different one under Linux LAPACK than under
macOS Accelerate, so the committed cards held a platform-specific basis.
The single-law chain card was unaffected.

_stoichiometric_basis now canonicalizes the conservation laws:
Gram-Schmidt in species order on the columns of the projector P = W^T W,
which is basis independent, so every platform gets the same rows (to
rounding) with a fixed sign. Cards regenerated; apart from the rotated
laws, oracle values move only at rounding level (<= 3e-14). Adds a test
that a rotated, sign-flipped basis canonicalizes back to the same rows.

Also merges main (#70-#73) into the branch, which GitHub reported as
behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After f20605b, Linux CI still failed test_cantera_card_oracle_and_provenance
and the card builder --check on reaction_cantera_h2o2_v1, again on
oracle['conservation_laws']. The basis itself now matches across platforms;
what differed were entries that are exactly zero in theory but come out as
round-off (1.07e-16, 5.37e-17 on macOS). Their values depend on the LAPACK
build, and the Cantera card is compared with atol=1e-18 (appropriate for its
~1e-14 concentrations, not for O(1) basis entries).

_canonical_row_basis now sets entries below 1e-12 to exactly zero, so the
stored basis is identical across platforms rather than merely close. Cards
regenerated: changes are those zeros plus rounding-level shifts (<= 1.8e-15).
Test asserts that injected 1e-16 noise canonicalizes to an exact zero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.70060% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/koopman_graph/datasets/reaction/oracles.py 97.87% 0 Missing and 1 partial ⚠️
Files with missing lines Coverage Δ
src/koopman_graph/datasets/__init__.py 100.00% <100.00%> (ø)
src/koopman_graph/datasets/reaction/__init__.py 100.00% <100.00%> (ø)
src/koopman_graph/datasets/reaction/benchmark.py 100.00% <100.00%> (ø)
src/koopman_graph/datasets/reaction/oracles.py 99.42% <97.87%> (-0.58%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tjkessler
tjkessler merged commit 8abe7ad into main Oct 15, 2026
13 checks passed
@tjkessler
tjkessler deleted the feature/reaction-benchmark branch October 15, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant