Skip to content

Commit a1aa02c

Browse files
authored
Composite esoh half cell (#5179)
* first pass * Add comprehensive tests for composite electrode half-cell - Add TestElectrodeSOHHalfCell class with composite electrode tests - Create _get_params_and_options_composite_half_cell helper method - Test composite positive electrode using Chen2020_composite negative parameters - Add voltage and SOC initialization tests with proper tolerances (1e-5) - Set voltage cutoffs: lower=0.00001V, upper=2.5V - Remove unnecessary parameters (particle radii, exchange current densities, diffusion coefficients) - Include comprehensive error handling tests - Use proper pytest.approx() assertions instead of manual abs() comparisons All tests pass and validate composite electrode SOH functionality. * fix test * coverage
1 parent acff2a2 commit a1aa02c

File tree

4 files changed

+530
-33
lines changed

4 files changed

+530
-33
lines changed

src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py

Lines changed: 107 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#
44
import pybamm
55

6+
from .util import check_if_composite
7+
68

79
class ElectrodeSOHHalfCell(pybamm.BaseModel):
810
"""Model to calculate electrode-specific SOH for a half-cell, adapted from
@@ -21,17 +23,32 @@ class ElectrodeSOHHalfCell(pybamm.BaseModel):
2123
2224
"""
2325

24-
def __init__(self, name="ElectrodeSOH model"):
26+
def __init__(self, name="ElectrodeSOH model", options=None):
2527
pybamm.citations.register("Mohtat2019")
2628
super().__init__(name)
27-
param = pybamm.LithiumIonParameters({"working electrode": "positive"})
29+
if options is None:
30+
options = {"working electrode": "positive"}
31+
is_composite = check_if_composite(options, "positive")
32+
param = pybamm.LithiumIonParameters(options)
2833

2934
x_100 = pybamm.Variable("x_100", bounds=(0, 1))
3035
x_0 = pybamm.Variable("x_0", bounds=(0, 1))
36+
if is_composite:
37+
x_100_2 = pybamm.Variable("x_100_2", bounds=(0, 1))
38+
x_0_2 = pybamm.Variable("x_0_2", bounds=(0, 1))
3139
Q_w = pybamm.InputParameter("Q_w")
40+
if is_composite:
41+
Q_w_2 = pybamm.InputParameter("Q_w_2")
3242
T_ref = param.T_ref
3343
U_w = param.p.prim.U
34-
Q = Q_w * (x_0 - x_100)
44+
if is_composite:
45+
U_w_2 = param.p.sec.U
46+
Q_1 = Q_w * (x_0 - x_100)
47+
if is_composite:
48+
Q_2 = Q_w_2 * (x_0_2 - x_100_2)
49+
else:
50+
Q_2 = pybamm.Scalar(0)
51+
Q = Q_1 + Q_2
3552

3653
V_max = param.ocp_soc_100
3754
V_min = param.ocp_soc_0
@@ -40,7 +57,13 @@ def __init__(self, name="ElectrodeSOH model"):
4057
x_100: U_w(x_100, T_ref) - V_max,
4158
x_0: U_w(x_0, T_ref) - V_min,
4259
}
60+
if is_composite:
61+
self.algebraic[x_100_2] = U_w_2(x_100_2, T_ref) - V_max
62+
self.algebraic[x_0_2] = U_w_2(x_0_2, T_ref) - V_min
4363
self.initial_conditions = {x_100: 0.8, x_0: 0.2}
64+
if is_composite:
65+
self.initial_conditions[x_100_2] = 0.8
66+
self.initial_conditions[x_0_2] = 0.2
4467

4568
self.variables = {
4669
"x_100": x_100,
@@ -50,6 +73,12 @@ def __init__(self, name="ElectrodeSOH model"):
5073
"Uw(x_0)": U_w(x_0, T_ref),
5174
"Q_w": Q_w,
5275
}
76+
if is_composite:
77+
self.variables["x_100_2"] = x_100_2
78+
self.variables["x_0_2"] = x_0_2
79+
self.variables["Q_w_2"] = Q_w_2
80+
self.variables["Uw(x_100_2)"] = U_w_2(x_100_2, T_ref)
81+
self.variables["Uw(x_0_2)"] = U_w_2(x_0_2, T_ref)
5382

