Source code for pybop.costs.base_cost
import numpy as np
from pybop._utils import add_spaces
from pybop.parameters.parameter import Inputs, Parameters
from pybop.processing.dataset import Dataset
from pybop.simulators.solution import Solution
[docs]
class BaseCost:
"""
Base cost.
Attributes
----------
_de : float
The gradient of the cost function to use if an error occurs during
evaluation. Defaults to 1.0.
minimising : bool, optional
If False, tells the optimiser to switch the sign of the cost and gradient
to maximise by default rather than minimise (default: True).
"""
def __init__(self):
self._de = 1.0
self.parameters = Parameters()
self.minimising = True
# TODO: Remove the default domain, target and dataset from the base cost as
# they are not relevant for all costs.
self._domain = "Time [s]"
self._target = ["Voltage [V]"]
self._domain_data = None
self._target_data = None
self._dataset = None
[docs]
def set_target(
self, target: list[str] | str | None = None, dataset: Dataset | None = None
):
"""Set the target variable and target data from a pybop.Dataset."""
self._target = [target] if isinstance(target, str) else target or self._target
self.n_outputs = len(self._target)
if not isinstance(dataset, Dataset | None):
raise ValueError("Dataset must be a pybop.Dataset object.")
if dataset is not None:
# Check that the dataset contains necessary variables
dataset.check(domain=dataset.domain, signal=self._target)
self._domain = dataset.domain
self._dataset = dataset.data
if self._dataset is not None:
# Unpack the domain and target data
self._domain_data = self._dataset[self.domain]
self.n_data = len(self._domain_data)
self._target_data = {var: self._dataset[var] for var in self._target}
[docs]
def evaluate(
self,
solution: Solution,
inputs: Inputs | None = None,
calculate_sensitivities: bool = False,
) -> float | tuple[float, np.ndarray]:
"""
Computes the cost function for the given predictions.
Parameters
----------
solution : pybop.Solution | pybamm.Solution
The simulation result.
inputs : Inputs, optional
Input parameters (default: None).
calculate_sensitivities : bool
Whether to also return the sensitivities (default: False).
Returns
-------
np.float64 or tuple[np.float64, np.ndarray[np.float64]]
If the solution has sensitivities, returns a tuple containing the cost (float) and the
gradient with dimension (len(parameters)), otherwise returns only the cost.
"""
raise NotImplementedError
[docs]
def stack_sensitivities(self, solution: Solution) -> dict[str, np.ndarray]:
"""
Stack the sensitivities for each output variable into a single array.
Parameters
----------
solution : pybop.Solution | pybamm.Solution
A solution object containing a dictionary of sensitivities for each output variable.
Returns
-------
dict[str, np.ndarray[np.float64]]
The sensitivities dy/dx(t) for each output variable y with respect to each parameter
x over the domain t. The dictionary keys are the parameter names and the arrays are
of dimensions (len(target), len(domain_data)).
"""
return {
key: np.vstack([solution[var].sensitivities[key] for var in self.target])
for key in solution.all_inputs[0].keys()
}
[docs]
def set_fail_gradient(self, de: float = 1.0):
"""
Set the fail gradient to a specified value.
The fail gradient is used if an error occurs during the calculation
of the gradient. This method allows updating the default gradient value.
Parameters
----------
de : float
The new fail gradient value to be used.
"""
if not isinstance(de, float):
de = float(de)
self._de = de
[docs]
def failure(self, parameter_names: list[str], calculate_sensitivities: bool = True):
if calculate_sensitivities:
return (
(np.inf, {key: self._de for key in parameter_names})
if self.minimising
else (-np.inf, {key: -self._de for key in parameter_names})
)
else:
return np.inf if self.minimising else -np.inf
@property
def name(self):
return add_spaces(type(self).__name__)
@property
def n_parameters(self):
return len(self.parameters)
@property
def domain(self):
return self._domain
@property
def domain_data(self):
return self._domain_data
@property
def target(self):
return self._target
@property
def target_data(self):
return self._target_data
[docs]
class LogPrior(BaseCost):
"""
The log-prior as a cost, to be used with the LogPosterior class.
"""
def __init__(self, parameters):
super().__init__()
self.parameters = parameters
self.minimising = False
[docs]
def evaluate(
self,
solution: Solution,
inputs: Inputs | None = None,
calculate_sensitivities: bool = False,
) -> float | tuple[float, np.ndarray]:
"""
Computes the log-prior for the given inputs, and optionally the sensitivities.
"""
# Get the values of all input parameters
inputs = inputs or self.parameters.to_dict("initial")
input_values = np.asarray(list(inputs.values()))
# Compute log prior (and gradient)
if calculate_sensitivities:
l, dl = self.parameters.distribution.logpdfS1(input_values)
dl = {key: dl[i] for i, key in enumerate(self.parameters.names)}
else:
l = self.parameters.distribution.logpdf(input_values)
if not np.isfinite(l).any():
return self.failure(
inputs=inputs, calculate_sensitivities=calculate_sensitivities
)
if calculate_sensitivities:
return l, dl
return l