Skip to content

ode: leave a rule-set parameter at its end-of-run value after a compiled warm run (#1358) - #1359

Open
Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1358-rule-param-after-compiled-run
Open

Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1358-rule-param-after-compiled-run

Conversation

@Montana

@Montana Montana commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Closes #1358.

What was wrong

A parameter that an assignment rule sets, here q := base*A, is written into its parameter slot by the interpreter's NetworkModel::evaluate_functions at every right-hand-side call. So after an interpreted run, get_param("q") reads the rule's value at the end.

The compiled right-hand side (cc or MIR) computes the rule in its own func[] and writes nothing back. On the warm fast path (Impl::run_warm, which takes a run with no events and no sensitivities), the rows read the functions through fill_row, and nothing else evaluates them, so the slot kept its value from load. On main 9a26f5b, with the AR_DEPENDENT fixture over t = 0–2:

run get_param("q") after it
interpreted 0.0183 (= base·A(2) = e⁻⁴)
compiled 1.0 (its value at load)

The trajectory is unaffected.

The general path (run(), taken by a run with events or sensitivities) was already right: it refreshes the model at each row it records, which evaluates the functions.

The fix

src/cvode_simulator.cpp, run_warm's write-back. After it publishes the final concentrations and time, sync_model_at(final_t, y_data, ns) evaluates the observables, the functions and the parameters they set at the state the run ends on, as run()'s per-row refresh leaves them:

         model.set_current_time(final_t);
+        // The functions, and the parameters assignment rules set, at the state
+        // the run ends on, as run() leaves them by its refresh at each row (issue #1358). ...
+        sync_model_at(final_t, y_data, ns);

I first added the same call to run() as well, and measured whether it changed anything. The tests below pass on main's run() with and without events and sensitivities, compiled and interpreted, so that copy was redundant and is not in this PR.

Tests (test_sbml_initial_assignment_param_lift.py)

test_a_run_leaves_the_rules_value_in_the_parameter checks get_param("q") after the run. It must equal 0.5·[A] at the run's last row to 1e-12, and e⁻⁴ to 1e-5. Six cases cover both paths:

case path on main
warm interpreted, warm pass
warm compiled cc, warm fail: 1.0
cold interpreted, run() (an event at t > 100) pass
cold compiled cc, run() pass
sensitivity interpreted, run() (sensitivity_ic=["A"]) pass
sensitivity compiled cc, run() pass

test_a_run_through_the_jit_leaves_it_too does the same under BNGSIM_CODEGEN_JIT=mir, asserting codegen_backend == "mir". It fails on main and skips on a build without MIR.

The only difference between main's core and this one is the added line, so the main-core runs above are the mutation check.

With #1350

#1350 marks test_a_write_after_a_run_does_not_move_it_to_the_rules_current_value as a strict xfail under a lowered BNGSIM_CODEGEN_THRESHOLD, citing this issue. With this fix, that test passes under the forced threshold, so the strict xfail would report an unexpected pass. Whichever of the two merges second should drop that one mark. The two #1357 marks stay.

Measured

  • test_sbml_initial_assignment_param_lift.py: 20 passed in the default configuration, under BNGSIM_CODEGEN_THRESHOLD=1, and under BNGSIM_CODEGEN_JIT=mir BNGSIM_CODEGEN_THRESHOLD=1. tests: declare what a forced-JIT run cannot pass, so the run is a signal (#1217) #1350's marked test is among them.
  • Full suite on this core: 16,404 passed, 0 failed. The one more skip than main comes from the environment: test_build_provenance.py skips a case when BNGSIM_ALLOW_STALE_CORE is set, as it was for this run.

…led warm run (lanl#1358)

A parameter an assignment rule sets is written by the interpreter's
evaluate_functions at every right-hand-side call. The compiled
right-hand side (cc or MIR) computes the rule in its own func[] and
writes nothing back, and run_warm's rows read the functions through
fill_row, so after a compiled run with no events or sensitivities
get_param("q") read its value at load: 1.0 where an interpreted run read
0.0183. run_warm's write-back now syncs the model at the state the run
ends on, as run()'s per-row refresh already leaves it.

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.

get_param on an assignment-rule parameter after a compiled run returns its load-time value, not the rule's

1 participant