Skip to content

Reaction catalog and oracle equilibria, spectra, timescales (#65) - #71

Merged
tjkessler merged 3 commits into
mainfrom
feature/reaction-catalog-oracles
Oct 15, 2026
Merged

tjkessler merged 3 commits into
mainfrom
feature/reaction-catalog-oracles

Conversation

@tjkessler

Copy link
Copy Markdown
Owner

Refs #65. Fourth PR for the reaction-network datasets. Stacked on #70, which is stacked on #69. Merge in order #69 → #70 → this one; each PR retargets to main when the one below it merges (branch auto-delete is on). CI runs only on PRs targeting main, so it starts once this PR retargets.

Changes

koopman_graph.datasets.reaction.catalog: reference systems with frozen defaults. The defaults are module constants, because changing them changes every dataset built from the catalog.

Constructor Closed Notes
first_order_chain(n, rate_constants=None) yes S0 → … → S(n-1); default rates 1, 0.5, 0.25, … keep the eigenvalues distinct
reversible_dimerization() yes 2 A ⇌ A2 (3.0, 0.5); element X counts monomer units
michaelis_menten() yes E + S ⇌ ES → E + P (10, 1, 0.5); elements Enz, Sub; equilibrium on the boundary
brusselator(a, b) no (1, 3): unstable focus with a limit cycle (b > 1 + a²)
lotka_volterra(α, β, γ) no neutral centre with eigenvalues ±i√(αγ)

koopman_graph.datasets.reaction.oracles

  • conservation_laws returns an orthonormal basis W of the left nullspace of N, so W N = 0. This covers element balance plus any extra stoichiometric conservation. stoichiometric_rank gives rank(N).
  • equilibrium(system, initial, *, guess=None) finds a steady state with the same conserved totals as initial.
    • Linear systems: an exact solve of [A; W] c = [-b; W c0]. Raises ValueError when there is no steady state, e.g. a feed into a species with no outflow.
    • Nonlinear systems: damped Newton on the projected rate equations plus the conservation constraints, with backtracking and c ≥ 0. Raises RuntimeError if it doesn't converge.
  • oracle_spectrum(system, state) returns an OracleSpectrum with:
    • the Jacobian (from mass_action_jacobian);
    • dynamic eigenvalues, slowest first, with the S − rank(N) conservation zero modes removed (J = N ∂r/∂c has rank ≤ rank(N));
    • timescales = −1/Re(λ): positive for decaying modes, inf for a centre, negative for growing modes;
    • is_stable, and discrete_eigenvalues(Δt) = exp(λ Δt) for comparison with a learned discrete Koopman operator.
  • oracle_timescales is a shortcut for the timescales alone.

Tests (all against closed forms)

  • The 4-species chain with rates (2, 1, 0.25) gives eigenvalues {−0.25, −1, −2} plus one removed zero, and timescales {4, 1, 0.5}.
  • Dimerization reaches equilibrium at (1/4, 3/8) with λ = −(4k_f A* + k_r) = −3.5. A long RK4 run converges to that equilibrium within 1e-10.
  • Michaelis-Menten reaches its boundary equilibrium (E0, 0, 0, S0). Its two dynamic eigenvalues match the free-substrate block, two conservation modes are removed, and RK4 converges to it.
  • The Brusselator fixed point (a, b/a) has λ = 0.5 ± i√3/2, so it's unstable with timescales −2.
  • Lotka-Volterra has λ = ±i, infinite timescales, and |exp(λΔt)| = 1. Its RK4 orbit conserves V = x − ln x + y − ln y.
  • The open linear steady state is (feed/k1, k1A*/k_out). A missing steady state and Newton non-convergence both raise.
  • W N = 0, W has orthonormal rows, and the element-matrix rows lie in the span of W.
  • Every closed catalog system passes check_element_balance.

Verification (macOS arm64, Python 3.13)

  • ruff check and ruff format --check on src/ tests/ pass.
  • pytest -n auto -m "not slow and not distributed and not ray" --cov=koopman_graph --cov-fail-under=90: 3410 passed, 26 skipped, 97.25%. catalog.py is at 100% and oracles.py at 99%.
  • sphinx-build -W --keep-going passes.

🤖 Generated with Claude Code

Refs #65. Fills in koopman_graph.datasets.reaction.catalog and .oracles:

- catalog: first_order_chain, reversible_dimerization, michaelis_menten
  (closed, element-balanced), brusselator and lotka_volterra (open), with
  frozen default parameters exposed as module constants.
- oracles: conservation_laws (orthonormal left nullspace of N),
  stoichiometric_rank, equilibrium (exact for linear systems; damped
  non-negative Newton within the compatibility class otherwise; reports
  missing steady states and non-convergence), and oracle_spectrum /
  oracle_timescales / OracleSpectrum (Jacobian, dynamic eigenvalues with
  conservation zero modes removed, -1/Re(λ) timescales, exp(λ Δt) flow-map
  eigenvalues).

Pure PyTorch; reuses mass_action_jacobian and linear_rate_equations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Linear equilibrium returns c_p + P0 (c0 - c_p), the t -> inf limit,
  so non-unique steady states (A -> B, A -> C) split by rate instead of
  falling back to lstsq's minimum-norm point; a defective zero mode raises.
  Rounding-level values are snapped to zero instead of going negative.
- Newton also requires a small residual to converge and raises when the
  line search stalls, instead of reporting a tiny clamped step as a root.
- OracleSpectrum gains zero_tolerance (1e-10 ||J||_F from oracle_spectrum)
  so near-zero real parts read as neutral in timescales and is_stable.
- One SVD of N gives rank, range basis and conservation laws.
- Every catalog constructor rejects non-finite or non-positive rates.

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

codecov Bot commented Oct 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/koopman_graph/datasets/reaction/__init__.py 100.00% <100.00%> (ø)
src/koopman_graph/datasets/reaction/catalog.py 100.00% <100.00%> (ø)
src/koopman_graph/datasets/reaction/oracles.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tjkessler added a commit that referenced this pull request Oct 15, 2026
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>
Base automatically changed from feature/reaction-graphs-integrate to main October 15, 2026 21:03
@tjkessler
tjkessler merged commit 8a323da into main Oct 15, 2026
13 checks passed
@tjkessler
tjkessler deleted the feature/reaction-catalog-oracles branch October 15, 2026 21:04
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