Skip to content

jacobian: decline a rate law too long written out before writing it (#1255) - #1394

Open
Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1255-diamond-jacobian-setup
Open

Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1255-diamond-jacobian-setup

Conversation

@Montana

@Montana Montana commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Closes #1255.

What was wrong

Building a Simulator with the analytical Jacobian (the default) took exponential time for a compartment whose size is read through a diamond of rules, r_i := sin(r_{i-1}) + cos(r_{i-2}).

rules main jacobian="fd"
12 3.1 s < 0.01 s
14 10.9 s
18 57 s
22 110 s

Past about 16 rules, main often ends on the finite-difference Jacobian anyway, after paying for it. Measured at 18 rules, beside a running suite: 28.7 s (25.6 s with codegen=True), then declined: the build-time derivation exceeded its 20.0s budget.

The GH #95 budget did not stop the cost, for two reasons:

  • it is checked only before each sp.diff, and most of the time went after, walking and printing the derivative (19.5 M nodes traversed at 12 rules);
  • the native saturable path runs first and wrote the text out with no memo (492,581 recursive _inline_functions calls at 22 rules).

The fix

A rate law past the inlining limit that does not collect now declines the analytical Jacobian before anything is written out.

  • The check: _inlined_for_derivation, in front of both derivation paths (differentiate_rate_law and the native differentiate_rate_law_native).
  • The decline has its own census kind, written_out, and names the law and its length.

After: 0.02-0.03 s at any depth (12, 18, 22, 30 rules), and the run is bit-identical to jacobian="fd".

Why declining is the right answer here, not a loss: a derivative this long costs every Newton setup more to evaluate than differences of the right-hand side do. It is the same bet GH #95 makes, decided from the length instead of after the time is spent. On the three corpus models below that kept a complete analytical Jacobian, it bought neither run time nor accuracy:

  • BIOMD0000000385 ran 0.28 s analytical against 0.14 s FD, in the same 177 steps;
  • BIOMD0000000628 ran 0.098 s against 0.03 s. Against a tight reference (rtol 1e-12), both are 4e-4 off at default tolerances: 4.05e-4 analytical, 4.36e-4 FD. The two tight runs agree to 6.5e-12.

Measured: the corpus, main against this branch

Real builds of 1,500 models (1,323 BioModels and 177 .net), 1,463 of which build on both sides. All runs used codegen=False, because the codegen cache key hashes bngsim's Python sources, so this branch compiles every model cold; the compile is not what this changes.

  • 1,454 models keep their status, and 1,452 of them end bit-identical. The other two (BIOMD0000000472, 473) differ by 4e-15 and 3e-17 with codegen on and are identical with it off.
  • 9 change status:
model main this branch build answer
BIOMD0000000246 declined: budget (20 s spent) written_out 30.7 s → 0.06 s identical
MODEL1006230049, -053, -077 declined: budget written_out 20 s → 5.0-6.3 s identical
MODEL1006230090 declined: budget written_out 20.0 s → 1.9 s its run fails on both (SimulationError)
BIOMD0000000385 complete written_out 9.6 s → 0.29 s 4e-15
BIOMD0000000628 complete written_out 18.1 s → 0.05 s 1.7e-4, within the integration error above
MODEL1006230100, -112 complete written_out 1.0 s → 0.6 s their runs fail on both (SimulationError)

Tests (test_jacobian_written_out_decline.py, 9)

test on main
the issue's model at 12, 18 and 30 rules builds in under 2 s, declines written_out, and runs bit-identically to jacobian="fd" fail (3.1 s, 57 s, minutes)
the same with codegen=True fail (25.6 s at 18 rules, then a budget decline)
neither path writes the law out (_inline_functions patched to raise), at 30 rules fail
written_out is a census kind fail
controls: 10 rules (18 thousand characters) keeps its analytical Jacobian; #1124's sum diamond past the limit gets its collected form, not a decline pass

The existing test_jacobian_diamond_of_rules.py (#1124) passes unchanged.

Mutations

mutant fails
G1: the law is written out instead of declined the 12-rule build
G2: the native path writes it out (as on main) the 30-rule build (3.5 min)
G3: the decline filed under budget the kind test
G4: the limit 100× higher the 12-rule build

Checked

  • Full suite: 16,760 passed, 138 skipped (on a core built at this commit). pre-commit run --all-files and sphinx -W are clean.

Not in this PR

  • The chain-rule derivation the issue sketches, differentiating through the rules by name. That would keep an analytical Jacobian for these models, but its derivative would have to reference intermediate values the emitters cannot yet hold.
  • Other places that inline a law as text (_codegen.py's RHS paths, _switch_sensitivity). They are not in the default build path this issue measures.

…anl#1255)

A compartment size read through a diamond of rules, r_i := sin(r_{i-1}) +
cos(r_{i-2}), gave the dilution term a rate law whose functions write out
exponentially in the depth and do not collect, and building the Simulator
derived, checked and printed a derivative as long: 3.1 s at 12 rules, 57 s
at 18. Past the inlining limit, a law that does not collect now declines the
analytical Jacobian from its predicted length (kind written_out), in front
of both the sympy and the native derivation paths, and the model runs on
the finite-difference Jacobian: 0.02-0.03 s at any depth.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant