Objective functions¶
Cost, error, regularizer, and constraint schemas used inside
FittingObjective / DesignObjective.
Schemas for objective_functions.
- class ionworks.pipeline.objective_functions.ChiSquare(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, normalization: str | int | float | None = None, type: Literal['ChiSquare'] = 'ChiSquare', variable_standard_deviations: dict[str, float])[source]¶
Bases:
ErrorFunctionChi-square cost function that measures the weighted sum of squared differences between observed and expected values, normalized by their standard deviations.
The chi-square statistic is calculated as: chi2 = sum((observed - expected) / sigma)**2 where sigma is the standard deviation for each variable.
- Parameters:
variable_standard_deviations (dict) – Dictionary mapping variable names to their standard deviations. For example: {“a”: 0.5, “b”: 0.3} means variable “a” has sigma=0.5 and variable “b” has sigma=0.3.
Notes
For a dataset with N points, if the model fits the data well and the errors are normally distributed, the chi-square value should be approximately N (the number of degrees of freedom).
Does not support
normalization(fixed at 1) orvariable_weights(derived fromvariable_standard_deviationsinstead);objective_weightsis also unsupported.- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.Constraint(fun, regularizer_weight=None, type='Constraint')[source]¶
Bases:
RegularizerEquality or inequality constraint evaluated at fit time.
A constraint is a soft penalty on the cost,
w * max(0, g(x) - h(x))**2orw * f(x)**2, applied the same way by every scipy optimizer and method. A fit can sit slightly past the boundary; raiseregularizer_weightto tighten it.- Parameters:
fun (pybamm.Symbol or Number) – Constraint expression. For inequality constraints, a Heaviside of the form
g(x) <= h(x). For equality constraints,f(x)evaluating to zero when satisfied.regularizer_weight (float, optional) – Weight applied to the constraint term. Default is 1.0.
Examples
>>> import pybamm >>> x0, x1 = pybamm.InputParameter("x0"), pybamm.InputParameter("x1") >>> con = iw.pipeline.objective_functions.Constraint(fun=x0 - 0.5 * x1) >>> obj = iw.pipeline.objectives.OCPHalfCell( ... electrode="positive", data_input="path/to/ocp.csv", ... constraints=[con], ... )
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.DesignFunction(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, type: Literal['DesignFunction'] = 'DesignFunction')[source]¶
Bases:
ObjectiveFunctionA generalized design cost function for optimization problems.
This class serves as a base for design optimization objectives where the goal is to maximize (or minimize) certain design metrics like energy density, power density, etc.
- Parameters:
objective_weights (dict, optional) – Dictionary of {name: weight} pairs for each objective in the cost function. If None, all objectives are weighted equally. If a name is not in the dictionary, it is given a weight of 1.
Notes
Does not support
nan_valuesorvariable_weights: design metrics are scalar-per-objective, so there is no per-variable axis to weight or fill NaNs on.- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.ErrorFunction(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, normalization: str | int | float | None = None)[source]¶
Bases:
ObjectiveFunctionBase for residual-based error measures (also known as distance functions).
Adds
normalizationon top ofObjectiveFunction.RMSE,MAE,MSE,Max,SSE,ChiSquare,Wasserstein, andMultiCostall inherit from here.- Parameters:
normalization (str or float, optional) –
How to normalize the model and data for each variable in the cost.
"mean"(default): use the mean of the data."identity": use 1 (no normalization)."range": use the range of the data."sum_squares": use the sum of squares of the data."mean_squares": use the mean of the sum of squares ofthe data.
"root_mean_squares": use the root mean square of thedata.
float: use that fixed value.
nan_values (str or float, optional) – How to replace NaN values in the model output — see
ObjectiveFunction.objective_weights (dict[str, float], optional) – Per-objective weights — see
ObjectiveFunction.variable_weights (dict[str, float], optional) – Per-variable weights — see
ObjectiveFunction.
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.GaussianLogLikelihood(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, type: Literal['GaussianLogLikelihood'] = 'GaussianLogLikelihood', sigma: dict[str, int | float | str])[source]¶
Bases:
ObjectiveFunctionGaussian negative log-likelihood cost function.
Computes the Gaussian NLL:
NLL = 0.5 * Σ_vars [ N_i * log(2π * σ_i²) + Σ_j (y_ij - ŷ_ij)² / σ_i² ]
This enables MLE with noise estimation, MAP estimation, and Bayesian posterior sampling (MCMC).
- Parameters:
sigma (dict[str, float | str]) –
Mapping of variable names to noise standard deviations (σ). Values can be:
A float: fixed known noise standard deviation.
A string: name of a fitting parameter to be optimised (looked up from the current inputs at evaluation time via
set_current_inputs).
nan_values (str or float, optional) – How to handle NaN values in model output. Options: a float constant (default: 1e6), “mean”, or “min”. The default fills NaN with a large penalty value to ensure solver failures produce high costs.
Notes
Does not support
objective_weightsorvariable_weights: every variable already carries its own noise scale viasigma, so a separate weight would double-count it.Examples
>>> cost = iw.pipeline.costs.GaussianLogLikelihood(sigma={"Voltage [V]": 0.005}) >>> fit = iw.pipeline.DataFit( ... objectives={ ... "cycle": iw.pipeline.objectives.CurrentDriven( ... data_input="path/to/cycle.csv", options={"model": "SPM"} ... ) ... }, ... parameters={ ... "x": iw.pipeline.Parameter("x", initial_value=1.0, bounds=(0.0, 2.0)) ... }, ... cost=cost, ... )
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.MAE(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, normalization: str | int | float | None = None, type: Literal['MAE'] = 'MAE')[source]¶
Bases:
ErrorFunctionMean-absolute-error cost function.
Instead of squaring residuals, this cost function uses the absolute values, which makes it less sensitive to outliers compared to squared-error metrics.
For scalar output, it returns the sum of absolute residuals divided by the number of points. For array output, it returns the signed square root of the absolute residuals, normalized by the square root of the number of points.
Examples
>>> cost = iw.pipeline.costs.MAE() >>> val = iw.pipeline.Validation( ... objectives={ ... "cycle": iw.pipeline.objectives.CurrentDriven( ... data_input="path/to/cycle.csv", options={"model": "SPM"} ... ) ... }, ... summary_stats=[cost], ... )
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.MSE(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, normalization: str | int | float | None = None, type: Literal['MSE'] = 'MSE')[source]¶
Bases:
ErrorFunctionMean-square-error cost function.
Similar to SSE, but normalizes by the number of data points. This makes the cost independent of the number of data points.
For scalar output, it returns the sum of squared residuals divided by the number of points. For array output, it returns the SSE residuals divided by the square root of the number of points.
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.Max(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, normalization: str | int | float | None = None, type: Literal['Max'] = 'Max')[source]¶
Bases:
ErrorFunctionCost function that reports the maximum error between the model and the data.
For scalar output, it returns the maximum absolute value of any residual. For array output, it returns a single-element array containing the square root of the maximum error.
Useful when you want to minimize the worst-case error rather than an average.
Examples
>>> cost = iw.pipeline.costs.Max() >>> val = iw.pipeline.Validation( ... objectives={ ... "cycle": iw.pipeline.objectives.CurrentDriven( ... data_input="path/to/cycle.csv", options={"model": "SPM"} ... ) ... }, ... summary_stats=[cost], ... )
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.MultiCost(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, normalization: str | int | float | None = None, type: Literal['MultiCost'] = 'MultiCost', costs: Annotated[list[Annotated[Annotated[RMSE | MAE | MSE | Max | SSE | Wasserstein | ChiSquare | MultiCost | GaussianLogLikelihood | DesignFunction, FieldInfo(annotation=NoneType, required=True, discriminator='type')] | WeightedCost, FieldInfo(annotation=NoneType, required=True, metadata=[_PydanticGeneralMetadata(union_mode='left_to_right')])]], MinLen(min_length=1)])[source]¶
Bases:
ErrorFunctionCost function combining several component costs into one weighted total.
- Parameters:
costs (list) – The component costs. A bare cost gets weight 1.0; wrap one in
WeightedCostto give it another. At least one component is required. A{cost: weight}mapping is not accepted — cost objects are not hashable. (.to_config()serializes every component to a{"cost": <cost>, "weight": <float>}record, which also validates back into this field — that is how a stored config round-trips, not a form to write by hand.)
Notes
A MultiCost only weights and sums its components’ results, so
normalization,nan_values,objective_weightsandvariable_weightsare rejected and everycalculation_structurevalue must beNone; set them on the component costs instead. Nothing enforces the last at construction: a variable list raises on conversion to a runtime cost and is dropped, with a warning, from a stored config. Every component must support the optimizer’s output mode (scalar or residuals);RMSEis scalar-only, so it needs a scalar optimizer.Examples
>>> # weight a worst-case term against the overall RMSE >>> cost = iw.pipeline.costs.MultiCost( ... costs=[iw.pipeline.costs.RMSE(), iw.pipeline.costs.WeightedCost(iw.pipeline.costs.Max(), 0.25)], ... ) >>> # equal weights: pass bare costs >>> equal = iw.pipeline.costs.MultiCost(costs=[iw.pipeline.costs.RMSE(), iw.pipeline.costs.Max()])
- costs: list[Annotated[CostUnion | WeightedCost, FieldInfo(annotation=NoneType, required=True, metadata=[_PydanticGeneralMetadata(union_mode='left_to_right')])]]¶
- to_config() dict[source]¶
Serialize to the wire format, normalizing every component to a record.
Every entry — bare cost or
WeightedCost— becomes a{"cost": ..., "weight": ...}record so the emitted config matches what the pipeline parser reads, regardless of which form was used to construct this instance.
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.ObjectiveFunction(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None)[source]¶
Bases:
BaseSchemaBase class for all cost / objective functions.
Concrete cost classes (
RMSE,MAE,Max,ChiSquare,GaussianLogLikelihood,DesignFunction, …) all inherit from this base. Construct one of those — fields that take “a cost” accept the concrete classes, not this base.- Parameters:
objective_weights (dict[str, float], optional) – Mapping of objective name to weight in the combined cost. If None, all objectives are weighted equally. Objectives not listed in the dict get a weight of 1.
variable_weights (dict[str, float], optional) – Mapping of variable name to weight. If None, all variables are weighted equally. Variables not listed in the dict get a weight of 1.
nan_values (str or float, optional) –
How to replace NaN values in the model output.
"mean": use the mean of the non-NaN model values."min": use the minimum of the non-NaN model values.float: use that fixed value.
Defaults to
"mean".calculation_structure (dict[str, list[str] or None], optional) – Explicit objective/variable scoping. Maps each objective name to the list of variable names to compute for it, or None to compute all of that objective’s variables (an empty list computes none). When None, the cost computes every objective and variable in the outputs.
objective_names (list[str], optional) – Deprecated. A flat list of objective names this cost applies to (all variables of each). Use
calculation_structureinstead.
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.Penalty(fun, regularizer_weight=None, type='Penalty')[source]¶
Bases:
RegularizerSoft penalty term added to the fit cost.
The penalty adds
regularizer_weight * fun**2to the cost: scalar optimizers get that value directly, and least-squares optimizers get a residual that they square,sqrt(regularizer_weight) * funfor sum- or mean-of-squares costs and rescaled for costs that scalarize residuals differently, so the term is the same. Pass the unsquared quantity asfun; squaring it first makes the term quartic.- Parameters:
fun (pybamm.Symbol or Number) – Penalty expression as a
pybamm.Symbolor a constant number. It is squared before it is added to the cost.regularizer_weight (float, optional) – Weight applied to the squared penalty term. Default is 1.0.
Examples
>>> import pybamm >>> x = pybamm.InputParameter("x") >>> pen = iw.pipeline.objective_functions.Penalty(fun=x, regularizer_weight=0.1) >>> obj = iw.pipeline.objectives.OCPHalfCell( ... electrode="positive", data_input="path/to/ocp.csv", ... penalties=[pen], ... )
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.Prior(name=None, distribution=None, regularizer_weight=None, *, type: Literal['Prior'] = 'Prior')[source]¶
Bases:
RegularizerRegularization prior on a parameter.
- Parameters:
name (str or list of str) – Parameter name(s) the prior applies to.
distribution (ionworks.pipeline.stats.Distribution) – Probability distribution describing prior beliefs about the parameter(s).
regularizer_weight (float, optional) – Weight applied to the prior’s contribution to the cost. Default is 1.0.
Examples
>>> prior = iw.pipeline.priors.Prior("Q_pe", iw.pipeline.stats.Normal(mean=3.0, std=0.2)) >>> fit = iw.pipeline.DataFit( ... objectives={"ocp": iw.pipeline.objectives.OCPHalfCell( ... electrode="positive", data_input="path/to/ocp.csv", ... )}, ... priors={"Q_pe": prior}, ... )
- distribution: Annotated[Dirichlet | LogNormal | MultivariateLogNormal | MultivariateNormal | Normal | PointMass | Uniform, FieldInfo(annotation=NoneType, required=True, discriminator='distribution'), WrapValidator(func=_resolve_distribution, json_schema_input_type=PydanticUndefined)]¶
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.Regularizer(*, regularizer_weight: int | float | None = None)[source]¶
Bases:
BaseSchemaBase class for regularizer terms added to the fit cost.
Concrete subclasses (
Prior,Constraint,Penalty) inherit the sharedregularizer_weightfield.- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.RMSE(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, normalization: str | int | float | None = None, type: Literal['RMSE'] = 'RMSE')[source]¶
Bases:
ErrorFunctionRoot-mean-square-error cost function.
Takes the square root of the MSE to provide a value in the same units as the original data. This is often used in scientific and engineering applications when the magnitude of error in the original units is important.
This cost function only supports scalar output, so it cannot be used with a least-squares optimizer — including when nested inside a
MultiCost.Examples
>>> cost = iw.pipeline.costs.RMSE() >>> # slot into a DataFit's `cost` or a Validation's `summary_stats` >>> val = iw.pipeline.Validation( ... objectives={ ... "cycle": iw.pipeline.objectives.CurrentDriven( ... data_input="path/to/cycle.csv", options={"model": "SPM"} ... ) ... }, ... summary_stats=[cost], ... )
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.SSE(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, normalization: str | int | float | None = None, type: Literal['SSE'] = 'SSE')[source]¶
Bases:
ErrorFunctionSum-of-squared-errors cost function.
Calculates the sum of squared differences between model and data: SSE = Σ(model - data)²
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.Wasserstein(*, objective_weights: dict[str, float] | None = None, variable_weights: dict[str, float] | None = None, nan_values: str | int | float | None = None, calculation_structure: dict[str, list[str] | None] | None = None, objective_names: list[str] | None = None, normalization: str | int | float | None = None, type: Literal['Wasserstein'] = 'Wasserstein', position_variable: str | None = None, weight_variable: str | None = None)[source]¶
Bases:
ErrorFunctionWasserstein distance cost function.
By default iterates per-objective-variable and computes the Wasserstein-1 distance between each pair of model / data sample arrays with uniform weights.
When both
position_variableandweight_variableare set, the cost switches to a weighted point-cloud mode: one variable supplies positions, the other supplies (sign-stripped, renormalised) weights, and a single Wasserstein-1 call is made per objective withW1(positions_model, positions_data, w_model, w_data). Useful for matching densities by position — e.g. lining up dQ/dV peaks in voltage rather than comparing sample-by-sample dQ/dV values.Both
position_variableandweight_variablemust be provided together; setting only one is rejected.- Parameters:
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks.pipeline.objective_functions.WeightedCost(cost=None, weight: float = 1.0)[source]¶
Bases:
BaseSchemaA cost paired with the weight applied to it in a
MultiCost.Wrap a cost in
WeightedCost(cost, weight)to give it a weight other than the default of 1.0 insideMultiCost(costs=[...]). It serializes to a bare{"cost": ..., "weight": ...}record, with no"type"key, so the emitted config matches what the pipeline parser reads.- Parameters:
cost (ObjectiveFunction) – The component cost function.
weight (float, optional) – Multiplier applied to this component before the weighted components are combined. Defaults to 1.0.
Examples
>>> weighted = iw.pipeline.costs.WeightedCost(iw.pipeline.costs.Max(), 0.25) >>> cost = iw.pipeline.costs.MultiCost(costs=[iw.pipeline.costs.RMSE(), weighted])
- cost: Annotated[RMSE | MAE | MSE | Max | SSE | Wasserstein | ChiSquare | MultiCost | GaussianLogLikelihood | DesignFunction, FieldInfo(annotation=NoneType, required=True, discriminator='type')]¶
- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].