diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a158f7d3f..841e8e72b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) +## Breaking changes + +- The composite electrode-SOH model's phase-capacity inputs are renamed `Q_n_1` -> `Primary: Negative electrode capacity [A.h]`, `Q_n_2` -> `Secondary: Negative electrode capacity [A.h]`, `Q_p_1` -> `Primary: Positive electrode capacity [A.h]`, and `Q_p_2` -> `Secondary: Positive electrode capacity [A.h]`. The numbered names matched the deprecated-MSMR pattern, so every composite solve emitted a `DeprecationWarning` for an internal input. Code solving a standalone `ElectrodeSOHComposite` simulation must use the new names. ([#5738](https://github.com/pybamm-team/PyBaMM/pull/5738)) + ## Bug fixes - The `integration` nox session no longer installs the `pydiffsol` extra on macOS Intel CI runners, where it has no working build. ([#5726](https://github.com/pybamm-team/PyBaMM/pull/5726)) diff --git a/packages/pybamm/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_composite.py b/packages/pybamm/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_composite.py index 2eb9d7fa9b..86e76f8a4e 100644 --- a/packages/pybamm/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_composite.py +++ b/packages/pybamm/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_composite.py @@ -133,6 +133,16 @@ def _get_prefix(electrode): return "x" +def _get_phase_capacity_name(electrode, phase): + """Get the input name for an electrode phase capacity.""" + return f"{phase.capitalize()}: {electrode.capitalize()} electrode capacity [A.h]" + + +def _get_phase_capacity_input(electrode, phase): + """Get the input parameter for an electrode phase capacity.""" + return pybamm.InputParameter(_get_phase_capacity_name(electrode, phase)) + + def _get_electrode_capacity_equation(options, electrode): """ Build equation for electrode capacity in composite electrodes. @@ -140,18 +150,17 @@ def _get_electrode_capacity_equation(options, electrode): Returns Q = sum_i Q_i * (stoich_100_i - stoich_0_i) for all phases. """ prefix = _get_prefix(electrode) - e = electrode[0] i_am_composite = check_if_composite(options, electrode) stoich_variables = _get_stoich_variables(options) direction = _get_direction(electrode) - Q_1 = pybamm.InputParameter(f"Q_{e}_1") + Q_1 = _get_phase_capacity_input(electrode, "primary") Q = ( direction * (stoich_variables[f"{prefix}_100_1"] - stoich_variables[f"{prefix}_0_1"]) * Q_1 ) if i_am_composite: - Q_2 = pybamm.InputParameter(f"Q_{e}_2") + Q_2 = _get_phase_capacity_input(electrode, "secondary") Q += ( direction * (stoich_variables[f"{prefix}_100_2"] - stoich_variables[f"{prefix}_0_2"]) @@ -168,20 +177,20 @@ def _get_cyclable_lithium_equation(options, soc="100"): """ x_soc_1 = pybamm.Variable(f"x_{soc}_1", bounds=(0, 1)) y_soc_1 = pybamm.Variable(f"y_{soc}_1", bounds=(0, 1)) - Q_n_1 = pybamm.InputParameter("Q_n_1") - Q_p_1 = pybamm.InputParameter("Q_p_1") - lithium_primary_phases = Q_n_1 * x_soc_1 + Q_p_1 * y_soc_1 + Q_n_prim = _get_phase_capacity_input("negative", "primary") + Q_p_prim = _get_phase_capacity_input("positive", "primary") + lithium_primary_phases = Q_n_prim * x_soc_1 + Q_p_prim * y_soc_1 lithium_secondary_phases = 0.0 is_positive_composite = check_if_composite(options, "positive") is_negative_composite = check_if_composite(options, "negative") if is_positive_composite: - Q_p_2 = pybamm.InputParameter("Q_p_2") + Q_p_sec = _get_phase_capacity_input("positive", "secondary") y_soc_2 = pybamm.Variable(f"y_{soc}_2", bounds=(0, 1)) - lithium_secondary_phases += Q_p_2 * y_soc_2 + lithium_secondary_phases += Q_p_sec * y_soc_2 if is_negative_composite: - Q_n_2 = pybamm.InputParameter("Q_n_2") + Q_n_sec = _get_phase_capacity_input("negative", "secondary") x_soc_2 = pybamm.Variable(f"x_{soc}_2", bounds=(0, 1)) - lithium_secondary_phases += Q_n_2 * x_soc_2 + lithium_secondary_phases += Q_n_sec * x_soc_2 return lithium_primary_phases + lithium_secondary_phases @@ -470,18 +479,26 @@ def __init__( ) elif initialization_method == "SOC": soc_init = pybamm.InputParameter("SOC_init") - negative_soc = x_init_1 * pybamm.InputParameter("Q_n_1") + negative_soc = x_init_1 * _get_phase_capacity_input("negative", "primary") if is_negative_composite: x_init_2 = variables["x_init_2"] - negative_soc += x_init_2 * pybamm.InputParameter("Q_n_2") + negative_soc += x_init_2 * _get_phase_capacity_input( + "negative", "secondary" + ) - negative_0_soc = x_0_1 * pybamm.InputParameter("Q_n_1") + negative_0_soc = x_0_1 * _get_phase_capacity_input("negative", "primary") if is_negative_composite: - negative_0_soc += x_0_2 * pybamm.InputParameter("Q_n_2") + negative_0_soc += x_0_2 * _get_phase_capacity_input( + "negative", "secondary" + ) - negative_100_soc = x_100_1 * pybamm.InputParameter("Q_n_1") + negative_100_soc = x_100_1 * _get_phase_capacity_input( + "negative", "primary" + ) if is_negative_composite: - negative_100_soc += x_100_2 * pybamm.InputParameter("Q_n_2") + negative_100_soc += x_100_2 * _get_phase_capacity_input( + "negative", "secondary" + ) self.algebraic[x_init_1] = ( (negative_soc - negative_0_soc) / (negative_100_soc - negative_0_soc) ) - soc_init @@ -584,15 +601,18 @@ def solve_split( is_positive_composite = check_if_composite(options, "positive") is_negative_composite = check_if_composite(options, "negative") - Q_n_1 = parameter_values.evaluate(param.n.prim.Q_init, inputs=inputs) - Q_p_1 = parameter_values.evaluate(param.p.prim.Q_init, inputs=inputs) - Qs = {"Q_n_1": Q_n_1, "Q_p_1": Q_p_1} + Q_n_prim = parameter_values.evaluate(param.n.prim.Q_init, inputs=inputs) + Q_p_prim = parameter_values.evaluate(param.p.prim.Q_init, inputs=inputs) + Qs = { + _get_phase_capacity_name("negative", "primary"): Q_n_prim, + _get_phase_capacity_name("positive", "primary"): Q_p_prim, + } if is_positive_composite: - Q_p_2 = parameter_values.evaluate(param.p.sec.Q_init, inputs=inputs) - Qs["Q_p_2"] = Q_p_2 + Q_p_sec = parameter_values.evaluate(param.p.sec.Q_init, inputs=inputs) + Qs[_get_phase_capacity_name("positive", "secondary")] = Q_p_sec if is_negative_composite: - Q_n_2 = parameter_values.evaluate(param.n.sec.Q_init, inputs=inputs) - Qs["Q_n_2"] = Q_n_2 + Q_n_sec = parameter_values.evaluate(param.n.sec.Q_init, inputs=inputs) + Qs[_get_phase_capacity_name("negative", "secondary")] = Q_n_sec Q_Li = parameter_values.evaluate(param.Q_Li_particles_init, inputs=inputs) @@ -621,8 +641,12 @@ def solve_split( f"{initial_value!r} of type {type(initial_value).__name__}" ) - Q_n_total = Q_n_1 + (Qs.get("Q_n_2", 0)) - Q_p_total = Q_p_1 + (Qs.get("Q_p_2", 0)) + Q_n_total = Q_n_prim + ( + Qs.get(_get_phase_capacity_name("negative", "secondary"), 0) + ) + Q_p_total = Q_p_prim + ( + Qs.get(_get_phase_capacity_name("positive", "secondary"), 0) + ) primary_options = _get_primary_only_options(options) # _ElectrodeSOH uses get_equilibrium_direction internally for the equilibrium @@ -819,15 +843,18 @@ def solve_full( is_positive_composite = check_if_composite(options, "positive") is_negative_composite = check_if_composite(options, "negative") - Q_n_1 = parameter_values.evaluate(param.n.prim.Q_init, inputs=inputs) - Q_p_1 = parameter_values.evaluate(param.p.prim.Q_init, inputs=inputs) - Qs = {"Q_n_1": Q_n_1, "Q_p_1": Q_p_1} + Q_n_prim = parameter_values.evaluate(param.n.prim.Q_init, inputs=inputs) + Q_p_prim = parameter_values.evaluate(param.p.prim.Q_init, inputs=inputs) + Qs = { + _get_phase_capacity_name("negative", "primary"): Q_n_prim, + _get_phase_capacity_name("positive", "primary"): Q_p_prim, + } if is_positive_composite: - Q_p_2 = parameter_values.evaluate(param.p.sec.Q_init, inputs=inputs) - Qs["Q_p_2"] = Q_p_2 + Q_p_sec = parameter_values.evaluate(param.p.sec.Q_init, inputs=inputs) + Qs[_get_phase_capacity_name("positive", "secondary")] = Q_p_sec if is_negative_composite: - Q_n_2 = parameter_values.evaluate(param.n.sec.Q_init, inputs=inputs) - Qs["Q_n_2"] = Q_n_2 + Q_n_sec = parameter_values.evaluate(param.n.sec.Q_init, inputs=inputs) + Qs[_get_phase_capacity_name("negative", "secondary")] = Q_n_sec Q_Li = parameter_values.evaluate(param.Q_Li_particles_init, inputs=inputs) diff --git a/packages/pybamm/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py b/packages/pybamm/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py index 460a11179a..1ddc3ee59f 100644 --- a/packages/pybamm/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py +++ b/packages/pybamm/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py @@ -3,6 +3,7 @@ # import contextlib +import warnings import pytest @@ -552,6 +553,41 @@ def test_chen2020_composite_solve_with_hysteresis(self): f"Stoichiometry {key} out of bounds: {results[key]}" ) + def test_phase_capacity_input_names(self): + options = {"particle phases": ("2", "1")} + simulation = pybamm.Simulation( + pybamm.lithium_ion.ElectrodeSOHComposite(options), + parameter_values=pybamm.ParameterValues("Chen2020_composite"), + ) + simulation.build() + + capacity_names = { + parameter.name + for parameter in simulation.built_model.input_parameters + if parameter.name.endswith("electrode capacity [A.h]") + } + + assert capacity_names == { + "Primary: Negative electrode capacity [A.h]", + "Secondary: Negative electrode capacity [A.h]", + "Primary: Positive electrode capacity [A.h]", + } + + def test_phase_capacity_inputs_are_not_deprecated_msmr_names(self): + pvals = pybamm.ParameterValues("Chen2020_composite") + options = {"particle phases": ("2", "1")} + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + pybamm.lithium_ion.get_initial_stoichiometries_composite( + 0.5, pvals, options=options + ) + + renamed = [ + w for w in caught if "host site occupancy capacity" in str(w.message) + ] + assert renamed == [] + class TestElectrodeSOHMSMR: def test_known_solution(self, options):