Repository navigation
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3302 +/- ##
==========================================
+ Coverage 80.06% 80.07% +0.01%
==========================================
Files 319 319
Lines 22876 22915 +39
Branches 1562 1561 -1
==========================================
+ Hits 18315 18350 +35
- Misses 4554 4558 +4
Partials 7 7
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
fix-3126-minmax-trigger-times
branch
from
September 30, 2026 08:58
117583a to
90da259
Compare
Member
Author
|
Update: the earlier CI failures (8 JAX PEtab v2 cases and case 0030) are resolved. The JAX 🤖 Generated with Claude Code |
dweindl
force-pushed
the
fix-3126-minmax-trigger-times
branch
2 times, most recently
from
October 13, 2026 08:45
6c0234d to
0d7140f
Compare
dweindl
added a commit
to dweindl/AMICI
that referenced
this pull request
Oct 19, 2026
`AmiciJaxCodePrinter` inherited `_print_And`/`_print_Or` from sympy's `NumPyPrinter`, which emit `numpy.logical_and.reduce((a, b, ...))`. `jax.numpy` rejects the tuple argument (`TypeError: logical_and.reduce requires ndarray or scalar arguments`). These are now printed as nested binary `jnp.logical_and`/`jnp.logical_or` calls. This is latent on `main`, but required for AMICI-dev#3302, where gated `Min`-shaped triggers yield trigger times like `Piecewise((t_root, And(...)), (oo, True))` (failing PEtab v2 test suite cases 0009, 0010, 0017, 0018, 0022, 0028, 0030, 0031). Adds `test_code_printer_and_or`, which evaluates the printed code for three-argument `And`/`Or` under `jax.jit`. It fails without the fix. Not addressed here: deriving from sympy's `JaxPrinter`, and auditing other inherited `NumPyPrinter` methods (see AMICI-dev#3306). Fixes AMICI-dev#3306. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
dweindl
force-pushed
the
fix-3126-minmax-trigger-times
branch
4 times, most recently
from
October 21, 2026 17:35
a3916e7 to
9d2095c
Compare
AMICI encodes And/Or-combined event triggers as real-valued root
functions built from Min/Max (`_parse_event_trigger` in the SBML
importer, and the PEtab v2 importer's period-start events).
`sympy.solve` raises NotImplementedError on Min/Max, which
`Event.__init__` silently swallowed, so `_t_root` stayed empty and such
events always fell back to numerical root-finding even where their
trigger time is statically known.
Add `solve_minmax_trigger_for_t()`, a deliberately narrow resolver for
exactly that structure. Every t-dependent leaf originates from a
relational trigger atom and is therefore affine in t, hence monotonic
with a single root. All leaves must be monotonic in the same direction,
so that the expression crosses zero exactly once: with increasing
leaves a Min (And) crosses at the latest of the per-leaf roots and a
Max (Or) at the earliest; with decreasing leaves the two roles swap.
Time-independent ("gating") leaves of a Min become the condition of a
`Piecewise((<root>, <all gating >= 0>), (oo, True))`; an infinite
trigger time is already treated downstream as "never triggers", both
during simulation and during pre-/post-equilibration.
For the negated counterpart, the gating leaves must be strictly
positive, since a zero gating leaf keeps it at zero. If the gating
condition is false for all parameter values, no trigger time is
returned.
A negated expression is normalised before any of that. AMICI tracks a
persisted Heaviside trigger together with its negated counterpart, and
the two cross zero at the same time; resolving only one of them would
give the pair different classifications and let `_reorder_events()`
separate them, which corrupts the event bookkeeping downstream.
Anything outside that shape -- non-affine leaves, leaves of mixed
monotonicity, mixed Min/Max nesting, a Max with a gating leaf --
returns None and keeps the previous empty-`_t_root` behaviour.
Also loosen `DEModel.has_implicit_event_assignments()`, which rejected
any state-updating event whose trigger referenced a free parameter,
even though only *state*-dependent trigger times are unsupported. It
now allows a model's static symbols rather than only its constants,
resolving the TODO that pointed at this issue. Switching it to
`has_explicit_trigger_times()`, as that TODO suggested, would instead
have rejected unsolvable-but-time-only triggers such as `sin(t) > 0.5`.
References AMICI-dev#3126
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dweindl
force-pushed
the
fix-3126-minmax-trigger-times
branch
from
October 22, 2026 13:30
9d2095c to
7dbee96
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.
Fixes #3126.
Event.__init__triessympy.solve(trigger, t)to precompute when an event fires, so that the event can skip numerical root-finding. But AMICI encodesAnd/Or-combined triggers as real-valued root functions built fromMin/Max(_parse_event_triggerin the SBML importer, and the PEtab v2 importer's period-start events), andsympy.solveraisesNotImplementedErroron those. That exception was silently swallowed, leaving_t_rootempty, so every such event — including every PEtab v2 period-start event — took the slower path even where its trigger time is statically known.solve_minmax_trigger_for_t()handles exactly that structure and nothing else. Everyt-dependent leaf of the tree comes from a relational trigger atom, so it is affine intand therefore monotonic with a single root. All leaves must run in the same direction, so that the expression crosses zero exactly once: with increasing leaves aMin(And) crosses at the latest of the per-leaf roots and aMax(Or) at the earliest; with decreasing leaves the two roles swap. Time-independent ("gating") leaves of aMin— e.g. the experiment/preequilibration indicator comparisons — become the condition ofPiecewise((<root>, <all gating >= 0>), (oo, True)). For the negated counterpart, the gating leaves must be strictly positive, since a zero gating leaf keeps it at zero. If the gating condition is false for all parameter values, the event never triggers and no trigger time is returned.A negated expression is normalised before any of that, which is load-bearing rather than cosmetic: AMICI tracks a persisted Heaviside trigger together with its negated counterpart, and the two cross zero at the same time. If only one of them resolved, the pair would end up with different classifications and
_reorder_events()would separate them, corrupting the event bookkeeping. Normalising means both twins reduce to the same expression, up to the strictness of the gating condition, which doesn't change their classification.Deliberately out of scope, all returning
Noneand keeping today's empty-_t_rootbehaviour: non-affine leaves, leaves of mixed monotonicity (which may cross zero more than once), mixedMin-inside-Maxnesting, andMaxwith a gating leaf. The tests pin each of these bail-outs so the conservative paths don't silently widen later.No codegen or C++ changes are needed for the conditional case.
sp.ooalready prints asstd::numeric_limits<double>::infinity(),Piecewisealready has C++ codegen support, and the runtime already treats "no next explicit trigger time" as infinity and takesmin(next_t_out, next_t_event)— so a trigger time that conditionally evaluates to+infis simply never reached. Pre-/post-equilibration guardsnext_t_eventwithstd::isfiniteand falls back to a geometric step, so the solver clock never becomes infinite there either.Verified end-to-end through the real PEtab v2 importer, on a problem with preequilibration plus a dynamic period starting at t=10: the period-start event's trigger time now resolves to 10 when the experiment is active and to
+inf(never) otherwise, and simulated observables are unchanged.On the PEtab v2 conversions in Benchmark-Models-PEtab#338 (34 problems), all 304 period-start events (Isensee_JCB2018, Raimundez_PCB2020, Brannmark_JBC2010, Weber_BMC2015, Zheng_PNAS2012) now have explicit trigger times; on
main, none had. No other trigger changes classification. All of these periods start at t=0; with additional periods at t=5 and t=10 added to Zheng_PNAS2012, those period-start events are explicit as well.Also resolves a TODO that referenced this issue:
DEModel.has_implicit_event_assignments()rejected any state-updating event whose trigger referenced a free parameter, although only state-dependent trigger times are actually unsupported. It now allows a model's static symbols rather than only its constants. The TODO suggested switching it tohas_explicit_trigger_times()instead, but that asks a different question — "can the firing time be solved for" rather than "does it depend on the trajectory" — and would have started rejecting unsolvable-but-time-only triggers such assin(t) > 0.5. The newly admitted case was checked against exact gradients, not just accepted.🤖 Generated with Claude Code