Skip to content

Fix JAX PEtab condition-table resolution: state reinit, noise distribution, and differentiability - #3207

Merged
FFroehlich merged 9 commits into
mainfrom
fix/jax-petab-reinit-noise-dist
Jul 10, 2026
Merged

Fix JAX PEtab condition-table resolution: state reinit, noise distribution, and differentiability#3207
FFroehlich merged 9 commits into
mainfrom
fix/jax-petab-reinit-noise-dist

Conversation

@FFroehlich

Copy link
Copy Markdown
Member

Recreated from #3206 to trigger CI (it did not run on the original after the rebase/force-push). History and diff are identical.

Now independent of #3196 (rebased onto main).

To stand alone it carries two prerequisite commits that also live in #3196 (they'll be reconciled when #3196 is rebased): Fix CI failures in JAX PEtab v1/v2 backend (adds _resolve_condition_target_value, the pristine-v1 snapshot, and the v2 simulation-df/experiment fixes) and Ignore another benign petab1to2 warning-turned-error (parameterScale-warning filter, needed for the log10 cases). Reviewers can focus on the last four commits — the rest is the actual new work.

1. Correctness: remaining PEtab test-suite failures (cases 0010–0013, 0016–0020; 0007 xfail)

  • State reinitialisation lookup (sim/jax/petab.py): the reinit methods read condition-table state overrides via the old wide-format condition_df.loc[condition, state_id], but petab.v2's condition_df is long format, so every override silently fell back to the SBML default. Rewired to the structured condition changes.
  • petab1to2 noise-distribution workaround (importers/petab/v1/_petab_import.py): petab1to2's update_noise_dist computes the merged v1→v2 noiseDistribution (e.g. loglog-normal) but never returns it, so upgraded observables revert to normal, corrupting iy_trafos/chi2 (LLH is unaffected). Added _fix_petab1to2_noise_distribution_bug (with a TODO to drop once fixed upstream). Fixes case 0016.
  • 0007 xfail: PEtab v2 has no log10-normal; log-normal is substituted and reproduces the ground-truth chi2 exactly once recomputed with log10. Inherent v1→v2 limitation → documented pytest.xfail.

2. Differentiability: condition-table target values were frozen at construction

Condition-table target values — both species initial values (_state_reinitialisation_value) and parameter overrides (_map_experiment_model_parameter_value) — were resolved through _parameter_mappings["targets_map"], which is built once at __init__, baking self.parameters[i] into a frozen pytree leaf. For any parameter used as an initial value, or mapped to a model parameter by the condition table (the standard pattern for condition-specific estimated parameters):

  • update_parameters(...) had no effect on it, and
  • eqx.filter_grad(run_simulations) routed the sensitivity into grad._parameter_mappings[...], so grad.parameters (what callers read) was 0.

Fix: resolve these values live from the raw condition-table changes via _resolve_condition_target_value, called inside the traced region (_prepare_experiments), so self.parameters is the live array. Autodiff then matches central finite differences to ~1e-10 with zero cache leak.

The petab suite skips derivative checks for jax (if jax: pass), so these were uncaught → added test_condition_table_initial_value_is_differentiable and test_condition_table_parameter_override_is_differentiable.

Verification

  • New gradient tests + representative petab cases (0010/0016/0017/0020) pass on the main base.
  • python/tests/test_jax.py passes locally (incl. the two new tests). Not chasing unrelated suite cases here to keep the diff reviewable.

🤖 Generated with Claude Code

FFroehlich and others added 6 commits July 9, 2026 20:01
Fixes a cluster of bugs surfaced by CI once the v1->v2 upgrade path
(from the two separate PRs merged into this branch) started actually
reaching previously-unreachable code:

* get_simulation_conditions_v2: a dynamic period may reference
  multiple condition ids (e.g. a synthetic preequilibration-indicator
  condition alongside the real experiment condition). Measurements are
  only ever queried by experiment id, so multiple condition-id rows
  per experiment produced duplicate/misaligned measurement arrays,
  causing vmap batch-size mismatches (`vmap got inconsistent sizes`).
  Collapse to one row per experiment.

* add_default_experiment_names_to_v2_problem: read condition ids from
  condition table elements instead of the long-format `condition_df`,
  which contributes zero rows for a condition with no changes (e.g.
  the default condition, or any no-op condition) -- exactly the
  "Experiment has no dynamic period with a condition id" case.

