Skip to content

steady_state: solve the sensitivity of a model with a fixed species, not refuse it as singular (#961) - #1424

Open
Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/961-ss-sens-fixed-species
Open

Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/961-ss-sens-fixed-species

Conversation

@Montana

@Montana Montana commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Part of #961: lifts the item "A $-fixed species in a model with no conservation law and no rule species is refused as 'dY_ss/dp does not exist'". It also lifts the same refusal beside a conservation law, which the item didn't list.

What was wrong

A $-fixed species has no equation, so its row of J is 0. compute_ss_sensitivity solved the columns −J⁻¹·∂f/∂p over the unknowns, and kept a fixed species among them in two cases:

  • No law, no mask: the full-species solve, and the fixed species' zero row made J singular.
  • Beside a conservation law (unmasked): ss_unknown_species returns the law's independents verbatim, with the fixed species among them. Singular the same way.

On main, both raise SimulationError: Steady-state dY_ss/dp does not exist for this model: the Jacobian at the steady state is singular … for columns that exist:

model exact main here
$S makes X at k1, X lost at k2; S set by S0 dS/d[k1,k2,S0] = [0,0,1], dX = [S/k2, −k1·S/k2², k1/k2] = [6, −24, 4] refused exact
A ⇌ B (kf, kr), and $S turns A into B at k1; A + B = 1 B* = a/(a+kr), a = kf + k1·S refused exact

The change

The columns' unknowns no longer include a fixed species. The solve for the state is unchanged: ss_unknown_species keeps its contract for its other two callers, the Newton solve and the eigenvalues.

The diff (src/steady_state.cpp)

@@ compute_ss_sensitivity
-    if (!all_laws.empty() || sub.masked) {
+    // A `$`-fixed species is no unknown either (issue #961): solved for as one,
+    // its zero row made J singular in a model with no law and no mask, and the
+    // columns were refused as "dY_ss/dp does not exist".
+    const bool any_fixed = std::any_of(model.species().begin(), model.species().end(),
+                                       [](const Species &s) { return s.fixed; });
+    if (!all_laws.empty() || sub.masked || any_fixed) {
         // Reduced-space sensitivity solve.
@@
-        ss_kept_species_system(model, all_laws, sub, ss_unknown_species(model, all_laws, sub), cl,
-                               solve_idx);
+        // A fixed species is no unknown of the columns (issue #961): its row of
+        // J is 0, and kept, it made the solve singular, beside a law as well as
+        // with none. Its own row of dY_ss/dp is what a parameter sets it to.
+        std::vector<int> unknowns = ss_unknown_species(model, all_laws, sub);
+        unknowns.erase(std::remove_if(unknowns.begin(), unknowns.end(),
+                                      [&](int i) { return model.species()[i].fixed; }),
+                       unknowns.end());
+        ss_kept_species_system(model, all_laws, sub, unknowns, cl, solve_idx);

Tests

python/tests/test_steady_state_sensitivity_fixed_species.py (5 tests):

  • the two models above, held to their closed forms at rtol 1e-6, including the fixed species' own row (dS/dS0 = 1). Both fail on main;
  • a model with no fixed species (control);
  • the state itself, under integration and Newton, unchanged (control).

Mutation tests (the core rebuilt for each):

mutant killed by
no any_fixed gate the no-law test
no filter at the call site the beside-a-law test

Corpus

steady_state(sensitivity_params=<first 4 parameters>) on 1,500 models (1,323 BioModels, 177 .net files in the repo), each in its own process, on main and here:

outcome models
solved on both, bit-identical (state and columns) 566
refused on main, solved here 44
refused on both, the same reason 881
crashed or timed out on both 9
solved on main, refused here 0
solved on both, changed 0

The 44 were checked against central differences of the steady state itself, at relative steps 1e-5 and 1e-3 per parameter:

  • 34 agree to 1e-4 of their column's scale or better.
  • 3 have columns that are zero to rounding on both sides: BIOMD0000000289 and 290 (1e-22 to 1e-62 against differences of 1e-14 to 1e-27 that change sign with the step), and sink_compart.net (0 against 1e-10 to 1e-9 of noise).
  • 5 agree within the differences' own noise, settling at one of the two steps. For example, BIOMD0000000325's BR is 2.51032 analytic, against 5.46 at step 1e-5 and 2.51011 at 1e-3. The others are MODEL6655501972, BIOMD0000000221, Motivating_example.net and BIOMD0000000015.
  • 2 agree to 6e-4 at the single step 1e-5 and were not inspected further: BIOMD0000000190 (5.9e-4) and 222 (2.5e-4).

Checks (macOS arm64)

  • python/tests: 17,209 passed, 0 failed.
  • parity_checks/tests: 1,015 passed. The 2 failures (test_bngsim_provenance and the reference-archive replay) also fail on main here: their subprocess can't import bngsim without my local import shim.

pre-commit run --all-files, sphinx -W, and ci/changelog.py check/required (changelog.d/961.fixed.3.md) are clean.

…not refuse it as singular (lanl#961)

A $-fixed species has no equation, and its row of J is 0. The columns
were solved with it among the unknowns where a model had no law and no
mask, and beside a law, whose independents it was among: J was singular
and dY_ss/dp was refused as not existing. It is taken out of the
columns' unknowns; the solve for the state is unchanged. 44 corpus
models solve where they were refused, each agreeing with central
differences, and the 566 that solved before are bit-identical.

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

Development

Successfully merging this pull request may close these issues.

1 participant