Skip to content

fix: keep the last completed state when an unsaved experiment cycle fails - #5898

Open
ToukoUrsin wants to merge 2 commits into
pybamm-team:mainfrom
ToukoUrsin:fix/experiment-failure-retention
Open

ToukoUrsin wants to merge 2 commits into
pybamm-team:mainfrom
ToukoUrsin:fix/experiment-failure-retention

Conversation

@ToukoUrsin

Copy link
Copy Markdown

Description

With save_at_cycles, if the solver fails on the first step of a cycle that is not saved, Simulation.solve returns a state from before the last completed cycle, and on_cycle_end can fire for the failed cycle with the previous cycle's summary.

Minimal repro (four 1 s rests, solver fails on the 3rd step):

import pybamm

class FailOnThirdStep(pybamm.IDAKLUSolver):
    calls = 0
    def step(self, *args, **kwargs):
        type(self).calls += 1
        if type(self).calls == 3:
            raise pybamm.SolverError("controlled failure")
        return super().step(*args, **kwargs)

sim = pybamm.Simulation(
    pybamm.lithium_ion.SPM(),
    experiment=pybamm.Experiment(["Rest for 1 second"] * 4),
    solver=FailOnThirdStep(),
)
sol = sim.solve(save_at_cycles=[1], calc_esoh=False)
print(sol.t[-1], sol.all_summary_variables[-1].last_state.t[-1])
# main: 1.0 2.0 (two cycles completed, but the returned state is from cycle 1)
# this PR: 2.0 2.0

Fix. When no step of the failing cycle completed, keep current_solution.last_state (if it is newer than what was retained), reset its solver statistics so discarded work is not counted twice, and stop before the cycle-end callback.

Tests. New tests/unit/test_experiments/test_failure_retention.py (14 cases: list/all/integer save_at_cycles, no failure, first/later failure, resume from a failed sparse solve, partial-cycle failure, solver statistics, two meshes). Six fail on current main, all pass with the fix; tests/unit/test_experiments and tests/unit/test_simulation.py pass.

Related: #4793 (this does not fix that convergence failure; it fixes what is returned after one).

Type of change

Bug fix, with a CHANGELOG.md entry.

🤖 Generated with Claude Code (Claude Opus 5.5).

…ails

When a step failed before the first step of a cycle not in save_at_cycles, the returned solution kept a state older than the last completed cycle and on_cycle_end fired for the failed cycle with the previous cycle's summary.
@ToukoUrsin
ToukoUrsin requested a review from a team as a code owner October 12, 2026 21:07

@rtimms rtimms 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.

Thanks @ToukoUrsin, the diagnosis is right, and the new tests fail on main and pass here. A few small things before merge:

  1. Please drop last_state.solver_statistics = pybamm.SolverStatistics() and its comment. Solution._build_last_state already sets last_state's statistics to zero, so the discarded work is not counted twice either way. All 14 tests pass without the line, and it avoids mutating the cached last_state, which SummaryVariables also holds.
  2. Please drop test_unsaved_failure_retention_is_mesh_independent. Mesh size isn't involved in this code path, and test_failure_keeps_latest_completed_state already covers the same assertions.
  3. CHANGELOG.md conflicts with main; please merge main and keep the entry under # [Unreleased]. Could you also say what changed for the user, e.g. "Simulation.solve with save_at_cycles now returns the last completed cycle's state when the solver fails on the first step of an unsaved cycle, and no longer calls on_cycle_end for the failed cycle."

#5899 changes the same solve and get_data_dict paths, so it'll need a rebase after this and #5724 land.

This branch has not been deployed

No deployments
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