* _build_simulation_df_v2: the synthetic default experiment id was
  overwritten with NaN before being reused to query
  observableParameters/noiseParameters from the measurement table,
  silently matching nothing.

* _get_parameter_mappings: a condition table's target value can be a
  reference to another PEtab parameter id (not just a numeric
  literal), which crashed trying to cast the symbol straight to a jax
  array. Resolve it the same way `_map_experiment_model_parameter_value`
  already resolves other parameter references.

* import_petab_problem (legacy v1 path): snapshot the pristine v1
  problem before SBML/PySB compilation mutates it in place, so the
  later v1->v2 upgrade doesn't serialize an already-mutated (and
  potentially v1-lint-failing) problem.

* pytest.ini: ignore the benign, documented petab1to2 warning when
  falling back from a v1-only noise distribution (log10-normal) to
  log-normal for v2 -- was being promoted to a hard error by this
  repo's `filterwarnings = error` policy, exactly when the v1->v2
  upgrade path first became reachable.

* ExampleJaxPEtab.ipynb / test_petab_suite.py: two consumers still
  expected `dynamic_conditions` to hold bare condition-id strings;
  it now holds tuples (to support multi-condition periods). Updated to
  match.

Brings the official PEtab v1/v2 test suite (jax=True) from 28 failing
down to 18 (case 0007's chi2 mismatch is a likely-inherent consequence
of the log10-normal->log-normal fallback; the remaining ~8 cases
cluster around condition-table-driven state reinitialisation and are
tracked separately). jax=False path re-verified unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes a regression: petab1to2's "Parameter scales are not supported
in PEtab v2" warning (emitted whenever a v1 problem uses non-linear
parameterScale, e.g. the lotka_volterra test fixture) was being
promoted to a hard error by this repo's `filterwarnings = error`
policy, breaking test_preequilibration_failure/test_serialisation.

Verified benign via direct SUNDIALS simulation of the same converted
v2 problem (bypassing JAX) for petab test suite cases 0019/0020, which
also trip this warning: llh matches the ground truth solution exactly,
confirming the dropped parameterScale is purely estimation-scale
metadata that doesn't affect simulation values (petab v1's
nominalValue is always stored in linear units).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…g in JAX PEtab backend

Resolves the remaining 18 official PEtab test suite (jax=True) failures
(cases 0007, 0010, 0011, 0013, 0016-0020, both formats):

* JAXProblem._state_needs_reinitialisation/_state_reinitialisation_value/
  load_reinitialisation looked up species-level condition-table overrides
  via the old wide-format `condition_df.loc[condition, state_id]`, but the
  current petab.v2 API returns `condition_df` in long format
  (conditionId/targetId/targetValue columns), so the lookup always missed
  and every such override silently fell back to the SBML default. Rewired
  to reuse `_parameter_mappings["targets_map"]` (built from `c.changes`),
  which parameter-target lookups already relied on correctly. Fixes cases
  0010, 0011, 0013, 0017, 0018, 0019, 0020.

* Traced case 0007/0016's chi2-only mismatches (LLH and simulated values
  already matched) to an upstream libpetab-python bug: petab1to2's
  `update_noise_dist` computes the merged v1->v2 `noiseDistribution` (e.g.
  folding `observableTransformation=log` into `log-normal`) but never
  returns it, so every upgraded observable silently reverts to `normal`,
  discarding any log/log10 transform. This corrupted `iy_trafos` (and thus
  chi2) even though AMICI's own log-likelihood code generation is
  unaffected, since it's derived from the pristine v1 problem directly.
  Added a workaround in `import_petab_problem` that recomputes the correct
  value from the pristine v1 problem and patches it onto the upgraded v2
  observables. Fixes case 0016 outright.

* With the above fixed, case 0007 has one genuinely inherent residual:
  PEtab v2 has no `log10-normal` distribution, so `log-normal` is
  substituted (with a warning); recomputing chi2 with log10 in place of
  log reproduces the expected ground-truth value exactly, confirming this
  is a real v1->v2 upgrade limitation, not an AMICI bug. Marked as an
  explicit, documented `pytest.xfail` rather than silently skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit resolved condition-table state initial values through
`_parameter_mappings["targets_map"]`, which `_get_parameter_mappings` builds
once at construction time. For a value referencing an estimated parameter,
that captured `self.parameters[i]` as a constant in a separate (frozen)
pytree leaf. Consequences:

