From 23bebd06f4089df182e920dff03d5b543c8c0906 Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:16:28 +0100 Subject: [PATCH 1/3] Fixes #4176 electrode diffusivity rename (#4179) * fix: revert logic change for failing 'particle diffusivity' key, updt. check_parameter_values to rename depreciated key * fix: removes bpx remapping requirement, creates 'particle diffusivity' without destroying 'electrode diffusivity', updts tests to verify functionality * Add changelog * Apply suggestions from code review Co-authored-by: Ferran Brosa Planella Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --------- Co-authored-by: Ferran Brosa Planella Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- CHANGELOG.md | 1 + pybamm/parameters/bpx.py | 22 ++++------ pybamm/parameters/parameter_values.py | 7 +-- pybamm/util.py | 6 +-- tests/unit/test_parameters/test_bpx.py | 61 ++++++++++++++++++-------- tests/unit/test_util.py | 7 ++- 6 files changed, 64 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae4616a2d4..8dfb4a7a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ ## Bug Fixes +- Fixed bug where passing deprecated `electrode diffusivity` parameter resulted in a breaking change and/or the corresponding diffusivity parameter not updating. Improved the deprecated translation around BPX. ([#4176](https://github.com/pybamm-team/PyBaMM/pull/4176)) - Fixed a bug where a factor of electrode surface area to volume ratio is missing in the rhs of the LeadingOrderDifferential conductivity model ([#4139](https://github.com/pybamm-team/PyBaMM/pull/4139)) - Fixes the breaking changes caused by [#3624](https://github.com/pybamm-team/PyBaMM/pull/3624), specifically enables the deprecated parameter `electrode diffusivity` to be used by `ParameterValues.update({name:value})` and `Solver.solve(inputs={name:value})`. Fixes parameter translation from old name to new name, with corrected tests. ([#4072](https://github.com/pybamm-team/PyBaMM/pull/4072) - Set the `remove_independent_variables_from_rhs` to `False` by default, and moved the option from `Discretisation.process_model` to `Discretisation.__init__`. This fixes a bug related to the discharge capacity, but may make the simulation slower in some cases. To set the option to `True`, use `Simulation(..., discretisation_kwargs={"remove_independent_variables_from_rhs": True})`. ([#4020](https://github.com/pybamm-team/PyBaMM/pull/4020)) diff --git a/pybamm/parameters/bpx.py b/pybamm/parameters/bpx.py index 63cb3008d2..680477a74b 100644 --- a/pybamm/parameters/bpx.py +++ b/pybamm/parameters/bpx.py @@ -336,21 +336,16 @@ def _conductivity(c_e, T, Ea, sigma_ref, constant=False): 0.0, ) pybamm_dict[ - phase_domain_pre_name.replace("electrode", "particle") - + "diffusivity activation energy [J.mol-1]" + phase_domain_pre_name + "diffusivity activation energy [J.mol-1]" ] = Ea_D D_ref = pybamm_dict[phase_domain_pre_name + "diffusivity [m2.s-1]"] if callable(D_ref): - pybamm_dict[ - phase_domain_pre_name.replace("electrode", "particle") - + "diffusivity [m2.s-1]" - ] = partial(_diffusivity, D_ref=D_ref, Ea=Ea_D) + pybamm_dict[phase_domain_pre_name + "diffusivity [m2.s-1]"] = partial( + _diffusivity, D_ref=D_ref, Ea=Ea_D + ) elif isinstance(D_ref, tuple): - pybamm_dict[ - phase_domain_pre_name.replace("electrode", "particle") - + "diffusivity [m2.s-1]" - ] = partial( + pybamm_dict[phase_domain_pre_name + "diffusivity [m2.s-1]"] = partial( _diffusivity, D_ref=partial( _interpolant_func, name=D_ref[0], x=D_ref[1][0], y=D_ref[1][1] @@ -358,10 +353,9 @@ def _conductivity(c_e, T, Ea, sigma_ref, constant=False): Ea=Ea_D, ) else: - pybamm_dict[ - phase_domain_pre_name.replace("electrode", "particle") - + "diffusivity [m2.s-1]" - ] = partial(_diffusivity, D_ref=D_ref, Ea=Ea_D, constant=True) + pybamm_dict[phase_domain_pre_name + "diffusivity [m2.s-1]"] = partial( + _diffusivity, D_ref=D_ref, Ea=Ea_D, constant=True + ) # electrolyte Ea_D_e = pybamm_dict.get( diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index a223ff061f..c8011c735c 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -392,7 +392,7 @@ def set_initial_ocps( @staticmethod def check_parameter_values(values): - for param in values: + for param in list(values.keys()): if "propotional term" in param: raise ValueError( f"The parameter '{param}' has been renamed to " @@ -405,12 +405,13 @@ def check_parameter_values(values): f"parameter '{param}' has been renamed to " "'Thermodynamic factor'" ) if "electrode diffusivity" in param: + new_param = param.replace("electrode", "particle") warn( - f"The parameter '{param}' has been renamed to '{param.replace('electrode', 'particle')}'", + f"The parameter '{param}' has been renamed to '{new_param}'", DeprecationWarning, stacklevel=2, ) - param = param.replace("electrode", "particle") + values[new_param] = values.get(param) return values diff --git a/pybamm/util.py b/pybamm/util.py index 69066dcb13..0fe02c835b 100644 --- a/pybamm/util.py +++ b/pybamm/util.py @@ -57,14 +57,14 @@ def __getitem__(self, key): try: return super().__getitem__(key) except KeyError as error: - if "electrode diffusivity" in key: + if "particle diffusivity" in key: warn( - f"The parameter '{key.replace('electrode', 'particle')}' " + f"The parameter '{key.replace('particle', 'electrode')}' " f"has been renamed to '{key}'", DeprecationWarning, stacklevel=2, ) - return super().__getitem__(key.replace("electrode", "particle")) + return super().__getitem__(key.replace("particle", "electrode")) if key in ["Negative electrode SOC", "Positive electrode SOC"]: domain = key.split(" ")[0] raise KeyError( diff --git a/tests/unit/test_parameters/test_bpx.py b/tests/unit/test_parameters/test_bpx.py index dbaa2b36c0..916eb8d161 100644 --- a/tests/unit/test_parameters/test_bpx.py +++ b/tests/unit/test_parameters/test_bpx.py @@ -8,6 +8,8 @@ import json import pybamm import copy +import numpy as np +import pytest class TestBPX(TestCase): @@ -107,28 +109,51 @@ def setUp(self): } def test_bpx(self): - bpx_obj = copy.copy(self.base) + bpx_objs = [ + { + **copy.deepcopy(self.base), + "Parameterisation": { + **copy.deepcopy(self.base["Parameterisation"]), + "Negative electrode": { + **copy.deepcopy( + self.base["Parameterisation"]["Negative electrode"] + ), + "Diffusivity [m2.s-1]": "8.3e-13 * exp(-13.4 * x) + 9.6e-15", # new diffusivity + }, + }, + }, + copy.copy(self.base), + ] - filename = "tmp.json" - with tempfile.NamedTemporaryFile( - suffix=filename, delete=False, mode="w" - ) as tmp: - # write to a tempory file so we can - # get the source later on using inspect.getsource - # (as long as the file still exists) - json.dump(bpx_obj, tmp) - tmp.flush() + model = pybamm.lithium_ion.DFN() + experiment = pybamm.Experiment( + [ + "Discharge at C/5 for 1 hour", + ] + ) - pv = pybamm.ParameterValues.create_from_bpx(tmp.name) + filename = "tmp.json" + sols = [] + for obj in bpx_objs: + with tempfile.NamedTemporaryFile( + suffix=filename, delete=False, mode="w" + ) as tmp: + # write to a temporary file so we can + # get the source later on using inspect.getsource + # (as long as the file still exists) + json.dump(obj, tmp) + tmp.flush() + + pv = pybamm.ParameterValues.create_from_bpx(tmp.name) + sim = pybamm.Simulation( + model, parameter_values=pv, experiment=experiment + ) + sols.append(sim.solve()) - model = pybamm.lithium_ion.DFN() - experiment = pybamm.Experiment( - [ - "Discharge at C/5 for 1 hour", - ] + with pytest.raises(AssertionError): + np.testing.assert_allclose( + sols[0]["Voltage [V]"].data, sols[1]["Voltage [V]"].data, atol=1e-7 ) - sim = pybamm.Simulation(model, parameter_values=pv, experiment=experiment) - sim.solve() def test_constant_functions(self): bpx_obj = copy.copy(self.base) diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 2daad1f6a3..603af50560 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -36,7 +36,7 @@ def test_fuzzy_dict(self): "SEI current": 3, "Lithium plating current": 4, "A dimensional variable [m]": 5, - "Positive particle diffusivity [m2.s-1]": 6, + "Positive electrode diffusivity [m2.s-1]": 6, } ) self.assertEqual(d["test"], 1) @@ -59,7 +59,10 @@ def test_fuzzy_dict(self): d.__getitem__("Open-circuit voltage at 100% SOC [V]") with self.assertWarns(DeprecationWarning): - self.assertEqual(d["Positive electrode diffusivity [m2.s-1]"], 6) + self.assertEqual( + d["Positive electrode diffusivity [m2.s-1]"], + d["Positive particle diffusivity [m2.s-1]"], + ) def test_get_parameters_filepath(self): tempfile_obj = tempfile.NamedTemporaryFile("w", dir=".") From 241e4cc511afcb026e485b7615346a9c37615b50 Mon Sep 17 00:00:00 2001 From: Alec Bills <48105066+aabills@users.noreply.github.com> Date: Fri, 21 Jun 2024 04:13:53 -0700 Subject: [PATCH 2/3] Issue 1778 - heat of mixing (#2837) * fix * fix exception * added a Newfile * pseudo * #1778 add heat of mixing option * #1778 fixed indentation * #1778 fixed test * #1778 fixed test * #1788 added heat of mixing tests * First commit * #1778 fixed test * First iteration * #1788 try to fix diff of U * first push from mac * Fixed full broadcast * Fixed domain in the integral * #1778 fix parameter values example * Added children[0].children[0] * Added half cell * style: pre-commit fixes * #1788 revert some unrelated changes * change ocv hack * Revert "change ocv hack" This reverts commit 3ad0c75b855b3f1d81fc8d812733d766f5200b59. * #1778 fix heat of mixing * #1839 fix thermal submodels to take x_average * #1778 add example heat of mixing * ruff * style: pre-commit fixes * #1778 fix lead acid models * fix SPM with the right broadcast of temperature * #1778 refactor heat of mixing * style: pre-commit fixes * Add Richardson2021 citation for heat of mixing * Fixed heat of mixing sign * Rewritten heat of mixing example, added comparison plot to compare with the paper * Fixed ruff formatting errors in the heat of mixing example script * #1778 fix x-full * #1778 fix tests * #1778 improve coverage * style: pre-commit fixes * #1778 Valentin's suggestion to remove the dUdsto function and compute the derivative in the model directly instead * #1778 fix failing test * #1778 add heat of mixing to CHANGELOG --------- Co-authored-by: Alec Bills <48105066+abillscmu@users.noreply.github.com> Co-authored-by: smitasahu2 Co-authored-by: Afgr1087 Co-authored-by: Ferran Brosa Planella Co-authored-by: Ivan Korotkin Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 5 +- .../compare_lithium_ion_heat_of_mixing.py | 112 ++++++++++++++++++ pybamm/CITATIONS.bib | 11 ++ .../full_battery_models/base_battery_model.py | 11 +- .../models/submodels/thermal/base_thermal.py | 94 ++++++++++++++- pybamm/models/submodels/thermal/isothermal.py | 4 +- pybamm/models/submodels/thermal/lumped.py | 4 +- .../pouch_cell_1D_current_collectors.py | 4 +- .../pouch_cell_2D_current_collectors.py | 4 +- .../submodels/thermal/pouch_cell/x_full.py | 4 +- pybamm/parameters/lithium_ion_parameters.py | 8 +- .../test_base_battery_model.py | 10 ++ .../base_lithium_ion_half_cell_tests.py | 4 + .../base_lithium_ion_tests.py | 70 +++++++++++ 14 files changed, 327 insertions(+), 18 deletions(-) create mode 100644 examples/scripts/compare_lithium_ion_heat_of_mixing.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dfb4a7a18..4e4638dff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Added new parameters `"f{pref]Initial inner SEI on cracks thickness [m]"` and `"f{pref]Initial outer SEI on cracks thickness [m]"`, instead of hardcoding these to `L_inner_0 / 10000` and `L_outer_0 / 10000`. ([#4168](https://github.com/pybamm-team/PyBaMM/pull/4168)) - Added `pybamm.DataLoader` class to fetch data files from [pybamm-data](https://github.com/pybamm-team/pybamm-data/releases/tag/v1.0.0) and store it under local cache. ([#4098](https://github.com/pybamm-team/PyBaMM/pull/4098)) -- Transport efficiency submodel has new options from the literature relating to different tortuosity factor models and also a new option called "tortuosity factor" for specifying the value or function directly as parameters ([#3437](https://github.com/pybamm-team/PyBaMM/pull/3437)) +- Added `time` as an option for `Experiment.termination`. Now allows solving up to a user-specified time while also allowing different cycles and steps in an experiment to be handled normally. ([#4073](https://github.com/pybamm-team/PyBaMM/pull/4073)) - Added `plot_thermal_components` to plot the contributions to the total heat generation in a battery ([#4021](https://github.com/pybamm-team/PyBaMM/pull/4021)) - Added functions for normal probability density function (`pybamm.normal_pdf`) and cumulative distribution function (`pybamm.normal_cdf`) ([#3999](https://github.com/pybamm-team/PyBaMM/pull/3999)) - "Basic" models are now compatible with experiments ([#3995](https://github.com/pybamm-team/PyBaMM/pull/3995)) @@ -20,7 +20,8 @@ - Add support for BPX version 0.4.0 which allows for blended electrodes and user-defined parameters in BPX([#3414](https://github.com/pybamm-team/PyBaMM/pull/3414)) - Added `by_submodel` feature in `print_parameter_info` method to allow users to print parameters and types of submodels in a tabular and readable format ([#3628](https://github.com/pybamm-team/PyBaMM/pull/3628)) - Added `WyciskOpenCircuitPotential` for differential capacity hysteresis state open-circuit potential submodel ([#3593](https://github.com/pybamm-team/PyBaMM/pull/3593)) -- Added `time` as an option for `Experiment.termination`. Now allows solving up to a user-specified time while also allowing different cycles and steps in an experiment to be handled normally. ([#4073](https://github.com/pybamm-team/PyBaMM/pull/4073)) +- Transport efficiency submodel has new options from the literature relating to different tortuosity factor models and also a new option called "tortuosity factor" for specifying the value or function directly as parameters ([#3437](https://github.com/pybamm-team/PyBaMM/pull/3437)) +- Heat of mixing source term can now be included into thermal models ([#2837](https://github.com/pybamm-team/PyBaMM/pull/2837)) ## Bug Fixes diff --git a/examples/scripts/compare_lithium_ion_heat_of_mixing.py b/examples/scripts/compare_lithium_ion_heat_of_mixing.py new file mode 100644 index 0000000000..455f5b1e8b --- /dev/null +++ b/examples/scripts/compare_lithium_ion_heat_of_mixing.py @@ -0,0 +1,112 @@ +# +# Compare SPMe model with and without heat of mixing +# +import pybamm +import numpy as np +import matplotlib.pyplot as plt + +pybamm.set_logging_level("INFO") + +# load models +models = [ + pybamm.lithium_ion.SPMe( + {"heat of mixing": "true", "thermal": "lumped"}, name="SPMe with heat of mixing" + ), + pybamm.lithium_ion.SPMe({"thermal": "lumped"}, name="SPMe without heat of mixing"), +] + +# set parametrisation (Ecker et al., 2015) +parameter_values = pybamm.ParameterValues("Ecker2015") + +# set mesh +# (increased number of points in particles to avoid oscillations for high C-rates) +var_pts = {"x_n": 16, "x_s": 8, "x_p": 16, "r_n": 128, "r_p": 128} + +# set the constant current discharge C-rate +C_rate = 5 + +# set the simulation time and increase the number of points +# for better integration in time +t_eval = np.linspace(0, 720, 360) + +# set up an extra plot with the heat of mixing vs time in each electrode and +# the integrated heat of mixing vs time in each electrode to compare with +# Figure 6(a) from Richardson et al. (2021) +fig, axs = plt.subplots(2, len(models), figsize=(12, 7)) + +# extract some of the parameters +L_n = parameter_values["Negative electrode thickness [m]"] +L_p = parameter_values["Positive electrode thickness [m]"] +A = parameter_values["Electrode width [m]"] * parameter_values["Electrode height [m]"] + +# create and run simulations +sims = [] +for m, model in enumerate(models): + sim = pybamm.Simulation( + model, parameter_values=parameter_values, var_pts=var_pts, C_rate=C_rate + ) + sim.solve(t_eval) + sims.append(sim) + + # extract solution for plotting + sol = sim.solution + + # extract variables from the solution + time = sol["Time [h]"].entries + Q_mix = sol["Heat of mixing [W.m-3]"].entries + + # heat of mixing in negative and positive electrodes multiplied by the electrode + # width, represents the integral of heat of mixing term across each of the + # electrodes (W.m-2) + Q_mix_n = Q_mix[0, :] * L_n + Q_mix_p = Q_mix[-1, :] * L_p + + # heat of mixing integrals (J.m-2) + Q_mix_n_int = 0.0 + Q_mix_p_int = 0.0 + + # data for plotting + Q_mix_n_plt = [] + Q_mix_p_plt = [] + + # performs integration in time + for i, t in enumerate(time[1:]): + dt = (t - time[i]) * 3600 # seconds + Q_mix_n_avg = (Q_mix_n[i] + Q_mix_n[i + 1]) * 0.5 + Q_mix_p_avg = (Q_mix_p[i] + Q_mix_p[i + 1]) * 0.5 + # convert J to kJ and divide the integral by the electrode area A to compare + # with Figure 6(a) from Richardson et al. (2021) + Q_mix_n_int += Q_mix_n_avg * dt / 1000 / A + Q_mix_p_int += Q_mix_p_avg * dt / 1000 / A + Q_mix_n_plt.append(Q_mix_n_int) + Q_mix_p_plt.append(Q_mix_p_int) + + # plots heat of mixing in each electrode vs time in minutes + axs[0, m].plot(time * 60, Q_mix_n, ls="-", label="Negative electrode") + axs[0, m].plot(time * 60, Q_mix_p, ls="--", label="Positive electrode") + axs[0, m].set_title(f"{model.name}") + axs[0, m].set_xlabel("Time [min]") + axs[0, m].set_ylabel("Heat of mixing [W.m-2]") + axs[0, m].grid(True) + axs[0, m].legend() + + # plots integrated heat of mixing in each electrode vs time in minutes + axs[1, m].plot(time[1:] * 60, Q_mix_n_plt, ls="-", label="Negative electrode") + axs[1, m].plot(time[1:] * 60, Q_mix_p_plt, ls="--", label="Positive electrode") + axs[1, m].set_xlabel("Time [min]") + axs[1, m].set_ylabel("Integrated heat of mixing [kJ.m-2]") + axs[1, m].grid(True) + axs[1, m].legend() + +# plot +pybamm.dynamic_plot( + sims, + output_variables=[ + "X-averaged cell temperature [K]", + "X-averaged heat of mixing [W.m-3]", + "X-averaged total heating [W.m-3]", + "Heat of mixing [W.m-3]", + "Voltage [V]", + "Current [A]", + ], +) diff --git a/pybamm/CITATIONS.bib b/pybamm/CITATIONS.bib index 00d41fadd3..33ac6055d3 100644 --- a/pybamm/CITATIONS.bib +++ b/pybamm/CITATIONS.bib @@ -418,6 +418,17 @@ @article{Richardson2020 doi = {10.1016/j.electacta.2020.135862}, } +@article{Richardson2021, + title = {Heat Generation and a Conservation Law for Chemical Energy in Li-ion Batteries}, + author = {Richardson, Giles and Korotkin, Ivan}, + journal = {Electrochimica Acta}, + volume = {392}, + pages = {138909}, + year = {2021}, + publisher = {Elsevier}, + doi = {10.1016/j.electacta.2021.138909}, +} + @article{Sripad2020, title={Kinetics of lithium electrodeposition and stripping}, author={Sripad, Shashank and Korff, Daniel and DeCaluwe, Steven C and Viswanathan, Venkatasubramanian}, diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 03114a59ed..fb8d001f93 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -238,6 +238,7 @@ def __init__(self, extra_options): "integrated", ], "exchange-current density": ["single", "current sigmoid"], + "heat of mixing": ["false", "true"], "hydrolysis": ["false", "true"], "intercalation kinetics": [ "symmetric Butler-Volmer", @@ -512,6 +513,11 @@ def __init__(self, extra_options): # Options not yet compatible with particle-size distributions if options["particle size"] == "distribution": + if options["heat of mixing"] != "false": + raise NotImplementedError( + "Heat of mixing submodels do not yet support particle-size " + "distributions." + ) if options["lithium plating"] != "none": raise NotImplementedError( "Lithium plating submodels do not yet support particle-size " @@ -1240,7 +1246,10 @@ def set_thermal_submodel(self): if self.options["dimensionality"] == 0: thermal_submodel = pybamm.thermal.pouch_cell.OneDimensionalX - self.submodels["thermal"] = thermal_submodel(self.param, self.options) + x_average = getattr(self, "x_average", False) + self.submodels["thermal"] = thermal_submodel( + self.param, self.options, x_average + ) def set_current_collector_submodel(self): if self.options["current collector"] in ["uniform"]: diff --git a/pybamm/models/submodels/thermal/base_thermal.py b/pybamm/models/submodels/thermal/base_thermal.py index 65a3f72a59..c5ebbc7dbd 100644 --- a/pybamm/models/submodels/thermal/base_thermal.py +++ b/pybamm/models/submodels/thermal/base_thermal.py @@ -2,6 +2,7 @@ # Base class for thermal effects # import pybamm +import numpy as np class BaseThermal(pybamm.BaseSubModel): @@ -16,8 +17,12 @@ class BaseThermal(pybamm.BaseSubModel): A dictionary of options to be passed to the model. """ - def __init__(self, param, options=None): + def __init__(self, param, options=None, x_average=False): super().__init__(param, options=options) + self.x_average = x_average + + if self.options["heat of mixing"] == "true": + pybamm.citations.register("Richardson2021") def _get_standard_fundamental_variables(self, T_dict): """ @@ -175,8 +180,12 @@ def _get_standard_coupled_variables(self, variables): Q_rev_n, pybamm.FullBroadcast(0, "separator", "current collector"), Q_rev_p ) + # Heat of mixing + Q_mix_s_n, Q_mix_s_s, Q_mix_s_p = self._heat_of_mixing(variables) + Q_mix = pybamm.concatenation(Q_mix_s_n, Q_mix_s_s, Q_mix_s_p) + # Total heating - Q = Q_ohm + Q_rxn + Q_rev + Q = Q_ohm + Q_rxn + Q_rev + Q_mix # 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 @@ -184,6 +193,7 @@ def _get_standard_coupled_variables(self, variables): Q_ohm_av = self._x_average(Q_ohm, Q_ohm_s_cn, Q_ohm_s_cp) Q_rxn_av = self._x_average(Q_rxn, 0, 0) Q_rev_av = self._x_average(Q_rev, 0, 0) + Q_mix_av = self._x_average(Q_mix, 0, 0) Q_av = self._x_average(Q, Q_ohm_s_cn, Q_ohm_s_cp) # Compute the integrated heat source per unit simulated electrode-pair area @@ -192,11 +202,14 @@ def _get_standard_coupled_variables(self, variables): Q_ohm_Wm2 = Q_ohm_av * param.L Q_rxn_Wm2 = Q_rxn_av * param.L Q_rev_Wm2 = Q_rev_av * param.L + Q_mix_Wm2 = Q_mix_av * param.L Q_Wm2 = Q_av * param.L + # Now average over the electrode height and width Q_ohm_Wm2_av = self._yz_average(Q_ohm_Wm2) Q_rxn_Wm2_av = self._yz_average(Q_rxn_Wm2) Q_rev_Wm2_av = self._yz_average(Q_rev_Wm2) + Q_mix_Wm2_av = self._yz_average(Q_mix_Wm2) Q_Wm2_av = self._yz_average(Q_Wm2) # Compute total heat source terms (in W) over the *entire cell volume*, not @@ -208,6 +221,7 @@ def _get_standard_coupled_variables(self, variables): Q_ohm_W = Q_ohm_Wm2_av * n_elec * A Q_rxn_W = Q_rxn_Wm2_av * n_elec * A Q_rev_W = Q_rev_Wm2_av * n_elec * A + Q_mix_W = Q_mix_Wm2_av * n_elec * A Q_W = Q_Wm2_av * n_elec * A # Compute volume-averaged heat source terms over the *entire cell volume*, not @@ -216,6 +230,7 @@ def _get_standard_coupled_variables(self, variables): Q_ohm_vol_av = Q_ohm_W / V Q_rxn_vol_av = Q_rxn_W / V Q_rev_vol_av = Q_rev_W / V + Q_mix_vol_av = Q_mix_W / V Q_vol_av = Q_W / V # Effective heat capacity @@ -228,6 +243,7 @@ def _get_standard_coupled_variables(self, variables): "Ohmic heating [W.m-3]": Q_ohm, "X-averaged Ohmic heating [W.m-3]": Q_ohm_av, "Volume-averaged Ohmic heating [W.m-3]": Q_ohm_vol_av, + "Volume-averaged heat of mixing [W.m-3]": Q_mix_vol_av, "Ohmic heating per unit electrode-pair area [W.m-2]": Q_ohm_Wm2, "Ohmic heating [W]": Q_ohm_W, # Irreversible @@ -244,6 +260,12 @@ def _get_standard_coupled_variables(self, variables): "Volume-averaged reversible heating [W.m-3]": Q_rev_vol_av, "Reversible heating per unit electrode-pair area " "[W.m-2]": Q_rev_Wm2, "Reversible heating [W]": Q_rev_W, + # Mixing + "Heat of mixing [W.m-3]": Q_mix, + "X-averaged heat of mixing [W.m-3]": Q_mix_av, + "Volume-averaged heating of mixing [W.m-3]": Q_mix_vol_av, + "Heat of mixing per unit electrode-pair area " "[W.m-2]": Q_mix_Wm2, + "Heat of mixing [W]": Q_mix_W, # Total "Total heating [W.m-3]": Q, "X-averaged total heating [W.m-3]": Q_av, @@ -290,6 +312,74 @@ def _current_collector_heating(self, variables): Q_s_cp = self.param.p.sigma_cc * pybamm.grad_squared(phi_s_cp) return Q_s_cn, Q_s_cp + def _heat_of_mixing(self, variables): + """Compute heat of mixing source terms.""" + param = self.param + + if self.options["heat of mixing"] == "true": + F = pybamm.constants.F.value + pi = np.pi + + # Compute heat of mixing in negative electrode + if self.options.electrode_types["negative"] == "planar": + Q_mix_s_n = pybamm.FullBroadcast( + 0, ["negative electrode"], "current collector" + ) + else: + a_n = variables["Negative electrode surface area to volume ratio [m-1]"] + R_n = variables["Negative particle radius [m]"] + N_n = a_n / (4 * pi * R_n**2) + if self.x_average: + c_n = variables[ + "X-averaged negative particle concentration [mol.m-3]" + ] + T_n = variables["X-averaged negative electrode temperature [K]"] + else: + c_n = variables["Negative particle concentration [mol.m-3]"] + T_n = variables["Negative electrode temperature [K]"] + T_n_part = pybamm.PrimaryBroadcast(T_n, ["negative particle"]) + dc_n_dr2 = pybamm.inner(pybamm.grad(c_n), pybamm.grad(c_n)) + D_n = param.n.prim.D(c_n, T_n_part) + dUeq_n = param.n.prim.U(c_n / param.n.prim.c_max, T_n_part).diff(c_n) + integrand_r_n = D_n * dc_n_dr2 * dUeq_n + integration_variable_r_n = [ + pybamm.SpatialVariable("r", domain=integrand_r_n.domain) + ] + integral_r_n = pybamm.Integral(integrand_r_n, integration_variable_r_n) + Q_mix_s_n = -F * N_n * integral_r_n + + # Compute heat of mixing in positive electrode + a_p = variables["Positive electrode surface area to volume ratio [m-1]"] + R_p = variables["Positive particle radius [m]"] + N_p = a_p / (4 * pi * R_p**2) + if self.x_average: + c_p = variables["X-averaged positive particle concentration [mol.m-3]"] + T_p = variables["X-averaged positive electrode temperature [K]"] + else: + c_p = variables["Positive particle concentration [mol.m-3]"] + T_p = variables["Positive electrode temperature [K]"] + T_p_part = pybamm.PrimaryBroadcast(T_p, ["positive particle"]) + dc_p_dr2 = pybamm.inner(pybamm.grad(c_p), pybamm.grad(c_p)) + D_p = param.p.prim.D(c_p, T_p_part) + dUeq_p = param.p.prim.U(c_p / param.p.prim.c_max, T_p_part).diff(c_p) + integrand_r_p = D_p * dc_p_dr2 * dUeq_p + integration_variable_r_p = [ + pybamm.SpatialVariable("r", domain=integrand_r_p.domain) + ] + integral_r_p = pybamm.Integral(integrand_r_p, integration_variable_r_p) + Q_mix_s_p = -F * N_p * integral_r_p + Q_mix_s_s = pybamm.FullBroadcast(0, ["separator"], "current collector") + else: + Q_mix_s_n = pybamm.FullBroadcast( + 0, ["negative electrode"], "current collector" + ) + Q_mix_s_p = pybamm.FullBroadcast( + 0, ["positive electrode"], "current collector" + ) + Q_mix_s_s = pybamm.FullBroadcast(0, ["separator"], "current collector") + + return Q_mix_s_n, Q_mix_s_s, Q_mix_s_p + def _x_average(self, var, var_cn, var_cp): """ Computes the X-average over the whole cell (including current collectors) diff --git a/pybamm/models/submodels/thermal/isothermal.py b/pybamm/models/submodels/thermal/isothermal.py index e3228a55cb..edcd47bbdf 100644 --- a/pybamm/models/submodels/thermal/isothermal.py +++ b/pybamm/models/submodels/thermal/isothermal.py @@ -18,8 +18,8 @@ class Isothermal(BaseThermal): A dictionary of options to be passed to the model. """ - def __init__(self, param, options=None): - super().__init__(param, options=options) + def __init__(self, param, options=None, x_average=False): + super().__init__(param, options=options, x_average=x_average) def get_fundamental_variables(self): # Set the x-averaged temperature to the ambient temperature, which can be diff --git a/pybamm/models/submodels/thermal/lumped.py b/pybamm/models/submodels/thermal/lumped.py index 66cd5c5831..4afde4fa57 100644 --- a/pybamm/models/submodels/thermal/lumped.py +++ b/pybamm/models/submodels/thermal/lumped.py @@ -20,8 +20,8 @@ class Lumped(BaseThermal): """ - def __init__(self, param, options=None): - super().__init__(param, options=options) + def __init__(self, param, options=None, x_average=False): + super().__init__(param, options=options, x_average=x_average) pybamm.citations.register("Timms2021") def get_fundamental_variables(self): diff --git a/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py b/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py index 548c6cba0e..a4908c6f5d 100644 --- a/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py +++ b/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py @@ -22,8 +22,8 @@ class CurrentCollector1D(BaseThermal): """ - def __init__(self, param, options=None): - super().__init__(param, options=options) + def __init__(self, param, options=None, x_average=True): + super().__init__(param, options=options, x_average=x_average) pybamm.citations.register("Timms2021") def get_fundamental_variables(self): diff --git a/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py b/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py index 206a8d1e82..7955ee4c38 100644 --- a/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py +++ b/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py @@ -22,8 +22,8 @@ class CurrentCollector2D(BaseThermal): """ - def __init__(self, param, options=None): - super().__init__(param, options=options) + def __init__(self, param, options=None, x_average=True): + super().__init__(param, options=options, x_average=x_average) pybamm.citations.register("Timms2021") def get_fundamental_variables(self): diff --git a/pybamm/models/submodels/thermal/pouch_cell/x_full.py b/pybamm/models/submodels/thermal/pouch_cell/x_full.py index 98065b2d5a..f4aa07c563 100644 --- a/pybamm/models/submodels/thermal/pouch_cell/x_full.py +++ b/pybamm/models/submodels/thermal/pouch_cell/x_full.py @@ -24,8 +24,8 @@ class OneDimensionalX(BaseThermal): """ - def __init__(self, param, options=None): - super().__init__(param, options=options) + def __init__(self, param, options=None, x_average=False): + super().__init__(param, options=options, x_average=x_average) pybamm.citations.register("Timms2021") def get_fundamental_variables(self): diff --git a/pybamm/parameters/lithium_ion_parameters.py b/pybamm/parameters/lithium_ion_parameters.py index 8d69ec49ca..f5a76c6d48 100644 --- a/pybamm/parameters/lithium_ion_parameters.py +++ b/pybamm/parameters/lithium_ion_parameters.py @@ -643,12 +643,14 @@ def U(self, sto, T, lithiation=None): u_ref = pybamm.FunctionParameter( f"{self.phase_prefactor}{Domain} electrode {lithiation}OCP [V]", inputs ) + + dudt_func = self.dUdT(sto) + u_ref = u_ref + (T - self.main_param.T_ref) * dudt_func + # add a term to ensure that the OCP goes to infinity at 0 and -infinity at 1 # this will not affect the OCP for most values of sto # see #1435 - u_ref = u_ref + 1e-6 * (1 / sto + 1 / (sto - 1)) - dudt_func = self.dUdT(sto) - out = u_ref + (T - self.main_param.T_ref) * dudt_func + out = u_ref + 1e-6 * (1 / sto + 1 / (sto - 1)) if self.domain == "negative": out.print_name = r"U_\mathrm{n}(c^\mathrm{surf}_\mathrm{s,n}, T)" diff --git a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py index 557c99eb98..caff0cda5d 100644 --- a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py @@ -26,6 +26,7 @@ 'dimensionality': 0 (possible: [0, 1, 2]) 'electrolyte conductivity': 'default' (possible: ['default', 'full', 'leading order', 'composite', 'integrated']) 'exchange-current density': 'single' (possible: ['single', 'current sigmoid']) +'heat of mixing': 'false' (possible: ['false', 'true']) 'hydrolysis': 'false' (possible: ['false', 'true']) 'intercalation kinetics': 'symmetric Butler-Volmer' (possible: ['symmetric Butler-Volmer', 'asymmetric Butler-Volmer', 'linear', 'Marcus', 'Marcus-Hush-Chidsey', 'MSMR']) 'interface utilisation': 'full' (possible: ['full', 'constant', 'current-driven']) @@ -373,6 +374,15 @@ def test_options(self): } ) + # thermal heat of mixing + with self.assertRaisesRegex(NotImplementedError, "Heat of mixing"): + pybamm.BaseBatteryModel( + { + "heat of mixing": "true", + "particle size": "distribution", + } + ) + # phases with self.assertRaisesRegex(pybamm.OptionError, "multiple particle phases"): pybamm.BaseBatteryModel({"particle phases": "2", "surface form": "false"}) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_half_cell_tests.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_half_cell_tests.py index 24ccd55d79..50fac7a04e 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_half_cell_tests.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_half_cell_tests.py @@ -82,3 +82,7 @@ def test_well_posed_asymmetric_ec_reaction_limited_sei(self): def test_well_posed_lumped_thermal(self): options = {"thermal": "lumped"} self.check_well_posedness(options) + + def test_well_posed_lumped_thermal_hom(self): + options = {"thermal": "lumped", "heat of mixing": "true"} + self.check_well_posedness(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py index 6b8a9d07ee..7d190875a6 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py @@ -67,6 +67,76 @@ def test_well_posed_thermal_2plus1D(self): } self.check_well_posedness(options) + def test_well_posed_isothermal_heat_source_hom(self): + options = { + "calculate heat source for isothermal models": "true", + "thermal": "isothermal", + "heat of mixing": "true", + } + self.check_well_posedness(options) + + def test_well_posed_2plus1D_hom(self): + options = { + "current collector": "potential pair", + "dimensionality": 1, + "heat of mixing": "true", + } + self.check_well_posedness(options) + + options = { + "current collector": "potential pair", + "dimensionality": 2, + "heat of mixing": "true", + } + self.check_well_posedness(options) + + def test_well_posed_lumped_thermal_model_1D_hom(self): + options = {"thermal": "lumped", "heat of mixing": "true"} + self.check_well_posedness(options) + + def test_well_posed_x_full_thermal_model_hom(self): + options = { + "thermal": "x-full", + "heat of mixing": "true", + } + self.check_well_posedness(options) + + def test_well_posed_lumped_thermal_1plus1D_hom(self): + options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "lumped", + "heat of mixing": "true", + } + self.check_well_posedness(options) + + def test_well_posed_lumped_thermal_2plus1D_hom(self): + options = { + "current collector": "potential pair", + "dimensionality": 2, + "thermal": "lumped", + "heat of mixing": "true", + } + self.check_well_posedness(options) + + def test_well_posed_thermal_1plus1D_hom(self): + options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "x-lumped", + "heat of mixing": "true", + } + self.check_well_posedness(options) + + def test_well_posed_thermal_2plus1D_hom(self): + options = { + "current collector": "potential pair", + "dimensionality": 2, + "thermal": "x-lumped", + "heat of mixing": "true", + } + self.check_well_posedness(options) + def test_well_posed_contact_resistance(self): options = {"contact resistance": "true"} self.check_well_posedness(options) From 8ba479118d4a57e55d090bdd7a4b2fbcb016eddf Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Fri, 21 Jun 2024 12:48:16 +0100 Subject: [PATCH 3/3] add Smita to contributors (#4204) --- .all-contributorsrc | 9 +++++++++ README.md | 2 +- all_contributors.md | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b4636edc9c..31584fbbd0 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -922,6 +922,15 @@ "code", "infra" ] + }, + { + "login": "smitasahu2", + "name": "Smita Sahu", + "avatar_url": "https://avatars.githubusercontent.com/u/57876346?v=4", + "profile": "https://github.com/smitasahu2", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 32dd1eb5ce..01a684a7b8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![code style](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![All Contributors](https://img.shields.io/badge/all_contributors-86-orange.svg)](#-contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-87-orange.svg)](#-contributors) diff --git a/all_contributors.md b/all_contributors.md index 646cd53b47..4479eb9ee9 100644 --- a/all_contributors.md +++ b/all_contributors.md @@ -116,6 +116,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Ivan Korotkin
Ivan Korotkin

💻 Santhosh
Santhosh

💻 🚇 + Smita Sahu
Smita Sahu

💻