Repository navigation
simulator: leave assignment-rule species at the value a run reports (#1510) - #1637
Conversation
…anl#1510) After a run, Model.get_concentration, get_state, Simulator.get_state, ReactionKernel, CouplingMap.read and snapshot() returned a rule target at its t=0 value when the rule was linear in species, and for every rule under SSA, while Result.species reported the live value. Simulator._stamp now writes each rule target back at the last row's reported value, bracketed by the carry-over capture/restore so a pending sensitivity seed survives the write.
…place before the variable-volume pass Each tolerance now names its reason and the error it was measured at. Four closed-form checks held a run at rtol 1e-10 to 1e-8 with 2 to 5x room (measured up to 4.2e-09); they and the rest share CLOSED_FORM = 1e-7, 20x room, which still tells the rule's 2.71 from main's 20. A write moved after the variable-volume pass passed every test. A rule target that is an amount in a compartment grown from 1 to 2 now pins the slot at 2*A(2) = 20 e^-1, the storage convention, where the pass reports 10 e^-1. get_concentration's Returns says what a rule species reads after a run, in which convention, and that steady_state() leaves it.
…mp their clone (lanl#1510) Two places where the write-back of lanl#1510 landed where it should not: - compute_all_sensitivities stamped each chunk against the caller's model rather than the clone it ran on, so the write put S's end value into a model the call never advances: A read 10 and S 2.706706. The chunk now stamps its clone, and every output is the same bits. - A .net has no line that marks a rule target a value rather than a count, so SSA and PSA rounded its slot at the start of a run and warned. With the rule's value written back, every run_until leg after the first rounded it again: S := A/3 warned on each. Model.from_net and build_model_from_parsed now mark the targets continuous through a new NetworkModel.set_species_continuous, as the SBML loader marks them. Tests pin both, each failing on its mutant. get_concentration's Returns names the static size, which set_param rescales, rather than the size at load.
|
Montana, thanks. The fix reproduces here. On the PR head (37d9421, arm64 with MIR), the new file's 11 cases pass; on its base (0b2055a), 9 fail and the two controls pass, as the body says, and v0.17.1 fails the same 9. The branch merges cleanly with today's main (23ac85b), and onto it with #1638, #1640, #1642 and #1643, which also touch What else we checked:
We've pushed one commit on top of yours, 0951155, for items 1 to 4:
|
Fixes #1510.
The problem
After a run, an SBML assignment-rule species read its t=0 value through the model, while
Result.speciesreported its live value. On main (0b2055a9), withS := 2*A(linear in species) andN := A^2/(5 + A)(nonlinear), A(0) = 10, A' = −0.5 A, at t = 4:Result.speciesget_concentration/get_stateon mainReactionKernel, 4 ×advance(1.0), SSo did
Simulator.get_state,ReactionKernel.advance's return value,CouplingMap.read(a co-simulation orchestrator readsSthrough it), andSimulator.snapshot. The same happened on the.netwritten bysbml_to_netand reloaded withModel.from_net, and withcodegen=True.The cause: a rule target is a
fixedslot that the integrator does not move, and only the report (Simulator._apply_ar_report_map) reads the rule.The fix
Simulator._stampnow writes each rule target back into the model at the value the run reports for its last row (_write_ar_targets_back), right after_apply_ar_report_map.Result.species' own number, in the model's storage convention. It runs before the variable-volume passes, so the issue's separate amount-over-V_livequestion for resized compartments is left as it is.run,run_until(soReactionKernel),run_replicates,run_batchrows and the chunked sensitivity runs all pass through_stamp, under every method and for the.netpath's report map.The carry-over state goes back around the write. The write uses the core's
set_concentration, which also drops a pending carry-over sensitivity seed (GH #210), since a set value is a fresh initial condition. So the write is bracketed by the Simulator's own_capture_carryover_state/_restore_carryover_state, the mechanism the GH #176 retry uses. A write the model did not ask for must not end a pre-equilibration. It calls the core directly rather thanModel.set_concentration, which would also log an initial-condition write.Nothing that follows moves. The slot is read by no rate law and no event. A fixed component's local error is 0, so its value does not enter CVODE's step control either, and a continuation run takes the steps it took (step 4).
How this was checked
Reproduced on main with the issue's script: every row of the table above.
Fixed, on the same script: every getter equals
Result.speciesand the rule, under ODE and SSA and through the kernel.The sibling routes the issue names, each against the rule's value:
codegen=True;CouplingMap.readthrough a kernel;snapshot()afterrun_until;set_state(get_state())round trip;Result.species;.netroute: Antimony-made SBML throughsbml_to_netandModel.from_net.Bit-identity of what follows a run: a corpus A/B over every
.netin the repository plus 300 BioModels (seeded sample), 485 models. Each runs 0 → 5, then continues 5 → 10 from the model's own state, on main and here.Two more cases are pinned by tests: a
carry_sensitivities=Truepre-equilibration (0 → 3, carried 3 → 6) gives the single 0 → 6 run's dA/dk, and a continuation reports the rule at its new end.New tests fail on main for the right reason.
test_rule_species_getters_after_a_run.pyhas 11 cases:Mutation tests: 5 mutants, all killed:
CLOSED_FORM = 1e-7, 20x room, which still tells the rule's 2.71 from main's 20.Existing tests nearest the change: every file that names
get_concentration,get_state(,ReactionKernel,CouplingMap,.snapshot(,carry_sensitivitiesor_ar_report_map, 80 files and the new one. 3,062 passed, 52 skipped, the same underBNGSIM_SKIP_AUDIT=strict; 3,056 passed and 58 skipped under forced cc and forced MIR.Full Python suite, macOS arm64, no corpus (as CI runs it), on
0b2055a9: 19,372 passed, 141 skipped, 1 xfailed, 0 failed, so 19,514 collected.PYTEST_COLLECTED_FLOOR(19,451), inside the 500 slack.Docker, Linux x86-64 (
python:3.12,--platform linux/amd64, gcc 14, built from this tree with the[antimony,hdf5]extras and scipy):test_codegen_compile_reaping,test_codegen_toolchain_verdict,test_run_tests_sh,test_vendor_script_git_cwd), which fail the same way on main in it; none touches the changed code.pre-commit
--all-filespasses on the final tree.ci/changelog.py checkandrequiredpass, and localsphinx-build -Wexits 0. No C++ changed.Checked against past review findings
steady_state()(steady_state() reports an SBML assignmentRule species at its frozen initial value, and that species' zero Jacobian row refuses the whole model's sensitivity solve #247), is left alone deliberately.steady_statedoes not write the model's state for any species: A also reads its initial 10 after it, where its steady value is 4, so S reading 20 is consistent with an unmoved model rather than a stale slot.The diff (abridged)
@@ python/bngsim/_simulator.py — Simulator._stamp ar_map, ar_blocked = self._ar_sensitivity_metadata(model) self._apply_ar_report_map(result, ar_map) + self._write_ar_targets_back(result, ar_map, model) self._apply_ar_sensitivity_map(result, ar_map) self._apply_varvol_conc_map(result, model)The write sits after the report map, so it writes the number
Result.speciesholds. It comes before the variable-volume passes, so the slot keeps the model's storage convention.A run on a clone (a
run_batchrow) writes the clone. A model with no rule targets has an empty map, and nothing is written.Tests
python/tests/test_rule_species_getters_after_a_run.py(new, 11 cases):advancereturn andCouplingMap.read;snapshot()afterrun_until; an hOSU=true target in a sized compartment; and thesbml_to_net→from_netroute.S := 2*Ain a compartment grown from 1 to 2 keeps the storage convention, 20 e^-1, whereResult.speciesreports it over the live size.Model.get_concentration's Returns says what a rule species reads after a run, in which convention, and thatsteady_state()leaves it.