Skip to content

Commit

Permalink
Merge branch 'develop' into 238b-multi-fitting
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolaCourtier committed Aug 5, 2024
2 parents d1fbd8d + 9e15f90 commit ab68d15
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 75 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Features

- [#364](https://github.com/pybop-team/PyBOP/pull/364) - Adds the MultiFittingProblem class and the multi_fitting example script.
- [#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.
- [#357](https://github.com/pybop-team/PyBOP/pull/357) - Adds `Transformation()` class with `LogTransformation()`, `IdentityTransformation()`, and `ScaledTransformation()`, `ComposedTransformation()` implementations with corresponding examples and tests.
- [#427](https://github.com/pybop-team/PyBOP/issues/427) - Adds the nbstripout pre-commit hook to remove unnecessary metadata from notebooks.
Expand All @@ -12,6 +13,8 @@

## Bug Fixes

- [#421](https://github.com/pybop-team/PyBOP/issues/421) - Adds a default value for the initial SOC for design problems.

## Breaking Changes

# [v24.6.1](https://github.com/pybop-team/PyBOP/tree/v24.6.1) - 2024-07-31
Expand Down
89 changes: 65 additions & 24 deletions pybop/models/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def rebuild(
parameters : pybop.Parameters or Dict, optional
A pybop Parameters class or dictionary containing parameter values to apply to the model.
parameter_set : pybop.parameter_set, optional
A PyBOP parameter set object or a dictionary containing the parameter values
A PyBOP parameter set object or a dictionary containing the parameter values.
check_model : bool, optional
If True, the model will be checked for correctness after construction.
init_soc : float, optional
Expand Down Expand Up @@ -462,18 +462,18 @@ def simulateS1(self, inputs: Inputs, t_eval: np.array):

def predict(
self,
inputs: Inputs = None,
t_eval: np.array = None,
parameter_set: ParameterSet = None,
experiment: Experiment = None,
inputs: Optional[Inputs] = None,
t_eval: Optional[np.array] = None,
parameter_set: Optional[ParameterSet] = None,
experiment: Optional[Experiment] = None,
init_soc: Optional[float] = None,
) -> dict[str, np.ndarray[np.float64]]:
"""
Solve the model using PyBaMM's simulation framework and return the solution.
This method sets up a PyBaMM simulation by configuring the model, parameters, experiment
(if any), and initial state of charge (if provided). It then solves the simulation and
returns the resulting solution object.
or time vector, and initial state of charge (if provided). Either 't_eval' or 'experiment'
must be provided. It then solves the simulation and returns the resulting solution object.
Parameters
----------
Expand Down Expand Up @@ -505,41 +505,52 @@ def predict(
if PyBaMM models are not supported by the current simulation method.
"""
inputs = self.parameters.verify(inputs)

if self.pybamm_model is None:
raise ValueError("This sim method currently only supports PyBaMM models")
if self._unprocessed_model is None:
raise ValueError(
"The predict method currently only supports PyBaMM models."
)
elif not self._unprocessed_model._built:
self._unprocessed_model.build_model()

parameter_set = parameter_set or self._unprocessed_parameter_set
if inputs is not None:
inputs = self.parameters.verify(inputs)
parameter_set.update(inputs)

if init_soc is not None and isinstance(
self.pybamm_model, pybamm.equivalent_circuit.Thevenin
):
parameter_set["Initial SoC"] = init_soc
init_soc = None

if self.check_params(
inputs=inputs,
parameter_set=parameter_set,
allow_infeasible_solutions=self.allow_infeasible_solutions,
):
if experiment is None:
return pybamm.Simulation(
self._unprocessed_model,
parameter_values=parameter_set,
).solve(t_eval=t_eval, initial_soc=init_soc)
else:
if experiment is not None:
return pybamm.Simulation(
self._unprocessed_model,
model=self._unprocessed_model,
experiment=experiment,
parameter_values=parameter_set,
).solve(initial_soc=init_soc)
elif t_eval is not None:
return pybamm.Simulation(
model=self._unprocessed_model,
parameter_values=parameter_set,
).solve(t_eval=t_eval, initial_soc=init_soc)
else:
raise ValueError(
"The predict method requires either an experiment or "
"t_eval to be specified."
)

else:
return [np.inf]

def check_params(
self,
inputs: Inputs = None,
parameter_set: ParameterSet = None,
inputs: Optional[Inputs] = None,
parameter_set: Optional[ParameterSet] = None,
allow_infeasible_solutions: bool = True,
):
"""
Expand All @@ -549,6 +560,8 @@ def check_params(
----------
inputs : Inputs
The input parameters for the simulation.
parameter_set : pybop.parameter_set, optional
A PyBOP parameter set object or a dictionary containing the parameter values.
allow_infeasible_solutions : bool, optional
If True, infeasible parameter values will be allowed in the optimisation (default: True).
Expand All @@ -558,14 +571,20 @@ def check_params(
A boolean which signifies whether the parameters are compatible.
"""
inputs = self.parameters.verify(inputs)
inputs = self.parameters.verify(inputs) or {}
parameter_set = parameter_set or self._parameter_set

return self._check_params(
inputs=inputs, allow_infeasible_solutions=allow_infeasible_solutions
inputs=inputs,
parameter_set=parameter_set,
allow_infeasible_solutions=allow_infeasible_solutions,
)

def _check_params(
self, inputs: Inputs = None, allow_infeasible_solutions: bool = True
self,
inputs: Inputs,
parameter_set: ParameterSet,
allow_infeasible_solutions: bool = True,
):
"""
A compatibility check for the model parameters which can be implemented by subclasses
Expand All @@ -575,6 +594,8 @@ def _check_params(
----------
inputs : Inputs
The input parameters for the simulation.
parameter_set : pybop.parameter_set
A PyBOP parameter set object or a dictionary containing the parameter values.
allow_infeasible_solutions : bool, optional
If True, infeasible parameter values will be allowed in the optimisation (default: True).
Expand Down Expand Up @@ -746,3 +767,23 @@ def solver(self):
@solver.setter
def solver(self, solver):
self._solver = solver.copy() if solver is not None else None

def get_parameter_info(self, print_info: bool = False):
"""
Extracts the parameter names and types and returns them as a dictionary.
"""
if not self.pybamm_model._built:
self.pybamm_model.build_model()

info = self.pybamm_model.get_parameter_info()

reduced_info = dict()
for param, param_type in info.values():
param_name = getattr(param, "name", str(param))
reduced_info[param_name] = param_type

if print_info:
for param, param_type in info.values():
print(param, " : ", param_type)

return reduced_info
11 changes: 9 additions & 2 deletions pybop/models/empirical/base_ecm.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pybop.models.base_model import BaseModel, Inputs
from pybop.parameters.parameter_set import ParameterSet


class ECircuitModel(BaseModel):
Expand Down Expand Up @@ -86,21 +87,27 @@ def __init__(
self._disc = None
self.geometric_parameters = {}

def _check_params(self, inputs: Inputs = None, allow_infeasible_solutions=True):
def _check_params(
self,
inputs: Inputs,
parameter_set: ParameterSet,
allow_infeasible_solutions: bool = True,
):
"""
Check the compatibility of the model parameters.
Parameters
----------
inputs : Inputs
The input parameters for the simulation.
parameter_set : pybop.parameter_set
A PyBOP parameter set object or a dictionary containing the parameter values.
allow_infeasible_solutions : bool, optional
If True, infeasible parameter values will be allowed in the optimisation (default: True).
Returns
-------
bool
A boolean which signifies whether the parameters are compatible.
"""
return True
10 changes: 7 additions & 3 deletions pybop/models/lithium_ion/base_echem.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pybamm import lithium_ion as pybamm_lithium_ion

from pybop.models.base_model import BaseModel, Inputs
from pybop.parameters.parameter_set import ParameterSet


class EChemBaseModel(BaseModel):
Expand Down Expand Up @@ -85,7 +86,10 @@ def __init__(
self.geometric_parameters = self.set_geometric_parameters()

def _check_params(
self, inputs: Inputs = None, parameter_set=None, allow_infeasible_solutions=True
self,
inputs: Inputs,
parameter_set: ParameterSet,
allow_infeasible_solutions: bool = True,
):
"""
Check compatibility of the model parameters.
Expand All @@ -94,6 +98,8 @@ def _check_params(
----------
inputs : Inputs
The input parameters for the simulation.
parameter_set : pybop.parameter_set
A PyBOP parameter set object or a dictionary containing the parameter values.
allow_infeasible_solutions : bool, optional
If True, infeasible parameter values will be allowed in the optimisation (default: True).
Expand All @@ -102,8 +108,6 @@ def _check_params(
bool
A boolean which signifies whether the parameters are compatible.
"""
parameter_set = parameter_set or self._parameter_set

if self.pybamm_model.options["working electrode"] == "positive":
electrode_params = [
(
Expand Down
14 changes: 0 additions & 14 deletions pybop/optimisers/base_optimiser.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,20 +199,6 @@ def _run(self):
"""
raise NotImplementedError

def store_optimised_parameters(self, x):
"""
Update the problem parameters with optimised values.
The optimised parameter values are stored within the associated PyBOP parameter class.
Parameters
----------
x : array-like
Optimised parameter values.
"""
for i, param in enumerate(self.cost.parameters):
param.update(value=x[i])

def check_optimal_parameters(self, x):
"""
Check if the optimised parameters are physically viable.
Expand Down
24 changes: 11 additions & 13 deletions pybop/parameters/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,15 @@ def get_initial_value(self) -> float:
Return the initial value of each parameter.
"""
if self.initial_value is None:
sample = self.rvs(1)
self.update(initial_value=sample[0])
if self.prior is not None:
sample = self.rvs(1)[0]
self.update(initial_value=sample)
else:
warnings.warn(
"Initial value or Prior are None, proceeding without initial value.",
UserWarning,
stacklevel=2,
)

return self.initial_value

Expand Down Expand Up @@ -409,17 +416,8 @@ def initial_value(self) -> np.ndarray:
initial_values = []

for param in self.param.values():
if param.initial_value is None:
if param.prior is not None:
initial_value = param.rvs(1)[0]
param.update(initial_value=initial_value)
else:
warnings.warn(
"Initial value or Prior are None, proceeding without initial value.",
UserWarning,
stacklevel=2,
)
initial_values.append(param.initial_value)
initial_value = param.get_initial_value()
initial_values.append(initial_value)

return np.asarray(initial_values)

Expand Down
30 changes: 12 additions & 18 deletions pybop/problems/design_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class DesignProblem(BaseProblem):
additional_variables : List[str], optional
Additional variables to observe and store in the solution (default additions are: ["Time [s]", "Current [A]"]).
init_soc : float, optional
Initial state of charge (default: None).
Initial state of charge (default: 1.0).
"""

def __init__(
Expand All @@ -47,10 +47,13 @@ def __init__(
signal = ["Voltage [V]"]
additional_variables.extend(["Time [s]", "Current [A]"])
additional_variables = list(set(additional_variables))
self.warning_patterns = [
"Ah is greater than",
"Non-physical point encountered",
]

if init_soc is None:
if "Initial SoC" in model._parameter_set.keys():
init_soc = model._parameter_set["Initial SoC"]
else:
init_soc = 1.0 # default value

super().__init__(
parameters,
model,
Expand All @@ -61,24 +64,15 @@ def __init__(
)
self.experiment = experiment

# Build the model if required
if experiment is not None:
# Leave the build until later to apply the experiment
self._model.classify_and_update_parameters(self.parameters)

elif self._model._built_model is None:
self._model.build(
experiment=self.experiment,
parameters=self.parameters,
check_model=self.check_model,
init_soc=self.init_soc,
)

# Add an example dataset for plotting comparison
sol = self.evaluate(self.parameters.as_dict("initial"))
self._time_data = sol["Time [s]"]
self._target = {key: sol[key] for key in self.signal}
self._dataset = None
self.warning_patterns = [
"Ah is greater than",
"Non-physical point encountered",
]

def evaluate(self, inputs: Inputs, update_capacity=False):
"""
Expand Down
Loading

0 comments on commit ab68d15

Please sign in to comment.