Skip to content

Commit

Permalink
Merge model build and rebuild (#451)
Browse files Browse the repository at this point in the history
* Merge rebuild into build

* Update CHANGELOG.md

* Update base_model.py

* Fix notebooks

* Apply suggestions from code review

Co-authored-by: Brady Planden <55357039+BradyPlanden@users.noreply.github.com>

* Update set_current_function and add test

* Include simulate in test_set_current_function

---------

Co-authored-by: Brady Planden <55357039+BradyPlanden@users.noreply.github.com>
  • Loading branch information
NicolaCourtier and BradyPlanden authored Aug 9, 2024
1 parent 15d21b1 commit df429f7
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 160 deletions.
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

- [#444](https://github.com/pybop-team/PyBOP/issues/444) - Merge `BaseModel` `build()` and `rebuild()` functionality.
- [#435](https://github.com/pybop-team/PyBOP/pull/435) - Adds SLF001 linting for private members.
- [#418](https://github.com/pybop-team/PyBOP/issues/418) - Wraps the `get_parameter_info` method from PyBaMM to get a dictionary of parameter names and types.
- [#413](https://github.com/pybop-team/PyBOP/pull/413) - Adds `DesignCost` functionality to `WeightedCost` class with additional tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1639,8 +1639,7 @@
"outputs": [],
"source": [
"problem.set_target(dataset_two_pulse)\n",
"model.parameter_set[\"Initial SoC\"] = 0.8 - 0.0075\n",
"model.rebuild(dataset_two_pulse)"
"model.build(dataset=dataset_two_pulse, initial_state={\"Initial SoC\": 0.8 - 0.0075})"
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion examples/notebooks/multi_model_identification.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
"metadata": {},
"outputs": [],
"source": [
"synth_model.build(dataset, initial_state=initial_state)\n",
"synth_model.build(dataset=dataset, initial_state=initial_state)\n",
"synth_model.signal = [\"Voltage [V]\"]\n",
"values = synth_model.simulate(t_eval=t_eval, inputs={})"
]
Expand Down
225 changes: 93 additions & 132 deletions pybop/models/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,10 @@ def __init__(
Additional Attributes
---------------------
pybamm_model : pybamm.BaseModel
An instance of a PyBaMM model.
parameters : pybop.Parameters
The input parameters.
output_variables : list[str], optional
A list of names of variables to include in the solution object.
rebuild_parameters : dict
A list of parameters which require the model to be rebuilt (default: {}).
standard_parameters : dict
A list of standard (i.e. not rebuild) parameters (default: {}).
param_check_counter : int
A counter for the number of parameter checks (default: 0).
allow_infeasible_solutions : bool, optional
Expand All @@ -106,45 +102,50 @@ def __init__(

self.pybamm_model = None
self.parameters = Parameters()
self.rebuild_parameters = {}
self.standard_parameters = {}
self.param_check_counter = 0
self.allow_infeasible_solutions = True
self.current_function = None

def build(
self,
dataset: Optional[Dataset] = None,
parameters: Union[Parameters, dict] = None,
check_model: bool = True,
inputs: Optional[Inputs] = None,
initial_state: Optional[dict] = None,
dataset: Optional[Dataset] = None,
check_model: bool = True,
) -> None:
"""
Construct the PyBaMM model if not already built, and set parameters.
Construct the PyBaMM model, if not already built or if there are changes to any
`rebuild_parameters` or the initial state.
This method initializes the model components, applies the given parameters,
sets up the mesh and discretisation if needed, and prepares the model
for simulations.
Parameters
----------
dataset : pybamm.Dataset, optional
The dataset to be used in the model construction.
parameters : pybop.Parameters or Dict, optional
A pybop Parameters class or dictionary containing parameter values to apply to the model.
check_model : bool, optional
If True, the model will be checked for correctness after construction.
inputs : Inputs
The input parameters to be used when building the model.
initial_state : dict, optional
A valid initial state, e.g. the initial state of charge or open-circuit voltage.
Defaults to None, indicating that the existing initial state of charge (for an ECM)
or initial concentrations (for an EChem model) will be used.
dataset : pybop.Dataset or dict, optional
The dataset to be used in the model construction.
check_model : bool, optional
If True, the model will be checked for correctness after construction.
"""
if parameters is not None:
self.parameters = parameters
self.classify_and_update_parameters(self.parameters)
if parameters is not None or inputs is not None:
# Classify parameters and clear the model if rebuild required
inputs = self.classify_parameters(parameters, inputs=inputs)

if initial_state is not None:
self.set_initial_state(initial_state)
# Clear the model if rebuild required (currently if any initial state)
self.set_initial_state(initial_state, inputs=inputs)

if dataset is not None:
self.set_current_function(dataset)

if self._built_model:
return
Expand All @@ -156,8 +157,8 @@ def build(
else:
if not self.pybamm_model._built: # noqa: SLF001
self.pybamm_model.build_model()
self.set_params(dataset=dataset)

self.set_parameters()
self._mesh = pybamm.Mesh(self.geometry, self.submesh_types, self.var_pts)
self._disc = pybamm.Discretisation(
mesh=self.mesh,
Expand Down Expand Up @@ -203,28 +204,35 @@ def convert_to_pybamm_initial_state(self, initial_state: dict):
else:
raise ValueError(f'Unrecognised initial state: "{list(initial_state)[0]}"')

def set_initial_state(self, initial_state: dict):
def set_initial_state(self, initial_state: dict, inputs: Optional[Inputs] = None):
"""
Set the initial state of charge or concentrations for the battery model.
Parameters
----------
initial_state : dict
A valid initial state, e.g. the initial state of charge or open-circuit voltage.
inputs : Inputs
The input parameters to be used when building the model.
"""
self.clear()

initial_state = self.convert_to_pybamm_initial_state(initial_state)

if isinstance(self.pybamm_model, pybamm.equivalent_circuit.Thevenin):
initial_state = self.get_initial_state(initial_state)
initial_state = self.get_initial_state(initial_state, inputs=inputs)
self._unprocessed_parameter_set.update({"Initial SoC": initial_state})

else:
# Temporary construction of attribute for PyBaMM
if not self.pybamm_model._built: # noqa: SLF001
self.pybamm_model.build_model()

# Temporary construction of attributes for PyBaMM
self.model = self._model = self.pybamm_model
self._unprocessed_parameter_values = self._unprocessed_parameter_set

# Set initial SOC via PyBaMM's Simulation class
pybamm.Simulation.set_initial_soc(self, initial_state, inputs=None)
# Set initial state via PyBaMM's Simulation class
pybamm.Simulation.set_initial_soc(self, initial_state, inputs=inputs)

# Update the default parameter set for consistency
self._unprocessed_parameter_set = self._parameter_values
Expand All @@ -238,37 +246,40 @@ def set_initial_state(self, initial_state: dict):
# Use a copy of the updated default parameter set
self._parameter_set = self._unprocessed_parameter_set.copy()

def set_params(self, rebuild: bool = False, dataset: Dataset = None):
def set_current_function(self, dataset: Union[Dataset, dict]):
"""
Update the input current function according to the data.
Parameters
----------
dataset : pybop.Dataset or dict, optional
The dataset to be used in the model construction.
"""
if "Current function [A]" in self._parameter_set.keys():
if "Current function [A]" not in self.parameters.keys():
current = pybamm.Interpolant(
dataset["Time [s]"],
dataset["Current function [A]"],
pybamm.t,
)
# Update both the active and unprocessed parameter sets for consistency
self._parameter_set["Current function [A]"] = current
self._unprocessed_parameter_set["Current function [A]"] = current

def set_parameters(self):
"""
Assign the parameters to the model.
This method processes the model with the given parameters, sets up
the geometry, and updates the model instance.
"""
if self.model_with_set_params and not rebuild:
if self._model_with_set_params:
return

# Mark any simulation inputs in the parameter set
for key in self.standard_parameters.keys():
self._parameter_set[key] = "[input]"

if "Current function [A]" in self._parameter_set.keys():
if dataset is not None and (not self.rebuild_parameters or not rebuild):
if "Current function [A]" not in self.parameters.keys():
self.current_function = pybamm.Interpolant(
dataset["Time [s]"],
dataset["Current function [A]"],
pybamm.t,
)
self._parameter_set["Current function [A]"] = self.current_function
elif rebuild and self.current_function is not None:
self._parameter_set["Current function [A]"] = self.current_function

self._model_with_set_params = self._parameter_set.process_model(
self._unprocessed_model, inplace=False
)
if self.geometry is not None:
self._parameter_set.process_geometry(self.geometry)
self._parameter_set.process_geometry(self._geometry)
self.pybamm_model = self._model_with_set_params

def clear(self):
Expand All @@ -281,71 +292,25 @@ def clear(self):
self._mesh = None
self._disc = None

def rebuild(
self,
dataset: Optional[Dataset] = None,
parameters: Union[Parameters, dict] = None,
check_model: bool = True,
initial_state: Optional[dict] = None,
) -> None:
"""
Rebuild the PyBaMM model for a given set of inputs.
This method requires the self.build() method to be called first, and
then rebuilds the model for a given parameter set. Specifically,
this method applies the given parameters, sets up the mesh and
discretisation if needed, and prepares the model for simulations.
Parameters
----------
dataset : pybamm.Dataset, optional
The dataset to be used in the model construction.
parameters : pybop.Parameters or Dict, optional
A pybop Parameters class or dictionary containing parameter values to apply to the model.
check_model : bool, optional
If True, the model will be checked for correctness after construction.
initial_state : dict, optional
A valid initial state, e.g. the initial state of charge or open-circuit voltage.
Defaults to None, indicating that the existing initial state of charge (for an ECM)
or initial concentrations (for an EChem model) will be used.
"""
if parameters is not None:
self.classify_and_update_parameters(parameters)

if initial_state is not None:
self.set_initial_state(initial_state)

if self._built_model is None:
raise ValueError("Model must be built before calling rebuild")

self.set_params(rebuild=True, dataset=dataset)
self._mesh = pybamm.Mesh(self.geometry, self.submesh_types, self.var_pts)
self._disc = pybamm.Discretisation(
mesh=self.mesh,
spatial_methods=self.spatial_methods,
check_model=check_model,
)
self._built_model = self._disc.process_model(
self._model_with_set_params, inplace=False
)

# Clear solver and setup model
self._solver._model_set_up = {} # noqa: SLF001

def classify_and_update_parameters(self, parameters: Parameters):
def classify_parameters(
self, parameters: Optional[Parameters] = None, inputs: Optional[Inputs] = None
):
"""
Update the parameter values according to their classification as either
'rebuild_parameters' which require a model rebuild and
'standard_parameters' which do not.
Check for any 'rebuild_parameters' which require a model rebuild and
update the unprocessed_parameter_set if a rebuild is required.
Parameters
----------
parameters : pybop.Parameters
The input parameters.
parameters : Parameters, optional
The optimisation parameters. Defaults to None, resulting in the internal `pybop.Parameters` object to be used.
inputs : Inputs, optional
The input parameters for the simulation (default: None).
"""
self.parameters = parameters or Parameters()
self.parameters = parameters or self.parameters

# Compile all parameters and inputs
parameter_dictionary = self.parameters.as_dict()
parameter_dictionary.update(inputs or {})

rebuild_parameters = {
param: parameter_dictionary[param]
Expand All @@ -358,16 +323,24 @@ def classify_and_update_parameters(self, parameters: Parameters):
if param not in self.geometric_parameters
}

self.rebuild_parameters.update(rebuild_parameters)
self.standard_parameters.update(standard_parameters)
# Mark any standard parameters in the active parameter set and pass as inputs
for key in standard_parameters.keys():
self._parameter_set[key] = "[input]"

if self.rebuild_parameters:
self._geometry = self.pybamm_model.default_geometry
# Clear any built model, update the parameter set and geometry if rebuild required
if rebuild_parameters:
requires_rebuild = False
# A rebuild is required if any of the rebuild parameter values have changed
for key, value in rebuild_parameters.items():
if value != self._unprocessed_parameter_set[key]:
requires_rebuild = True
if requires_rebuild:
self.clear()
self._geometry = self.pybamm_model.default_geometry
self._parameter_set.update(rebuild_parameters)
self._unprocessed_parameter_set.update(rebuild_parameters)

# Update both the active and unprocessed parameter sets for consistency
if self._parameter_set is not None:
self._parameter_set.update(parameter_dictionary)
self._unprocessed_parameter_set = self._parameter_set
return standard_parameters

def reinit(
self, inputs: Inputs, t: float = 0.0, x: Optional[np.ndarray] = None
Expand Down Expand Up @@ -445,22 +418,8 @@ def simulate(
"""
inputs = self.parameters.verify(inputs)

if self._built_model is None:
raise ValueError("Model must be built before calling simulate")

requires_rebuild = False
# A rebuild is required if any of the rebuild parameter values have changed
for key, value in inputs.items():
if key in self.rebuild_parameters:
if value != self.parameters[key].value:
requires_rebuild = True
# Or if the simulation is set to start from a specific initial value
if initial_state is not None:
requires_rebuild = True

if requires_rebuild:
self.parameters.update(values=list(inputs.values()))
self.rebuild(parameters=self.parameters, initial_state=initial_state)
# Build or rebuild if required
self.build(inputs=inputs, initial_state=initial_state)

if not self.check_params(
inputs=inputs,
Expand Down Expand Up @@ -501,14 +460,16 @@ def simulateS1(
"""
inputs = self.parameters.verify(inputs)

if self._built_model is None:
raise ValueError("Model must be built before calling simulate")

if self.rebuild_parameters or initial_state is not None:
if initial_state is not None or any(
key in self.geometric_parameters for key in inputs.keys()
):
raise ValueError(
"Cannot use sensitivies for parameters which require a model rebuild"
)

# Build if required
self.build(inputs=inputs, initial_state=initial_state)

if not self.check_params(
inputs=inputs,
allow_infeasible_solutions=self.allow_infeasible_solutions,
Expand Down
4 changes: 2 additions & 2 deletions pybop/models/empirical/base_ecm.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ def get_initial_state(

if isinstance(initial_value, str) and initial_value.endswith("V"):
V_init = float(initial_value[:-1])
V_min = parameter_values.evaluate(param.voltage_low_cut)
V_max = parameter_values.evaluate(param.voltage_high_cut)
V_min = parameter_values.evaluate(param.voltage_low_cut, inputs=inputs)
V_max = parameter_values.evaluate(param.voltage_high_cut, inputs=inputs)

if not V_min <= V_init <= V_max:
raise ValueError(
Expand Down
Loading

0 comments on commit df429f7

Please sign in to comment.