5483
@property
5584
def default_solver(self):
@@ -94,7 +123,13 @@ def get_initial_stoichiometry_half_cell(
94123
The initial stoichiometry that give the desired initial state of charge
95124
"""
96125
param = pybamm.LithiumIonParameters(options)
97-
x_0, x_100 = get_min_max_stoichiometries(parameter_values, inputs=inputs)
126+
x_dict = get_min_max_stoichiometries(
127+
parameter_values, inputs=inputs, options=options
128+
)
129+
x_0, x_100 = x_dict["x_0"], x_dict["x_100"]
130+
is_composite = check_if_composite(options, "positive")
131+
if is_composite:
132+
x_0_2, x_100_2 = x_dict["x_0_2"], x_dict["x_100_2"]
98133

99134
if isinstance(initial_value, str) and initial_value.endswith("V"):
100135
V_init = float(initial_value[:-1])
@@ -106,38 +141,67 @@ def get_initial_stoichiometry_half_cell(
106141
f"Initial voltage {V_init}V is outside the voltage limits "
107142
f"({V_min}, {V_max})"
108143
)
109-
110144
# Solve simple model for initial soc based on target voltage
111-
soc_model = pybamm.BaseModel()
112-
soc = pybamm.Variable("soc")
145+
model = pybamm.BaseModel()
146+
x = pybamm.Variable("x")
113147
Up = param.p.prim.U
114148
T_ref = parameter_values["Reference temperature [K]"]
115-
x = x_0 + soc * (x_100 - x_0)
116-
117-
soc_model.algebraic[soc] = Up(x, T_ref) - V_init
118-
# initial guess for soc linearly interpolates between 0 and 1
149+
model.variables["x"] = x
150+
model.algebraic[x] = Up(x, T_ref) - V_init
151+
# initial guess for x linearly interpolates between 0 and 1
119152
# based on V linearly interpolating between V_max and V_min
120-
soc_model.initial_conditions[soc] = (V_init - V_min) / (V_max - V_min)
121-
soc_model.variables["soc"] = soc
122-
parameter_values.process_model(soc_model)
123-
initial_soc = (
124-
pybamm.AlgebraicSolver(tol=tol)
125-
.solve(soc_model, [0], inputs=inputs)["soc"]
126-
.data[0]
153+
soc_initial_guess = (V_init - V_min) / (V_max - V_min)
154+
model.initial_conditions[x] = 1 - soc_initial_guess
155+
if is_composite:
156+
Up_2 = param.p.sec.U
157+
x_2 = pybamm.Variable("x_2")
158+
model.algebraic[x_2] = Up_2(x_2, T_ref) - V_init
159+
model.variables["x_2"] = x_2
160+
model.initial_conditions[x_2] = 1 - soc_initial_guess
161+
162+
parameter_values.process_model(model)
163+
sol = pybamm.AlgebraicSolver("lsq__trf", tol=tol).solve(
164+
model, [0], inputs=inputs
127165
)
166+
x = sol["x"].data[0]
167+
if is_composite:
168+
x_2 = sol["x_2"].data[0]
128169
elif isinstance(initial_value, int | float):
129-
initial_soc = initial_value
130-
if not 0 <= initial_soc <= 1:
170+
if not 0 <= initial_value <= 1:
131171
raise ValueError("Initial SOC should be between 0 and 1")
132-
172+
if not is_composite:
173+
x = x_0 + initial_value * (x_100 - x_0)
174+
else:
175+
model = pybamm.BaseModel()
176+
x = pybamm.Variable("x")
177+
x_2 = pybamm.Variable("x_2")
178+
U_p = param.p.prim.U
179+
U_p_2 = param.p.sec.U
180+
T_ref = parameter_values["Reference temperature [K]"]
181+
model.algebraic[x] = U_p(x, T_ref) - U_p_2(x_2, T_ref)
182+
model.initial_conditions[x] = x_0 + initial_value * (x_100 - x_0)
183+
model.initial_conditions[x_2] = x_0_2 + initial_value * (x_100_2 - x_0_2)
184+
Q_w = parameter_values.evaluate(param.p.prim.Q_init, inputs=inputs)
185+
Q_w_2 = parameter_values.evaluate(param.p.sec.Q_init, inputs=inputs)
186+
Q_min = x_100 * Q_w + x_100_2 * Q_w_2
187+
Q_max = x_0 * Q_w + x_0_2 * Q_w_2
188+
Q_now = Q_w * x + Q_w_2 * x_2
189+
soc = (Q_now - Q_min) / (Q_max - Q_min)
190+
model.algebraic[x_2] = soc - initial_value
191+
model.variables["x"] = x
192+
model.variables["x_2"] = x_2
193+
parameter_values.process_model(model)
194+
sol = pybamm.AlgebraicSolver(tol=tol).solve(model, [0], inputs=inputs)
195+
x = sol["x"].data[0]
196+
x_2 = sol["x_2"].data[0]
133197
else:
134198
raise ValueError(
135199
"Initial value must be a float between 0 and 1, or a string ending in 'V'"
136200
)
137-
138-
x = x_0 + initial_soc * (x_100 - x_0)
139-
140-
return x
201+
ret_dict = {"x": x}
202+
if is_composite:
203+
ret_dict["x_2"] = x_2
204+
return ret_dict
141205

142206

143207
def get_min_max_stoichiometries(parameter_values, options=None, inputs=None):
@@ -156,12 +220,26 @@ def get_min_max_stoichiometries(parameter_values, options=None, inputs=None):
156220
inputs = inputs or {}
157221
if options is None:
158222
options = {"working electrode": "positive"}
159-
esoh_model = pybamm.lithium_ion.ElectrodeSOHHalfCell("ElectrodeSOH")
223+
esoh_model = pybamm.lithium_ion.ElectrodeSOHHalfCell(
224+
"ElectrodeSOH", options=options
225+
)
160226
param = pybamm.LithiumIonParameters(options)
161-
Q_w = parameter_values.evaluate(param.p.Q_init, inputs=inputs)
227+
is_composite = check_if_composite(options, "positive")
228+
if is_composite:
229+
Q_w = parameter_values.evaluate(param.p.prim.Q_init, inputs=inputs)
230+
Q_w_2 = parameter_values.evaluate(param.p.sec.Q_init, inputs=inputs)
231+
Q_inputs = {"Q_w": Q_w, "Q_w_2": Q_w_2}
232+
else:
233+
Q_w = parameter_values.evaluate(param.p.prim.Q_init, inputs=inputs)
234+
Q_inputs = {"Q_w": Q_w}
162235
# Add Q_w to input parameters
163-
all_inputs = {**inputs, "Q_w": Q_w}
236+
all_inputs = {**inputs, **Q_inputs}
164237
esoh_sim = pybamm.Simulation(esoh_model, parameter_values=parameter_values)
165238
esoh_sol = esoh_sim.solve([0], inputs=all_inputs)
166239
x_0, x_100 = esoh_sol["x_0"].data[0], esoh_sol["x_100"].data[0]
167-
return x_0, x_100
240+
if is_composite:
241+
x_0_2, x_100_2 = esoh_sol["x_0_2"].data[0], esoh_sol["x_100_2"].data[0]
242+
ret_dict = {"x_0": x_0, "x_100": x_100, "x_0_2": x_0_2, "x_100_2": x_100_2}
243+
else:
244+
ret_dict = {"x_0": x_0, "x_100": x_100}
245+
return ret_dict

src/pybamm/models/full_battery_models/lithium_ion/initial_state.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def set_initial_state(
6969
Set the initial stoichiometry of the working electrode, based on the initial
7070
SOC or voltage.
7171
"""
72-
x = pybamm.lithium_ion.get_initial_stoichiometry_half_cell(
72+
results = pybamm.lithium_ion.get_initial_stoichiometry_half_cell(
7373
initial_value,
7474
parameter_values,
7575
param=param,
@@ -78,8 +78,24 @@ def set_initial_state(
7878
inputs=inputs,
7979
)
8080
_set_concentration_from_stoich(
81-
parameter_values, param, "positive", "primary", x, inputs, options
81+
parameter_values,
82+
param,
83+
"positive",
84+
"primary",
85+
results["x"],
86+
inputs,
87+
options,
8288
)
89+
if check_if_composite(options, "positive"):
90+
_set_concentration_from_stoich(
91+
parameter_values,
92+
param,
93+
"positive",
94+
"secondary",
95+
results["x_2"],
96+
inputs,
97+
options,
98+
)
8399
elif options is not None and (
84100
check_if_composite(options, "positive")
85101
or check_if_composite(options, "negative")

0 commit comments

Comments
 (0)