Skip to content

Fix JAX PEtab backend: discontinuities, PEtab v1 import, event assignments#3196

Open
FFroehlich wants to merge 6 commits into
mainfrom
fix/jax-events-and-petab-v1-v2
Open

Fix JAX PEtab backend: discontinuities, PEtab v1 import, event assignments#3196
FFroehlich wants to merge 6 commits into
mainfrom
fix/jax-events-and-petab-v1-v2

Conversation

@FFroehlich

Copy link
Copy Markdown
Member

Summary

A cluster of related fixes to the JAX simulation/PEtab backend, found while investigating how explicit (time-triggered) discontinuities are handled:

  • Precise discontinuity stepping: clip the diffrax step-size controller onto known (explicit) discontinuity times (ClipStepSizeController(controller, jump_ts=...)) instead of relying solely on root-finding. Verified 10 orders of magnitude accuracy improvement and zero rejected steps on a synthetic explicit-discontinuity model.
  • Explicit heavisides as tracked variables: time-triggered Heaviside placeholders are now generated as tracked variables (like implicit/state-dependent ones) instead of being inlined as symbolic jnp.select functions of time. Keeps the vector field smooth within each integration segment.
  • PEtab v1 → v2 conversion: PetabImporter now upgrades an in-memory petab.v1.Problem to v2 (via to_files_generic + petab.v2.Problem.from_yaml, which auto-upgrades) instead of raising NotImplementedError. This lets the JAX backend (which requires v2) consume v1 PEtab problems, e.g. from the PEtab benchmark collection.
  • Noise/observable placeholder bug: JAX gave wrong sigmas (and thus wrong chi2/log-likelihood) for any model using PEtab's standard noiseParameter{n}_{observableId} / observableParameter{n}_{observableId} naming convention. The anchored classification regex only matched the bare noiseParameter{n} form, so these placeholders leaked into ordinary model parameters instead of being resolved via the per-measurement override arrays — every measurement ended up using the first observable's noise value. Fixed by porting the substitution already used by the legacy PEtab v1 JAX importer (v1/_sbml_import.py) into the newer PetabImporter, JAX-gated, operating only on an ephemeral importer-internal copy (never touching the PEtab problem itself or the SUNDIALS path). Verified against the PEtab benchmark model Boehm_JProteomeRes2014: chi2/llh now match SUNDIALS exactly (was off by ~7.4 in llh).
  • Event assignments silently dropped: _apply_event_assignments processed roots in fixed windows of two, assuming every event contributes a Heaviside +/− root pair. A plain SBML <event> with an assignment (no associated Heaviside) contributes a single root, so whenever a model's total root count was odd, the last root (in declaration order) was silently dropped — no error, no warning, no state update. Fixed by iterating over every root individually. Investigated and confirmed this doesn't regress the paired-Heaviside mechanism, including when Heaviside pairs and plain events are mixed in the same model (explicit events always sort before Heaviside pairs internally, so a dropped tail root there is always a Heaviside root with a zero delta).

Test plan

  • python/tests/test_jax.py: existing discontinuity tests pass; added test_explicit_discontinuity and test_event_assignments_odd_root_count (verified the latter fails without the fix and passes with it)
  • python/tests/petab_/test_petab_v2.py: added test_petab_importer_upgrades_v1_problem and test_jax_matches_sundials_with_per_observable_noise_parameters (JAX vs SUNDIALS llh agreement)
  • Verified SUNDIALS (v1 and v2-via-conversion) llh unaffected by the JAX-gated noise-parameter fix, on Boehm_JProteomeRes2014 and Fujita_SciSignal2010

Notes for reviewers

