Skip to content

Make LAM/crack degradation submodels well-posed under 1+1D current collectors (#5296) - #5543

Open
gaoflow wants to merge 1 commit into
pybamm-team:mainfrom
gaoflow:fix/lam-lli-yz-average-dimensionality-1
Open

Make LAM/crack degradation submodels well-posed under 1+1D current collectors (#5296)#5543
gaoflow wants to merge 1 commit into
pybamm-team:mainfrom
gaoflow:fix/lam-lli-yz-average-dimensionality-1

Conversation

@gaoflow

@gaoflow gaoflow commented May 28, 2026

Copy link
Copy Markdown

Description

Fixes #5296.

Discretising any lithium_ion model with "dimensionality": 1 plus the degradation stack ("loss of active material": "stress-driven", "particle mechanics": "swelling and cracking", …) fails with

ModelError: rhs and initial conditions must have the same shape after
discretisation but rhs.shape = (10, 1) and initial_conditions.shape = (1, 1)
for variable 'Loss of lithium due to loss of active material in negative electrode [mol]'.

Two independent shape gaps are responsible, both activated only when the current-collector mesh has more than one node.

Root cause

(a) submodels/active_material/loss_active_material.py:192 — the LAM loss-of-lithium accumulator is declared as a scalar (pybamm.Variable(...) with no domain, IC pybamm.Scalar(0)). Its rhs was

-V * pybamm.x_average(c_s_rav * deps_solid_dt)

x_average averages through the cell thickness but keeps the current-collector axis. For "dimensionality": 1 the rhs is (z_n, 1) while the IC is (1, 1).

(b) submodels/particle_mechanics/crack_propagation.py:175 — for DFN (x_average is False) the crack-length state is declared

pybamm.Variable(
    "<Domain> particle crack length [m]",
    domain="<dom> electrode",
    auxiliary_domains={"secondary": "current collector"},
)

but its IC was pybamm.PrimaryBroadcast(l_cr_0, "<dom> electrode"), which only broadcasts across the electrode mesh. Under "dimensionality": 1 the rhs is (x_n, z_n) while the IC is (x_n, 1). PyBaMM's discretisation check raises:

ModelError: ... rhs.shape = (40, 1) and initial_conditions.shape = (5, 1)
for variable 'Negative particle crack length [m]'.

The SPMe reproducer in the issue hits (a) first; DFN with the same options additionally hits (b).

Fix

Two surgical changes.

  • In loss_active_material.py:192 wrap the existing x_average in pybamm.yz_average so the rhs collapses to a scalar in 1+1D and 2+1D current collectors. yz_average is the identity on 0-D current collectors, so 0-D models are unaffected. Total lithium loss is preserved because V = L * A_cc already carries L_y * L_z (see interface/lithium_plating/base_plating.py:87 and interface/sei/base_sei.py:132 for the same pattern on plating and SEI accumulators).
  • In crack_propagation.py:175 replace PrimaryBroadcast(l_cr_0, "<dom> electrode") with FullBroadcast(l_cr_0, "<dom> electrode", "current collector") so the IC spans both the electrode and the current-collector mesh. FullBroadcast is the identity on 0-D current collectors.

Reproducer

Exact options from the issue (only the Simulation.solve t_eval was shortened to [0, 600] to keep CI fast):

import pybamm
param = pybamm.ParameterValues("OKane2022")
L_y = param["Electrode width [m]"]
L_z = param["Electrode height [m]"]
options = {
    "current collector": "potential pair",
    "dimensionality": 1,
    "thermal": "x-lumped",
    "SEI": "solvent-diffusion limited",
    "SEI porosity change": "true",
    "lithium plating": "partially reversible",
    "lithium plating porosity change": "true",
    "particle mechanics": ("swelling and cracking", "swelling only"),
    "SEI on cracks": "true",
    "loss of active material": "stress-driven",
}
model = pybamm.lithium_ion.SPMe(options=options)
# … thermal/tab parameter overrides per the issue body …
sim = pybamm.Simulation(model, parameter_values=param,
                        var_pts={"x_n": 5, "x_s": 5, "x_p": 5, "r_n": 30, "r_p": 30, "z": 10},
                        solver=pybamm.IDAKLUSolver())
sim.solve([0, 600])  # ModelError on main, OK on this branch

Verification

  • The exact issue reproducer (SPMe + dimensionality=1 + full degradation stack) now solves end-to-end. Loss of lithium due to loss of active material in negative electrode [mol] evolves to ~3.0e-7 mol after 600 s.
  • DFN and SPM with the same options also now solve (their LLI_LAM_n[-1] matches SPMe within solver tolerance: ~3.0e-7 mol).
  • 0-D regression (SPM/SPMe/DFN with the same degradation options but no high-dim current collector) produces totals indistinguishable from main (~3.0e-7 mol after 600 s).
  • pytest tests/unit/test_models/test_full_battery_models/test_lithium_ion/ — 637 passed, 12 skipped (+4 new regression tests).
  • ruff check + ruff format --check clean.

Regression test

New tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_lam_lli_dimensionality_5296.py pins four guards:

  1. SPMe + dimensionality=1 + full degradation stack discretises and solves (locks fix (a)).
  2. Direct shape guard: after Discretisation every LAM-LLI variable's rhs must match its IC shape (catches future regressions in either direction).
  3. DFN + dimensionality=1 + full degradation stack discretises and solves (locks fix (b)).
  4. 0-D non-regression: stress-driven LAM with the standard current collector still produces a strictly positive but bounded total lithium loss.

Checklist

  • No style issues (ruff check, ruff format --check)
  • All tests passed
  • CHANGELOG.md updated (Bug fixes / Unreleased)
  • Updated docs if needed — n/a, no public-API change
  • Issue reference in the PR description and the commit message

@gaoflow
gaoflow requested a review from a team as a code owner May 28, 2026 15:34
@gaoflow
gaoflow force-pushed the fix/lam-lli-yz-average-dimensionality-1 branch 2 times, most recently from 1c34da3 to d67aaec Compare June 9, 2026 11:09
@gaoflow
gaoflow force-pushed the fix/lam-lli-yz-average-dimensionality-1 branch from d67aaec to 5f32880 Compare June 12, 2026 15:22
…llectors (pybamm-team#5296)

Fixes pybamm-team#5296.

Discretising any `lithium_ion` model with `"dimensionality": 1` plus
the degradation stack (`"loss of active material": "stress-driven"`,
`"particle mechanics": "swelling and cracking"`, etc.) failed with::

    ModelError: rhs and initial conditions must have the same shape
    after discretisation but rhs.shape = (10, 1) and
    initial_conditions.shape = (1, 1) for variable 'Loss of lithium
    due to loss of active material in negative electrode [mol]'.

Two independent shape gaps were responsible, both activated only when
the current-collector mesh has more than one node.

1. `submodels/active_material/loss_active_material.py:192` — the LAM
   loss-of-lithium accumulator is declared as a scalar
   (`pybamm.Variable(...)` with no domain, IC `pybamm.Scalar(0)`).
   Its rhs was `-V * pybamm.x_average(c_s_rav * deps_solid_dt)` which
   averages through the cell thickness but keeps the
   current-collector axis, so the rhs shape was `(z_n, 1)` while the
   IC was `(1, 1)`. The fix wraps the existing `x_average` in
   `pybamm.yz_average`, collapsing to a scalar on 1-D current
   collectors (the identity operation) and to a true scalar on 1+1D
   and 2+1D current collectors. Total lithium loss is preserved
   because `V = L * A_cc` already carries `L_y * L_z`.

2. `submodels/particle_mechanics/crack_propagation.py:175` — for DFN
   (`x_average is False`) the crack-length state is declared
   `pybamm.Variable(..., domain="<dom> electrode",
   auxiliary_domains={"secondary": "current collector"})`, but its
   IC was `pybamm.PrimaryBroadcast(l_cr_0, "<dom> electrode")` which
   only broadcasts across the electrode mesh. Under
   `"dimensionality": 1` the rhs is `(x_n, z_n)` while the IC is
   `(x_n, 1)`. Switching to `pybamm.FullBroadcast(l_cr_0,
   "<dom> electrode", "current collector")` makes the IC span both
   axes; `FullBroadcast` is the identity on a 0-D current collector,
   so no existing model changes shape.

Verified end-to-end against the exact reproducer from the issue
(SPMe + OKane2022 + dimensionality=1 + full degradation stack) and the
equivalent DFN/SPM configurations, all of which now build, discretise
and solve. The 0-D model fleet (SPM/SPMe/DFN on OKane2022 with stress-
driven LAM and cracking) produces `Loss of lithium due to LAM` totals
indistinguishable from main within solver tolerance. The 637-test
lithium-ion unit suite passes (+4 new regression tests).

Regression coverage is in
`tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_lam_lli_dimensionality_5296.py`
and pins (a) the issue reproducer for SPMe and DFN, (b) a direct rhs-vs-IC
shape guard after `Discretisation`, and (c) the 0-D non-regression.
@gaoflow
gaoflow force-pushed the fix/lam-lli-yz-average-dimensionality-1 branch from 5f32880 to 4fb5a7e Compare June 18, 2026 22:20
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.

[Bug]: Degradation submodels fail to run with high dimensional thermal submodels

1 participant