Skip to content

Commit 0afb069

Browse files
Merge pull request pybamm-team#2529 from pybamm-team/porosity-times-concentration
Porosity times concentration
2 parents ea1f675 + 22e9d5a commit 0afb069

File tree

14 files changed

+137
-63
lines changed

14 files changed

+137
-63
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
## Features
44

5+
- Added variables "Loss of lithium due to loss of active material in negative/positive electrode [mol]". These should be included in the calculation of "total lithium in system" to make sure that lithium is truly conserved. ([#2529](https://github.com/pybamm-team/PyBaMM/pull/2529))
56
- `initial_soc` can now be a string "x V", in which case the simulation is initialized to start from that voltage ([#2508](https://github.com/pybamm-team/PyBaMM/pull/2508))
67
- The `ElectrodeSOH` solver can now calculate electrode balance based on a target "cell capacity" (requires cell capacity "Q" as input), as well as the default "cyclable cell capacity" (requires cyclable lithium capacity "Q_Li" as input). Use the keyword argument `known_value` to control which is used. ([#2508](https://github.com/pybamm-team/PyBaMM/pull/2508))
78

89
## Bug fixes
910

11+
- Fixed "constant concentration" electrolyte model so that "porosity times concentration" is conserved when porosity changes ([#2529](https://github.com/pybamm-team/PyBaMM/pull/2529))
1012
- Fix installation on `Google Colab` (`pybtex` and `Colab` issue) ([#2526](https://github.com/pybamm-team/PyBaMM/pull/2526))
1113

1214
## Breaking changes
1315

16+
- Renamed "Negative/Positive electrode SOC" to "Negative/Positive electrode stoichiometry" to avoid confusion with cell SOC ([#2529](https://github.com/pybamm-team/PyBaMM/pull/2529))
1417
- Removed external variables and submodels. InputParameter should now be used in all cases ([#2502](https://github.com/pybamm-team/PyBaMM/pull/2502))
1518
- Trying to use a solver to solve multiple models results in a RuntimeError exception ([#2481](https://github.com/pybamm-team/PyBaMM/pull/2481))
1619
- Inputs for the `ElectrodeSOH` solver are now (i) "Q_Li", the total cyclable capacity of lithium in the electrodes (previously "n_Li", the total number of moles, n_Li = 3600/F \* Q_Li) (ii) "Q_n", the capacity of the negative electrode (previously "C_n"), and "Q_p", the capacity of the positive electrode (previously "C_p") ([#2508](https://github.com/pybamm-team/PyBaMM/pull/2508))

examples/notebooks/models/SPM.ipynb

Lines changed: 8 additions & 3 deletions
Large diffs are not rendered by default.

examples/notebooks/models/electrode-state-of-health.ipynb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@
8888
"spm_sol.plot([\n",
8989
" \"Terminal voltage [V]\", \n",
9090
" \"Current [A]\", \n",
91-
" \"Negative electrode SOC\",\n",
92-
" \"Positive electrode SOC\",\n",
91+
" \"Negative electrode stoichiometry\",\n",
92+
" \"Positive electrode stoichiometry\",\n",
9393
"])"
9494
]
9595
},
@@ -290,8 +290,8 @@
290290
],
291291
"source": [
292292
"t = spm_sol[\"Time [h]\"].data\n",
293-
"x_spm = spm_sol[\"Negative electrode SOC\"].data\n",
294-
"y_spm = spm_sol[\"Positive electrode SOC\"].data\n",
293+
"x_spm = spm_sol[\"Negative electrode stoichiometry\"].data\n",
294+
"y_spm = spm_sol[\"Positive electrode stoichiometry\"].data\n",
295295
"\n",
296296
"x_0 = esoh_sol[\"x_0\"].data * np.ones_like(t)\n",
297297
"y_0 = esoh_sol[\"y_0\"].data * np.ones_like(t)\n",
@@ -635,4 +635,4 @@
635635
},
636636
"nbformat": 4,
637637
"nbformat_minor": 4
638-
}
638+
}

examples/notebooks/models/lithium-plating.ipynb

Lines changed: 9 additions & 15 deletions
Large diffs are not rendered by default.

pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,19 @@ def set_degradation_variables(self):
178178

179179
# Lithium lost to side reactions
180180
# Different way of measuring LLI but should give same value
181-
LLI_sei = self.variables["Loss of lithium to SEI [mol]"]
182-
LLI_reactions = LLI_sei
181+
n_Li_lost_sei = self.variables["Loss of lithium to SEI [mol]"]
182+
n_Li_lost_reactions = n_Li_lost_sei
183183
if "negative electrode" in domains:
184-
LLI_sei_cracks = self.variables["Loss of lithium to SEI on cracks [mol]"]
185-
LLI_pl = self.variables["Loss of lithium to lithium plating [mol]"]
186-
LLI_reactions += LLI_sei_cracks + LLI_pl
184+
n_Li_lost_sei_cracks = self.variables[
185+
"Loss of lithium to SEI on cracks [mol]"
186+
]
187+
n_Li_lost_pl = self.variables["Loss of lithium to lithium plating [mol]"]
188+
n_Li_lost_reactions += n_Li_lost_sei_cracks + n_Li_lost_pl
187189

188190
self.variables.update(
189191
{
190-
"Total lithium lost to side reactions [mol]": LLI_reactions,
191-
"Total capacity lost to side reactions [A.h]": LLI_reactions
192+
"Total lithium lost to side reactions [mol]": n_Li_lost_reactions,
193+
"Total capacity lost to side reactions [A.h]": n_Li_lost_reactions
192194
* param.F
193195
/ 3600,
194196
}

pybamm/models/submodels/active_material/constant_active_material.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,11 @@ def get_fundamental_variables(self):
3535
self._get_standard_active_material_change_variables(deps_solid_dt)
3636
)
3737

38+
variables.update(
39+
{
40+
"Loss of lithium due to loss of active material "
41+
f"in {domain} electrode [mol]": pybamm.Scalar(0)
42+
}
43+
)
44+
3845
return variables

pybamm/models/submodels/active_material/loss_active_material.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ def get_fundamental_variables(self):
5353
auxiliary_domains={"secondary": "current collector"},
5454
)
5555
variables = self._get_standard_active_material_variables(eps_solid)
56+
lli_due_to_lam = pybamm.Variable(
57+
"Loss of lithium due to loss of active material "
58+
f"in {domain} electrode [mol]"
59+
)
60+
variables.update(
61+
{
62+
"Loss of lithium due to loss of active material "
63+
f"in {domain} electrode [mol]": lli_due_to_lam
64+
}
65+
)
5666
return variables
5767

5868
def get_coupled_variables(self, variables):
@@ -133,7 +143,23 @@ def set_rhs(self, variables):
133143
f"{Domain} electrode active material volume fraction change"
134144
]
135145

136-
self.rhs = {eps_solid: deps_solid_dt}
146+
# Loss of lithium due to loss of active material
147+
# See eq 37 in "Sulzer, Valentin, et al. "Accelerated battery lifetime
148+
# simulations using adaptive inter-cycle extrapolation algorithm."
149+
# Journal of The Electrochemical Society 168.12 (2021): 120531.
150+
lli_due_to_lam = variables[
151+
"Loss of lithium due to loss of active material "
152+
f"in {domain} electrode [mol]"
153+
]
154+
# Multiply by mol.m-3 * m3 to get mol
155+
c_s_av = variables[f"Average {domain} particle concentration [mol.m-3]"]
156+
V = self.domain_param.L * self.param.A_cc
157+
158+
self.rhs = {
159+
# minus sign because eps_solid is decreasing and LLI measures positive
160+
lli_due_to_lam: -c_s_av * V * pybamm.x_average(deps_solid_dt),
161+
eps_solid: deps_solid_dt,
162+
}
137163

138164
def set_initial_conditions(self, variables):
139165
domain, Domain = self.domain_Domain
@@ -148,3 +174,9 @@ def set_initial_conditions(self, variables):
148174
else:
149175
eps_solid = variables[f"{Domain} electrode active material volume fraction"]
150176
self.initial_conditions = {eps_solid: eps_solid_init}
177+
178+
lli_due_to_lam = variables[
179+
"Loss of lithium due to loss of active material "
180+
f"in {domain} electrode [mol]"
181+
]
182+
self.initial_conditions[lli_due_to_lam] = pybamm.Scalar(0)

pybamm/models/submodels/electrolyte_diffusion/constant_concentration.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ def __init__(self, param, options=None):
2323
super().__init__(param, options)
2424

2525
def get_fundamental_variables(self):
26-
c_e_dict = {
27-
domain: pybamm.FullBroadcast(1, domain, "current collector")
26+
eps_c_e_dict = {
27+
domain: self.param.domain_params[domain.split()[0]].epsilon_init * 1
2828
for domain in self.options.whole_cell_domains
2929
}
30-
variables = self._get_standard_concentration_variables(c_e_dict)
31-
30+
variables = self._get_standard_porosity_times_concentration_variables(
31+
eps_c_e_dict
32+
)
3233
N_e = pybamm.FullBroadcastToEdges(
3334
0,
3435
[domain for domain in self.options.whole_cell_domains],
@@ -40,15 +41,21 @@ def get_fundamental_variables(self):
4041
return variables
4142

4243
def get_coupled_variables(self, variables):
43-
eps_c_e_dict = {}
44+
c_e_dict = {}
4445
for domain in self.options.whole_cell_domains:
4546
Domain = domain.capitalize()
4647
eps_k = variables[f"{Domain} porosity"]
47-
c_e_k = variables[f"{Domain.split()[0]} electrolyte concentration"]
48-
eps_c_e_dict[domain] = eps_k * c_e_k
49-
variables.update(
50-
self._get_standard_porosity_times_concentration_variables(eps_c_e_dict)
48+
eps_c_e_k = variables[f"{Domain} porosity times concentration"]
49+
c_e_k = eps_c_e_k / eps_k
50+
c_e_dict[domain] = c_e_k
51+
52+
variables["Electrolyte concentration concatenation"] = pybamm.concatenation(
53+
*c_e_dict.values()
5154
)
55+
variables.update(self._get_standard_domain_concentration_variables(c_e_dict))
56+
57+
c_e = variables["Porosity times concentration"] / variables["Porosity"]
58+
variables.update(self._get_standard_whole_cell_concentration_variables(c_e))
5259

5360
return variables
5461

pybamm/models/submodels/particle/base_particle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def _get_total_concentration_variables(self, variables):
139139

140140
variables.update(
141141
{
142-
f"{Domain} electrode {phase_name}SOC": c_s_vol_av,
142+
f"{Domain} electrode {phase_name}stoichiometry": c_s_vol_av,
143143
f"{Domain} electrode {phase_name}volume-averaged "
144144
"concentration": c_s_vol_av,
145145
f"{Domain} electrode {phase_name}volume-averaged "

pybamm/solvers/processed_variable.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import numpy as np
77
import pybamm
88
import scipy.interpolate as interp
9+
from scipy.integrate import cumulative_trapezoid
910

1011

1112
class ProcessedVariable(object):
@@ -61,6 +62,7 @@ def __init__(
6162

6263
# Set timescale
6364
self.timescale = solution.timescale_eval
65+
self.t_pts_nondim = solution.t
6466
self.t_pts = solution.t * self.timescale
6567

6668
# Store length scales
@@ -114,30 +116,24 @@ def initialise_0D(self):
114116
# initialise empty array of the correct size
115117
entries = np.empty(len(self.t_pts))
116118
idx = 0
117-
last_t = 0
119+
120+
entries = np.empty(len(self.t_pts))
121+
idx = 0
118122
# Evaluate the base_variable index-by-index
119123
for ts, ys, inputs, base_var_casadi in zip(
120124
self.all_ts, self.all_ys, self.all_inputs_casadi, self.base_variables_casadi
121125
):
122126
for inner_idx, t in enumerate(ts):
123127
t = ts[inner_idx]
124128
y = ys[:, inner_idx]
125-
if self.cumtrapz_ic is not None:
126-
if idx == 0:
127-
new_val = t * base_var_casadi(t, y, inputs).full()[0, 0]
128-
entries[idx] = self.cumtrapz_ic + (
129-
t * base_var_casadi(t, y, inputs).full()[0, 0]
130-
)
131-
else:
132-
new_val = (t - last_t) * (
133-
base_var_casadi(t, y, inputs).full()[0, 0]
134-
)
135-
entries[idx] = new_val + entries[idx - 1]
136-
else:
137-
entries[idx] = base_var_casadi(t, y, inputs).full()[0, 0]
129+
entries[idx] = float(base_var_casadi(t, y, inputs))
138130

139131
idx += 1
140-
last_t = t
132+
133+
if self.cumtrapz_ic is not None:
134+
entries = cumulative_trapezoid(
135+
entries, self.t_pts_nondim, initial=float(self.cumtrapz_ic)
136+
)
141137

142138
# set up interpolation
143139
if len(self.t_pts) == 1:

pybamm/util.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ def __getitem__(self, key):
5757
try:
5858
return super().__getitem__(key)
5959
except KeyError:
60+
if key in ["Negative electrode SOC", "Positive electrode SOC"]:
61+
domain = key.split(" ")[0]
62+
raise KeyError(
63+
f"Variable '{domain} electrode SOC' has been renamed to "
64+
f"'{domain} electrode stoichiometry' to avoid confusion "
65+
"with cell SOC"
66+
)
6067
best_matches = self.get_best_matches(key)
6168
raise KeyError(f"'{key}' not found. Best matches are {best_matches}")
6269

tests/integration/test_models/standard_output_tests.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,13 @@ def __init__(self, model, param, disc, solution, operating_condition):
293293
self.N_s_n = solution[f"Negative {self.phase_name_n}particle flux"]
294294
self.N_s_p = solution[f"Positive {self.phase_name_p}particle flux"]
295295

296-
self.c_SEI_tot = solution["Loss of lithium to SEI [mol]"]
297-
self.c_Li_tot = solution["Loss of lithium to lithium plating [mol]"]
296+
self.n_Li_side = solution["Total lithium lost to side reactions [mol]"]
297+
self.n_Li_LAM_n = solution[
298+
"Loss of lithium due to loss of active material in negative electrode [mol]"
299+
]
300+
self.n_Li_LAM_p = solution[
301+
"Loss of lithium due to loss of active material in positive electrode [mol]"
302+
]
298303

299304
if model.options["particle size"] == "distribution":
300305
# These concentration variables are only present for distribution models.
@@ -404,8 +409,9 @@ def test_conservation(self):
404409
c_s_tot = (
405410
self.c_s_n_tot(self.solution.t)
406411
+ self.c_s_p_tot(self.solution.t)
407-
+ self.c_SEI_tot(self.solution.t)
408-
+ self.c_Li_tot(self.solution.t)
412+
+ self.n_Li_side(self.solution.t)
413+
+ self.n_Li_LAM_n(self.solution.t)
414+
+ self.n_Li_LAM_p(self.solution.t)
409415
)
410416
diff = (c_s_tot[1:] - c_s_tot[:-1]) / c_s_tot[:-1]
411417
if self.model.options["particle"] == "quartic profile":
@@ -800,20 +806,32 @@ def __init__(self, model, param, disc, solution, operating_condition):
800806
self.LLI = solution["Loss of lithium inventory [%]"]
801807
self.n_Li_lost = solution["Total lithium lost [mol]"]
802808
self.n_Li_lost_rxn = solution["Total lithium lost to side reactions [mol]"]
809+
self.n_Li_lost_LAM_n = solution[
810+
"Loss of lithium due to loss of active material in negative electrode [mol]"
811+
]
812+
self.n_Li_lost_LAM_p = solution[
813+
"Loss of lithium due to loss of active material in positive electrode [mol]"
814+
]
803815

804816
def test_degradation_modes(self):
805817
"""Test degradation modes are between 0 and 100%"""
806818
np.testing.assert_array_less(-3e-3, self.LLI(self.t))
807819
np.testing.assert_array_less(-1e-13, self.LAM_ne(self.t))
808820
np.testing.assert_array_less(-1e-13, self.LAM_pe(self.t))
821+
np.testing.assert_array_less(-1e-13, self.n_Li_lost_LAM_n(self.t))
822+
np.testing.assert_array_less(-1e-13, self.n_Li_lost_LAM_p(self.t))
809823
np.testing.assert_array_less(self.LLI(self.t), 100)
810824
np.testing.assert_array_less(self.LAM_ne(self.t), 100)
811825
np.testing.assert_array_less(self.LAM_pe(self.t), 100)
812826

813827
def test_lithium_lost(self):
814828
"""Test the two ways of measuring lithium lost give the same value"""
815829
np.testing.assert_array_almost_equal(
816-
self.n_Li_lost(self.t), self.n_Li_lost_rxn(self.t), decimal=3
830+
self.n_Li_lost(self.t),
831+
self.n_Li_lost_rxn(self.t)
832+
+ self.n_Li_lost_LAM_n(self.t)
833+
+ self.n_Li_lost_LAM_p(self.t),
834+
decimal=5,
817835
)
818836

819837
def test_all(self):

tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,12 @@ def test_sei_asymmetric_ec_reaction_limited(self):
209209
)
210210
self.run_basic_processing_test(options, parameter_values=parameter_values)
211211

212-
def test_loss_active_material_stress_negative(self):
212+
def test_loss_active_material_stress_positive(self):
213213
options = {"loss of active material": ("none", "stress-driven")}
214214
parameter_values = pybamm.ParameterValues("Ai2020")
215215
self.run_basic_processing_test(options, parameter_values=parameter_values)
216216

217-
def test_loss_active_material_stress_positive(self):
217+
def test_loss_active_material_stress_negative(self):
218218
options = {"loss of active material": ("stress-driven", "none")}
219219
parameter_values = pybamm.ParameterValues("Ai2020")
220220
self.run_basic_processing_test(options, parameter_values=parameter_values)

tests/unit/test_util.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ def test_fuzzy_dict(self):
6969
with self.assertRaisesRegex(KeyError, "'test3' not found. Best matches are "):
7070
d.__getitem__("test3")
7171

72+
with self.assertRaisesRegex(KeyError, "stoichiometry"):
73+
d.__getitem__("Negative electrode SOC")
74+
7275
def test_get_parameters_filepath(self):
7376
tempfile_obj = tempfile.NamedTemporaryFile("w", dir=".")
7477
self.assertTrue(

0 commit comments

Comments
 (0)