Two related pre-existing bugs were found during this investigation but are out of scope for this PR (tracked separately):

  • _known_discs(self, p) crashes with NameError when an explicit trigger time depends on a model expression (w) rather than pure parameters (e.g. Fujita_SciSignal2010's EGF pulse timing) — predates this change (PR Update PEtab SciML support to PEtab v2 #3165).

🤖 Generated with Claude Code

Several related fixes to the JAX simulation backend surfaced while
investigating discontinuity handling:

* Clip the step-size controller onto known (explicit) discontinuity
  times so time-triggered events are integrated precisely instead of
  relying solely on root-finding.
* Represent explicit (time-triggered) Heaviside placeholders as
  tracked variables instead of inlining them as `jnp.select`, matching
  how implicit/state-dependent triggers are already handled. This
  keeps the vector field smooth within each integration segment.
* `PetabImporter` now upgrades in-memory PEtab v1 problems to v2
  instead of raising `NotImplementedError`, so the JAX backend (which
  requires v2) can consume v1 PEtab problems.
* Fix wrong sigmas/log-likelihood for observables using PEtab's
  standard `noiseParameter{n}_{observableId}` /
  `observableParameter{n}_{observableId}` placeholder naming: these
  were misclassified as ordinary model parameters instead of
  per-measurement overrides, so every measurement got the first
  observable's noise value. Ported the substitution already used by
  the legacy v1 JAX importer to the newer PEtab v2 importer.
* Fix event assignments being silently dropped whenever a model's
  total root count is odd (e.g. any odd number of plain SBML events
  with assignments): `_apply_event_assignments` processed roots in
  fixed windows of two, assuming every event comes in a Heaviside
  +/- pair. Now processes every root individually.

Adds regression tests for each of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 4, 2026 08:08
@FFroehlich FFroehlich requested a review from a team as a code owner July 4, 2026 08:08
@FFroehlich

Copy link
Copy Markdown
Member Author

known_discs fix is here #3195

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR delivers a set of targeted fixes to AMICI’s JAX simulation/PEtab integration to improve correctness around discontinuities/events and broaden PEtab compatibility (v1→v2 upgrade), while aligning JAX results with SUNDIALS for PEtab-standard noise/observable placeholder naming.

Changes:

  • Clip Diffrax step-size control to known explicit discontinuity times and keep explicit time-triggered Heaviside terms as tracked variables (avoid inlined symbolic time functions).
  • Fix event assignment application for models with an odd number of roots by applying assignments per-root instead of in fixed pairs.
  • Upgrade in-memory PEtab v1 problems to v2 during import and correct JAX handling of per-observable noise/observable placeholder parameters.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
python/tests/test_jax.py Adds regression tests for explicit discontinuity handling and odd-root-count event assignments.
python/tests/petab_/test_petab_v2.py Adds tests for v1→v2 in-memory upgrade and JAX vs SUNDIALS agreement for per-observable noise placeholders.
python/sdist/amici/sim/jax/_simulation.py Clips the step-size controller to known discontinuities; fixes per-root event assignment application logic.
python/sdist/amici/importers/petab/_petab_importer.py Implements PEtab v1 in-memory upgrade; adds JAX-only placeholder substitution/filtering; validates upgraded problem.
python/sdist/amici/exporters/jax/ode_export.py Stops inlining Heaviside variables as symbolic functions of time so discontinuities can be handled via tracked variables/events.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread python/tests/test_jax.py Outdated
@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.41176% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.23%. Comparing base (6d10d6f) to head (a6733dd).

Files with missing lines Patch % Lines
...hon/sdist/amici/importers/petab/_petab_importer.py 78.26% 5 Missing ⚠️
python/sdist/amici/exporters/jax/ode_export.py 75.00% 1 Missing ⚠️
python/sdist/amici/sim/jax/_simulation.py 85.71% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (6d10d6f) and HEAD (a6733dd). Click for more details.

HEAD has 16 uploads less than BASE
Flag BASE (6d10d6f) HEAD (a6733dd)
python 9 3
petab 4 1
cpp_python 2 1
cpp 2 0
petab_sciml 2 1
sbmlsuite-jax 3 0
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #3196       +/-   ##
===========================================
- Coverage   78.13%   47.23%   -30.91%     
===========================================
  Files         317      311        -6     
  Lines       20895    20656      -239     
  Branches     1483     1484        +1     
===========================================
- Hits        16327     9756     -6571     
- Misses       4560    10868     +6308     
- Partials        8       32       +24     
Flag Coverage Δ
cpp ?
cpp_python 36.70% <5.88%> (-0.04%) ⬇️
petab 39.02% <79.41%> (-7.79%) ⬇️
petab_sciml 16.10% <52.94%> (+0.05%) ⬆️
python 35.51% <5.88%> (-32.93%) ⬇️
sbmlsuite-jax ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...on/sdist/amici/importers/petab/v1/_petab_import.py 38.46% <ø> (ø)
python/sdist/amici/exporters/jax/ode_export.py 88.00% <75.00%> (-2.91%) ⬇️
python/sdist/amici/sim/jax/_simulation.py 93.61% <85.71%> (-4.17%) ⬇️
...hon/sdist/amici/importers/petab/_petab_importer.py 84.41% <78.26%> (-3.44%) ⬇️

... and 232 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread python/tests/test_jax.py Outdated
assert stats["num_rejected_steps"] == 0

except NotImplementedError as err:
if "The JAX backend does not support" in str(err):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's supposed to be supported - do we need these checks?

@FFroehlich FFroehlich requested a review from BSnelling July 7, 2026 18:54
FFroehlich and others added 3 commits July 7, 2026 21:31
Removing the try/except skip-guards in test_jax.py surfaced three
issues, now fixed:

* A syntax error (missing closing parenthesis) introduced while
  removing a guard in test_time_dependent_discontinuity_equilibration.
* _known_discs generated int64 literals for purely numeric explicit
  trigger times (e.g. `jnp.array([5])`), which diffrax's
  ClipStepSizeController rejects as jump_ts under jax_enable_x64
  (requires floating point). Force a float literal for pure-number
  resolved trigger times while keeping the code-printer path for
  symbolic (e.g. Piecewise) expressions.
* The legacy `amici.importers.petab.v1.import_petab_problem(...,
  jax=True)` still constructed JAXProblem directly from a PEtab v1
  problem, which JAXProblem rejects. Upgrade to v2 first, mirroring
  the conversion already used by PetabImporter.

Two further failures (test_preequilibration_failure,
test_serialisation) are left failing: they hit separate, deeper
pre-existing bugs (a naive substring-based preequilibration-condition
heuristic, and a petab.v2 Problem.to_files() API mismatch in
JAXProblem.save()) that predate this branch and are tracked
separately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# temporary PEtab v1 problem on disk and let petab auto-upgrade it
# (``petab.v2.Problem.from_yaml`` upgrades v1 YAML files via
# ``petab1to2``). The resulting problem holds the model and tables in
# memory, so the temporary directory can be removed afterwards.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment (or a more concise version of it) should be moved into the docstring of this function.

"""
# force the solver to step exactly onto known (explicit) discontinuities so
# the discontinuous vector field is integrated accurately instead of relying
# solely on root finding

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the comment? The code is self explanatory.

#: generic, index-based placeholder name (``noiseParameter1``).
_JAX_PLACEHOLDER_PARAMETER_RE = re.compile(
r"((?:observable|noise)Parameter\d+)_\w+"
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this regex pattern (observableParameter${n}_${observableId}) required by the PEtab v2 specification? Some test cases have placeholders which don't follow that syntax (e.g. https://github.com/PEtab-dev/petab_test_suite/blob/main/petabtests/cases/v2.0.0/sbml/0021/_observables.tsv).

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.

4 participants