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 basic models compatible with experiments #3995

Merged
merged 17 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

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

- Added `plot_thermal_components` to plot the contributions to the total heat generation in a battery ([#4021](https://github.com/pybamm-team/PyBaMM/pull/4021))
- Added functions for normal probability density function (`pybamm.normal_pdf`) and cumulative distribution function (`pybamm.normal_cdf`) ([#3999](https://github.com/pybamm-team/PyBaMM/pull/3999))
- "Basic" models are now compatible with experiments ([#3995](https://github.com/pybamm-team/PyBaMM/pull/3995))
- Updates multiprocess `Pool` in `BaseSolver.solve()` to be constructed with context `fork`. Adds small example for multiprocess inputs. ([#3974](https://github.com/pybamm-team/PyBaMM/pull/3974))
- Added custom experiment steps ([#3835](https://github.com/pybamm-team/PyBaMM/pull/3835))
- Added support for macOS arm64 (M-series) platforms. ([#3789](https://github.com/pybamm-team/PyBaMM/pull/3789))
Expand Down
14 changes: 0 additions & 14 deletions pybamm/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,20 +201,6 @@ def on_cycle_end(self, logs):
f"is below stopping capacity ({cap_stop:.3f} Ah)."
)

voltage_stop = logs["stopping conditions"]["voltage"]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't do this if we don't allow calculating summary variables on the fly. I'm not sure how useful setting a voltage cut-off at the experiment level in this way is anyway? Surely we would set cut-offs at the step level?

Copy link
Member

Choose a reason for hiding this comment

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

It was added in response to this request #1826. I completely forgot we had this feature but it's quite useful for GITT! The lines commented here are just for logging, the line that does the check is in the Simulation class, we should keep it but evaluate the voltage inside the Simulation class instead of using the summary variable. It would only get called if voltage_stop is not None, so no performance hit in general.

if voltage_stop is not None:
min_voltage = logs["summary variables"]["Minimum voltage [V]"]
if min_voltage > voltage_stop[0]:
self.logger.notice(
f"Minimum voltage is now {min_voltage:.3f} V "
f"(will stop at {voltage_stop[0]:.3f} V)"
)
else:
self.logger.notice(
f"Stopping experiment since minimum voltage ({min_voltage:.3f} V) "
f"is below stopping voltage ({voltage_stop[0]:.3f} V)."
)

def on_experiment_end(self, logs):
elapsed_time = logs["elapsed time"]
self.logger.notice(f"Finish experiment simulation, took {elapsed_time}")
Expand Down
10 changes: 6 additions & 4 deletions pybamm/experiment/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,14 @@ def read_termination(termination):
"'80%', '4Ah', or '4A.h'"
)
elif term.endswith("V"):
end_discharge_V = term.split("V")[0]
termination_dict["voltage"] = (float(end_discharge_V), "V")
raise ValueError(
"Voltage termination at the experiment-level is no longer supported. "
"Please set voltage termination conditions at the step level."
)
rtimms marked this conversation as resolved.
Show resolved Hide resolved
else:
raise ValueError(
"Only capacity or voltage can be provided as a termination reason, "
"e.g. '80% capacity', '4 Ah capacity', or '2.5 V'"
"Only capacity can be provided as an experiment-level termination reason, "
"e.g. '80% capacity' or '4 Ah capacity'"
)
return termination_dict

Expand Down
14 changes: 10 additions & 4 deletions pybamm/experiment/step/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def get_parameter_values(self, variables):

def get_submodel(self, model):
return pybamm.external_circuit.VoltageFunctionControl(
model.param, model.options
model.param, model.options, add_discharge_capacity=False
)


Expand Down Expand Up @@ -211,7 +211,9 @@ def get_parameter_values(self, variables):
return {"Power function [W]": self.value}

def get_submodel(self, model):
return pybamm.external_circuit.PowerFunctionControl(model.param, model.options)
return pybamm.external_circuit.PowerFunctionControl(
model.param, model.options, add_discharge_capacity=False
)


def power(value, **kwargs):
Expand Down Expand Up @@ -243,7 +245,7 @@ def get_parameter_values(self, variables):

def get_submodel(self, model):
return pybamm.external_circuit.ResistanceFunctionControl(
model.param, model.options
model.param, model.options, add_discharge_capacity=False
)


Expand Down Expand Up @@ -410,7 +412,11 @@ def __init__(self, current_rhs_function, control="algebraic", **kwargs):

def get_submodel(self, model):
return pybamm.external_circuit.FunctionControl(
model.param, self.current_rhs_function, model.options, control=self.control
model.param,
self.current_rhs_function,
model.options,
control=self.control,
add_discharge_capacity=False,
)

def copy(self):
Expand Down
1 change: 1 addition & 0 deletions pybamm/models/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def __init__(self, name="Unnamed model"):
self._boundary_conditions = {}
self._variables_by_submodel = {}
self._variables = pybamm.FuzzyDict({})
self._summary_variables = []
self._events = []
self._concatenated_rhs = None
self._concatenated_algebraic = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def set_degradation_variables(self):
}
)

def set_summary_variables(self):
def set_default_summary_variables(self):
"""
Sets the default summary variables.
"""
Expand Down
15 changes: 15 additions & 0 deletions pybamm/models/full_battery_models/lithium_ion/basic_dfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(self, name="Doyle-Fuller-Newman model"):
######################
# Variables that depend on time only are created without a domain
Q = pybamm.Variable("Discharge capacity [A.h]")

# Variables that vary spatially are created with a domain
c_e_n = pybamm.Variable(
"Negative electrolyte concentration [mol.m-3]",
Expand Down Expand Up @@ -240,18 +241,32 @@ def __init__(self, name="Doyle-Fuller-Newman model"):
# (Some) variables
######################
voltage = pybamm.boundary_value(phi_s_p, "right")
num_cells = pybamm.Parameter(
"Number of cells connected in series to make a battery"
)
# The `variables` dictionary contains all variables that might be useful for
# visualising the solution of the model
self.variables = {
"Negative particle concentration [mol.m-3]": c_s_n,
"Negative particle surface concentration [mol.m-3]": c_s_surf_n,
"Electrolyte concentration [mol.m-3]": c_e,
"Negative electrolyte concentration [mol.m-3]": c_e_n,
"Separator electrolyte concentration [mol.m-3]": c_e_s,
"Positive electrolyte concentration [mol.m-3]": c_e_p,
"Positive particle concentration [mol.m-3]": c_s_p,
"Positive particle surface concentration [mol.m-3]": c_s_surf_p,
"Current [A]": I,
"Current variable [A]": I, # for compatibility with pybamm.Experiment
"Negative electrode potential [V]": phi_s_n,
"Electrolyte potential [V]": phi_e,
"Negative electrolyte potential [V]": phi_e_n,
"Separator electrolyte potential [V]": phi_e_s,
"Positive electrolyte potential [V]": phi_e_p,
"Positive electrode potential [V]": phi_s_p,
"Voltage [V]": voltage,
"Battery voltage [V]": voltage * num_cells,
"Time [s]": pybamm.t,
"Discharge capacity [A.h]": Q,
}
# Events specify points at which a solution should terminate
self.events += [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,18 +341,40 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
ocp_av_p = pybamm.x_average(ocp_p)
a_j_n_p1_av = pybamm.x_average(a_j_n_p1)
a_j_n_p2_av = pybamm.x_average(a_j_n_p2)
num_cells = pybamm.Parameter(
"Number of cells connected in series to make a battery"
)
# The `variables` dictionary contains all variables that might be useful for
# visualising the solution of the model
self.variables = {
"Negative primary particle concentration [mol.m-3]": c_s_n_p1,
"Negative secondary particle concentration [mol.m-3]": c_s_n_p2,
"R-averaged negative primary particle concentration "
"[mol.m-3]": c_s_rav_n_p1,
"R-averaged negative secondary particle concentration "
"[mol.m-3]": c_s_rav_n_p2,
"Average negative primary particle concentration "
"[mol.m-3]": c_s_xrav_n_p1,
"Average negative secondary particle concentration "
"[mol.m-3]": c_s_xrav_n_p2,
"Positive particle concentration [mol.m-3]": c_s_p,
"Average positive particle concentration [mol.m-3]": c_s_xrav_p,
"Electrolyte concentration [mol.m-3]": c_e,
"Negative electrolyte concentration [mol.m-3]": c_e_n,
"Separator electrolyte concentration [mol.m-3]": c_e_s,
"Positive electrolyte concentration [mol.m-3]": c_e_p,
"Negative electrode potential [V]": phi_s_n,
"Electrolyte potential [V]": phi_e,
"Positive electrode potential [V]": phi_s_p,
"Electrolyte potential [V]": phi_e,
"Negative electrolyte potential [V]": phi_e_n,
"Separator electrolyte potential [V]": phi_e_s,
"Positive electrolyte potential [V]": phi_e_p,
"Current [A]": I,
"Current variable [A]": I, # for compatibility with pybamm.Experiment
"Discharge capacity [A.h]": Q,
"Time [s]": pybamm.t,
"Voltage [V]": voltage,
"Battery voltage [V]": voltage * num_cells,
"Negative electrode primary open-circuit potential [V]": ocp_n_p1,
"Negative electrode secondary open-circuit potential [V]": ocp_n_p2,
"X-averaged negative electrode primary open-circuit potential "
Expand All @@ -361,15 +383,6 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
"[V]": ocp_av_n_p2,
"Positive electrode open-circuit potential [V]": ocp_p,
"X-averaged positive electrode open-circuit potential [V]": ocp_av_p,
"R-averaged negative primary particle concentration "
"[mol.m-3]": c_s_rav_n_p1,
"R-averaged negative secondary particle concentration "
"[mol.m-3]": c_s_rav_n_p2,
"Average negative primary particle concentration "
"[mol.m-3]": c_s_xrav_n_p1,
"Average negative secondary particle concentration "
"[mol.m-3]": c_s_xrav_n_p2,
"Average positive particle concentration [mol.m-3]": c_s_xrav_p,
"Negative electrode primary interfacial current density [A.m-2]": j_n_p1,
"Negative electrode secondary interfacial current density [A.m-2]": j_n_p2,
"X-averaged negative electrode primary interfacial current density "
Expand All @@ -385,7 +398,12 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
"X-averaged negative electrode secondary volumetric "
"interfacial current density [A.m-3]": a_j_n_p2_av,
}
# Events specify points at which a solution should terminate
self.events += [
pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut),
pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage),
]

@property
def default_parameter_values(self):
return pybamm.ParameterValues("Chen2020_composite")
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
vdrop_cell = pybamm.boundary_value(phi_s_w, "right") - ref_potential
vdrop_Li = -eta_Li - delta_phis_Li
voltage = vdrop_cell + vdrop_Li
num_cells = pybamm.Parameter(
"Number of cells connected in series to make a battery"
)

c_e_total = pybamm.x_average(eps * c_e)
c_s_surf_w_av = pybamm.x_average(c_s_surf_w)

Expand Down Expand Up @@ -286,16 +290,22 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
"Positive particle concentration [mol.m-3]": c_s_w,
"Total lithium in positive electrode [mol]": c_s_vol_av * L_w * param.A_cc,
"Electrolyte concentration [mol.m-3]": c_e,
"Separator electrolyte concentration [mol.m-3]": c_e_s,
"Positive electrolyte concentration [mol.m-3]": c_e_w,
"Total lithium in electrolyte [mol]": c_e_total * param.L_x * param.A_cc,
"Current [A]": I,
"Current variable [A]": I, # for compatibility with pybamm.Experiment
"Current density [A.m-2]": i_cell,
"Positive electrode potential [V]": phi_s_w,
"Positive electrode open-circuit potential [V]": U_w(sto_surf_w, T),
"Electrolyte potential [V]": phi_e,
"Separator electrolyte potential [V]": phi_e_s,
"Positive electrolyte potential [V]": phi_e_w,
"Voltage drop in the cell [V]": vdrop_cell,
"Negative electrode exchange current density [A.m-2]": j_Li,
"Negative electrode reaction overpotential [V]": eta_Li,
"Negative electrode potential drop [V]": delta_phis_Li,
"Voltage [V]": voltage,
"Battery voltage [V]": voltage * num_cells,
"Instantaneous power [W.m-2]": i_cell * voltage,
}
9 changes: 9 additions & 0 deletions pybamm/models/full_battery_models/lithium_ion/basic_spm.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,26 +139,33 @@ def __init__(self, name="Single Particle Model"):
phi_e = -eta_n - param.n.prim.U(sto_surf_n, T)
phi_s_p = eta_p + phi_e + param.p.prim.U(sto_surf_p, T)
V = phi_s_p
num_cells = pybamm.Parameter(
"Number of cells connected in series to make a battery"
)

whole_cell = ["negative electrode", "separator", "positive electrode"]
# The `variables` dictionary contains all variables that might be useful for
# visualising the solution of the model
# Primary broadcasts are used to broadcast scalar quantities across a domain
# into a vector of the right shape, for multiplying with other vectors
self.variables = {
"Time [s]": pybamm.t,
"Discharge capacity [A.h]": Q,
"X-averaged negative particle concentration [mol.m-3]": c_s_n,
"Negative particle surface "
"concentration [mol.m-3]": pybamm.PrimaryBroadcast(
c_s_surf_n, "negative electrode"
),
"Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast(
param.c_e_init_av, whole_cell
),
"X-averaged positive particle concentration [mol.m-3]": c_s_p,
"Positive particle surface "
"concentration [mol.m-3]": pybamm.PrimaryBroadcast(
c_s_surf_p, "positive electrode"
),
"Current [A]": I,
"Current variable [A]": I, # for compatibility with pybamm.Experiment
"Negative electrode potential [V]": pybamm.PrimaryBroadcast(
phi_s_n, "negative electrode"
),
Expand All @@ -167,7 +174,9 @@ def __init__(self, name="Single Particle Model"):
phi_s_p, "positive electrode"
),
"Voltage [V]": V,
"Battery voltage [V]": V * num_cells,
}
# Events specify points at which a solution should terminate
self.events += [
pybamm.Event("Minimum voltage [V]", V - param.voltage_low_cut),
pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - V),
Expand Down
3 changes: 3 additions & 0 deletions pybamm/models/full_battery_models/lithium_ion/dfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,6 @@ def set_electrolyte_potential_submodel(self):
self.submodels[f"{domain} surface potential difference"] = surf_model(
self.param, domain, self.options
)

def set_summary_variables(self):
self.set_default_summary_variables()
3 changes: 3 additions & 0 deletions pybamm/models/full_battery_models/lithium_ion/spm.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,6 @@ def set_electrolyte_potential_submodel(self):
self.submodels[f"{domain} surface potential difference"] = surf_model(
self.param, domain, options=self.options
)

def set_summary_variables(self):
self.set_default_summary_variables()
25 changes: 14 additions & 11 deletions pybamm/models/submodels/external_circuit/base_external_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
class BaseModel(pybamm.BaseSubModel):
"""Model to represent the behaviour of the external circuit."""

def __init__(self, param, options):
def __init__(self, param, options, add_discharge_capacity=True):
super().__init__(param, options=options)
self.add_discharge_capacity = add_discharge_capacity

def get_fundamental_variables(self):
Q_Ah = pybamm.Variable("Discharge capacity [A.h]")
Expand Down Expand Up @@ -41,23 +42,25 @@ def get_fundamental_variables(self):
return variables

def set_initial_conditions(self, variables):
Q_Ah = variables["Discharge capacity [A.h]"]
Qt_Ah = variables["Throughput capacity [A.h]"]
self.initial_conditions[Q_Ah] = pybamm.Scalar(0)
self.initial_conditions[Qt_Ah] = pybamm.Scalar(0)
if self.add_discharge_capacity:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We add these by default when using this as a submodel, but pass add_discharge_capacity=False when creating the submodel to update the "Current [A]" variable for experiments to avoid adding these equations to models that don't already contain them. I'd welcome alternative implementations.

Copy link
Member

Choose a reason for hiding this comment

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

How about just having a separate submodel for discharge capacity and related variables?

Q_Ah = variables["Discharge capacity [A.h]"]
Qt_Ah = variables["Throughput capacity [A.h]"]
self.initial_conditions[Q_Ah] = pybamm.Scalar(0)
self.initial_conditions[Qt_Ah] = pybamm.Scalar(0)
if self.options["calculate discharge energy"] == "true":
Q_Wh = variables["Discharge energy [W.h]"]
Qt_Wh = variables["Throughput energy [W.h]"]
self.initial_conditions[Q_Wh] = pybamm.Scalar(0)
self.initial_conditions[Qt_Wh] = pybamm.Scalar(0)

def set_rhs(self, variables):
# ODEs for discharge capacity and throughput capacity
Q_Ah = variables["Discharge capacity [A.h]"]
Qt_Ah = variables["Throughput capacity [A.h]"]
I = variables["Current [A]"]
self.rhs[Q_Ah] = I / 3600 # Returns to zero after a complete cycle
self.rhs[Qt_Ah] = abs(I) / 3600 # Increases with each cycle
if self.add_discharge_capacity:
# ODEs for discharge capacity and throughput capacity
Q_Ah = variables["Discharge capacity [A.h]"]
Qt_Ah = variables["Throughput capacity [A.h]"]
I = variables["Current [A]"]
self.rhs[Q_Ah] = I / 3600 # Returns to zero after a complete cycle
self.rhs[Qt_Ah] = abs(I) / 3600 # Increases with each cycle
if self.options["calculate discharge energy"] == "true":
Q_Wh = variables["Discharge energy [W.h]"]
Qt_Wh = variables["Throughput energy [W.h]"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def get_fundamental_variables(self):
}

# Add discharge capacity variable
variables.update(super().get_fundamental_variables())
if self.add_discharge_capacity:
variables.update(super().get_fundamental_variables())

return variables

Expand Down