Objective functions¶
Cost, error, regularizer, and constraint schemas used inside
FittingObjective / DesignObjective. Mirrors
ionworkspipeline.data_fits.objective_functions and the regularizers in
ionworkspipeline.priors.
Schemas for objective_functions.
- class ionworks_schema.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])¶
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_deviationsdict
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.Extends:
ionworks_schema.objective_functions.objective_functions.ErrorFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.objective_functions.Constraint(fun, regularizer_weight=None, type='Constraint')¶
Bases:
RegularizerEquality or inequality constraint evaluated at fit time.
Inequality constraints use
pybammHeaviside operators (<=,>=); equality constraints use apybamm.Symbolthat evaluates to zero when satisfied.Parameters¶
- funpybamm.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_weightfloat, optional
Weight applied to the constraint term. Default is 1.0.
Examples¶
>>> import pybamm >>> x0, x1 = pybamm.InputParameter("x0"), pybamm.InputParameter("x1") >>> con = iws.objective_functions.Constraint(fun=x0 - 0.5 * x1) >>> obj = iws.objectives.OCPHalfCell( ... electrode="positive", data_input="path/to/ocp.csv", ... constraints=[con], ... )
Extends:
ionworks_schema.objective_functions.regularizers.Regularizer- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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')¶
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_weightsdict, 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.Extends:
ionworks_schema.objective_functions.objective_functions.ObjectiveFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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)¶
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¶
- normalizationstr 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_valuesstr or float, optional
How to replace NaN values in the model output — see
ObjectiveFunction.- objective_weightsdict[str, float], optional
Per-objective weights — see
ObjectiveFunction.- variable_weightsdict[str, float], optional
Per-variable weights — see
ObjectiveFunction.
Extends:
ionworks_schema.objective_functions.objective_functions.ObjectiveFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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])¶
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¶
- sigmadict[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_valuesstr 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 = iws.costs.GaussianLogLikelihood(sigma={"Voltage [V]": 0.005}) >>> fit = iws.DataFit( ... objectives={ ... "cycle": iws.objectives.CurrentDriven( ... data_input="path/to/cycle.csv", options={"model": "SPM"} ... ) ... }, ... parameters={"x": iws.Parameter("x", initial_value=1.0, bounds=(0.0, 2.0))}, ... cost=cost, ... )
Extends:
ionworks_schema.objective_functions.objective_functions.ObjectiveFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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')¶
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 = iws.costs.MAE() >>> val = iws.Validation( ... objectives={ ... "cycle": iws.objectives.CurrentDriven( ... data_input="path/to/cycle.csv", options={"model": "SPM"} ... ) ... }, ... summary_stats=[cost], ... )
Extends:
ionworks_schema.objective_functions.objective_functions.ErrorFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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')¶
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.
Extends:
ionworks_schema.objective_functions.objective_functions.ErrorFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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')¶
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 = iws.costs.Max() >>> val = iws.Validation( ... objectives={ ... "cycle": iws.objectives.CurrentDriven( ... data_input="path/to/cycle.csv", options={"model": "SPM"} ... ) ... }, ... summary_stats=[cost], ... )
Extends:
ionworks_schema.objective_functions.objective_functions.ErrorFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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)])¶
Bases:
ErrorFunctionCost function combining several component costs into one weighted total.
Each component cost is evaluated on the shared model/data outputs, scaled by its weight, and the weighted values are summed. Use it to make a fit trade off two error measures at once — for example an
RMSEover the whole curve plus aMaxterm that punishes the worst-case point.Parameters¶
- costslist
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¶
normalization,nan_values,objective_weightsandvariable_weightsare rejected here — aMultiCostonly weights and sums what its components return, so it cannot apply them. Set them on the component costs instead.calculation_structureaccepts onlyNonefor every value here. A MultiCost only weights and sums what its components return, so per-variable scoping belongs on the component costs. Nothing enforces that at construction: building this object with a variable list succeeds, and what happens next depends on how the configuration is used. Converting it directly to a runtime cost raises; sending it as a stored configuration drops the variable list and logs a warning, because a configuration on the wire carries nothing to distinguish one written today from one written a year ago.Every component cost must support the output mode (scalar or residuals) that the optimizer requests.
RMSEsupports scalar output only, so aMultiCostcontaining it needs a scalar optimizer, not a least-squares one.Examples¶
>>> # weight a worst-case term against the overall RMSE >>> cost = iws.costs.MultiCost( ... costs=[iws.costs.RMSE(), iws.costs.WeightedCost(iws.costs.Max(), 0.25)], ... ) >>> # equal weights: pass bare costs >>> equal = iws.costs.MultiCost(costs=[iws.costs.RMSE(), iws.costs.Max()])
Extends:
ionworks_schema.objective_functions.objective_functions.ErrorFunction- costs: list[Annotated[CostUnion | WeightedCost, FieldInfo(annotation=NoneType, required=True, metadata=[_PydanticGeneralMetadata(union_mode='left_to_right')])]]¶
- to_config() dict¶
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, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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)¶
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_weightsdict[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_weightsdict[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_valuesstr 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_structuredict[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_nameslist[str], optional
Deprecated. A flat list of objective names this cost applies to (all variables of each). Use
calculation_structureinstead.
Extends:
ionworks_schema.base.BaseSchema- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.objective_functions.Penalty(fun, regularizer_weight=None, type='Penalty')¶
Bases:
RegularizerSoft penalty term added to the fit cost.
Parameters¶
- funpybamm.Symbol or Number
Penalty expression as a
pybamm.Symbolor a constant number.- regularizer_weightfloat, optional
Weight applied to the penalty term. Default is 1.0.
Examples¶
>>> import pybamm >>> x = pybamm.InputParameter("x") >>> pen = iws.objective_functions.Penalty(fun=x ** 2, regularizer_weight=0.1) >>> obj = iws.objectives.OCPHalfCell( ... electrode="positive", data_input="path/to/ocp.csv", ... penalties=[pen], ... )
Extends:
ionworks_schema.objective_functions.regularizers.Regularizer- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.objective_functions.Prior(name=None, distribution=None, regularizer_weight=None, *, type: Literal['Prior'] = 'Prior')¶
Bases:
RegularizerRegularization prior on a parameter.
Parameters¶
- namestr or list of str
Parameter name(s) the prior applies to.
- distributionionworks_schema.stats.Distribution
Probability distribution describing prior beliefs about the parameter(s).
- regularizer_weightfloat, optional
Weight applied to the prior’s contribution to the cost. Default is 1.0.
Examples¶
>>> prior = iws.priors.Prior("Q_pe", iws.stats.Normal(mean=3.0, std=0.2)) >>> fit = iws.DataFit( ... objectives={"ocp": iws.objectives.OCPHalfCell( ... electrode="positive", data_input="path/to/ocp.csv", ... )}, ... priors={"Q_pe": prior}, ... )
Extends:
ionworks_schema.objective_functions.regularizers.Regularizer- 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, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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')¶
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 = iws.costs.RMSE() >>> # slot into a DataFit's `cost` or a Validation's `summary_stats` >>> val = iws.Validation( ... objectives={ ... "cycle": iws.objectives.CurrentDriven( ... data_input="path/to/cycle.csv", options={"model": "SPM"} ... ) ... }, ... summary_stats=[cost], ... )
Extends:
ionworks_schema.objective_functions.objective_functions.ErrorFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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')¶
Bases:
ErrorFunctionSum-of-squared-errors cost function.
Calculates the sum of squared differences between model and data: SSE = Σ(model - data)²
Extends:
ionworks_schema.objective_functions.objective_functions.ErrorFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.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)¶
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¶
- position_variablestr, optional
Name of the variable supplying point positions in weighted mode.
- weight_variablestr, optional
Name of the variable supplying point weights in weighted mode. Absolute value is used and renormalised internally.
Extends:
ionworks_schema.objective_functions.objective_functions.ErrorFunction- model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_assignment': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ionworks_schema.objective_functions.WeightedCost(cost=None, weight: float = 1.0)¶
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¶
- costObjectiveFunction
The component cost function.
- weightfloat, optional
Multiplier applied to this component before the weighted components are combined. Defaults to 1.0.
Examples¶
>>> weighted = iws.costs.WeightedCost(iws.costs.Max(), 0.25) >>> cost = iws.costs.MultiCost(costs=[iws.costs.RMSE(), weighted])
Extends:
ionworks_schema.base.BaseSchema- 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, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].