PyBaMM’s Built-in Parameter Sets, Measured: Four Ways They Fail Silently
PyBaMM’s Built-in Parameter Sets, Measured: Four Ways They Fail Silently
Search
Ask the AI

PyBaMM’s Built-in Parameter Sets, Measured: Four Ways They Fail Silently

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.0 and ends at the set’s own Lower voltage cut-off [V]. “Actual capacity” is the C/20 discharge capacity.
  • Cold runs set both Ambient temperature [K] and Initial 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

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.

Leave a Reply

Scroll down