PyBaMM ships 18 parameter sets, and most tutorials pick one — usually Chen2020 — without saying why. I ran the same experiments on every one of them with PyBaMM 26.5.0. The failures that raise an error are not the problem; a KeyError tells you exactly which parameter is missing. The problem is the four ways a parameter set can be wrong for your study and still run to completion without a single warning.
PyBaMM battery modelling series · Silent failures, measured (part 1 of 7): Next: PyBaMM Experiments That Finish and Are Wrong: Five Traps, Measured · Series hub: Data pipeline (4) + Silent failures, measured (7) · RSS feed.
1. What is actually in the box
Only 11 of the 18 run in the standard pybamm.lithium_ion.DFN(). The rest need a specific model — half-cells, an equivalent-circuit set, lead-acid, sodium-ion, a composite electrode and an MSMR example — and loading them into a plain DFN fails loudly, which is fine.
| Set | Cell / chemistry | Window (V) | Runs in plain DFN |
|---|---|---|---|
| Marquis2019 | Kokam SLPB78205130H (the default) | 3.105–4.1 | yes |
| Chen2020 | LG M50, graphite / NMC811 | 2.5–4.2 | yes |
| OKane2022 | LG M50, based on Chen2020, with degradation parameters | 2.5–4.2 | yes |
| ORegan2022 | LG M50, with temperature-dependent thermal and electrolyte data | 2.5–4.4 | yes |
| Ecker2015 | Kokam SLPB 75106100 | 2.5–4.2 | yes |
| Mohtat2020 | graphite / NMC532 pouch | 2.8–4.2 | yes |
| Ai2020 | Enertech pouch, LCO | 3.0–4.2 | yes |
| NCA_Kim2011 | graphite / NCA “nominal design” pouch | 2.7–4.2 | yes |
| Prada2013 | LFP | 2.0–3.6 | yes |
| Ramadass2004 | graphite / LCO, assembled from several sources | 2.8–4.2 | yes |
| Chayambuka2022 | sodium-ion | 2.0–4.2 | yes (runs, but see below) |
| Chen2020_composite | graphite + silicon composite negative | 2.5–4.2 | no — needs particle-phase options |
| MSMR_Example | graphite / NMC622, MSMR thermodynamics | 2.8–4.2 | no — needs MSMR options |
| Ecker2015_graphite_halfcell, OKane2022_graphite_SiOx_halfcell, Xu2019 | half-cells | — | no — need the half-cell option |
| ECM_Example | equivalent circuit, demonstration only | 3.2–4.2 | no — needs the ECM model |
| Sulzer2019 | lead-acid | 1.75–2.42 | no — needs the lead-acid model |
Two things the source code says that the parameter-set list does not. Ramadass2004 is described in its own docstring as “a bit of a Frankenstein parameter set” that “should be used with caution”. And for OKane2022: “This parameter set does not claim to be representative of the true parameter values”. Both notes matter below.
Chayambuka2022 is a sodium-ion set, and it runs inside pybamm.lithium_ion.DFN() without complaint because the porous-electrode equations have the same form. PyBaMM has a separate pybamm.sodium_ion module; if sodium-ion is what you are studying, use that rather than relying on the lithium-ion model happening to accept the numbers.
2. Silent failure one: “1C” is not one hour
PyBaMM converts a C-rate to a current using "Nominal cell capacity [A.h]". That is a single number typed into the parameter set. The capacity the cell actually holds comes from something else entirely — the electrode loadings and stoichiometry windows. Nothing forces the two to agree.
I discharged every set from full to its own lower cut-off at C/20 (close to equilibrium) and at 1C:
| Set | Nominal (A·h) | Actual at C/20 (A·h) | Difference | “1C” lasts |
|---|---|---|---|---|
| Ramadass2004 | 1.000 | 1.775 | +77.5% | 106.0 min |
| Marquis2019 (default) | 0.681 | 0.872 | +28.1% | 75.3 min |
| NCA_Kim2011 | 0.430 | 0.483 | +12.3% | 66.4 min |
| Ecker2015 | 0.156 | 0.171 | +9.4% | 65.3 min |
| Ai2020 | 2.280 | 2.463 | +8.0% | 63.5 min |
| Chen2020 | 5.000 | 5.144 | +2.9% | 59.9 min |
| OKane2022 | 5.000 | 5.100 | +2.0% | 59.5 min |
| ORegan2022 | 5.000 | 5.010 | +0.2% | 53.1 min |
| Mohtat2020 | 5.000 | 4.961 | −0.8% | 58.0 min |
| Prada2013 | 2.300 | 2.285 | −0.7% | 50.6 min |
The default parameter set — the one you get from pybamm.lithium_ion.DFN().default_parameter_values — holds 28% more charge than its nominal capacity. Anyone who runs “1C” on the defaults is really running about 0.78C, and a “1C” discharge takes 75 minutes. Rate-capability plots, heat-generation estimates and anything normalised by C-rate are shifted by that factor.
The fix is to measure the capacity once and write it back, so that C-rates mean what you think they mean. For Marquis2019 this brings the 1C discharge from 75.3 to 58.5 minutes (the remaining gap is polarisation, which is expected at 1C):
pv = pybamm.ParameterValues("Marquis2019")
q_real = audit("Marquis2019")["actual_capacity_Ah"] # 0.8717, see section 6
pv.update({"Nominal cell capacity [A.h]": q_real})
Alternatively, specify currents in amps in the experiment and leave C-rates out of it — but give slow steps an explicit duration such as "for 60 hours or until 2.5 V". A step in amps without one is cut off at 24 hours with no exception; the details are in PyBaMM experiments that finish and are wrong.
3. Silent failure two: temperature does almost nothing in Chen2020
Chen2020 is the parameter set most people reach for. It is an LG M50 cell, the same cell as OKane2022 and ORegan2022. So I ran the same 1C discharge on all three at 25°C and at −10°C:
| LG M50 set | 1C at 25°C | 1C at −10°C | Change |
|---|---|---|---|
| Chen2020 | 4.992 A·h | 4.961 A·h | −0.6% |
| OKane2022 | 4.955 A·h | 3.191 A·h | −35.6% |
| ORegan2022 | 4.422 A·h | 1.922 A·h | −56.5% |
Same cell, same temperature, same current: 4.96, 3.19 and 1.92 A·h. A cold-weather study built on Chen2020 will report that the cold barely matters, and nothing in PyBaMM will tell you otherwise.
The reason is visible if you evaluate every temperature-capable parameter at 298.15 K and 263.15 K and see which values move:
| Parameter | Chen2020 | OKane2022 |
|---|---|---|
| Negative exchange-current density | −84.7% | −84.7% |
| Positive exchange-current density | −61.5% | −61.5% |
| Electrolyte diffusivity | unchanged | −59.8% |
| Electrolyte conductivity | unchanged | −59.8% |
| Negative particle diffusivity | constant, not a function | −80.3% |
| Positive particle diffusivity | constant, not a function | −73.9% |
In Chen2020 the electrolyte diffusivity and conductivity functions take T as an argument and then do not use it. The solid diffusivities are plain numbers. Only the reaction kinetics have an Arrhenius term — and at 1C, capacity is limited by transport, not by kinetics. So the one thing that changes is the one thing that barely matters for this test.
Ai2020 (−0.5%) and Prada2013 (−0.3%) behave the same way. For any study where temperature is a variable, check this first; OKane2022 and ORegan2022 are the LG M50 sets that actually carry the temperature dependence.
The same gap shows up when the cell heats itself. With the lumped thermal model on, Chen2020 warms by 58 K during a 3C discharge and gains 4% capacity; OKane2022, same cell, gains 114%. That and the other thermal pitfalls are measured in PyBaMM thermal model pitfalls.
A caution before you “fix” it by mixing sets
ORegan2022’s electrolyte transference number comes from the Landesfeind & Gasteiger (2019) fit, and it changes sign in the cold. At 1 mol/L it is 0.221 at 25°C, crosses zero near 10°C, and reaches −0.383 at −10°C. Chen2020 and OKane2022 use a constant 0.2594.
Negative transference numbers are not impossible in concentrated-solution theory, so this is not by itself an error. To see how much it drives the cold result, I held it fixed while everything else stayed cold:
| ORegan2022 at −10°C | 1C capacity | Loss vs 25°C |
|---|---|---|
| As shipped | 1.922 A·h | 56.5% |
| Transference number frozen at its 25°C function | 2.665 A·h | 39.7% |
| Transference number replaced with Chen2020’s 0.2594 | 1.249 A·h | 71.7% |
The temperature dependence of the transference number accounts for about 30% of the cold loss. But swapping in a “sensible” constant from another set made the result worse, not better, because the transference number, thermodynamic factor and conductivity in ORegan2022 were fitted together. Replacing one of them breaks the consistency between them. That is exactly what Ramadass2004’s docstring warns about when it calls itself a Frankenstein set.
4. Silent failure three: your experiment string overrides the cell’s limits
A common pattern is to write one cycling protocol and loop it over several parameter sets:
["Discharge at 1C until 2.5 V", "Rest for 1 hour",
"Charge at C/2 until 4.2 V", "Hold at 4.2 V until C/50"]
This ran to completion on all 11 sets. That includes Prada2013, an LFP cell whose own upper cut-off is 3.6 V, charged to 4.2 V, and Marquis2019, whose lower cut-off is 3.105 V, discharged to 2.5 V. I captured every warning emitted during both runs: zero.
The voltages in an experiment string replace the parameter set’s cut-offs rather than being checked against them. During the LFP overcharge the positive-particle surface stoichiometry fell to 0.0002 — the electrode was driven to the very edge of its open-circuit-potential curve. The solver was happy; the physics had long since stopped meaning anything.
Build the protocol from the parameter set instead of typing the numbers:
def cycle_within_window(pv, discharge="1C", charge="C/2"):
lo = pv["Lower voltage cut-off [V]"]
hi = pv["Upper voltage cut-off [V]"]
return pybamm.Experiment([
f"Discharge at {discharge} until {lo} V",
"Rest for 1 hour",
f"Charge at {charge} until {hi} V",
f"Hold at {hi} V until C/50",
])
With this, the same Prada2013 run stays between 2.000 and 3.600 V.
5. Silent failure four: SEI runs everywhere, but it is not your cell’s SEI
I built a DFN with each common degradation and thermal option on each of the 11 sets. Missing parameters raise a KeyError naming the parameter, which is the good kind of failure:
| Set | SEI (solvent-diffusion) | SEI (EC reaction) | Lithium plating | Particle cracking | Stress-driven LAM | Lumped thermal |
|---|---|---|---|---|---|---|
| OKane2022 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Ecker2015 | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |
| Ai2020 | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| Chen2020 | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ |
| Marquis2019 | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ |
| Mohtat2020 | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ |
| NCA_Kim2011 | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ |
| Ramadass2004 | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| ORegan2022 | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Prada2013 | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Chayambuka2022 | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
OKane2022 is the only set that supports every option, which is why it is the usual starting point for degradation work. The missing parameters are what you would expect: Exchange-current density for stripping [A.m-2] for plating, Negative electrode initial crack length [m] for cracking, Initial SEI thickness [m] for SEI on the three sets that lack it.
The quiet trap is the SEI column. Eight sets build with SEI — but the docstrings of Chen2020, Marquis2019, Mohtat2020 and Ai2020 all say the same thing: “SEI parameters are example parameters for SEI growth from the papers Ramadass2004, Ploehn2004…”. The model runs and produces a fade curve. It is a fade curve for generic SEI numbers borrowed from other cells, not one fitted to the cell named in the set. If you are going to compare capacity-fade rates against data, that distinction is the whole study.
The same applies to OKane2022: its SEI rate constants are Chen2020’s generic example values, and only its activation energy and lithium-per-SEI ratio differ. What that does to the fade curve is measured in PyBaMM SEI and lithium plating pitfalls.
6. A check you can run before trusting a set
This covers the two silent failures that need a simulation to detect. It takes a few seconds per set:
import pybamm
def audit(name, T_cold=263.15):
pv = pybamm.ParameterValues(name)
q_nom = pv["Nominal cell capacity [A.h]"]
v_lo = pv["Lower voltage cut-off [V]"]
v_hi = pv["Upper voltage cut-off [V]"]
def capacity(rate, T=None):
p = pybamm.ParameterValues(name)
if T is not None:
p.update({"Ambient temperature [K]": T, "Initial temperature [K]": T})
sim = pybamm.Simulation(
pybamm.lithium_ion.DFN(),
parameter_values=p,
experiment=pybamm.Experiment([f"Discharge at {rate} until {v_lo} V"]),
)
return float(sim.solve(initial_soc=1.0)["Discharge capacity [A.h]"].entries[-1])
q_slow = capacity("C/20")
q_warm = capacity("1C")
q_cold = capacity("1C", T=T_cold)
return {
"nominal_vs_actual_%": round(100 * (q_slow / q_nom - 1), 1),
"capacity_change_at_T_cold_%": round(100 * (q_cold / q_warm - 1), 1),
"voltage_window_V": (v_lo, v_hi),
"actual_capacity_Ah": round(q_slow, 4),
}
Marquis2019 {'nominal_vs_actual_%': 28.1, 'capacity_change_at_T_cold_%': -26.6, 'voltage_window_V': (3.105, 4.1), ...}
Chen2020 {'nominal_vs_actual_%': 2.9, 'capacity_change_at_T_cold_%': -0.6, 'voltage_window_V': (2.5, 4.2), ...}
OKane2022 {'nominal_vs_actual_%': 2.0, 'capacity_change_at_T_cold_%': -35.6, 'voltage_window_V': (2.5, 4.2), ...}
Read it as three questions. Is the nominal capacity within a few percent of the real one, or do your C-rates need rescaling? Does the result move with temperature at all? And what voltage window must your experiment strings respect?
For the degradation question, read the set’s docstring — help(pybamm.parameter_sets["Chen2020"]) — before reading the fade curve. The words “example parameters” are the difference between a result and an illustration.
7. How this was measured
- PyBaMM 26.5.0, Python 3.13, Apple M3. Default solver (
IDAKLUSolver),pybamm.lithium_ion.DFN()with default options unless stated. - Every discharge starts from
initial_soc=1.0and ends at the set’s ownLower voltage cut-off [V]. “Actual capacity” is the C/20 discharge capacity. - Cold runs set both
Ambient temperature [K]andInitial temperature [K]to 263.15 K in the default isothermal model. - Option compatibility means
Simulation.build()succeeded; it does not mean the resulting degradation rates are physically calibrated. - Each set ran in its own process with a timeout, so a hang in one could not contaminate the others. None hung.
References
- PyBaMM ParameterValues API
- PyBaMM: simulating long experiments
- Chen et al. (2020), Development of experimental techniques for parameterization of multi-scale lithium-ion battery models, J. Electrochem. Soc.
- O’Kane et al. (2022), Lithium-ion battery degradation: how to model it, Phys. Chem. Chem. Phys.
- Landesfeind & Gasteiger (2019), Temperature and concentration dependence of the ionic transport properties of lithium-ion battery electrolytes, J. Electrochem. Soc.
Related: fitting PyBaMM parameters and identifiability, on what to do once a built-in set is not good enough; PyBaMM solver convergence failures, for when a set runs but the solver will not; PyBaMM architecture, on choosing between SPM, SPMe and DFN; and PyBaMM thermal model pitfalls, on the thermal parameters these sets carry.
