Repository navigation
Fix events crossed back or moved onto thresholds by event assignments, using post-hoc root filtering - #3491
Draft
dweindl wants to merge 1 commit into
Draft
Fix events crossed back or moved onto thresholds by event assignments, using post-hoc root filtering#3491dweindl wants to merge 1 commit into
dweindl wants to merge 1 commit into
Conversation
This was referenced Oct 26, 2026
Draft
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3491 +/- ##
==========================================
+ Coverage 80.23% 80.32% +0.08%
==========================================
Files 331 331
Lines 23420 23481 +61
Branches 1631 1645 +14
==========================================
+ Hits 18792 18860 +68
+ Misses 4622 4615 -7
Partials 6 6
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
dweindl
force-pushed
the
root_filtering
branch
2 times, most recently
from
October 28, 2026 19:50
8570521 to
9491564
Compare
…ing roots Event assignments that move a just-crossed root function back across zero, events moving their own explicit trigger time, and assignments exactly onto a threshold (e.g., a bouncing ball) left Heaviside variables inconsistent with the root functions, so events were missed or `piecewise` took the wrong branch. After event assignments, root functions are compared to `h`. Root functions within their uncertainty of zero are resolved by the direction in which they move, also at the initial time and for unchanged root functions at zero. A Heaviside function that keeps switching back and forth at the same time raises an error. The solver reports all root crossings; those that don't change `h` are dropped and the integration continues. A dropped root that is exactly zero is deactivated until it becomes nonzero, as SUNDIALS does at a restart. This replaces the ignore-after-reinit machinery and the `ROOT_AFTER_REINIT` warning: that filter can't distinguish a genuine fast re-crossing from a re-detection and is redundant now. Adjoint sensitivities: pass the event-time terms through secondary discontinuities without event assignments; forward and adjoint sensitivities pick the event-time root by the same rule. Fixes AMICI-dev#3425. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dweindl
force-pushed
the
root_filtering
branch
from
October 29, 2026 13:12
9491564 to
ece48c5
Compare
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.
Fix event handling for event assignments that move a root function back across or onto its threshold, using post-hoc root filtering: the solver reports every root crossing, and those that don't change the Heaviside variables are discarded. Alternative to #3490 (root masking: the solver only reports crossings in the direction that would change the Heaviside variables), which has the same functionality and passes the same tests; one of them supersedes #3439. Fixes #3425.
If an event assignment moved a just-crossed root function back across zero, e.g.,
E1: at x < thr: x = 1.5 * x, the Heaviside variable kept the post-crossing value, and later crossings incremented it further (h= 2, 3, …), sopiecewiseexpressions took the wrong branch. Likewise, an event moving its own explicit trigger time,E: at time >= s: s = s + 1, fired only once, and an assignment exactly onto a threshold, e.g., a bouncing ball (E: at x <= 0: x = 0, v = -e * v) or onto another event's threshold, left stale Heaviside variables, as did an event that changes the direction of a root function sitting at its threshold.After each event assignment, the root functions are compared to
h. Root functions within their uncertainty of zero, estimated from a state perturbation within the integration tolerances and from the time tolerance of root-finding, are resolved as follows: an assignment that moves a root function onto zero triggers the event (the trigger is true at zero); afterwards, the direction in which the root function moves decides (central finite difference in time). The same holds for root functions that crossed zero at the current time, for unchanged root functions at zero whose direction may have changed, and, at the start of a simulation or steady-state computation, for root functions at zero. A switch that flips back and forth at the same time, e.g.,x' = piecewise(-1, x > 0, 1)atx = 0, now fails with an error instead of looping or giving wrong results.The solver reports all root crossings. Those that don't change
hare dropped (debug messageROOT_IGNORED) and the integration continues without handling a discontinuity. This covers a just-crossed root that is re-detected after reinitialization, and a root function whosehwas set by its direction and that crosses zero a few steps later, which previously changedhto -1 or 2 and, e.g., triggered events twice. A dropped root that is exactly zero is deactivated until it becomes nonzero, as SUNDIALS does at a restart, since otherwise CVODES/IDAS fail with "Root found at and very near t" on the next step (SBML cases 00354, 00754–00756). This requires access to SUNDIALS internals (cv_gactive/ida_gactive).This replaces the ignore-after-reinit machinery from #3266 (
Solver::ignore_roots_after_reinit, the root filtering inreinit_post_process_f, theROOT_AFTER_REINITwarning). That filter can't be made correct: from the root index, its direction, and the fact that it was found in the first step after reinitialization, a genuine fast re-crossing (e.g., the next bounce of a ball within that step) can't be distinguished from a re-detection, so it either misses events or leaveshinconsistent. With roots that don't changehdropped, it's redundant: all tests and the SBML cases below pass without it.With adjoint sensitivities, the event-time terms are now passed through secondary discontinuities without event assignments, and forward and adjoint sensitivities use the last rising root, or else the last crossing root, for the event time, so that secondary events triggered at a discontinuity with only falling roots get event-time sensitivities (not covered by a test).
Tests (
test_events.py, identical to #3490):test_event_crosses_own_trigger_back,test_event_moves_own_trigger_time,test_event_assignment_onto_thresholdtest_bouncing_ball: event counts, trajectory, and forward/adjoint sensitivities against the analytical solutiontest_sawtooth_many_events: 1000 periods of events that reverse their own root function; event counts and forward/adjoint sensitivities against the analytical solution (fails onmainwith adjoint sensitivities)test_root_at_threshold_changes_direction(fails onmain),test_assignment_onto_other_threshold,test_trigger_leaves_threshold(after an assignment / att0),test_event_moves_own_trigger_back_within_tolerance,test_chattering_switch_failstest_root_after_reinit_direction_is_checkedis renamed totest_event_assignment_just_below_other_trigger, and theROOT_AFTER_REINITchecks are removed.The SBML semantic test suite cases with events or
piecewisegive the same results as onmain(627 passed, including forward and adjoint sensitivity checks).Comparison with root masking (#3490):
h(CVodeSetRootDirection/IDASetRootDirection), so no SUNDIALS internals are needed for exact zeros, buthhas to be up to date whenever the solver is called. Here, the solver stops at these crossings; this resumes the integration without reinitialization and didn't measurably affect performance.main: +210/−144 here, +232/−144 in Fix events crossed back or moved onto thresholds by event assignments, using root masking #3490.maindiffered from the one without sensitivities (−896682 vs. −888198); both branches give −888198 in all modes.Known limitations (both): The direction of a root function within its uncertainty of zero is estimated by finite differences, which is unreliable for very small slopes, and for DAEs uses the algebraic states' derivatives only approximately. An event that moves its own root function back by less than that uncertainty, e.g.,
at x >= 1e6: x = x - 0.005with default tolerances, does not trigger again, and a root function that dips below zero by less than it within a single step is not detected (as onmain). Event assignments are applied as state changes, sox = 1may result inxone rounding error off (as onmain).🤖 Generated with Claude Code