Repository navigation
events: run a strict trigger after its non-strict twin on a crossing (#947) - #1326
Open
elyfmiller wants to merge 2 commits into
Open
elyfmiller wants to merge 2 commits into
elyfmiller wants to merge 2 commits into
Conversation
…anl#947) `lhs > tau` beside `lhs >= tau` turned true at one read wherever the read landed past the crossing, and executed there as one batch by priority, but in sequence where it landed on it. Which one a run gave depended on the rounding of tau: on aedadd8, over 307 values of tau, `time/3` batched for 95 and `time - 1` for 103, and a state pair batched wherever CVODE's root landed past the threshold. The SSA drain did the same. In exact arithmetic the non-strict trigger is true on the crossing and the strict one only past it. So where both turn true at one read on a crossing, the strict one is held back to a second sub-instant of the same time. It is triggered (trigger re-read, values and delay read) once everything else of the instant has executed, at the state that left. - NetworkModel::event_trigger_comparison() gives the comparison the trigger's residual comes from, oriented to rise where the trigger turns true, and whether it is strict. strict_beside_non_strict() pairs a rising set. - process_firing_batch() in both engines holds such a pair only on a crossing: one step back along the run (CVODE's interpolant in the ODE, the time alone in SSA) the twin reads false and the residual moves as it moved over the step before. A pair a jump turns true (a firing, a piecewise that switches) stays one batch. The step doubles where the residual moves by less than its rounding. The run-start batch and size jumps are left alone. - A sensitivity run keeps a held pair whose strict event reads and writes nothing the first sub-instant wrote, which the jump composes as one of the batch, and refuses the rest by name.
…rder (lanl#947) An audit against the scientific-testing rules: - Every closed form was compared at pytest.approx's default rel 1e-6, ten orders looser than the events' own arithmetic. X is never integrated, and all 300 sequence runs give 0.15 to the bit, so they now share EXACT = 1e-12, with the reason beside it. The float == comparisons (213, 0, (1, 2)) go through it too. - The sensitivity test states its bounds: dA/dk at 1e-8, 100x the solver's 1e-10 (measured 6e-16 off); dY/dk and dZ/dk at EXACT, measured 0. - The refusal test's "refused > 0" is 57 of 60 here; the SSA test's X >= 3 fails a correct run with probability 3.6e-15. Mutation check, each rebuilt and run: holding nothing (ODE, SSA), skipping either crossing check, ignoring on_crossing, and never or always refusing a held pair are each caught. Pairing a strict trigger with any trigger on its residual was not: two strict ones held together release as one batch. The new test puts a third event beside them on the same read, which one batch orders 3, 2, 1 (321) and the mutation 1, 3, 2 (132, all 60 tau).
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #947.
Problem
Two events on one threshold, one strict and one not:
In exact arithmetic
lhs >= tauis true on the crossing andlhs > tauonly past it. So E2 executes first and E1 after it, reading the X that E2 left: X = 0.15.Where both triggers read true at one read, they were seeded into one batch. E1 executed first by priority, and E2 then overwrote it with the value it froze at the trigger: X = 0.50.
Which of the two a run gave depended on where the read landed. The stop for a clock trigger and CVODE's located root land on the crossing, or a few to about a hundred doubles past it, and past it both triggers read true. #933 fixed the issue's own spelling, the bare
time ⋛ tau. #1218 left event-trigger stops where the resolvers put them. The neighbours in the 2026-10-07 comment are still decided by rounding.On main (aedadd8), 307 values of tau over 1e-3 to 1e6,
rtol=1e-10:time0.1*timetime/3time - 13*timeThe other cases:
A' = k,A > taubesideA >= taubatches on all 300 of 300 random (k, tau) pairs, for k over 1e-6 to 1e6.time/3, 16 of 36 values of tau batch.The rule
The rule was agreed for this issue before any code was written:
Which comparisons pair. The comparison is the residual that
event_trigger_residual_expralready parses out of a single-comparison trigger, oriented to rise where the trigger turns true, with whitespace and outer parentheses dropped.tau <= time/3pairs withtime/3 > tau.time/3 - tau >= 0does not: it is a different pair of sides.==,!=and joined conditions never pair.On a crossing. The read must be on a crossing, not past a jump. One step back along the run, the non-strict twin must read false, and the residual must move over that step as it moved over the step before. Two triggers that turn true together through a jump have no instant between them, so they stay one batch:
piecewiseof the time that switches;The step back:
2·max(100ε(|t|+|h|), 64ε·max(|t|,1)), past where a root or a stop lands, and doubles, up to 2^20 times, where the residual moves by less than its own rounding.Unchanged:
on_crossing=false) stay one batch.Where
NetworkModel::event_trigger_comparison(ei, &strict)andstrict_beside_non_strict(rising)are insrc/model.cpp. They are cached beside the residual, and a clone rebuilds them.process_firing_batchinsrc/cvode_simulator.cppandsrc/ssa_simulator.cppholds the strict seeds out of the first sub-instant.fires,CASCADE_LIMITand the trigger baseline with the first drain.Sensitivity runs
A held release is composed by the sensitivity jump like any member of the batch, because the jump replays
executed_firesin order from x⁻. That holds where the held event reads nothing the first sub-instant wrote and writes nothing it wrote. Then the sequence leaves what the batch left, and the run keeps its sensitivities.Otherwise the run refuses by name:
Forward sensitivity: event 'E1', a strict comparison, and event 'E2', the non-strict one on the same residual, turn true together on its crossing at t=… (issue #947), and 'E1' reads or writes a species they wrote: a second state jump at the same instant, which bngsim has no sensitivity jump to compose with the first (issue #144).The reads checked are the assignment values and the rescale sizes.Before this change, such a run returned the batch's trajectory, which the plain run no longer gives. This is the consequence accepted with the rule.
RoadRunner
libRoadRunner 2.9.2 batches every such pair. So a corpus model with one would read as a DIFF in
rr_odeagainst it. That divergence comes from the rule, and was accepted with it.Results
After this change, every spelling gives 0.15 on the same 307 values of tau:
time,0.1*time,time/3,time - 1,3*timeAlso 0.15 for every one of 107 values of tau:
time > tau/7andtime > tau + 0.1, each beside its>=twin;tau < time/3besidetau <= time/3, andtime/3 > taubesidetau <= time/3;fromTriggertrue or false;Tests
python/tests/test_strict_trigger_after_its_non_strict_twin.pyhas 29 tests. The oracles are closed forms: no reaction moves X, so X after the pair is 0.3·0.5 in sequence.0.1*time,time/3,time - 1,3*timetime(control)tau/7,tau + 0.1, reversed and mixed spellingsfromTrigger=false, where a batch gives 0.15 too (control)Y = Xreads 0.5, not 1)>=of priority 1 and 2 and a>of 3 run 2, 1, 3piecewisejump turns true is one batch (control)0.1*timeandtime/3, in sequence19 tests fail on main and pass here. The other 10 are controls that pass on both: they pin what the rule must leave alone.
Test audit
Checked against the test-writing rules Bill sent: each expected value's oracle, each tolerance's reason, and each test shown to fail against broken code. Commit 9aec8bc.
pytest.approx's default rel 1e-6, about ten orders looser than the events' own arithmetic.EXACT = 1e-12. The float==comparisons go through it too.piecewisejump controlon_crossingTwo strict triggers held together are released together, which reads as one batch, so that last mutation went unseen. The new test adds an event that reads the same comparison inside an
&&: it rises at the same read without being paired.refused > 0is 57 of 60 here. The SSA control'sX >= 3fails a correct run with probability 3.6e-15.Other checks:
test_run_tests_sh.py::test_the_script_itself_runs_walk_up_dependent_tests_green, which fails the same way on main in this environment, because the venv has no jax, diffrax or roadrunner and the stand-in run reports their modules as skips.parity_checks/tests: 910 passed, 49 skipped.ctest, the native-tests.yml configuration): 6 of 6 pass.benchmarks/perf_ab.py --quick, main aedadd8 against this branch (21 models up to 150 species, theodeandcodegenworkloads). No significant slowdown, and the solver's work counters are the same on all 42 rows. The geometric-mean ratios are 0.991 warm and 0.997 cold forode, and 0.989 warm and 1.073 cold forcodegen, where no model is flagged. None of the suite's networks has an event, so the changed drains are not reached. I stopped the full run for time.Review
There was no second session to review this, so a separate review agent read the diff twice, read-only, given the issue and the diff but not the reasoning behind it.
First pass. Nothing lost or fired twice. Four semantic or scope issues were fixed here:
Also fixed: the wording of the docs, and the event id in the message. The run-start hold could fake a crossing; the twin's trigger is now read directly.
Second pass. "The state restore is complete in both engines. The narrowed refusal is enough. I found nothing newly broken." Its three remaining points are fixed: the step doubles where a residual moves by less than its rounding, the ODE reads the arriving trajectory from CVODE's interpolant, and the refusal reads the rescale sizes.
Not covered