Source code for pybop.costs.base_cost

from typing import Optional, Union

import numpy as np
from numpy import ndarray

from pybop import BaseProblem
from pybop.parameters.parameter import Inputs, Parameters


[docs] class BaseCost: """ Base class for defining cost functions. This class is intended to be subclassed to create specific cost functions for evaluating model predictions against a set of data. The cost function quantifies the goodness-of-fit between the model predictions and the observed data, with a lower cost value indicating a better fit. Parameters ---------- problem : object A problem instance containing the data and functions necessary for evaluating the cost function. target : array-like An array containing the target data to fit. n_outputs : int The number of outputs in the model. has_separable_problem : bool If True, the problem is separable from the cost function and will be evaluated in advance of the call to self.compute() (default: False). _de : float The gradient of the cost function to use if an error occurs during evaluation. Defaults to 1.0. """ def __init__(self, problem: Optional[BaseProblem] = None):
[docs] self.parameters = Parameters()
[docs] self.transformation = None
[docs] self.problem = problem
[docs] self.verbose = False
[docs] self._has_separable_problem = False
[docs] self.y = None
[docs] self.dy = None
[docs] self._de = 1.0
if isinstance(self.problem, BaseProblem): self._target = self.problem.target self.parameters.join(self.problem.parameters) self.n_outputs = self.problem.n_outputs self.signal = self.problem.signal self.transformation = self.parameters.construct_transformation() self._has_separable_problem = True self.grad_fail = None self.set_fail_gradient() @property
[docs] def n_parameters(self): return len(self.parameters)
@property
[docs] def has_separable_problem(self): return self._has_separable_problem
@property
[docs] def target(self): return self._target
[docs] def __call__( self, inputs: Union[Inputs, list], calculate_grad: bool = False, apply_transform: bool = False, ): """ This method calls the forward model via problem.evaluate(inputs), and computes the cost for the given output by calling self.compute(). Parameters ---------- inputs : Inputs or array-like The parameters for which to compute the cost and gradient. calculate_grad : bool, optional A bool condition designating whether to calculate the gradient. Returns ------- float The calculated cost function value. Raises ------ ValueError If an error occurs during the calculation of the cost. """ # Apply transformation if needed self.has_transform = self.transformation is not None and apply_transform if self.has_transform: inputs = self.transformation.to_model(inputs) inputs = self.parameters.verify(inputs) self.parameters.update(values=list(inputs.values())) y, dy = None, None if self._has_separable_problem: if calculate_grad: y, dy = self.problem.evaluateS1(self.problem.parameters.as_dict()) cost, grad = self.compute(y, dy=dy, calculate_grad=calculate_grad) if self.has_transform and np.isfinite(cost): jac = self.transformation.jacobian(inputs) grad = np.matmul(grad, jac) return cost, grad y = self.problem.evaluate(self.problem.parameters.as_dict()) return self.compute(y, dy=dy, calculate_grad=calculate_grad)
[docs] def compute(self, y: dict, dy: ndarray, calculate_grad: bool = False): """ Compute the cost and if `calculate_grad` is True, its gradient with respect to the predictions. This method only computes the cost, without calling the `problem.evaluate()`. This method must be implemented by subclasses. Parameters ---------- y : dict The dictionary of predictions with keys designating the signals for fitting. dy : np.ndarray, optional The corresponding gradient with respect to the parameters for each signal. calculate_grad : bool, optional A bool condition designating whether to calculate the gradient. Returns ------- float The calculated cost function value. Raises ------ NotImplementedError If the method has not been implemented by the subclass. """ raise NotImplementedError
[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 self.grad_fail = self._de * np.ones(self.n_parameters)
[docs] def verify_prediction(self, y): """ Verify that the prediction matches the target data. Parameters ---------- y : dict The model predictions. Returns ------- bool True if the prediction matches the target data, otherwise False. """ if any( len(y.get(key, [])) != len(self._target.get(key, [])) for key in self.signal ): return False return True
[docs] def verify_args(self, dy: ndarray, calculate_grad: bool): if calculate_grad and dy is None: raise ValueError( "Forward model sensitivities need to be provided alongside `calculate_grad=True` for `cost.compute`." )