Skip to content

Reaction graph encodings and integrators (#65) - #70

Merged
tjkessler merged 5 commits into
mainfrom
feature/reaction-graphs-integrate
Oct 15, 2026
Merged

tjkessler merged 5 commits into
mainfrom
feature/reaction-graphs-integrate

Conversation

@tjkessler

Copy link
Copy Markdown
Owner

Refs #65. Third PR for the reaction-network datasets. Stacked on #69: it targets #69's branch, so the diff shows only this PR's changes. Merge #69 first, then retarget this PR to main if GitHub doesn't do it automatically.

Changes

koopman_graph.datasets.reaction.graphs (built from stoichiometry only)

  • species_influence_graph(system, *, self_loops=False) returns (edge_index, edge_weight). There is an edge s → t when species s is a reactant of a reaction that changes t, which is the sparsity pattern of the mass-action Jacobian ∂(dc_t/dt)/∂c_s. Each edge's weight is the number of reactions that link the pair. Edges are unique and sorted.
  • reaction_hyperedge_index gives one hyperedge per reaction, over its reactants and products, in the library's hyperedge_index layout ((2, nnz), row 0 = species, row 1 = reaction).
  • reaction_directed_hyperedges returns tail_index (reactants) and head_index (products) in the same layout. Source and sink reactions leave a side empty, so the docstring says to pass num_reactions explicitly.
  • species_reaction_bipartite returns a PyG HeteroData with species and reaction nodes and stoichiometric coefficients as edge weights. Reaction nodes carry rate_constant. The node/edge type names are exported as constants.

koopman_graph.datasets.reaction.integrate (all paths return (len(times), ..., S), with times[0] the time of the initial state)

  • integrate_expm: exact propagation for linear systems (every order ≤ 1). It folds dc/dt = A c + b into one augmented (S+1)×(S+1) matrix exponential, so zeroth-order feeds are exact too. Takes batched initial states. linear_rate_equations exposes (A, b).
  • integrate_rk4: fixed-step RK4 in torch with substeps per output interval. Batched, keeps the input's dtype and device, and is differentiable.
  • integrate_stiff: SciPy solve_ivp with BDF, Radau, or LSODA and the exact Jacobian. SciPy is imported only through require_scipy(), whose ImportError names [reaction]. Raises RuntimeError if the solver fails.
  • integrate_reaction_system(..., method="auto") uses expm for linear systems and rk4 otherwise. Stiff methods must be requested explicitly.
  • mass_action_jacobian(system, c): exact ∂(dc/dt)/∂c, finite at c = 0 (it never divides by c). The upcoming oracles module can reuse it.

Other

  • New names are exported from koopman_graph.datasets.reaction; the root and datasets __all__ are unchanged. CHANGELOG [Unreleased] entry added.
  • Unrelated flaky-test fix (separate commit): tests/operators/test_hetero_rectangular.py::test_rectangular_model_forward_and_gradients had no seed and fails for about 7.5% of random initializations (15 of 200 seeds give an all-zero relation gradient). Adding tests reshuffled the xdist workers and it began failing every time locally, so the test now sets torch.manual_seed(0).

Tests

  • Matrix exponential vs the closed-form A → B → C solution, to 1e-10 relative.
  • RK4 vs exact on a fed chain (batched), and total mass growing at exactly the feed rate.
  • Element conservation along RK4 (dimerization) and stiff trajectories, via element_balance_violation.
  • Robertson's stiff problem with BDF, Radau, and LSODA; BDF matches the reference A(1) ≈ 0.9664597.
  • Analytic Jacobian vs autograd, at a generic state and at c = 0.
  • Influence-graph edges equal the Jacobian's nonzero pattern.
  • Missing SciPy, simulated by putting None in sys.modules, raises the [reaction] hint.
  • Validation of times, initial states, and options.

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: 3385 passed, 26 skipped, 97.24%. graphs.py and integrate.py are at 100%.
  • sphinx-build -W --keep-going passes.

🤖 Generated with Claude Code

tjkessler and others added 2 commits October 15, 2026 15:16
test_rectangular_model_forward_and_gradients asserts a nonzero relation
gradient, which about 7.5% of random initializations (15 of 200 seeds)
fail through dead ReLU paths on the tiny graph. It inherited RNG state
from whatever ran earlier in its xdist worker, so adding unrelated tests
could flip it. Pin torch.manual_seed(0).

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

- graphs: species_influence_graph (Jacobian-sparsity species graph with
  linking-reaction counts), reaction_hyperedge_index (library hyperedge
  layout), reaction_directed_hyperedges (reactant tails / product heads),
  species_reaction_bipartite (HeteroData with stoichiometric weights).
- integrate: integrate_expm (exact augmented matrix exponential for
  linear systems, including zeroth-order feeds), linear_rate_equations,
  integrate_rk4 (batched, differentiable), integrate_stiff (SciPy
  BDF/Radau/LSODA with an exact Jacobian, behind require_scipy and the
  [reaction] extra), integrate_reaction_system (method="auto"), and
  mass_action_jacobian.

SciPy is imported only inside the stiff path, so import hygiene holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- integrate_reaction_system: method="auto" accepts substeps for linear
  and nonlinear systems alike (ignored on the exact path), rejects other
  options up front, and type-checks the system before dispatch.
- integrate_expm: build the generator and propagators on the input's
  device instead of always on CPU.
- species_reaction_bipartite: keep rate_constant in float64 so tiny or
  huge rate constants do not under/overflow.
- mass_action_jacobian: evaluate over padded reactant slots, so memory
  is O(R * width^2) instead of a dense (R, S, S) tensor.
- integrate_stiff: precompute stoichiometry and Jacobian slots once
  rather than revalidating and recasting on every solver callback.
- Share one _require_system helper and reuse
  ReactionSystem._validate_concentrations; finiteness errors name the
  argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tjkessler
tjkessler added this pull request to stack #76 October 15, 2026 20:43
@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/graphs.py 100.00% <100.00%> (ø)
src/koopman_graph/datasets/reaction/integrate.py 100.00% <100.00%> (ø)
src/koopman_graph/datasets/reaction/system.py 99.23% <100.00%> (+0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

test_doubly_stochastic_row_and_column_sums drew unseeded torch.randn
logits and asserted row/column sums within 1e-4. The default 8 Sinkhorn
sweeps are documented as approximate: 63 of 500 random seeds leave a row
sum off by more than 1e-4 (max ~2.6e-3), so the test failed about 13% of
the time (seen while propagating the reaction stack). Pin
torch.manual_seed(0) (error ~5e-7) instead of loosening the tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tjkessler
tjkessler merged commit 8664755 into main Oct 15, 2026
11 checks passed
@tjkessler
tjkessler deleted the feature/reaction-graphs-integrate branch October 15, 2026 21:03
tjkessler added a commit that referenced this pull request Oct 15, 2026
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>
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