Most questions people ask about PyBaMM degradation runs come down to one word. Why does the fade curve start at a value that has nothing to do with the nominal capacity? Why does termination="80% capacity" never fire, or fire late? Why don’t the “loss of capacity to SEI / plating” variables add up to the capacity the cell lost? Why does capacity go up? In each case the answer is that PyBaMM has several different things called capacity, and a degradation study quietly mixes them. This article measures each one in PyBaMM 26.5.0. Unless stated otherwise the model is the DFN with the OKane2022 (LG M50, 5 A·h) parameter set at 25 °C.
PyBaMM battery modelling series · Silent failures, measured (part 4 of 7): Previous: PyBaMM Thermal Models, Measured: Zero Heating, Missing Entropy and a Cold Start at 25 °C · Next: PyBaMM SEI and Lithium Plating Pitfalls: Clock-Driven SEI, Placeholder Rates, Temperature and Pore Clogging · Series hub: Data pipeline (4) + Silent failures, measured (7) · RSS feed.
1. Several capacities in one fresh cell
The same cell, with no degradation switched on:
| Quantity | 25 °C | 0 °C |
|---|---|---|
"Nominal cell capacity [A.h]" |
5.0 A·h: sets what “1C” means, nothing else | |
Summary variable "Capacity [A.h]" |
5.110 A·h | 5.110 A·h |
| C/20 discharge after a C/20 charge and CV to C/100 | 5.088 A·h (−0.4%) | 5.046 A·h (−1.3%) |
| 1C discharge after a C/3 charge and CV to C/20 | 4.884 A·h (−4.4%) | 4.469 A·h (−12.5%) |
The summary "Capacity [A.h]" is an electrode state-of-health (eSOH) quantity: the charge between the parameter set’s open-circuit limits (2.5 and 4.2 V here) at zero current and the reference temperature, computed from the electrode capacities and the lithium in the particles. That is why it reads 5.110 A·h at 0 °C, where the cell actually delivers 4.469 A·h at 1C. A further candidate, the variable "Discharge capacity [A.h]", is a running integral of current since t = 0 — it goes down again while charging, so its final value in a cycling run is not a capacity at all.
None of these is wrong. They answer different questions, and a fade curve is only meaningful if every point on it answers the same one.
2. What termination="80% capacity" actually compares
The experiment docstring says only “e.g. 80% capacity”. The code (simulation.py, lines 1097–1129) does this:
- After each full cycle, it reads the summary
"Capacity [A.h]"— the eSOH value above, evaluated on the cycle’s last state. - It compares it with 80% of the same quantity at the end of cycle 1 (or of the first cycle of a
starting_solution). Not the nominal capacity, not the pristine state, and not a discharge. - Progress and the final stop are logged at NOTICE level. PyBaMM’s default logging level is WARNING, so by default you see nothing.
I checked this against a native run (two lithium per SEI molecule, SEI solvent diffusivity ×3000, pore clogging off): PyBaMM stopped at exactly the cycle the offline rule predicts (cycle 143, eSOH agreeing to 1.2 × 10⁻⁶ A·h). Nothing was logged at WARNING or above; the stop appeared only as a NOTICE line, “Stopping experiment since capacity (4.013 Ah) is below stopping capacity (4.014 Ah).”
So "80% capacity" means: stop when the zero-current capacity at the reference temperature, computed from each cycle’s last state, falls to 80% of its own end-of-cycle-1 value. A cycler’s end of life usually means 80% of what the cell delivers. Section 3 measures how far apart those are.
3. When the two definitions part ways
eSOH sees lithium inventory and electrode capacity. It cannot see resistance, electrolyte transport or whether the cell still accepts charge. So the gap depends on what drives the fade. In this section “measured” means the protocol’s own 1C discharge after a C/3 CC-CV charge, relative to cycle 2; a slow reference test would sit much closer to eSOH, so these are gaps between eSOH and in-test 1C capacity. All rows are DFN with OKane2022’s partially reversible plating switched on and pore clogging on ("SEI porosity change": "true"); the parameters we multiplied are named in the first column:
| What drives fade | Measured 1C capacity reaches 80% | PyBaMM’s “80% capacity” stop | 1C capacity when PyBaMM stops | SEI as a share of the pore-fill amount |
|---|---|---|---|---|
| SEI, pore room raised 4× (see box), SEI solvent diffusivity ×3000 | cycle 68 | cycle 77 (+13%) | 78.6% | 47% |
| Same, SEI solvent diffusivity ×1000 | cycle 183 | cycle 211 (+15%) | 78.5% | 46% |
| Loss of positive active material, current-driven, 1 × 10⁻⁸ (A/m²)⁻¹ s⁻¹ (our choice) | cycle 62 | cycle 82 (+32%) | 69.7% | 1% |
| Same at 5 × 10⁻⁹ (A/m²)⁻¹ s⁻¹ | cycle 122 | cycle 164 (+34%) | 69.4% | 2% |
| SEI at OKane2022’s own pore room, SEI solvent diffusivity ×1000 | collapses at cycle 143 (87.9% → 68.5%)¹ | not fired at the collapse (eSOH 91.1%); cannot fire validly (see box) | — | 80% |
| Plating at 0 °C, 1C charge, dead-lithium formation ×10 | collapses at cycle 144 (87% → 54%)² | not fired (eSOH 89.3%) | — | 1% |
¹ x_n = 80; the default mesh (x_n = 20) puts the collapse at cycle 145 (87.9% → 65.7%). ² x_n = 80 or a mesh graded toward the separator. x_n = 40 puts it at cycle 151, and the default x_n = 20 misses the collapse entirely, fading gradually until the electrolyte concentration goes negative at cycle 166. OKane2022’s plating kinetics also carry no temperature dependence (see the companion article).
Three regimes. When fade is lithium loss, PyBaMM stopped with the 1C capacity at about 78.5% — about 1.5 points past the measured 80% — in both runs; in cycles that was 13–15% late at these accelerations, and we did not measure the lag at unaccelerated rates. When fade is lost positive active material, the stop came about a third of the life late, with the cell at about 69.5%. When fade is loss of charge acceptance — pores next to the separator clogging with SEI and plated lithium, so the constant-voltage step reaches its cut-off early — the eSOH capacity barely moves, so the criterion has not fired when the delivered capacity collapses.
Two further contributions add to the gap. The eSOH reference (4.99 A·h at the end of cycle 1 in the first row) is 5% larger than the 1C reference (4.75 A·h at cycle 2), so the same A·h loss is a smaller percentage of it. And the default "SEI film resistance": "distributed" costs 1C capacity that eSOH ignores. In runs with two lithium per SEI molecule, SEI solvent diffusivity ×3000 and pore clogging off — the SEI there fills 86–93% of the pores, whose transport loss the model then ignores — switching the film resistance to "none" shrank the lag from +18% (121 vs 143 cycles) to +7% (136 vs 145), so film resistance was most of the lag in that setting.
Box: why OKane2022’s SEI cannot reach 80% on its own. With OKane2022’s SEI geometry (specific surface area 3.84 × 10⁵ m⁻¹, porosity 0.25, SEI molar volume 9.585 × 10⁻⁵ m³/mol, and one lithium per SEI molecule — a value the parameter file itself labels “not physical but true to paper”), 651 nm of SEI growth fills the initial negative-electrode pores. That is 0.612 A·h of lithium, 8.04% of the lithium in the particles. The eSOH capacity reaches 80% only after 1.07 A·h of lithium is lost. So with the shipped stoichiometry no geometrically possible SEI-only OKane2022 run can trigger the stop. With two lithium per SEI molecule the pores could hold enough (1.22 A·h), but in our run the electrolyte ran out first, at cycle 115.
With "SEI porosity change" at its default "false", the model does not track the film’s volume at all, so nothing stops the SEI from exceeding the pore volume, and the stop does eventually fire. In one of our runs (SEI solvent diffusivity ×3000, partially reversible plating, default film resistance) it fired at cycle 267 against a measured 194, which looks like a 38% lag. It is not a result: the SEI already held 1.57 times the pore-fill lithium when the measured capacity crossed 80%, and 1.83 times at the stop. That is why the first two table rows raise the pore room instead (two lithium per SEI molecule and half the molar volume, both disclosed).
Invalid states pass silently
The SEI + plating case above, run on PyBaMM’s default mesh (knee at cycle 145) and continued to 300 cycles, drives the minimum electrolyte concentration negative at the end of each charge from cycle 163 onward (−0.57 mol/m³ by cycle 300). There is no warning and no error; the run completes and returns a fade curve, and every number after cycle 162, eSOH included, describes a state no cell can be in. The check in section 8 looks for this.
4. The read point: summary variables describe the end of each cycle
Every summary variable, eSOH included, is read from the last state of each cycle (summary_variable.py, lines 164–196). If your cycle tuple ends with the charge, you are reading a charged cell. With lithium plating on, a charged cell holds reversibly plated lithium that will strip back on the next discharge — and PyBaMM counts it as lost.
Here is the same 20-cycle experiment (DFN, partially reversible plating, pore clogging off, 30-minute rests) written twice, once with each cycle ending after the charge and once ending after the discharge. The physics is identical; only the tuple order changes:
| After 20 cycles | 25 °C, C/3 charge: ends charged | ends discharged | −5 °C, 1C charge: ends charged | ends discharged |
|---|---|---|---|---|
| Summary “loss” to plating | 0.0107 A·h | 0.0019 A·h | 0.0285 A·h | 0.0174 A·h |
| Summary LLI (absolute, includes SEI) | 0.184% | 0.069% | 0.383% | 0.237% |
| Summary “Capacity” deficit versus a run without plating | 0.0100 A·h | 0.0018 A·h | 0.0267 A·h | 0.0163 A·h |
| Measured 1C deficit versus a run without plating | 0.0032 A·h | 0.0032 A·h | 0.0177 A·h | 0.0177 A·h |
At 25 °C, reordering the tuple moves both the reported plating loss and the reported capacity deficit about 5.5-fold, while the measured 1C deficit stays at 0.0032 A·h. In a 50-cycle run with 5-minute rests and pore clogging on, cycles ending charged report 0.0178 A·h lost to plating at 25 °C, of which only 0.0035 A·h is dead lithium still there after the last discharge; the measured deficit is 0.0047 A·h. With 5-minute rests the charged-state reading also lowers eSOH itself, by 0.014 A·h (0.27%, 25 °C) to 0.029 A·h (0.58%, 0 °C with a 1C charge), constant over the 8 cycles we checked; longer rests let some plated lithium strip first and shrink it (about 0.008 A·h at 25 °C with the 30-minute rests in the table).
Fix: put the discharge last in each cycle tuple. Reading LLI and plating variables from sol.cycles[i].steps[k] at the end of the discharge step also works for those variables, but the eSOH "Capacity [A.h]" and the "80% capacity" stop can only be fixed by reordering.
5. “Loss of capacity to X” is lithium, not capacity
A PyBaMM maintainer put it plainly in discussion #3502: “capacity” in these names means cyclable lithium. Four consequences.
Plating “loss” is a snapshot
"Loss of capacity to negative lithium plating [A.h]" is the lithium currently held as metal, strippable plus dead (base_plating.py, lines 119–126). After a −5 °C, 2C constant-current constant-voltage charge with reversible plating (pore clogging off) it reads 0.206 A·h at its peak, 0.058 A·h at the end of the CV step, 0.005 A·h after a 10-hour rest and 0 after a C/3 discharge. Partially reversible plating leaves 0.0016 A·h behind. Which of those numbers you report depends only on when you look.
LLI [%] has a different denominator
"Loss of lithium inventory [%]" divides by all the lithium in both electrodes’ particles — 7.611 A·h for OKane2022 — not by the 5 A·h capacity. How lost lithium turns into lost capacity then depends on electrode balancing, and it differs between parameter sets:
| Parameter set | eSOH capacity change per 1% LLI (first 1%) |
|---|---|
| Ai2020 | −1.70% |
| Chen2020 | −1.45% |
| Ecker2015 | −1.33% |
| OKane2022 | −1.28% (cumulative −14.2% at 10% LLI, −28.4% at 20%) |
| Prada2013, Mohtat2020 | −1.03%, −1.02% |
| Ramadass2004³, NCA_Kim2011 | −0.09%, −0.02% |
| ORegan2022 | +0.03% |
| Marquis2019 (PyBaMM’s default) | +1.40% |
³ Ramadass2004’s cathode open-circuit fit has a pole, so its eSOH problem has three roots; the value is for the middle one. For OKane2022 the simulated C/20 and C/3 capacities follow the eSOH conversion to within 2%. The last row is section 6.
Lithium in lost active material is booked separately
With loss of active material on, lithium inside the removed particles goes to its own variable, "Loss of lithium due to loss of active material in negative/positive electrode [mol]". "Loss of lithium inventory [%]" counts it; "Total lithium lost to side reactions [mol]" does not. The stranded-lithium variable itself is not among the default summary variables. In the full OKane2022 degradation model over 200 cycles (read after discharge) it is 14% of the LLI; at ten times the shipped LAM rate it is 63%. This is one reason the per-mechanism losses do not add up to the total. (Separately: "loss of active material": "reaction-driven" does nothing with the shipped parameters, because its rate factor is 0.0 in all 10 sets that define it, including OKane2022.)
The older problem of SEI lithium “disappearing” when active material is lost (issue #3006) is fixed since v25.10 (PR #4869). In 19 DFN and SPM runs with up to 7.7% negative-electrode LAM (reaction-driven LAM with an amplified test factor), the SEI ledger never decreased and matched an independently integrated SEI current to 8.5 × 10⁻¹² mol in the DFN and 7.6 × 10⁻¹⁰ mol in the SPM, out of about 5 × 10⁻⁴ mol.
Box: an LLI error from 26.4 onward. From v26.4.0 (PR #5448) PyBaMM’s x-average simplifier treats particle variables as uniform across the electrode, so the DFN computes the lithium in the particles as a product of averages. For an electrode that starts and stays uniform that is harmless; with LAM on, the active-material fraction becomes non-uniform and the ledger error is small but non-zero (1.4 × 10⁻⁶ mol at OKane2022’s LAM rate). For a DFN whose active-material fraction varies through the thickness from the start (we used 0.60 to 0.75) it reports up to 0.256% LLI with every degradation mechanism switched off, while the lithium actually in the particles, summed cell by cell, is conserved to 3.5 × 10⁻¹⁴ mol. The SPM is unaffected. We reported it upstream as issue #5810 on 27 September 2026. It is fixed on main by PR #5813, merged on 30 September 2026, which corrects _is_independent_of and _try_separable in averages.py. The merge came after the 26.9.0.0 release of 28 September, so the fix ships in the next release; every release from 26.4 to 26.9 is affected.
6. Why capacity goes up: PyBaMM’s own default parameter set
“My capacity increases with SEI on” is a recurring question — for example discussion #2075, where the summary “Capacity” rose while the measured capacity fell (the thread names no parameter set, so we assume PyBaMM’s default). With that default — Marquis2019, the default of SPM, SPMe and DFN — a rising capacity is what the model predicts with these parameters; a maintainer has called the excess lithium in some sets “a bug in the parameters” (discussion #2472). Marquis2019’s particles hold 6.8% more lithium (2.079 A·h) than its positive electrode can accept (1.946 A·h), so every discharge ends on a full cathode with the graphite still 18% lithiated. Losing lithium moves the window to a better place first:
- eSOH capacity rises by 1.40% for the first 1% of lithium lost, peaks at +11.9% at 9.5% LLI, and only then falls.
- The measured capacity rises too, with shipped parameters and no acceleration: with
"SEI": "ec reaction limited"the 1C discharge climbs from 0.8613 to 0.9293 A·h (+7.9%) by cycle 27 in the SPM, and by +7.5% at cycle 26 in the DFN with pore clogging on (the same at x_n = 20, 40 and 80, with the SEI then filling 38% of the pores). A C/20 discharge rises by 8.3%, so it is electrode balancing, not a rate effect. - With the milder
"reaction limited"SEI, the measured capacity rises steadily for all 200 SPM cycles tested (+0.37%).
The rule behind the table in section 5: per unit of lithium lost, the eSOH capacity changes by (f₀ − f₁₀₀), where f is the positive electrode’s share of the combined open-circuit slope at 0% and 100% state of charge. Marquis2019 has f₀ = 0.98 and f₁₀₀ = 0.47, so each A·h of lithium lost adds about 0.51 A·h of capacity until the graphite reaches its own knee; OKane2022 has f₀ = 0.02 and f₁₀₀ = 0.95 and loses capacity. Having excess lithium is not enough on its own: NCA_Kim2011 has 5% more lithium than its cathode holds, yet its capacity never rises — it is flat for the first 1–3% of LLI and then falls.
One more trap in the same machinery. PyBaMM’s eSOH solve has no bounds on the stoichiometries and reuses its previous answer as the starting guess. For Ai2020 it can land on a second, unphysical root with a graphite stoichiometry of 1.0002, reached by extrapolating the cathode’s open-circuit curve. A simulation started 10% lithium-poor then reports a constant 2.917 A·h for five cycles while delivering 1.98 A·h; the only signal is one extrapolation warning.
7. Starting from an aged cell, and splitting long runs
Raising the initial SEI thickness does not age the cell
A forum answer suggests emulating an aged cell by raising "Initial SEI thickness [m]". In 26.5 that changes the resistance and the rate of further SEI growth, but removes no lithium and fills no pore volume, because by design the SEI lithium and porosity ledgers count only growth beyond the initial thickness (base_sei.py, 112–118; reaction_driven_porosity.py, 67–72). Going from 5 to 200 nm raises the 10-second DC resistance from 39.4 to 51.1 mΩ and makes further SEI growth about 28 times slower, but the C/10 capacity moves only from 5.0893 to 5.0883 A·h. A cell genuinely aged to 200 nm in PyBaMM (87,900 h of storage, SEI at 30% of the pore-fill amount) has lost 0.183 A·h; the emulated cell overstates its C/10 capacity by 3.5%.
What does reproduce the aged cell — to within 10⁻⁵ A·h on C/10 and 1C capacity and on the next 20 cycles’ fade, and to 0.01 mΩ on the 10-second resistance — is setting four things together: the aged SEI thickness; "Initial concentration in negative electrode [mol.m-3]" lowered by the lost lithium, followed by solve(initial_soc=1); the aged negative-electrode porosity; and the aged electrolyte concentration. With pore clogging off, the first two suffice. With OKane2022 plating, also carry over the aged "Initial plated lithium concentration [mol.m-3]" (PyBaMM books it as already lost in its side-reaction totals) and scale "Dead lithium decay constant [s-1]" by (5 nm / aged thickness), because the dead-lithium rate is written as γ₀·L₀/L. Without that rescaling, the “aged” cell faded 2.88 mAh over the next 20 cycles against 0.16 mAh for the real one.
Splitting a long run
Handing the state over with sim.solve(starting_solution=...) or model.set_initial_conditions_from(...) is safe: 200 DFN cycles with SEI and plating (x_n = 10, 1C CC-CV charging), run as four blocks, match one continuous run to within 3.3 × 10⁻⁷ A·h per cycle. Handing it over through parameters — updating the initial SEI thickness and restarting from initial_soc, as in discussion #4786 — silently resets lithium inventory, plated and dead lithium, porosity and SEI on cracks. On that mesh the measured fade over 200 cycles came out at about a third of the continuous run’s (8.9 vs 24.2 mAh); on the default mesh over 40 cycles, a quarter (1.75 vs 6.88 mAh). The per-block losses a user would add up overstated LLI by 45% and plating by 80% over the 200 cycles.
8. A check to run after every degradation simulation
Two functions: one returns the capacity a cycler would measure, the other flags the traps above that run without an error.
import numpy as np
import pybamm
def discharge_capacity(sol):
"""Ah delivered by the discharge steps of each saved cycle (None for unsaved)."""
out = []
for cycle in sol.cycles:
if cycle is None:
out.append(None)
continue
q = 0.0
for step in cycle.steps:
dq = step["Discharge capacity [A.h]"].entries
if dq[-1] > dq[0]:
q += dq[-1] - dq[0]
out.append(float(q))
return out
def fade_audit(sim):
"""Warnings for a solved degradation run whose capacity numbers can mislead."""
opts, pv, sol = sim.model.options, sim.parameter_values, sim.solution
out = []
moving = [s for s in sol.cycles[-1].steps
if abs(s["Current [A]"].entries).max() > 1e-9]
dq = moving[-1]["Discharge capacity [A.h]"].entries
if dq[-1] < dq[0]:
out.append("cycles end after a charge: summary variables (Capacity, LLI, "
"plating) are read in the charged state")
exp = getattr(sim, "experiment", None)
if exp is not None and "capacity" in (exp.termination or {}):
out.append("'% capacity' termination compares the zero-current eSOH "
"'Capacity [A.h]', not a measured discharge")
c_e = sol["Electrolyte concentration [mol.m-3]"].entries
if c_e.min() < 0:
out.append(f"electrolyte concentration went negative ({c_e.min():.2f} mol/m3): "
"results after that point describe no real cell")
sei = opts["SEI"][0] if isinstance(opts["SEI"], tuple) else opts["SEI"]
clogging = "true" in (opts["SEI porosity change"], opts["lithium plating porosity change"])
if sei not in ("none", "constant") and not clogging:
v_n = (pv["Negative electrode thickness [m]"] * pv["Electrode height [m]"]
* pv["Electrode width [m]"])
pore_mol = (pv["Negative electrode porosity"] * v_n
* pv["Ratio of lithium moles to SEI moles"]
/ pv["SEI partial molar volume [m3.mol-1]"])
sei_mol = sol["Loss of lithium to negative SEI [mol]"].entries.max()
if sei_mol > 0.5 * pore_mol:
out.append(f"SEI holds {sei_mol / pore_mol:.0%} of the lithium that would fill "
"the negative pores, and pore clogging is off")
return out
Output on four OKane2022 runs:
T1 DFN, SEI + plating, 10 cycles ending on charge, termination="80% capacity"
discharge_capacity: 4.9547, 4.882, 4.8817 ... 4.88
cycles end after a charge: summary variables (Capacity, LLI, plating) are read in the charged state
'% capacity' termination compares the zero-current eSOH 'Capacity [A.h]', not a measured discharge
T2 same model with pore clogging on, cycles ending on discharge
discharge_capacity: 4.882, 4.8817, 4.8815 ... 4.8797
(no warnings)
T3 SPM, "ec reaction limited" SEI, 60 cycles, pore clogging off
discharge_capacity: 4.9305, 4.7684, 4.708 ... 3.7698
cycles end after a charge: summary variables (Capacity, LLI, plating) are read in the charged state
SEI holds 174% of the lithium that would fill the negative pores, and pore clogging is off
T4 DFN, SEI solvent diffusivity x1000 + plating + pore clogging, 170 cycles
discharge_capacity: 4.9379, 4.8389, 4.8217 ... 2.1018
cycles end after a charge: summary variables (Capacity, LLI, plating) are read in the charged state
electrolyte concentration went negative (-0.01 mol/m3): results after that point describe no real cell
Use cycle 2, not cycle 1, as the reference for measured fade: cycle 1 starts from the initial_soc equilibrium rather than after the protocol’s own charge (4.955 vs 4.882 A·h in T1). The pore check covers SEI on the particle surface only; SEI on cracks and plated lithium also take pore volume. What the audit cannot tell you is whether the fade is the right fade — the SEI rate constants in the shipped sets are generic examples, a topic for the companion article.
9. How this was measured
- PyBaMM 26.5.0, Python 3.13, Apple M3,
IDAKLUSolver. DFN with OKane2022 unless stated; 25 °C isothermal unless stated; cycles of 1C discharge to 2.5 V and C/3 charge to 4.2 V with CV to C/20. - “Measured capacity” is the step-integrated A·h of each cycle’s discharge, relative to cycle 2.
- Where a run was accelerated, the multiplied parameter is named. Every SEI result was checked against the pore-fill amount; results beyond full pores appear only as labelled counterexamples (the box in section 3 and T3 in section 8).
- Most headline numbers were checked at x_n = 20, 40 and 80 and at rtol 10⁻⁶, and move by at most a few tenths of a percent or one cycle. The exceptions are stated where they occur: the SEI + plating knee (cycle 145 on the default mesh, 142–143 on finer ones), the 0 °C plating collapse (which the default mesh misses), and the block-splitting comparison (x_n = 10 for 200 cycles).
- The work ran in two rounds: a first set of measurements, then an adversarial re-verification against four fixed standards (pore-fill amount, read point, solver tolerance, mesh). Several first-round figures did not survive and are not in this article, apart from the one retraction in the box.
References
- S. E. J. O’Kane et al., Lithium-ion battery degradation: how to model it, Phys. Chem. Chem. Phys. 24 (2022) 7909–7922.
- PyBaMM Experiment API and coupled degradation example
- PyBaMM discussion #3502 (names of the loss variables), #2075 and #2472 (capacity increasing with SEI), #4786 (block-wise runs)
- PyBaMM issue #3006 and PR #4869; PR #5448
Related: PyBaMM SEI and lithium plating pitfalls, on what actually drives the fade; PyBaMM Experiment pitfalls, for experiments that finish early without an error; and PyBaMM’s built-in parameter sets, measured, on what “1C” means for each set.
Companion resources
Download the companion scripts
Battery Modeling for AI / GUIDE
PyBaMM silent-failure checks README
Pins pybamm==26.5.0 (verified 2026-09-26), says what each file does and how to run the two test scripts, and links both articles.
Battery Modeling for AI / CODE
fade_audit.py: capacity-reading audit
Checks a solved degradation run: cycles that end after a charge, "% capacity" termination comparing the eSOH capacity, negative electrolyte concentration, and SEI past half of what would fill the negative pores with clogging off.
Battery Modeling for AI / CODE
test_audit.py: four fade_audit cases
Runs four degradation cases on OKane2022, one per argument T1 to T4, and prints the discharge capacity and fade_audit warnings.
Battery Modeling for AI / CODE
sei_preflight.py: SEI and plating preflight
Checks a Simulation before it is solved: zero SEI activation energy, pore clogging off, solver tolerance for potential-dependent SEI in an SPM, temperature-independent plating exchange current, irreversible plating, and start_time with repeated steps.
Battery Modeling for AI / CODE
test_preflight.py: sei_preflight cases
Builds the seven Simulation configurations from the SEI article (P1–P6 and P4b) and prints the sei_preflight warnings for each, without solving.
![What PyBaMM’s Capacity Numbers Measure: “80% capacity”, Capacity [A.h] and Loss of Capacity to SEI](https://www.haotianblog.com/wp-content/mu-plugins/personal-site-performance-assets/mirrorlake-q74-1600.webp)