Costs

Cost-function schemas. This is a convenience alias for a subset of ionworks_schema.objective_functions that mirrors ionworkspipeline.costs.

Schemas for costs.

class ionworks_schema.costs.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: ErrorFunction

Chi-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) or variable_weights (derived from variable_standard_deviations instead); objective_weights is also unsupported.

Extends: ionworks_schema.objective_functions.objective_functions.ErrorFunction

type: Literal['ChiSquare']
variable_standard_deviations: dict[str, float]
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ObjectiveFunction

A 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_values or variable_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

type: Literal['DesignFunction']
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ObjectiveFunction

Base for residual-based error measures (also known as distance functions).

Adds normalization on top of ObjectiveFunction. RMSE, MAE, MSE, Max, SSE, ChiSquare, Wasserstein, and MultiCost all 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 of

    the data.

  • "root_mean_squares": use the root mean square of the

    data.

  • 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

normalization: str | int | float | None
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ObjectiveFunction

Gaussian 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_weights or variable_weights: every variable already carries its own noise scale via sigma, 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

type: Literal['GaussianLogLikelihood']
sigma: dict[str, int | float | str]
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ErrorFunction

Mean-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

type: Literal['MAE']
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ErrorFunction

Cost 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

type: Literal['Max']
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ErrorFunction

Mean-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

type: Literal['MSE']
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ErrorFunction

Cost 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 RMSE over the whole curve plus a Max term that punishes the worst-case point.

Parameters

costslist

The component costs. A bare cost gets weight 1.0; wrap one in WeightedCost to 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_weights and variable_weights are rejected here — a MultiCost only weights and sums what its components return, so it cannot apply them. Set them on the component costs instead.

calculation_structure accepts only None for 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. RMSE supports scalar output only, so a MultiCost containing 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

type: Literal['MultiCost']
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: BaseSchema

Base 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_structure instead.

Extends: ionworks_schema.base.BaseSchema

objective_weights: dict[str, float] | None
variable_weights: dict[str, float] | None
nan_values: str | int | float | None
calculation_structure: dict[str, list[str] | None] | None
objective_names: list[str] | None
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ErrorFunction

Root-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

type: Literal['RMSE']
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ErrorFunction

Sum-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

type: Literal['SSE']
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.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: ErrorFunction

Wasserstein 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_variable and weight_variable are 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 with W1(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_variable and weight_variable must 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

type: Literal['Wasserstein']
position_variable: str | None
weight_variable: str | None
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.

class ionworks_schema.costs.WeightedCost(cost=None, weight: float = 1.0)

Bases: BaseSchema

A 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 inside MultiCost(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')]
weight: float
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].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Args:

self: The BaseModel instance. context: The context.