* `update_parameters(...)` only replaces `.parameters`, never the cached
  `targets_map`, so the initial value stayed pinned at the nominal parameter
  value -- re-simulating after an update silently ignored it.
* Differentiating via the documented `eqx.filter_grad(run_simulations)`
  idiom put the sensitivity into `grad._parameter_mappings[...]` rather than
  `grad.parameters`, so `grad.parameters` (what callers read) was 0 for any
  parameter used as an initial value.

Verified on petab test suite case 0020 (initial_A estimated, entering the
likelihood only through A(0)): pre-fix, updating initial_A left llh
unchanged and grad.parameters[initial_A] was 0 while -8.70 leaked into the
cache.

Fix: resolve the reinitialisation value live from the raw condition-table
change (`_condition_reinit_target_value` + `_resolve_condition_target_value`)
inside `_state_reinitialisation_value`, which runs within the traced region
via `_prepare_experiments`. Reading `self.parameters` there keeps the value
a function of the current parameters. After the fix, autodiff matches
central finite differences to ~1e-10 with zero gradient leaking into the
cache, for estimated initial values (cases 0020, 0019), parameter-referenced
initial values (case 0013) and ordinary rate parameters alike.

Adds `test_condition_table_initial_value_is_differentiable` (the petab test
suite skips derivative checks for jax, so this bug was uncaught).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same construction-time freezing bug as the preceding commit, on the
parameter-mapping path: `_map_experiment_model_parameter_value` read the
override value from `_parameter_mappings["targets_map"]`, which caches
`self.parameters[i]` at construction time. So a model parameter mapped by
the condition table to an estimated parameter -- the standard PEtab pattern
for condition-specific estimated parameters -- was frozen at the nominal
value: `update_parameters` had no effect and its gradient leaked into the
cache instead of `grad.parameters`.

Verified: a condition setting model parameter `k1` to estimated `k1_c0`
previously left llh unchanged under `update_parameters(k1_c0)` with
`grad.parameters[k1_c0] == 0` (and -0.40 leaking into the cache); after the
fix, forward responds and autodiff matches central finite differences.

Fix: build the override lookup from the raw condition-table changes and
resolve it live via `_resolve_condition_target_value` (which reads the live
`self.parameters` inside the traced region), mirroring the reinitialisation
fix. Adds `test_condition_table_parameter_override_is_differentiable`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

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 fixes remaining correctness gaps in the JAX PEtab (v1→v2-upgraded) workflow around condition-table handling and observable noise distributions, and adds regression tests to ensure condition-table–driven initial values/parameter overrides remain differentiable w.r.t. JAXProblem.parameters.

Changes:

  • Fix condition-table resolution in the JAX PEtab backend (state reinitialisation + condition-mapped parameter overrides), ensuring values are resolved “live” so gradients/update_parameters(...) behave correctly.
  • Work around a petab1to2 upgrade bug where v1 observable transformations fail to propagate into v2 noiseDistribution, restoring correct chi2/transform behavior.
  • Add JAX-specific test-suite adjustments (xfail for PEtab case 0007) and new gradient regression tests for differentiability.

Reviewed changes

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

Show a summary per file
File Description
tests/petab_test_suite/test_petab_suite.py Adds a JAX-only xfail for PEtab case 0007 and aligns simulation-condition IDs for v1 expectations.
python/tests/test_jax.py Adds regression tests ensuring condition-table initial values and parameter overrides remain differentiable.
python/sdist/amici/sim/jax/petab.py Fixes condition-table lookup for v2 long format, resolves targets live for differentiability, and adjusts v2 experiment/condition handling.
python/sdist/amici/importers/petab/v1/_petab_import.py Adds a workaround to patch v2 noiseDistribution after v1→v2 upgrade.
pytest.ini Filters additional known/benign petab1to2 warnings.
doc/examples/example_jax_petab/ExampleJaxPEtab.ipynb Updates example to use tuple-form experiment conditions consistent with runtime output.

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

