Source code for pybop.costs.base_cost

from collections.abc import Iterable

import numpy as np

from pybop._utils import add_spaces
from pybop.costs.evaluation import Evaluation
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 | list[Solution], inputs: Inputs | list[Inputs] | None = None, calculate_sensitivities: bool = False, ) -> Evaluation: """ Computes the cost function for the given predictions. Parameters ---------- solution : Solution | list[Solution] The simulation result or a list of results. inputs : Inputs | list[Inputs], optional Input parameters or a list of inputs (default: None). calculate_sensitivities : bool Whether to also return the sensitivities (default: False). """ solution_list = solution if isinstance(solution, list) else [solution] inputs = inputs or self.parameters.to_dict("initial") inputs_list = inputs if isinstance(inputs, list) else [inputs] if len(inputs_list) != len(solution_list): raise ValueError( f"The number of solutions ({len(solution_list)}) must match " f"the number of inputs ({len(inputs_list)})." ) return self.evaluate_batch( solution=solution_list, inputs=inputs_list, calculate_sensitivities=calculate_sensitivities, )
[docs] def evaluate_batch( self, solution: list[Solution], inputs: list[Inputs], calculate_sensitivities: bool = False, ) -> Evaluation: """ Computes the cost function for the given predictions. Parameters ---------- solution : list[Solution] A list of simulation results. inputs : list[Inputs] The corresponding list of input parameters. calculate_sensitivities : bool Whether to also return the sensitivities (default: False). """ 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: Iterable[str], calculate_sensitivities: bool = True ): sign = 1.0 if self.minimising else -1.0 if calculate_sensitivities: return (sign * np.inf, {key: sign * self._de for key in parameter_names}) return sign * 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_batch( self, solution: list[Solution], inputs: list[Inputs], calculate_sensitivities: bool = False, ) -> Evaluation: """ Computes the log-prior for the given inputs, and optionally the sensitivities. Parameters ---------- solution : list[Solution] A list of simulation results. inputs : list[Inputs] The corresponding list of input parameters. calculate_sensitivities : bool Whether to also return the sensitivities (default: False). """ l = np.empty(len(solution)) dl = ( {key: np.zeros(len(solution)) for key in inputs[0].keys()} if calculate_sensitivities else None ) for i, x in enumerate(inputs): # Get the values of all input parameters input_values = np.asarray(list(x.values())) # Compute log prior (and gradient) if calculate_sensitivities: l[i], dl_array = self.parameters.distribution.logpdfS1(input_values) dl_i = {key: dl_array[i_key] for i_key, key in enumerate(x.keys())} # Use failure gradient for any infinite log prior if not np.isfinite(l[i]): l[i], dl_i = self.failure(x.keys(), calculate_sensitivities) for key in x.keys(): dl[key][i] = dl_i[key] else: l[i] = self.parameters.distribution.logpdf(input_values) return Evaluation(values=l, sensitivities=dl)