Skip to content

Set closest_event_idx in IDAKLUSolver after IDA_ROOT_RETURN#5502

Merged
rtimms merged 6 commits intopybamm-team:mainfrom
rtimms:idaklu-closest-event-idx
May 8, 2026
Merged

Set closest_event_idx in IDAKLUSolver after IDA_ROOT_RETURN#5502
rtimms merged 6 commits intopybamm-team:mainfrom
rtimms:idaklu-closest-event-idx

Conversation

@rtimms
Copy link
Copy Markdown
Contributor

@rtimms rtimms commented May 8, 2026

Summary

After a SUNDIALS root return, BaseSolver.get_termination_reason needs to label solution.termination with the specific event that fired. The casadi solver records this in Solution.closest_event_idx and get_termination_reason short-circuits. IDAKLU never set it, so the slow fallback path re-walked every TERMINATION event's symbolic expression on the Python side every step — dominating per-step allocations on long event-terminated cycling runs.

The combined-events root function (rootfn) is already built and serialised for the C++ wrapper. This PR keeps a Python-callable handle on it in self._setup["rootfn_casadi"] so _post_process_solution can evaluate it once at (t_event, y_event, p_stacked) and set closest_event_idx = nanargmin(|values|). Round-trip through __getstate__/__setstate__ is supported by deserialising from the same rootfn_pkl the C++ wrapper consumes.

Also extracts the duplicated inputs-dict-flattening pattern in _integrate into a small _flatten_inputs helper, reused at both call sites.

Where the allocations were

memray on a 1000-cycle SPM (output_variables set, IDAKLUSolver, save_at_cycles=N so most cycles are discarded):

Frame Baseline With fix
StateVector._base_evaluate (state_vector.py:299) 326,168 0
_matmul_vector (scipy.sparse._compressed.py:395) 305,144 0

Both eliminated. Top-line:

1000-cycle SPM Wall Cumulative bytes Cumulative allocs
baseline 13.10 s 1756 MB 4,431,707
with fix 10.95 s 1311 MB 3,978,320
saved 16% 445 MB (25%) 453k (10%)

Test plan

  • New regression test test_closest_event_idx_set_after_root_return in tests/unit/test_solvers/test_idaklu_solver.py — asserts every event-terminated step has closest_event_idx populated AND that the index resolves to the same event name step.termination carries. Fails on main (always None for IDAKLU), passes after this change.
  • All 37 IDAKLU unit tests pass (was 36 + 1 new).
  • All 309 other solver unit tests pass.
  • All 9 tests/memory/test_memory_leaks.py tests pass.
  • All 212 simulation/experiment unit tests pass.

After a root return, BaseSolver.get_termination_reason needs to know
which TERMINATION event fired so it can label solution.termination.
The casadi solver records this in solution.closest_event_idx and
get_termination_reason short-circuits. IDAKLU never set it, so the
slow fallback path re-walked every event's symbolic expression on the
Python side every step — generating tens of thousands of small numpy
allocations from StateVector._base_evaluate and scipy._matmul_vector
on long event-terminated cycling runs.

The combined-events root function (rootfn) is already built and
serialised for the C++ wrapper. Keep a Python-callable handle on it
in self._setup so _post_process_solution can evaluate it once at
(t_event, y_event, p_stacked) and set closest_event_idx via
np.nanargmin(|values|). Round-trip through __getstate__/__setstate__
is supported by deserialising from the same rootfn_pkl the C++
wrapper consumes.

Also extracts the duplicated inputs-dict-flattening pattern in
_integrate into a small _flatten_inputs helper, reused at both call
sites.

Benchmark: 1000-cycle SPM with output_variables set, IDAKLUSolver,
save_at_cycles=N — baseline 1756 MB / 4.43M allocs / 13.10 s wall;
with fix 1311 MB / 3.98M allocs / 10.95 s wall (25% bytes, 10%
allocs, 16% wall). StateVector._base_evaluate count drops 326k → 0;
_matmul_vector 305k → 0.

Adds tests/unit/test_solvers/test_idaklu_solver.py::
test_closest_event_idx_set_after_root_return — fails on main
(closest_event_idx is None for IDAKLU), passes after this change,
and verifies the index resolves to the same event the slow path
would have picked.
@rtimms rtimms requested a review from a team as a code owner May 8, 2026 09:23
@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.17%. Comparing base (bf9437e) to head (726f747).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5502      +/-   ##
==========================================
- Coverage   98.17%   98.17%   -0.01%     
==========================================
  Files         338      338              
  Lines       31160    31222      +62     
==========================================
+ Hits        30591    30651      +60     
- Misses        569      571       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

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

The behavioural test in test_idaklu_solver.py asserts the contract;
this adds a count-based memory test alongside the existing
test_memory_leaks.py to catch the regression at its measurable
symptom. Counts direct calls to StateVector._base_evaluate during a
5-cycle solve — 1266 without the fix, 161 with — separated by a
threshold of 300.

Comments trimmed in idaklu_solver.py: the long WHY paragraphs were
too prescriptive next to short code; the changelog and PR description
already cover the rationale.
Adds tests/unit/test_solvers/test_idaklu_solver.py::
test_pickle_roundtrip_preserves_closest_event_idx which exercises
the __getstate__/__setstate__ path: serialise an IDAKLUSolver,
deserialise, run a fresh solve, and confirm closest_event_idx is
still populated for event-terminated steps. This covers the
new self._setup["rootfn_casadi"] entry and its rehydration via
casadi.Function.deserialize from the existing rootfn_pkl.
BradyPlanden
BradyPlanden previously approved these changes May 8, 2026
Copy link
Copy Markdown
Member

@BradyPlanden BradyPlanden left a comment

Choose a reason for hiding this comment

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

Nice find!

@rtimms rtimms enabled auto-merge (squash) May 8, 2026 14:53
@rtimms rtimms merged commit 82ee554 into pybamm-team:main May 8, 2026
26 of 27 checks passed
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.

2 participants