Source code for pybop.costs.weighted_cost
import numpy as np
from pybop.costs.base_cost import BaseCost
from pybop.costs.design_cost import DesignCost
from pybop.parameters.parameter import Inputs
from pybop.simulators.solution import Solution
[docs]
class WeightedCost(BaseCost):
"""
A subclass for constructing a linear combination of cost functions as
a single weighted cost function.
Parameters
----------
costs : pybop.BaseCost
The individual PyBOP cost objects.
weights : list[float]
A list of values with which to weight the cost values.
"""
def __init__(self, *costs, weights: list[float] | None = None):
if not all(isinstance(cost, BaseCost) for cost in costs):
raise TypeError("All costs must be instances of BaseCost.")
if len(set(isinstance(cost, DesignCost) for cost in costs)) > 1:
raise TypeError(
"Costs must be either all design costs or all error measures."
)
self.costs = [cost for cost in costs]
if len(set(cost.domain for cost in self.costs)) > 1:
raise ValueError("All costs must have the same domain.")
super().__init__()
self._domain = self.costs[0].domain
for cost in self.costs:
self.parameters.join(cost.parameters)
self.set_target([cost.target for cost in self.costs])
# Check if weights are provided
if weights is not None:
try:
self.weights = np.asarray(weights, dtype=float)
except ValueError:
raise ValueError("Weights must be numeric values.") from None
if self.weights.size != len(self.costs):
raise ValueError("Number of weights must match number of costs.")
else:
self.weights = np.ones(len(self.costs))
# Apply the minimising property from each cost
for i, cost in enumerate(self.costs):
self.weights[i] = self.weights[i] * (1 if cost.minimising else -1)
if all(not cost.minimising for cost in self.costs):
# If all costs are maximising, convert the weighted cost to maximising
self.weights = -self.weights
self.minimising = False
[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.
"""
e = np.empty_like(self.costs)
de = {key: np.zeros(len(self.costs)) for key in inputs.keys()}
for i, cost in enumerate(self.costs):
if calculate_sensitivities:
e[i], sensitivities = cost.evaluate(
solution,
inputs=inputs,
calculate_sensitivities=calculate_sensitivities,
)
for key, value in sensitivities.items():
de[key][i] = value
else:
e[i] = cost.evaluate(
solution,
inputs=inputs,
calculate_sensitivities=calculate_sensitivities,
)
e = np.dot(e, self.weights)
if calculate_sensitivities:
for key in de.keys():
de[key] = np.dot(de[key], self.weights)
return e, de
return e
[docs]
def set_target(self, target: list[list[str]] | list[str] | str | None = None):
"""Set the target variable for all costs. Expecting a list of list[str] the same length as self.costs."""
target = [target] if isinstance(target, str) else target or self._target
if isinstance(target[0], str):
target = [target] * len(self.costs)
self._target = []
for i, cost in enumerate(self.costs):
cost.set_target(target[i])
self._target.extend(cost.target)
@property
def target(self):
return list(set(self._target))