From dd0c6df5b445d749a0605fc927408973a7d3b38f Mon Sep 17 00:00:00 2001 From: ChenYi-Nnergy Date: Mon, 12 Oct 2026 11:06:38 +0800 Subject: [PATCH 1/5] Include contact resistance heating in lumped total heating --- .../src/pybamm/models/submodels/thermal/lumped.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/pybamm/src/pybamm/models/submodels/thermal/lumped.py b/packages/pybamm/src/pybamm/models/submodels/thermal/lumped.py index a0305e23cf..6c6b5002b1 100644 --- a/packages/pybamm/src/pybamm/models/submodels/thermal/lumped.py +++ b/packages/pybamm/src/pybamm/models/submodels/thermal/lumped.py @@ -73,20 +73,28 @@ def get_coupled_variables(self, variables): # Contact resistance "Lumped contact resistance heating [W.m-3]": Q_cr_vol_av, "Lumped contact resistance heating [W]": Q_cr_W, + # Contact resistance heating contributes to the total heating + # (it is included in the temperature equation in `set_rhs`) + "Total heating [W]": variables["Total heating [W]"] + Q_cr_W, + "Volume-averaged total heating [W.m-3]": variables[ + "Volume-averaged total heating [W.m-3]" + ] + + Q_cr_vol_av, } ) return variables def set_rhs(self, variables): T_vol_av = variables["Volume-averaged cell temperature [K]"] + # "Volume-averaged total heating [W.m-3]" already includes contact + # resistance heating (see `get_coupled_variables`) Q_vol_av = variables["Volume-averaged total heating [W.m-3]"] Q_cool_vol_av = variables["Surface total cooling [W.m-3]"] - Q_cr_vol_av = variables["Lumped contact resistance heating [W.m-3]"] rho_c_p_eff_av = variables[ "Volume-averaged effective heat capacity [J.K-1.m-3]" ] - self.rhs = {T_vol_av: (Q_vol_av + Q_cr_vol_av + Q_cool_vol_av) / rho_c_p_eff_av} + self.rhs = {T_vol_av: (Q_vol_av + Q_cool_vol_av) / rho_c_p_eff_av} def set_initial_conditions(self, variables): T_vol_av = variables["Volume-averaged cell temperature [K]"] From dc9fe75aae78994e71666db3ac7a72dd6895dbf3 Mon Sep 17 00:00:00 2001 From: ChenYi-Nnergy Date: Mon, 12 Oct 2026 11:18:38 +0800 Subject: [PATCH 2/5] Add CHANGELOG entry for #5890 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f8ccc197..b0cc8bbe85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) +## Bug fixes + +- The lumped thermal model's "Total heating [W]" and "Volume-averaged total heating [W.m-3]" now include the contact resistance heating, which was already part of the temperature equation. ([#5890](https://github.com/pybamm-team/PyBaMM/pull/5890)) + # [v26.10.0.0](https://github.com/pybamm-team/PyBaMM/tree/pybamm-v26.10.0.0) - 2026-10-09 ## Breaking changes From bb3e0bc2bb940b6534daa72f30e24825d4ff584a Mon Sep 17 00:00:00 2001 From: ChenYi-Nnergy Date: Mon, 12 Oct 2026 15:08:01 +0800 Subject: [PATCH 3/5] Move contact resistance heating to BaseThermal totals Added contact resistance heating calculation to total heating. --- .../models/submodels/thermal/base_thermal.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py b/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py index fa5e0b590f..4fa5548940 100644 --- a/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py +++ b/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py @@ -244,6 +244,20 @@ def _get_standard_coupled_variables(self, variables): Q = Q_ohm + Q_rxn + Q_rev + Q_mix + Q_hys # Compute the X-average over the entire cell, including current collectors + # Contact resistance heating (I^2*R), added here so every thermal + # model includes it in the total heating variables + if self.options["contact resistance"] == "true": + I = variables["Current [A]"] + T_vol_av = variables["Volume-averaged cell temperature [K]"] + Q_cr_W = I**2 * self.param.R_contact(T_vol_av) + Q_cr_vol_av = Q_cr_W / self.param.V_cell + Q_cr = pybamm.FullBroadcast( + Q_cr_vol_av, + ["negative electrode", "separator", "positive electrode"], + "current collector", + ) + Q = Q + Q_cr + # Note: this can still be a function of y and z for higher-dimensional pouch # cell models Q_ohm_av = self._x_average(Q_ohm, Q_ohm_s_cn, Q_ohm_s_cp) From ad52f1dba88763354c9f158aa88d1d41d3641d87 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:08:19 +0000 Subject: [PATCH 4/5] style: pre-commit fixes --- .../pybamm/src/pybamm/models/submodels/thermal/base_thermal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py b/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py index 4fa5548940..e107746801 100644 --- a/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py +++ b/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py @@ -244,7 +244,7 @@ def _get_standard_coupled_variables(self, variables): Q = Q_ohm + Q_rxn + Q_rev + Q_mix + Q_hys # Compute the X-average over the entire cell, including current collectors - # Contact resistance heating (I^2*R), added here so every thermal + # Contact resistance heating (I^2*R), added here so every thermal # model includes it in the total heating variables if self.options["contact resistance"] == "true": I = variables["Current [A]"] From 339dab976e486b217bc1551dc05cbd35aed6fe13 Mon Sep 17 00:00:00 2001 From: ChenYi-Nnergy Date: Sat, 17 Oct 2026 11:25:52 +0800 Subject: [PATCH 5/5] Address rtimms review: fix double count, volume normalisation, regression test --- CHANGELOG.md | 2 +- .../models/submodels/thermal/base_thermal.py | 16 ++--- .../pybamm/models/submodels/thermal/lumped.py | 9 +-- .../test_lithium_ion/test_thermal_models.py | 66 ++++++++++++++++++- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0cc8bbe85..3027411c7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Bug fixes -- The lumped thermal model's "Total heating [W]" and "Volume-averaged total heating [W.m-3]" now include the contact resistance heating, which was already part of the temperature equation. ([#5890](https://github.com/pybamm-team/PyBaMM/pull/5890)) +- Contact resistance heating is now included in the total heating of every thermal model, instead of only in the `lumped` temperature equation. ([#5890](https://github.com/pybamm-team/PyBaMM/pull/5890)) # [v26.10.0.0](https://github.com/pybamm-team/PyBaMM/tree/pybamm-v26.10.0.0) - 2026-10-09 diff --git a/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py b/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py index e107746801..dab4a730f2 100644 --- a/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py +++ b/packages/pybamm/src/pybamm/models/submodels/thermal/base_thermal.py @@ -243,21 +243,21 @@ def _get_standard_coupled_variables(self, variables): # Total heating Q = Q_ohm + Q_rxn + Q_rev + Q_mix + Q_hys - # Compute the X-average over the entire cell, including current collectors # Contact resistance heating (I^2*R), added here so every thermal # model includes it in the total heating variables if self.options["contact resistance"] == "true": I = variables["Current [A]"] T_vol_av = variables["Volume-averaged cell temperature [K]"] - Q_cr_W = I**2 * self.param.R_contact(T_vol_av) - Q_cr_vol_av = Q_cr_W / self.param.V_cell - Q_cr = pybamm.FullBroadcast( - Q_cr_vol_av, - ["negative electrode", "separator", "positive electrode"], - "current collector", + V_stack = ( + self.param.n_electrodes_parallel + * self.param.L_y + * self.param.L_z + * self.param.L_x ) - Q = Q + Q_cr + Q_cr = I**2 * self.param.R_contact(T_vol_av) / V_stack + Q = Q + pybamm.FullBroadcast(Q_cr, Q.domain, "current collector") + # Compute the X-average over the entire cell, including current collectors # Note: this can still be a function of y and z for higher-dimensional pouch # cell models Q_ohm_av = self._x_average(Q_ohm, Q_ohm_s_cn, Q_ohm_s_cp) diff --git a/packages/pybamm/src/pybamm/models/submodels/thermal/lumped.py b/packages/pybamm/src/pybamm/models/submodels/thermal/lumped.py index 6c6b5002b1..3c0e36381a 100644 --- a/packages/pybamm/src/pybamm/models/submodels/thermal/lumped.py +++ b/packages/pybamm/src/pybamm/models/submodels/thermal/lumped.py @@ -73,13 +73,6 @@ def get_coupled_variables(self, variables): # Contact resistance "Lumped contact resistance heating [W.m-3]": Q_cr_vol_av, "Lumped contact resistance heating [W]": Q_cr_W, - # Contact resistance heating contributes to the total heating - # (it is included in the temperature equation in `set_rhs`) - "Total heating [W]": variables["Total heating [W]"] + Q_cr_W, - "Volume-averaged total heating [W.m-3]": variables[ - "Volume-averaged total heating [W.m-3]" - ] - + Q_cr_vol_av, } ) return variables @@ -87,7 +80,7 @@ def get_coupled_variables(self, variables): def set_rhs(self, variables): T_vol_av = variables["Volume-averaged cell temperature [K]"] # "Volume-averaged total heating [W.m-3]" already includes contact - # resistance heating (see `get_coupled_variables`) + # resistance heating (added in BaseThermal) Q_vol_av = variables["Volume-averaged total heating [W.m-3]"] Q_cool_vol_av = variables["Surface total cooling [W.m-3]"] rho_c_p_eff_av = variables[ diff --git a/packages/pybamm/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py b/packages/pybamm/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py index 7f2609c4e8..d686d3ddd8 100644 --- a/packages/pybamm/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py +++ b/packages/pybamm/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py @@ -212,8 +212,10 @@ def R_contact(T): ) sol_fn = sim_fn.solve([0, 3600]) - dphi_const = sol_const["Contact overpotential [V]"].entries - dphi_fn = sol_fn["Contact overpotential [V]"].entries + # Compare on a common time grid: the two solves take different steps + t = np.linspace(0, 3600, 100) + dphi_const = sol_const["Contact overpotential [V]"](t) + dphi_fn = sol_fn["Contact overpotential [V]"](t) if thermal == "isothermal": # temperature is fixed at the reference, so the two agree @@ -222,3 +224,63 @@ def R_contact(T): # cell heats up above the reference, so R (and hence the contact # overpotential magnitude) is larger than the constant case np.testing.assert_array_less(np.abs(dphi_const[1:]), np.abs(dphi_fn[1:])) + + @pytest.mark.parametrize( + "thermal, extra_options", + [ + ("lumped", {}), + ("x-full", {}), + ( + "x-lumped", + {"current collector": "potential pair", "dimensionality": 1}, + ), + ( + "x-lumped", + {"current collector": "potential pair", "dimensionality": 2}, + ), + ], + ) + def test_contact_resistance_energy_balance(self, thermal, extra_options): + # With the temperature held at ambient, the extra "Total heating [W]" + # when the contact resistance is switched on must equal the electrical + # energy dissipated in the contact resistance + R_c = 0.05 + t_end = 600 + t = np.linspace(0, t_end, 61) + + parameter_values = pybamm.ParameterValues("NCA_Kim2011") + parameter_values.update( + { + "Current function [A]": 1, + # Hold the temperature at ambient for every thermal option + "Total heat transfer coefficient [W.m-2.K-1]": 1e6, + "Negative current collector surface heat transfer coefficient [W.m-2.K-1]": 1e6, + "Positive current collector surface heat transfer coefficient [W.m-2.K-1]": 1e6, + "Negative tab heat transfer coefficient [W.m-2.K-1]": 1e6, + "Positive tab heat transfer coefficient [W.m-2.K-1]": 1e6, + "Edge heat transfer coefficient [W.m-2.K-1]": 1e6, + } + ) + + heating, current = {}, {} + for R in [0, R_c]: + params = parameter_values.copy() + params.update({"Contact resistance [Ohm]": R}) + sim = pybamm.Simulation( + pybamm.lithium_ion.SPMe( + { + "thermal": thermal, + "contact resistance": "true", + **extra_options, + } + ), + parameter_values=params, + ) + sol = sim.solve([0, t_end]) + heating[R] = sol["Total heating [W]"](t) + current[R] = sol["Current [A]"](t) + + extra_heat = np.trapezoid(heating[R_c] - heating[0], t) + expected = np.trapezoid(current[R_c] ** 2 * R_c, t) + assert extra_heat > 0 + np.testing.assert_allclose(extra_heat, expected, rtol=1e-2)