#
# Base class for particle cracking models.
#
from __future__ import annotations
from typing import TYPE_CHECKING
import pybamm
if TYPE_CHECKING:
from typing import Any
[docs]
class BaseMechanics(pybamm.BaseSubModel):
"""
Base class for particle mechanics models, referenced from :footcite:t:`Ai2019` and
:footcite:t:`Deshpande2012`.
Parameters
----------
param : parameter class
The parameters to use for this submodel
domain : dict, optional
Dictionary of either the electrode for "positive" or "Negative"
options: dict
A dictionary of options to be passed to the model.
See :class:`pybamm.BaseBatteryModel`
phase : str, optional
Phase of the particle (default is "primary")
"""
# Physical constants for stress/displacement calculations (Ai2019)
STRESS_GEOMETRIC_FACTOR = 3.0
DISPLACEMENT_GEOMETRIC_FACTOR = 3.0
CRACK_ROUGHNESS_FACTOR = 2.0
def __init__(
self,
param: Any,
domain: str | dict[str, str],
options: dict[str, Any],
phase: str = "primary",
) -> None:
self.size_distribution = options["particle size"] == "distribution"
super().__init__(param, domain, options=options, phase=phase)
def _build_var_name(
self, quantity: str, unit: str = "", averaged: bool = False
) -> str:
"""
Construct standardized variable names for mechanics quantities.
Parameters
----------
quantity : str
The physical quantity (e.g., "particle crack length")
unit : str, optional
The unit of measurement (e.g., "[m]")
averaged : bool, optional
Whether this is an x-averaged quantity
Returns
-------
str
The formatted variable name
"""
domain, Domain = self.domain_Domain
phase_name = self.phase_param.phase_name
prefix = f"X-averaged {domain}" if averaged else Domain
unit_str = f" {unit}" if unit else ""
return f"{prefix} {phase_name}{quantity}{unit_str}"
def _get_standard_variables(self, l_cr: pybamm.Symbol) -> dict[str, pybamm.Symbol]:
"""Generate standard crack length variables."""
l_cr_av = pybamm.x_average(l_cr)
return {
self._build_var_name("particle crack length", "[m]"): l_cr,
self._build_var_name(
"particle crack length", "[m]", averaged=True
): l_cr_av,
}
def _get_standard_size_distribution_variables(
self, l_cr_dist: pybamm.Symbol
) -> dict[str, pybamm.Symbol]:
"""Generate standard crack length distribution variables."""
l_cr_av_dist = pybamm.x_average(l_cr_dist)
return {
self._build_var_name(
"particle crack length distribution", "[m]"
): l_cr_dist,
self._build_var_name(
"particle crack length distribution", "[m]", averaged=True
): l_cr_av_dist,
}
def _get_mechanical_size_distribution_results(
self, variables: dict[str, pybamm.Symbol]
) -> dict[str, pybamm.Symbol]:
"""
Calculate mechanical results for particle size distributions.
Computes stress and displacement for particle size distributions,
following the models in Ai2019 and Deshpande2012.
"""
domain, Domain = self.domain_Domain
phase_name = self.phase_param.phase_name
phase_param = self.phase_param
# Extract concentration variables
c_s_rav = variables[
f"R-averaged {domain} {phase_name}"
"particle concentration distribution [mol.m-3]"
]
c_s_surf = variables[
f"{Domain} {phase_name}"
"particle surface concentration distribution [mol.m-3]"
]
sto = variables[f"{Domain} {phase_name}particle concentration distribution"]
# Broadcast temperature to particle size and particle domains
T = self._broadcast_temperature_to_particle_size(variables)
# Compute material properties (tangential approximation for Omega)
Omega = pybamm.r_average(phase_param.Omega(sto, T))
E = pybamm.r_average(phase_param.E(sto, T))
# Calculate stress and displacement (Ai2019 equations)
stress_disp = self._compute_distribution_stress_displacement(
c_s_rav, c_s_surf, Omega, E, phase_param
)
# Add results to variables dictionary
self._add_distribution_mechanics_variables(variables, stress_disp)
return variables
def _broadcast_temperature_to_particle_size(
self, variables: dict[str, pybamm.Symbol]
) -> pybamm.Symbol:
"""Broadcast electrode temperature to particle size and particle domains."""
domain, Domain = self.domain_Domain
phase_name = self.phase_param.phase_name
T = pybamm.PrimaryBroadcast(
variables[f"{Domain} electrode temperature [K]"],
[f"{domain} {phase_name}particle size"],
)
return pybamm.PrimaryBroadcast(T, [f"{domain} {phase_name}particle"])
def _compute_distribution_stress_displacement(
self,
c_s_rav: pybamm.Symbol,
c_s_surf: pybamm.Symbol,
Omega: pybamm.Symbol,
E: pybamm.Symbol,
phase_param: Any,
) -> dict[str, pybamm.Symbol]:
"""
Compute stress and displacement for size distributions.
Based on Ai2019 equations:
- Eq [7]: Radial stress at surface (r=R) is zero
- Eq [8]: Tangential stress from concentration gradient
- Eq [10]: Surface displacement from volume change
"""
c_0 = phase_param.c_0
R0 = phase_param.R
nu = phase_param.nu
# Surface displacement (Ai2019 eq [10])
# c_0 is reference concentration for no deformation
disp_surf = Omega * R0 * (c_s_rav - c_0) / self.DISPLACEMENT_GEOMETRIC_FACTOR
# Surface stresses (Ai2019 eqs [7-8])
# Radial stress at particle surface is zero (eq [7] with r=R)
stress_r_surf = pybamm.Scalar(0)
# Tangential stress (eq [8] with r=R)
# Note: c_s_rav is already multiplied by 3/R^3 inside r_average
stress_t_surf = (
Omega * E * (c_s_rav - c_s_surf) / self.STRESS_GEOMETRIC_FACTOR / (1.0 - nu)
)
return {
"stress_r_surf": stress_r_surf,
"stress_t_surf": stress_t_surf,
"disp_surf": disp_surf,
"stress_r_surf_av": pybamm.x_average(stress_r_surf),
"stress_t_surf_av": pybamm.x_average(stress_t_surf),
"disp_surf_av": pybamm.x_average(disp_surf),
}
def _add_distribution_mechanics_variables(
self, variables: dict[str, pybamm.Symbol], stress_disp: dict[str, pybamm.Symbol]
) -> None:
"""Add distribution mechanics variables to the variables dictionary."""
domain, Domain = self.domain_Domain
phase_name = self.phase_param.phase_name
variables.update(
{
f"{Domain} {phase_name}"
"particle surface radial stress distribution [Pa]": stress_disp[
"stress_r_surf"
],
f"{Domain} {phase_name}"
"particle surface tangential stress distribution [Pa]": stress_disp[
"stress_t_surf"
],
f"{Domain} {phase_name}"
"particle surface displacement distribution [m]": stress_disp[
"disp_surf"
],
f"X-averaged {domain} {phase_name}"
"particle surface radial stress distribution [Pa]": stress_disp[
"stress_r_surf_av"
],
f"X-averaged {domain} {phase_name}"
"particle surface tangential stress distribution [Pa]": stress_disp[
"stress_t_surf_av"
],
f"X-averaged {domain} {phase_name}"
"particle surface displacement distribution [m]": stress_disp[
"disp_surf_av"
],
}
)
def _get_mechanical_results(
self, variables: dict[str, pybamm.Symbol]
) -> dict[str, pybamm.Symbol]:
"""
Calculate mechanical results including stress, displacement, and thickness changes.
This method computes particle surface stress and displacement based on
concentration gradients, following the mechanical models in Ai2019 and
Deshpande2012.
"""
# Extract required variables and compute particle mechanics
mech_vars = self._extract_mechanical_variables(variables)
stress_disp = self._compute_stress_and_displacement(mech_vars)
# Update variables with computed results
self._add_particle_mechanics_variables(variables, stress_disp)
# Aggregate thickness changes at electrode and cell level
self._aggregate_thickness_changes(variables, mech_vars)
return variables
def _extract_mechanical_variables(
self, variables: dict[str, pybamm.Symbol]
) -> dict[str, Any]:
"""Extract and compute intermediate variables needed for mechanics calculations."""
domain, Domain = self.domain_Domain
phase_name = self.phase_name
phase_param = self.phase_param
c_s_rav = variables[
f"R-averaged {domain} {phase_name}particle concentration [mol.m-3]"
]
sto_rav = variables[f"R-averaged {domain} {phase_name}particle concentration"]
c_s_surf = variables[
f"{Domain} {phase_name}particle surface concentration [mol.m-3]"
]
T_xav = variables["X-averaged cell temperature [K]"]
# Broadcast temperature to particle domain
T = pybamm.PrimaryBroadcast(
variables[f"{Domain} electrode temperature [K]"],
[f"{domain} {phase_name}particle"],
)
eps_s = variables[
f"{Domain} electrode {phase_name}active material volume fraction"
]
sto = variables[f"{Domain} {phase_name}particle concentration"]
# Compute material properties (tangential approximation for Omega)
Omega = pybamm.r_average(phase_param.Omega(sto, T))
E = pybamm.r_average(phase_param.E(sto, T))
sto_init = pybamm.r_average(phase_param.c_init / phase_param.c_max)
# Compute volume change for thickness calculation
v_change = pybamm.x_average(
eps_s * phase_param.t_change(sto_rav)
) - pybamm.x_average(eps_s * phase_param.t_change(sto_init))
electrode_thickness_change = (
self.param.n_electrodes_parallel * v_change * self.domain_param.L
)
return {
"c_s_rav": c_s_rav,
"c_s_surf": c_s_surf,
"c_0": phase_param.c_0,
"R": phase_param.R,
"Omega": Omega,
"E": E,
"nu": phase_param.nu,
"T_xav": T_xav,
"electrode_thickness_change": electrode_thickness_change,
}
def _compute_stress_and_displacement(
self, mech_vars: dict[str, Any]
) -> dict[str, pybamm.Symbol]:
"""
Compute particle surface stress and displacement.
Based on Ai2019 equations:
- Eq [7]: Radial stress at surface (r=R) is zero
- Eq [8]: Tangential stress from concentration gradient
- Eq [10]: Surface displacement from volume change
"""
# Surface displacement (Ai2019 eq [10])
# Reference concentration c_0 gives zero deformation
disp_surf = (
mech_vars["Omega"]
* mech_vars["R"]
* (mech_vars["c_s_rav"] - mech_vars["c_0"])
/ self.DISPLACEMENT_GEOMETRIC_FACTOR
)
# Surface stresses (Ai2019 eqs [7-8])
# Radial stress at particle surface is zero (eq [7] with r=R)
stress_r_surf = pybamm.Scalar(0)
# Tangential stress from concentration gradient (eq [8] with r=R)
# Note: c_s_rav is already multiplied by 3/R^3 inside r_average
stress_t_surf = (
mech_vars["Omega"]
* mech_vars["E"]
* (mech_vars["c_s_rav"] - mech_vars["c_s_surf"])
/ self.STRESS_GEOMETRIC_FACTOR
/ (1.0 - mech_vars["nu"])
)
return {
"stress_r_surf": stress_r_surf,
"stress_t_surf": stress_t_surf,
"disp_surf": disp_surf,
"stress_r_surf_av": pybamm.x_average(stress_r_surf),
"stress_t_surf_av": pybamm.x_average(stress_t_surf),
"disp_surf_av": pybamm.x_average(disp_surf),
"electrode_thickness_change": mech_vars["electrode_thickness_change"],
}
def _add_particle_mechanics_variables(
self,
variables: dict[str, pybamm.Symbol],
stress_disp: dict[str, pybamm.Symbol],
) -> None:
"""Add computed stress and displacement variables to the variables dictionary."""
domain, Domain = self.domain_Domain
phase_name = self.phase_name
variables.update(
{
# Spatial distributions
f"{Domain} {phase_name}particle surface radial stress [Pa]": stress_disp[
"stress_r_surf"
],
f"{Domain} {phase_name}particle surface tangential stress [Pa]": stress_disp[
"stress_t_surf"
],
f"{Domain} {phase_name}particle surface displacement [m]": stress_disp[
"disp_surf"
],
# X-averaged quantities
f"X-averaged {domain} {phase_name}particle surface radial stress [Pa]": stress_disp[
"stress_r_surf_av"
],
f"X-averaged {domain} {phase_name}particle surface tangential stress [Pa]": stress_disp[
"stress_t_surf_av"
],
f"X-averaged {domain} {phase_name}particle surface displacement [m]": stress_disp[
"disp_surf_av"
],
# Thickness change
f"{Domain} electrode {phase_name}thickness change [m]": stress_disp[
"electrode_thickness_change"
],
}
)
def _aggregate_thickness_changes(
self, variables: dict[str, pybamm.Symbol], mech_vars: dict[str, Any]
) -> None:
"""
Aggregate thickness changes from phases to electrode level and from
electrodes to cell level.
"""
_domain, Domain = self.domain_Domain
# Aggregate primary and secondary phase thickness changes
self._aggregate_phase_thickness_changes(variables, Domain)
# Aggregate negative and positive electrode thickness changes to cell level
self._aggregate_cell_thickness_change(variables, mech_vars["T_xav"])
def _aggregate_phase_thickness_changes(
self, variables: dict[str, pybamm.Symbol], Domain: str
) -> None:
"""Combine primary and secondary phase thickness changes for an electrode."""
primary_key = f"{Domain} electrode primary thickness change [m]"
secondary_key = f"{Domain} electrode secondary thickness change [m]"
combined_key = f"{Domain} electrode thickness change [m]"
if primary_key in variables and secondary_key in variables:
variables[combined_key] = variables[primary_key] + variables[secondary_key]
def _aggregate_cell_thickness_change(
self, variables: dict[str, pybamm.Symbol], T_xav: pybamm.Symbol
) -> None:
"""
Calculate total cell thickness change from electrode changes and thermal expansion.
Based on Ai2019 eq [13] for thermal expansion contribution.
"""
neg_key = "Negative electrode thickness change [m]"
pos_key = "Positive electrode thickness change [m]"
if neg_key in variables and pos_key in variables:
# Thermal expansion contribution (Ai2019 eq [13])
thermal_expansion = self.param.alpha_T_cell * (T_xav - self.param.T_ref)
# Total cell thickness change
variables["Cell thickness change [m]"] = (
variables[neg_key] + variables[pos_key] + thermal_expansion
)
def _get_standard_surface_variables(
self, variables: dict[str, pybamm.Symbol]
) -> dict[str, pybamm.Symbol]:
"""Calculate crack surface variables including roughness and surface area ratio."""
domain, Domain = self.domain_Domain
phase_name = self.phase_param.phase_name
l_cr = variables[f"{Domain} {phase_name}particle crack length [m]"]
a = variables[
f"{Domain} electrode {phase_name}surface area to volume ratio [m-1]"
]
rho_cr = self.phase_param.rho_cr
w_cr = self.phase_param.w_cr
# Roughness is ratio of cracks to normal surface
roughness = 1 + self.CRACK_ROUGHNESS_FACTOR * l_cr * rho_cr * w_cr
# Crack surface area to volume ratio
a_cr = (roughness - 1) * a
roughness_xavg = pybamm.x_average(roughness)
return {
f"{Domain} {phase_name}crack surface to volume ratio [m-1]": a_cr,
f"{Domain} {phase_name}electrode roughness ratio": roughness,
f"X-averaged {domain} {phase_name}electrode roughness ratio": roughness_xavg,
}