Repository navigation
routing: send a structurally empty Jacobian to the dense solver (jacobian="fd" refused for a flag never set) - #1292
Conversation
A model of 50 or more species whose Jacobian has no structural nonzero passed the size and density test with a density of 0 and went to KLU, which then had nothing to factor and, under jacobian="fd", nothing to color: the run was refused for force_sparse_linear_solver, a flag the caller never set (SBML Test Suite 01530). Codegen's own reading of the rule already required a nonzero. Forcing KLU is still refused by name.
|
Thanks. We reviewed this independently, Opus 5.5 and Fable 5.1, each on our own build of main c0877fa with f66d1e4 merged (clean), on macOS arm64 with KLU on. Neither of us found a correctness defect. The rule change is right where it sits. The one conjunct
What we checked
Low items
So it's faster at these sizes and grows as n² in memory. Nothing on disk is that shape, so it's worth a line in the body rather than a change. Not checked: a KLU-off build (the |
…lanl#1296) Review follow-up on the empty-pattern rule: - solvers.md's KLU row and the force_sparse_linear_solver docstring name the structural-nonzero gate. The docstring's steady-state note and the comment above ss_use_sparse_linsol describe what remains: only the forced route meets an empty pattern, where run() takes KLU with the analytical Jacobian and refuses under jacobian="fd", and the march factors densely. - The run tests pin the route (linear_solver != 1) as well as the answer, which they now compare exactly, and a codegen=True case runs under "auto" and "fd". The SBML is built in module-scoped fixtures, not at import.
|
Following up on our review above: we've pushed its low items 1–4 as one commit on top, 2c5f165, and are merging now that CI is green. Your change itself is untouched: What the commit changes
What we checked
Item 5 of the review, the dense route's memory, stays a note rather than a change. The dense route stores the Newton matrix as n²: at 6,000 species with no reactions, 720 MB against KLU's 185 MB, though faster at that size. Nothing on disk is that shape. This lands ahead of #1336, as our review there suggested. Without it, an unflagged empty-pattern model run with |
Fixes #1296. Found by the differential suite's
ajac_vs_fdjaccheck (#1257, PR #1291).Root cause
route_to_sparse_linear_solver(include/bngsim/sparse_jacobian.hpp) sends a model to KLU when it has 50 or more species and a pattern density below 10%. A structurally empty Jacobian (nnz == 0: no reaction moves any species, and an event assigns them) has a density of 0, so it passes, and the automatic routing picks KLU.KLU then has nothing to factor. Under
jacobian="fd"the colored differences have nothing to color, and the run reaches the backstop refusal incvode_simulator.cpp. That refusal was written forforce_sparse_linear_solverand says so:The caller never set the flag. Codegen's own reading of the same rule already required a nonzero: its fallback for a core that doesn't report
routes_sparseisns >= 50 and nnz > 0 and density < 0.10. The C++ rule had drifted from it.Change
The size-and-density branch of the rule also requires
sp.nnz > 0. That's one condition, in the one header the time-course solver, the steady-state march and the codegen emitter all read (routes_sparse), so the three stay in agreement.force_sparse_linear_solveris untouched: it still asks for KLU, and is still refused by name where nothing can fill the matrix, which is right for a caller who forced it.Why it is right
A structurally empty Jacobian is J = 0, so the Newton matrix is I − γJ = I. The dense solver differences the full matrix with CVODE's difference quotient. That needs no pattern and is exact for J = 0. KLU had no advantage to lose: it factors the identity at best, and here it couldn't even be filled.
Tests
python/tests/test_empty_jacobian_routes_dense.py, on a 60-species model that no reaction moves, with an event at t = 5 assigningS_i = S_i + 1:nnz == 0)routes_sparseis False for itjacobian="auto"gives the closed form,S_i = itheni + 1jacobian="fd"gives the closed formforce_sparse_linear_solver=Trueis still refused by nameBlast radius: every model whose route changes
The engine's own
routes_sparsewas compared on main and on this branch for every model on disk:.netcorpusThe six are exactly the models with an empty pattern and 50 or more species. Against the SBML Test Suite's expected results:
autofdautofdSuite
16,071 passed, 148 skipped, on macOS against main (0062aeb). The 1 failure,
test_a_relative_interpreter_stays_in_its_venv, is my scratch venv's/tmpreading as/private/tmp, and fails the same way on main. All 28 test files that use KLU or the routing pass (454 tests). clang-format (pinned 17.0.6), ruff andci/changelog.py checkare clean.Remaining risks
nnzcondition would never run in CI. The Python tests exercise it where KLU exists: through the engine's ownroutes_sparseand end to end.Maintainer review: the three strongest objections
nnz == 0is a corner case." It's 6 SBML Test Suite cases, and every model whose species only events move. More to the point, codegen already assumed the rule excluded it. This makes the engine agree with the code that reads it..netmodel and no event model.Diff (source; the test and the changelog fragment are not shown)