Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make particle size distribution work with composite electrode. #4687

Merged
merged 21 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features

- Made composite electrode model compatible with particle size distribution ([#4687](https://github.com/pybamm-team/PyBaMM/pull/4687))
- Added `Symbol.post_order()` method to return an iterable that steps through the tree in post-order fashion. ([#4684](https://github.com/pybamm-team/PyBaMM/pull/4684))
- Added two more submodels (options) for the SEI: Lars von Kolzenberg (2020) model and Tunneling Limit model ([#4394](https://github.com/pybamm-team/PyBaMM/pull/4394))

Expand Down
4 changes: 2 additions & 2 deletions docs/source/examples/notebooks/models/using-submodels.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -527,10 +527,10 @@
" model.param, \"positive\", model.options, cracks=True\n",
")\n",
"model.submodels[\"Negative lithium plating\"] = pybamm.lithium_plating.NoPlating(\n",
" model.param, \"Negative\"\n",
" model.param, \"negative\"\n",
")\n",
"model.submodels[\"Positive lithium plating\"] = pybamm.lithium_plating.NoPlating(\n",
" model.param, \"Positive\"\n",
" model.param, \"positive\"\n",
")"
]
},
Expand Down
42 changes: 38 additions & 4 deletions src/pybamm/expression_tree/averages.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ def xyz_average(symbol: pybamm.Symbol) -> pybamm.Symbol:
return yz_average(x_average(symbol))


def xyzs_average(symbol: pybamm.Symbol) -> pybamm.Symbol:
return xyz_average(size_average(symbol))


def r_average(symbol: pybamm.Symbol) -> pybamm.Symbol:
"""
Convenience function for creating an average in the r-direction.
Expand Down Expand Up @@ -373,7 +377,15 @@ def size_average(
# If symbol doesn't have a domain, or doesn't have "negative particle size"
# or "positive particle size" as a domain, it's average value is itself
if symbol.domain == [] or not any(
domain in [["negative particle size"], ["positive particle size"]]
domain
in [
["negative particle size"],
["positive particle size"],
["negative primary particle size"],
["positive primary particle size"],
["negative secondary particle size"],
["positive secondary particle size"],
]
for domain in list(symbol.domains.values())
):
return symbol
Expand All @@ -394,13 +406,35 @@ def size_average(
else:
if f_a_dist is None:
geo = pybamm.geometric_parameters
if "negative" in symbol.domain[0]:
aabills marked this conversation as resolved.
Show resolved Hide resolved
if "primary" in symbol.domain[0]:
name = "R_n_prim"
elif "secondary" in symbol.domain[0]:
name = "R_n_sec"
else:
name = "R_n"
else:
if "primary" in symbol.domain[0]:
name = "R_p_prim"
elif "secondary" in symbol.domain[0]:
name = "R_p_sec"
else:
name = "R_p"
R = pybamm.SpatialVariable(
"R", domains=symbol.domains, coord_sys="cartesian"
name, domains=symbol.domains, coord_sys="cartesian"
)
if ["negative particle size"] in symbol.domains.values():
if ["negative particle size"] in symbol.domains.values() or [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI geo.domain_params["negative"] is equivalent to geo.n

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to interpret this comment, as far as I can tell, I never use geo.domain_params['negative']. Is there something here you want changed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I wasn’t clear. I meant that instead of doing an if statement by domain and getting eg geo.n you can do geo.domain_params[domain]. More of a comment then necessarily a suggestion / required change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like I left this comment in the wrong place too lol

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, thanks

"negative primary particle size"
] in symbol.domains.values():
f_a_dist = geo.n.prim.f_a_dist(R)
elif ["positive particle size"] in symbol.domains.values():
elif ["negative secondary particle size"] in symbol.domains.values():
f_a_dist = geo.n.sec.f_a_dist(R)
elif ["positive particle size"] in symbol.domains.values() or [
"positive primary particle size"
] in symbol.domains.values():
f_a_dist = geo.p.prim.f_a_dist(R)
elif ["positive secondary particle size"] in symbol.domains.values():
f_a_dist = geo.p.sec.f_a_dist(R)
return SizeAverage(symbol, f_a_dist)


Expand Down
24 changes: 24 additions & 0 deletions src/pybamm/geometry/battery_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ def battery_geometry(
"negative particle size": {"R_n": {"min": R_min_n, "max": R_max_n}},
}
)
phases = int(options.negative["particle phases"])
if phases >= 2:
geometry.update(
{
"negative primary particle size": {
"R_n_prim": {"min": R_min_n, "max": R_max_n}
},
"negative secondary particle size": {
"R_n_sec": {"min": R_min_n, "max": R_max_n}
},
}
)
if (
options is not None
and options.positive["particle size"] == "distribution"
Expand All @@ -95,6 +107,18 @@ def battery_geometry(
"positive particle size": {"R_p": {"min": R_min_p, "max": R_max_p}},
}
)
phases = int(options.positive["particle phases"])
if phases >= 2:
geometry.update(
{
"positive primary particle size": {
"R_p_prim": {"min": R_min_p, "max": R_max_p}
},
"positive secondary particle size": {
"R_p_sec": {"min": R_min_p, "max": R_max_p}
},
}
)

# Add current collector domains
current_collector_dimension = options["dimensionality"]
Expand Down
39 changes: 39 additions & 0 deletions src/pybamm/geometry/standard_spatial_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
},
coord_sys="cartesian",
)

R_p = pybamm.SpatialVariable(
"R_p",
domain=["positive particle size"],
Expand All @@ -108,6 +109,44 @@
coord_sys="cartesian",
)

R_n_prim = pybamm.SpatialVariable(
"R_n_prim",
domain=["negative primary particle size"],
auxiliary_domains={
"secondary": "negative electrode",
"tertiary": "current collector",
},
coord_sys="cartesian",
)
R_p_prim = pybamm.SpatialVariable(
"R_p_prim",
domain=["positive primary particle size"],
auxiliary_domains={
"secondary": "positive electrode",
"tertiary": "current collector",
},
coord_sys="cartesian",
)

R_n_sec = pybamm.SpatialVariable(
"R_n_sec",
domain=["negative secondary particle size"],
auxiliary_domains={
"secondary": "negative electrode",
"tertiary": "current collector",
},
coord_sys="cartesian",
)
R_p_sec = pybamm.SpatialVariable(
"R_p_sec",
domain=["positive secondary particle size"],
auxiliary_domains={
"secondary": "positive electrode",
"tertiary": "current collector",
},
coord_sys="cartesian",
)

# Domains at cell edges
x_n_edge = pybamm.SpatialVariableEdge(
"x_n",
Expand Down
13 changes: 12 additions & 1 deletion src/pybamm/models/full_battery_models/base_battery_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,6 @@ def __init__(self, extra_options):
if options["particle phases"] not in ["1", ("1", "1")]:
if not (
options["surface form"] != "false"
and options["particle size"] == "single"
and options["particle"] == "Fickian diffusion"
):
raise pybamm.OptionError(
Expand Down Expand Up @@ -868,6 +867,10 @@ def default_var_pts(self):
"z": 10,
"R_n": 30,
"R_p": 30,
"R_n_prim": 30,
"R_p_prim": 30,
"R_n_sec": 30,
"R_p_sec": 30,
}
# Reduce the default points for 2D current collectors
if self.options["dimensionality"] == 2:
Expand All @@ -888,6 +891,10 @@ def default_submesh_types(self):
"positive secondary particle": pybamm.Uniform1DSubMesh,
"negative particle size": pybamm.Uniform1DSubMesh,
"positive particle size": pybamm.Uniform1DSubMesh,
"negative primary particle size": pybamm.Uniform1DSubMesh,
"positive primary particle size": pybamm.Uniform1DSubMesh,
"negative secondary particle size": pybamm.Uniform1DSubMesh,
"positive secondary particle size": pybamm.Uniform1DSubMesh,
}
if self.options["dimensionality"] == 0:
base_submeshes["current collector"] = pybamm.SubMesh0D
Expand All @@ -910,6 +917,10 @@ def default_spatial_methods(self):
"positive secondary particle": pybamm.FiniteVolume(),
"negative particle size": pybamm.FiniteVolume(),
"positive particle size": pybamm.FiniteVolume(),
"negative primary particle size": pybamm.FiniteVolume(),
"positive primary particle size": pybamm.FiniteVolume(),
"negative secondary particle size": pybamm.FiniteVolume(),
"positive secondary particle size": pybamm.FiniteVolume(),
}
if self.options["dimensionality"] == 0:
# 0D submesh - use base spatial method
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,19 @@ def _get_standard_active_material_variables(self, eps_solid):
R = self.phase_param.R
elif domain_options["particle size"] == "distribution":
if self.domain == "negative":
R_ = pybamm.standard_spatial_vars.R_n
if self.phase_name == "primary ":
R_ = pybamm.standard_spatial_vars.R_n_prim
elif self.phase_name == "secondary ":
R_ = pybamm.standard_spatial_vars.R_n_sec
else:
R_ = pybamm.standard_spatial_vars.R_n
elif self.domain == "positive":
R_ = pybamm.standard_spatial_vars.R_p
if self.phase_name == "primary ":
R_ = pybamm.standard_spatial_vars.R_p_prim
elif self.phase_name == "secondary ":
R_ = pybamm.standard_spatial_vars.R_p_sec
else:
R_ = pybamm.standard_spatial_vars.R_p
R = pybamm.size_average(R_)

R_av = pybamm.x_average(R)
Expand Down
43 changes: 36 additions & 7 deletions src/pybamm/models/submodels/interface/base_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def __init__(self, param, domain, reaction, options, phase="primary"):
self.reaction_name = self.phase_name + self.reaction_name

self.reaction = reaction
domain_options = getattr(self.options, domain)
self.size_distribution = domain_options["particle size"] == "distribution"

def _get_exchange_current_density(self, variables):
"""
Expand Down Expand Up @@ -93,8 +95,10 @@ def _get_exchange_current_density(self, variables):
# "current collector"
c_e = pybamm.PrimaryBroadcast(c_e, ["current collector"])
# broadcast c_e, T onto "particle size"
c_e = pybamm.PrimaryBroadcast(c_e, [f"{domain} particle size"])
T = pybamm.PrimaryBroadcast(T, [f"{domain} particle size"])
c_e = pybamm.PrimaryBroadcast(
c_e, [f"{domain} {phase_name}particle size"]
)
T = pybamm.PrimaryBroadcast(T, [f"{domain} {phase_name}particle size"])

else:
c_s_surf = variables[
Expand Down Expand Up @@ -215,13 +219,23 @@ def _get_standard_interfacial_current_variables(self, j):

# Size average. For j variables that depend on particle size, see
# "_get_standard_size_distribution_interfacial_current_variables"
if j.domain in [["negative particle size"], ["positive particle size"]]:
if j.domain in [
["negative particle size"],
["positive particle size"],
["negative primary particle size"],
["positive primary particle size"],
["negative secondary particle size"],
["positive secondary particle size"],
]:
j = pybamm.size_average(j)
# Average, and broadcast if necessary
j_av = pybamm.x_average(j)
if j.domain == []:
j = pybamm.FullBroadcast(j, f"{domain} electrode", "current collector")
elif j.domain == ["current collector"]:
elif (
j.domain == ["current collector"]
and getattr(self, "reaction_loc", None) != "interface"
):
j = pybamm.PrimaryBroadcast(j, f"{domain} electrode")

variables = {
Expand All @@ -230,6 +244,13 @@ def _get_standard_interfacial_current_variables(self, j):
f"X-averaged {domain} electrode {reaction_name}"
"interfacial current density [A.m-2]": j_av,
}
if self.phase_name == reaction_name:
variables.update(
{
f"{Domain} electrode {reaction_name}interfacial current density [A.m-2]": j,
f"X-averaged {domain} electrode {reaction_name}interfacial current density [A.m-2]": j_av,
}
)

return variables

Expand Down Expand Up @@ -298,7 +319,6 @@ def _get_standard_volumetric_current_density_variables(self, variables):
a = variables[
f"{Domain} electrode {phase_name}surface area to volume ratio [m-1]"
]

j = variables[
f"{Domain} electrode {reaction_name}interfacial current density [A.m-2]"
]
Expand Down Expand Up @@ -335,7 +355,14 @@ def _get_standard_overpotential_variables(self, eta_r):

# Size average. For eta_r variables that depend on particle size, see
# "_get_standard_size_distribution_overpotential_variables"
if eta_r.domain in [["negative particle size"], ["positive particle size"]]:
if eta_r.domain in [
["negative particle size"],
["positive particle size"],
["negative primary particle size"],
["positive primary particle size"],
["negative secondary particle size"],
["positive secondary particle size"],
]:
eta_r = pybamm.size_average(eta_r)

# X-average, and broadcast if necessary
Expand Down Expand Up @@ -410,9 +437,11 @@ def _get_standard_size_distribution_interfacial_current_variables(self, j):
if j.domains["secondary"] == [f"{domain} electrode"]:
# x-average
j_xav = pybamm.x_average(j)
else:
elif getattr(self, "reaction_loc", None) != "interface":
j_xav = j
j = pybamm.SecondaryBroadcast(j_xav, [f"{domain} electrode"])
else:
j_xav = j

variables = {
f"{Domain} electrode {reaction_name}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ def get_coupled_variables(self, variables):
self.reaction == "lithium-ion main"
and domain_options["particle size"] == "distribution"
):
delta_phi = pybamm.PrimaryBroadcast(delta_phi, [f"{domain} particle size"])
delta_phi = pybamm.PrimaryBroadcast(
delta_phi, [f"{domain} {phase_name}particle size"]
)

# Get exchange-current density. For MSMR models we calculate the exchange
# current density for each reaction, then sum these to give a total exchange
Expand Down Expand Up @@ -169,7 +171,7 @@ def get_coupled_variables(self, variables):
elif j0.domain in ["current collector", ["current collector"]]:
T = variables["X-averaged cell temperature [K]"]
u = variables[f"X-averaged {domain} electrode interface utilisation"]
elif j0.domain == [f"{domain} particle size"]:
elif j0.domain == [f"{domain} {phase_name}particle size"]:
if j0.domains["secondary"] != [f"{domain} electrode"]:
T = variables["X-averaged cell temperature [K]"]
u = variables[f"X-averaged {domain} electrode interface utilisation"]
Expand All @@ -178,7 +180,7 @@ def get_coupled_variables(self, variables):
u = variables[f"{Domain} electrode interface utilisation"]

# Broadcast T onto "particle size" domain
T = pybamm.PrimaryBroadcast(T, [f"{domain} particle size"])
T = pybamm.PrimaryBroadcast(T, [f"{domain} {phase_name}particle size"])
else:
T = variables[f"{Domain} electrode temperature [K]"]
u = variables[f"{Domain} electrode interface utilisation"]
Expand All @@ -198,7 +200,9 @@ def get_coupled_variables(self, variables):
else:
j = self._get_kinetics(j0, ne, eta_r, T, u)

if j.domain == [f"{domain} particle size"]:
if j.domain == [f"{domain} particle size"] or j.domain == [
f"{domain} {phase_name}particle size"
]:
# If j depends on particle size, get size-dependent "distribution"
# variables first
variables.update(
Expand Down
Loading
Loading