Comment thread python/sdist/amici/sim/jax/petab.py Outdated
Comment on lines +2092 to +2096
@@ -2056,7 +2093,7 @@ def _build_simulation_df_v2(problem, y, dyn_conditions):
{
petabv2.C.MODEL_ID: [float("nan")] * len(t),
petabv2.C.OBSERVABLE_ID: obs,
petabv2.C.EXPERIMENT_ID: [experiment_id] * len(t),
petabv2.C.EXPERIMENT_ID: [reported_experiment_id] * len(t),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in ab21bf9. The DataFrame now applies the per-experiment padding mask (_ts_masks[ic]) consistently to the index and every column, so experiments with differing numbers of timepoints no longer cause length mismatches or leak padded/duplicated indices. Added test_petab_simulate_ragged_experiments (two experiments with 1 vs 2 timepoints) as a regression test, since no PEtab test-suite case exercises this. Also dropped the fragile sc[0] experiment-id reverse-lookup in favor of the experiment ids already produced per-experiment by run_simulations.

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.09524% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.75%. Comparing base (9a3f523) to head (ab21bf9).

Files with missing lines Patch % Lines
python/sdist/amici/sim/jax/petab.py 88.09% 5 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #3207       +/-   ##
===========================================
+ Coverage   41.45%   75.75%   +34.30%     
===========================================
  Files         311      311               
  Lines       20638    20642        +4     
  Branches     1484     1483        -1     
===========================================
+ Hits         8556    15638     +7082     
+ Misses      12057     4992     -7065     
+ Partials       25       12       -13     
Flag Coverage Δ
cpp 72.06% <88.09%> (?)
cpp_python 36.72% <0.00%> (ø)
petab_sciml 16.09% <73.80%> (?)
python 70.18% <88.09%> (+34.64%) ⬆️
sbmlsuite-jax ?

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

Files with missing lines Coverage Δ
python/sdist/amici/sim/jax/petab.py 85.65% <88.09%> (+71.12%) ⬆️

... and 222 files with indirect coverage changes

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

FFroehlich and others added 2 commits July 9, 2026 21:58
The `update_noise_dist` missing-return bug in petab.v2.petab1to2 (which
silently reverted every upgraded observable's noiseDistribution to
`normal`, corrupting iy_trafos/chi2 for non-linear observable
transformations) has been fixed upstream in
PEtab-dev/libpetab-python#502.

Remove the `_fix_petab1to2_noise_distribution_bug` shim and bump the v1
petab-suite CI job's libpetab pin from 44c8062 to 1b8599dd (the #502
merge commit) so the fix is present. Case 0016 now passes via the
upstream conversion; case 0007's xfail (inherent log10-normal ->
log-normal substitution) and its warning filter remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pristine-v1-problem snapshot (deepcopy before compilation, used for
the v1->v2 upgrade instead of the possibly-mutated problem) is not needed:
the full v1.0.0 jax petab test suite -- including the placeholder cases
(0003/0004/0005/0009) that trigger `_workaround_observable_parameters`'s
in-place mutation -- passes upgrading `petab_problem` directly, as on main.
Restore the file to match main so this PR leaves it untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

…ookup

Two issues in the PEtab v2 simulation-DataFrame builder:

* Length inconsistency (reviewer comment): the DataFrame mixed
  masked/valid-only arrays (obs, t[mask], y[mask]) with unmasked lengths
  (`len(t)`, `index=_petab_measurement_indices[ic, :]`). Since
  `_get_measurements` pads every experiment to a common length, any
  problem whose experiments have differing numbers of timepoints would
  raise `ValueError: arrays must all be same length` or leak
  padded/duplicated indices. Apply the per-experiment `_ts_masks[ic]`
  mask consistently to the index and every column.

* Fragile zero-indexing: the experiment id was recovered by reverse-
  mapping `dyn_conditions`' first condition id (`sc[0]`) through
  `_conditions_to_experiment_map`. `run_simulations` already builds these
  per experiment, so thread the experiment ids through directly and drop
  both the `sc[0]` lookup and the now-unused `_conditions_to_experiment_map`.

No PEtab test-suite case exercises ragged (differing-timepoint)
experiments, so add `test_petab_simulate_ragged_experiments` as a
regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@FFroehlich
FFroehlich requested a review from BSnelling July 10, 2026 07:48
@FFroehlich

Copy link
Copy Markdown
Member Author

addresses some additional issues exposed by #3199 and surfaced by #3202. Remaining test failures will be adressed in #3196

@FFroehlich
FFroehlich merged commit bb1d452 into main Jul 10, 2026
26 of 29 checks passed
@FFroehlich
FFroehlich deleted the fix/jax-petab-reinit-noise-dist branch July 10, 2026 10:42
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.

3 participants