Skip to content

Commit

Permalink
Merge pull request #3921 from abillscmu/fix-esoh-input-params-try2
Browse files Browse the repository at this point in the history
Allow input parameters in ESOH model
  • Loading branch information
valentinsulzer authored Mar 27, 2024
2 parents 43b516f + 8b2d812 commit a3db966
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 56 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- Updated `_steps_util.py` to throw a specific exception when drive cycle starts at t>0 ([#3756](https://github.com/pybamm-team/PyBaMM/pull/3756))
- Updated `plot_voltage_components.py` to support both `Simulation` and `Solution` objects. Added new methods in both `Simulation` and `Solution` classes for allow the syntax `simulation.plot_voltage_components` and `solution.plot_voltage_components`. Updated `test_plot_voltage_components.py` to reflect these changes ([#3723](https://github.com/pybamm-team/PyBaMM/pull/3723)).
- The SEI thickness decreased at some intervals when the 'electron-migration limited' model was used. It has been corrected ([#3622](https://github.com/pybamm-team/PyBaMM/pull/3622))
- Allow input parameters in ESOH model ([#3921](https://github.com/pybamm-team/PyBaMM/pull/3921))

## Optimizations

Expand Down
53 changes: 34 additions & 19 deletions pybamm/models/full_battery_models/lithium_ion/electrode_soh.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ def __init__(
Q_Li = pybamm.InputParameter("Q_Li")
elif known_value == "cell capacity":
Q = pybamm.InputParameter("Q")
else:
raise ValueError(
"Known value must be cell capacity or cyclable lithium capacity"
)

# Define variables for 100% state of charge
if "x_100" in solve_for:
Expand Down Expand Up @@ -207,6 +211,10 @@ def __init__(
Q_Li = pybamm.InputParameter("Q_Li")
elif known_value == "cell capacity":
Q = pybamm.InputParameter("Q")
else:
raise ValueError(
"Known value must be cell capacity or cyclable lithium capacity"
)

# Define variables for 0% state of charge
# TODO: thermal effects (include dU/dT)
Expand Down Expand Up @@ -287,6 +295,10 @@ def __init__(
):
self.parameter_values = parameter_values
self.param = param or pybamm.LithiumIonParameters(options)
if known_value not in ["cell capacity", "cyclable lithium capacity"]:
raise ValueError(
"Known value must be cell capacity or cyclable lithium capacity"
)
self.known_value = known_value
self.options = options or pybamm.BatteryModelOptions({})

Expand Down Expand Up @@ -380,7 +392,8 @@ def solve(self, inputs):
# Calculate theoretical energy
# TODO: energy calc for MSMR
if self.options["open-circuit potential"] != "MSMR":
energy = self.theoretical_energy_integral(sol_dict)
energy_inputs = {**sol_dict, **inputs}
energy = self.theoretical_energy_integral(energy_inputs)
sol_dict.update({"Maximum theoretical energy [W.h]": energy})
return sol_dict

Expand Down Expand Up @@ -604,12 +617,10 @@ def _check_esoh_feasible(self, inputs):
else:
# address numpy 1.25 deprecation warning: array should have ndim=0
# before conversion
V_lower_bound = float(
self.OCV_function.evaluate(inputs={"x": x0_min, "y": y0_max}).item()
)
V_upper_bound = float(
self.OCV_function.evaluate(inputs={"x": x100_max, "y": y100_min}).item()
)
all_inputs = {**inputs, "x": x0_min, "y": y0_max}
V_lower_bound = float(self.OCV_function.evaluate(inputs=all_inputs).item())
all_inputs.update({"x": x100_max, "y": y100_min})
V_upper_bound = float(self.OCV_function.evaluate(inputs=all_inputs).item())

# Check that the min and max achievable voltages span wider than the desired
# voltage range
Expand All @@ -632,7 +643,7 @@ def _check_esoh_feasible(self, inputs):
)
)

def get_initial_stoichiometries(self, initial_value, tol=1e-6):
def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None):
"""
Calculate initial stoichiometries to start off the simulation at a particular
state of charge, given voltage limits, open-circuit potentials, etc defined by
Expand All @@ -657,7 +668,7 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6):
"""
parameter_values = self.parameter_values
param = self.param
x_0, x_100, y_100, y_0 = self.get_min_max_stoichiometries()
x_0, x_100, y_100, y_0 = self.get_min_max_stoichiometries(inputs=inputs)

if isinstance(initial_value, str) and initial_value.endswith("V"):
V_init = float(initial_value[:-1])
Expand Down Expand Up @@ -714,7 +725,7 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6):

return x, y

def get_min_max_stoichiometries(self):
def get_min_max_stoichiometries(self, inputs=None):
"""
Calculate min/max stoichiometries
given voltage limits, open-circuit potentials, etc defined by parameter_values
Expand All @@ -724,20 +735,23 @@ def get_min_max_stoichiometries(self):
x_0, x_100, y_100, y_0
The min/max stoichiometries
"""
inputs = inputs or {}
parameter_values = self.parameter_values
param = self.param

Q_n = parameter_values.evaluate(param.n.Q_init)
Q_p = parameter_values.evaluate(param.p.Q_init)
Q_n = parameter_values.evaluate(param.n.Q_init, inputs=inputs)
Q_p = parameter_values.evaluate(param.p.Q_init, inputs=inputs)

if self.known_value == "cyclable lithium capacity":
Q_Li = parameter_values.evaluate(param.Q_Li_particles_init)
inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}
Q_Li = parameter_values.evaluate(param.Q_Li_particles_init, inputs=inputs)
all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}
elif self.known_value == "cell capacity":
Q = parameter_values.evaluate(param.Q / param.n_electrodes_parallel)
inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q": Q}
Q = parameter_values.evaluate(
param.Q / param.n_electrodes_parallel, inputs=inputs
)
all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q": Q}
# Solve the model and check outputs
sol = self.solve(inputs)
sol = self.solve(all_inputs)
return [sol["x_0"], sol["x_100"], sol["y_100"], sol["y_0"]]

def get_initial_ocps(self, initial_value, tol=1e-6):
Expand Down Expand Up @@ -814,7 +828,7 @@ def theoretical_energy_integral(self, inputs, points=1000):
param = self.param
T = param.T_amb_av(0)
Vs = self.parameter_values.evaluate(
param.p.prim.U(y_vals, T) - param.n.prim.U(x_vals, T)
param.p.prim.U(y_vals, T) - param.n.prim.U(x_vals, T), inputs=inputs
).flatten()
# Calculate dQ
Q = Q_p * (y_0 - y_100)
Expand All @@ -831,6 +845,7 @@ def get_initial_stoichiometries(
known_value="cyclable lithium capacity",
options=None,
tol=1e-6,
inputs=None,
):
"""
Calculate initial stoichiometries to start off the simulation at a particular
Expand Down Expand Up @@ -866,7 +881,7 @@ def get_initial_stoichiometries(
The initial stoichiometries that give the desired initial state of charge
"""
esoh_solver = ElectrodeSOHSolver(parameter_values, param, known_value, options)
return esoh_solver.get_initial_stoichiometries(initial_value, tol)
return esoh_solver.get_initial_stoichiometries(initial_value, tol, inputs=inputs)


def get_min_max_stoichiometries(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ def get_initial_stoichiometry_half_cell(
initial_value,
parameter_values,
param=None,
known_value="cyclable lithium capacity",
options=None,
inputs=None,
**kwargs,
):
"""
Calculate initial stoichiometry to start off the simulation at a particular
Expand All @@ -86,7 +87,7 @@ def get_initial_stoichiometry_half_cell(
The initial stoichiometry that give the desired initial state of charge
"""
param = pybamm.LithiumIonParameters(options)
x_0, x_100 = get_min_max_stoichiometries(parameter_values)
x_0, x_100 = get_min_max_stoichiometries(parameter_values, inputs=inputs)

if isinstance(initial_value, str) and initial_value.endswith("V"):
V_init = float(initial_value[:-1])
Expand Down Expand Up @@ -129,7 +130,7 @@ def get_initial_stoichiometry_half_cell(
return x


def get_min_max_stoichiometries(parameter_values, options=None):
def get_min_max_stoichiometries(parameter_values, options=None, inputs=None):
"""
Get the minimum and maximum stoichiometries from the parameter values
Expand All @@ -142,12 +143,15 @@ def get_min_max_stoichiometries(parameter_values, options=None):
:class:`pybamm.BatteryModelOptions`.
If None, the default is used: {"working electrode": "positive"}
"""
inputs = inputs or {}
if options is None:
options = {"working electrode": "positive"}
esoh_model = pybamm.lithium_ion.ElectrodeSOHHalfCell("ElectrodeSOH")
param = pybamm.LithiumIonParameters(options)
Q_w = parameter_values.evaluate(param.p.Q_init, inputs=inputs)
# Add Q_w to input parameters
all_inputs = {**inputs, "Q_w": Q_w}
esoh_sim = pybamm.Simulation(esoh_model, parameter_values=parameter_values)
Q_w = parameter_values.evaluate(param.p.Q_init)
esoh_sol = esoh_sim.solve([0], inputs={"Q_w": Q_w})
esoh_sol = esoh_sim.solve([0], inputs=all_inputs)
x_0, x_100 = esoh_sol["x_0"].data[0], esoh_sol["x_100"].data[0]
return x_0, x_100
22 changes: 19 additions & 3 deletions pybamm/parameters/parameter_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,14 +292,20 @@ def set_initial_stoichiometry_half_cell(
known_value="cyclable lithium capacity",
inplace=True,
options=None,
inputs=None,
):
"""
Set the initial stoichiometry of the working electrode, based on the initial
SOC or voltage
"""
param = param or pybamm.LithiumIonParameters(options)
x = pybamm.lithium_ion.get_initial_stoichiometry_half_cell(
initial_value, self, param=param, known_value=known_value, options=options
initial_value,
self,
param=param,
known_value=known_value,
options=options,
inputs=inputs,
)
if inplace:
parameter_values = self
Expand All @@ -324,6 +330,7 @@ def set_initial_stoichiometries(
known_value="cyclable lithium capacity",
inplace=True,
options=None,
inputs=None,
tol=1e-6,
):
"""
Expand All @@ -338,6 +345,7 @@ def set_initial_stoichiometries(
known_value=known_value,
options=options,
tol=tol,
inputs=inputs,
)
if inplace:
parameter_values = self
Expand Down Expand Up @@ -769,7 +777,7 @@ def _process_symbol(self, symbol):
# Backup option: return the object
return symbol

def evaluate(self, symbol):
def evaluate(self, symbol, inputs=None):
"""
Process and evaluate a symbol.
Expand All @@ -787,7 +795,15 @@ def evaluate(self, symbol):
if processed_symbol.is_constant():
return processed_symbol.evaluate()
else:
raise ValueError("symbol must evaluate to a constant scalar or array")
# In the case that the only issue is an input parameter contained in inputs,
# go ahead and try and evaluate it with the inputs. If it doesn't work, raise
# the value error.
try:
return processed_symbol.evaluate(inputs=inputs)
except Exception as exc:
raise ValueError(
"symbol must evaluate to a constant scalar or array"
) from exc

def _ipython_key_completions_(self):
return list(self._dict_items.keys())
Expand Down
Loading

0 comments on commit a3db966

Please sign in to comment.