Skip to content

Commit b0498dc

Browse files
aabillsmedha-14
authored andcommitted
Adds support for particle size distributions combined with particle mechanics. (#4807)
* add particle stress distribution variables * add cracking * add test * fix tests and mpm issue * coverage * changelog * changelog format * eric comment * fix test failure
1 parent bd5bfd5 commit b0498dc

File tree

8 files changed

+184
-81
lines changed

8 files changed

+184
-81
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# [Unreleased](https://github.com/pybamm-team/PyBaMM/)
22

3+
## Features
4+
5+
- Added support for particle size distributions combined with particle mechanics. ([#4807](https://github.com/pybamm-team/PyBaMM/pull/4807))
6+
37
# [v25.1.1](https://github.com/pybamm-team/PyBaMM/tree/v25.1.1) - 2025-01-20
48

59
## Features

src/pybamm/models/full_battery_models/base_battery_model.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -545,21 +545,11 @@ def __init__(self, extra_options):
545545
"'quadratic' and 'quartic' concentration profiles have not yet "
546546
"been implemented for particle-size ditributions"
547547
)
548-
if options["particle mechanics"] != "none":
549-
raise NotImplementedError(
550-
"Particle mechanics submodels do not yet support particle-size"
551-
" distributions."
552-
)
553548
if options["particle shape"] != "spherical":
554549
raise NotImplementedError(
555550
"Particle shape must be 'spherical' for particle-size distribution"
556551
" submodels."
557552
)
558-
if options["stress-induced diffusion"] == "true":
559-
raise NotImplementedError(
560-
"stress-induced diffusion cannot yet be included in "
561-
"particle-size distributions."
562-
)
563553
if options["thermal"] == "x-full":
564554
raise NotImplementedError(
565555
"X-full thermal submodels do not yet support particle-size"

src/pybamm/models/submodels/active_material/loss_active_material.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Class for varying active material volume fraction, driven by stress
2+
# Class for varying active material volume fraction
33
#
44
import pybamm
55

src/pybamm/models/submodels/particle_mechanics/base_mechanics.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ class BaseMechanics(pybamm.BaseSubModel):
2424
"""
2525

2626
def __init__(self, param, domain, options, phase="primary"):
27+
if options["particle size"] == "distribution":
28+
self.size_distribution = True
29+
else:
30+
self.size_distribution = False
2731
super().__init__(param, domain, options=options, phase=phase)
2832

2933
def _get_standard_variables(self, l_cr):
@@ -35,6 +39,71 @@ def _get_standard_variables(self, l_cr):
3539
}
3640
return variables
3741

42+
def _get_standard_size_distribution_variables(self, l_cr_dist):
43+
domain, Domain = self.domain_Domain
44+
l_cr_av_dist = pybamm.x_average(l_cr_dist)
45+
variables = {
46+
f"{Domain} {self.phase_param.phase_name}particle crack length distribution [m]": l_cr_dist,
47+
f"X-averaged {domain} {self.phase_param.phase_name}particle crack length distribution [m]": l_cr_av_dist,
48+
}
49+
return variables
50+
51+
def _get_mechanical_size_distribution_results(self, variables):
52+
domain, Domain = self.domain_Domain
53+
phase_name = self.phase_param.phase_name
54+
phase_param = self.phase_param
55+
c_s_rav = variables[
56+
f"R-averaged {domain} {phase_name}particle concentration distribution [mol.m-3]"
57+
]
58+
c_s_surf = variables[
59+
f"{Domain} {phase_name}particle surface concentration distribution [mol.m-3]"
60+
]
61+
T = pybamm.PrimaryBroadcast(
62+
variables[f"{Domain} electrode temperature [K]"],
63+
[f"{domain} {phase_name}particle size"],
64+
)
65+
T = pybamm.PrimaryBroadcast(
66+
T,
67+
[f"{domain} {phase_name}particle"],
68+
)
69+
70+
# use a tangential approximation for omega
71+
c_0 = phase_param.c_0
72+
R0 = phase_param.R
73+
sto = variables[f"{Domain} {phase_name}particle concentration distribution"]
74+
Omega = pybamm.r_average(phase_param.Omega(sto, T))
75+
76+
E0 = pybamm.r_average(phase_param.E(sto, T))
77+
nu = phase_param.nu
78+
# Ai2019 eq [10]
79+
disp_surf = Omega * R0 / 3 * (c_s_rav - c_0)
80+
# c0 reference concentration for no deformation
81+
# stress evaluated at the surface of the particles
82+
# Ai2019 eq [7] with r=R
83+
stress_r_surf = pybamm.Scalar(0)
84+
# Ai2019 eq [8] with r=R
85+
# c_s_rav is already multiplied by 3/R^3 inside r_average
86+
stress_t_surf = Omega * E0 * (c_s_rav - c_s_surf) / 3.0 / (1.0 - nu)
87+
88+
# Averages
89+
stress_r_surf_av = pybamm.x_average(stress_r_surf)
90+
stress_t_surf_av = pybamm.x_average(stress_t_surf)
91+
disp_surf_av = pybamm.x_average(disp_surf)
92+
93+
variables.update(
94+
{
95+
f"{Domain} {phase_name}particle surface radial stress distribution [Pa]": stress_r_surf,
96+
f"{Domain} {phase_name}particle surface tangential stress distribution [Pa]": stress_t_surf,
97+
f"{Domain} {phase_name}particle surface displacement distribution [m]": disp_surf,
98+
f"X-averaged {domain} {phase_name}particle surface "
99+
"radial stress distribution [Pa]": stress_r_surf_av,
100+
f"X-averaged {domain} {phase_name}particle surface "
101+
"tangential stress distribution [Pa]": stress_t_surf_av,
102+
f"X-averaged {domain} {phase_name}particle surface displacement distribution [m]": disp_surf_av,
103+
}
104+
)
105+
return variables
106+
38107
def _get_mechanical_results(self, variables):
39108
domain_param = self.domain_param
40109
domain, Domain = self.domain_Domain
@@ -58,10 +127,11 @@ def _get_mechanical_results(self, variables):
58127
]
59128

60129
# use a tangential approximation for omega
130+
c_0 = phase_param.c_0
131+
R0 = phase_param.R
61132
sto = variables[f"{Domain} {phase_name}particle concentration"]
62133
Omega = pybamm.r_average(phase_param.Omega(sto, T))
63-
R0 = phase_param.R
64-
c_0 = phase_param.c_0
134+
65135
E0 = pybamm.r_average(phase_param.E(sto, T))
66136
nu = phase_param.nu
67137
L0 = domain_param.L
@@ -79,7 +149,7 @@ def _get_mechanical_results(self, variables):
79149
stress_r_surf = pybamm.Scalar(0)
80150
# Ai2019 eq [8] with r=R
81151
# c_s_rav is already multiplied by 3/R^3 inside r_average
82-
stress_t_surf = Omega * E0 / 3.0 / (1.0 - nu) * (c_s_rav - c_s_surf)
152+
stress_t_surf = Omega * E0 * (c_s_rav - c_s_surf) / 3.0 / (1.0 - nu)
83153

84154
# Averages
85155
stress_r_surf_av = pybamm.x_average(stress_r_surf)

src/pybamm/models/submodels/particle_mechanics/crack_propagation.py

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,50 @@ def __init__(self, param, domain, x_average, options, phase="primary"):
3737
def get_fundamental_variables(self):
3838
domain, Domain = self.domain_Domain
3939
phase_name = self.phase_param.phase_name
40-
if self.x_average is True:
41-
l_cr_av = pybamm.Variable(
42-
f"X-averaged {domain} {phase_name}particle crack length [m]",
43-
domain="current collector",
44-
scale=self.phase_param.l_cr_0,
45-
)
40+
if self.x_average:
41+
if self.size_distribution:
42+
l_cr_av_dist = pybamm.Variable(
43+
f"X-averaged {domain} {phase_name}particle crack length distribution [m]",
44+
domains={
45+
"primary": f"{domain} particle size",
46+
"secondary": "current collector",
47+
},
48+
scale=self.phase_param.l_cr_0,
49+
)
50+
l_cr_dist = pybamm.SecondaryBroadcast(
51+
l_cr_av_dist, f"{domain} electrode"
52+
)
53+
l_cr_av = pybamm.size_average(l_cr_av_dist)
54+
else:
55+
l_cr_av = pybamm.Variable(
56+
f"X-averaged {domain} {phase_name}particle crack length [m]",
57+
domain="current collector",
58+
scale=self.phase_param.l_cr_0,
59+
)
4660
l_cr = pybamm.PrimaryBroadcast(l_cr_av, f"{domain} electrode")
4761
else:
48-
l_cr = pybamm.Variable(
49-
f"{Domain} {phase_name}particle crack length [m]",
50-
domain=f"{domain} electrode",
51-
auxiliary_domains={"secondary": "current collector"},
52-
scale=self.phase_param.l_cr_0,
53-
)
62+
if self.size_distribution:
63+
l_cr_dist = pybamm.Variable(
64+
f"{Domain} {phase_name}particle crack length distribution [m]",
65+
domains={
66+
"primary": f"{domain} particle size",
67+
"secondary": f"{domain} electrode",
68+
"tertiary": "current collector",
69+
},
70+
scale=self.phase_param.l_cr_0,
71+
)
72+
l_cr = pybamm.size_average(l_cr_dist)
73+
else:
74+
l_cr = pybamm.Variable(
75+
f"{Domain} {phase_name}particle crack length [m]",
76+
domain=f"{domain} electrode",
77+
auxiliary_domains={"secondary": "current collector"},
78+
scale=self.phase_param.l_cr_0,
79+
)
5480

5581
variables = self._get_standard_variables(l_cr)
82+
if self.size_distribution:
83+
variables.update(self._get_standard_size_distribution_variables(l_cr_dist))
5684

5785
return variables
5886

@@ -61,14 +89,26 @@ def get_coupled_variables(self, variables):
6189
phase_name = self.phase_param.phase_name
6290
variables.update(self._get_standard_surface_variables(variables))
6391
variables.update(self._get_mechanical_results(variables))
92+
if self.size_distribution:
93+
variables.update(self._get_mechanical_size_distribution_results(variables))
6494
T = variables[f"{Domain} electrode temperature [K]"]
6595
k_cr = self.phase_param.k_cr(T)
6696
m_cr = self.phase_param.m_cr
6797
b_cr = self.phase_param.b_cr
68-
stress_t_surf = variables[
69-
f"{Domain} {phase_name}particle surface tangential stress [Pa]"
70-
]
71-
l_cr = variables[f"{Domain} {phase_name}particle crack length [m]"]
98+
if self.size_distribution:
99+
stress_t_surf = variables[
100+
f"{Domain} {phase_name}particle surface tangential stress distribution [Pa]"
101+
]
102+
else:
103+
stress_t_surf = variables[
104+
f"{Domain} {phase_name}particle surface tangential stress [Pa]"
105+
]
106+
if self.size_distribution:
107+
l_cr = variables[
108+
f"{Domain} {phase_name}particle crack length distribution [m]"
109+
]
110+
else:
111+
l_cr = variables[f"{Domain} {phase_name}particle crack length [m]"]
72112
# # compressive stress will not lead to crack propagation
73113
dK_SIF = stress_t_surf * b_cr * pybamm.sqrt(np.pi * l_cr) * (stress_t_surf >= 0)
74114
dl_cr = k_cr * (dK_SIF**m_cr) / 3600 # divide by 3600 to replace t0_cr
@@ -86,14 +126,24 @@ def set_rhs(self, variables):
86126
domain, Domain = self.domain_Domain
87127
phase_name = self.phase_param.phase_name
88128
if self.x_average is True:
89-
l_cr = variables[
90-
f"X-averaged {domain} {phase_name}particle crack length [m]"
91-
]
129+
if self.size_distribution:
130+
l_cr = variables[
131+
f"X-averaged {domain} {phase_name}particle crack length distribution [m]"
132+
]
133+
else:
134+
l_cr = variables[
135+
f"X-averaged {domain} {phase_name}particle crack length [m]"
136+
]
92137
dl_cr = variables[
93138
f"X-averaged {domain} {phase_name}particle cracking rate [m.s-1]"
94139
]
95140
else:
96-
l_cr = variables[f"{Domain} {phase_name}particle crack length [m]"]
141+
if self.size_distribution:
142+
l_cr = variables[
143+
f"{Domain} {phase_name}particle crack length distribution [m]"
144+
]
145+
else:
146+
l_cr = variables[f"{Domain} {phase_name}particle crack length [m]"]
97147
dl_cr = variables[f"{Domain} {phase_name}particle cracking rate [m.s-1]"]
98148
self.rhs = {l_cr: dl_cr}
99149

@@ -102,12 +152,25 @@ def set_initial_conditions(self, variables):
102152
phase_name = self.phase_param.phase_name
103153
l_cr_0 = self.phase_param.l_cr_0
104154
if self.x_average is True:
105-
l_cr = variables[
106-
f"X-averaged {domain} {phase_name}particle crack length [m]"
107-
]
155+
if self.size_distribution:
156+
l_cr = variables[
157+
f"X-averaged {domain} {phase_name}particle crack length distribution [m]"
158+
]
159+
l_cr_0 = pybamm.PrimaryBroadcast(l_cr_0, f"{domain} particle size")
160+
else:
161+
l_cr = variables[
162+
f"X-averaged {domain} {phase_name}particle crack length [m]"
163+
]
108164
else:
109-
l_cr = variables[f"{Domain} {phase_name}particle crack length [m]"]
110-
l_cr_0 = pybamm.PrimaryBroadcast(l_cr_0, f"{domain} electrode")
165+
if self.size_distribution:
166+
l_cr = variables[
167+
f"{Domain} {phase_name}particle crack length distribution [m]"
168+
]
169+
l_cr_0 = pybamm.PrimaryBroadcast(l_cr_0, f"{domain} electrode")
170+
l_cr_0 = pybamm.PrimaryBroadcast(l_cr_0, f"{domain} particle size")
171+
else:
172+
l_cr = variables[f"{Domain} {phase_name}particle crack length [m]"]
173+
l_cr_0 = pybamm.PrimaryBroadcast(l_cr_0, f"{domain} electrode")
111174
self.initial_conditions = {l_cr: l_cr_0}
112175

113176
def add_events_from(self, variables):

src/pybamm/models/submodels/particle_mechanics/swelling_only.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,6 @@ def get_fundamental_variables(self):
4848
def get_coupled_variables(self, variables):
4949
variables.update(self._get_standard_surface_variables(variables))
5050
variables.update(self._get_mechanical_results(variables))
51+
if self.size_distribution:
52+
variables.update(self._get_mechanical_size_distribution_results(variables))
5153
return variables

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,22 @@ def test_well_posed_psd(self):
525525
options = {"particle size": "distribution", "surface form": "algebraic"}
526526
self.check_well_posedness(options)
527527

528+
def test_well_posed_psd_swelling_and_cracking(self):
529+
options = {
530+
"particle size": "distribution",
531+
"particle mechanics": "swelling and cracking",
532+
"surface form": "algebraic",
533+
}
534+
self.check_well_posedness(options)
535+
536+
def test_well_posed_psd_swelling_only(self):
537+
options = {
538+
"particle size": "distribution",
539+
"particle mechanics": "swelling only",
540+
"surface form": "algebraic",
541+
}
542+
self.check_well_posedness(options)
543+
528544
def test_well_posed_transport_efficiency_Bruggeman(self):
529545
options = {"transport efficiency": "Bruggeman"}
530546
self.check_well_posedness(options)

tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,6 @@ def test_nonspherical_particle_not_implemented(self):
7878
with pytest.raises(NotImplementedError):
7979
pybamm.lithium_ion.MPM(options)
8080

81-
def test_loss_active_material_stress_negative_not_implemented(self):
82-
options = {"loss of active material": ("stress-driven", "none")}
83-
with pytest.raises(NotImplementedError):
84-
pybamm.lithium_ion.MPM(options)
85-
86-
def test_loss_active_material_stress_positive_not_implemented(self):
87-
options = {"loss of active material": ("none", "stress-driven")}
88-
with pytest.raises(NotImplementedError):
89-
pybamm.lithium_ion.MPM(options)
90-
91-
def test_loss_active_material_stress_both_not_implemented(self):
92-
options = {"loss of active material": "stress-driven"}
93-
with pytest.raises(NotImplementedError):
94-
pybamm.lithium_ion.MPM(options)
95-
9681
def test_reversible_plating_with_porosity_not_implemented(self):
9782
options = {
9883
"lithium plating": "reversible",
@@ -101,11 +86,6 @@ def test_reversible_plating_with_porosity_not_implemented(self):
10186
with pytest.raises(pybamm.OptionError, match="distributions"):
10287
pybamm.lithium_ion.MPM(options)
10388

104-
def test_stress_induced_diffusion_not_implemented(self):
105-
options = {"stress-induced diffusion": "true"}
106-
with pytest.raises(NotImplementedError):
107-
pybamm.lithium_ion.MPM(options)
108-
10989
def test_msmr(self):
11090
options = {
11191
"open-circuit potential": "MSMR",
@@ -186,25 +166,3 @@ def test_ec_reaction_limited_not_implemented(self):
186166
}
187167
with pytest.raises(NotImplementedError):
188168
pybamm.lithium_ion.MPM(options)
189-
190-
191-
class TestMPMWithMechanics:
192-
def test_well_posed_negative_cracking_not_implemented(self):
193-
options = {"particle mechanics": ("swelling and cracking", "none")}
194-
with pytest.raises(NotImplementedError):
195-
pybamm.lithium_ion.MPM(options)
196-
197-
def test_well_posed_positive_cracking_not_implemented(self):
198-
options = {"particle mechanics": ("none", "swelling and cracking")}
199-
with pytest.raises(NotImplementedError):
200-
pybamm.lithium_ion.MPM(options)
201-
202-
def test_well_posed_both_cracking_not_implemented(self):
203-
options = {"particle mechanics": "swelling and cracking"}
204-
with pytest.raises(NotImplementedError):
205-
pybamm.lithium_ion.MPM(options)
206-
207-
def test_well_posed_both_swelling_only_not_implemented(self):
208-
options = {"particle mechanics": "swelling only"}
209-
with pytest.raises(NotImplementedError):
210-
pybamm.lithium_ion.MPM(options)

0 commit comments

Comments
 (0)