Skip to main content
A DataFit has two coupled pieces:
  • Objectives (iws.objectives.*) — what experiments to compare model output against.
  • Cost (iws.costs.*) — how the per-point disagreements are aggregated into a single number.
For the math behind each cost, see the Objective Functions Guide.

Available cost functions

SchemaFormulaWhen to use
iws.costs.SSE()iri2\sum_i r_i^2Default; works with every optimiser
iws.costs.MSE()1Niri2\frac{1}{N}\sum_i r_i^2Scale-aware mean of squared residuals
iws.costs.RMSE()1Niri2\sqrt{\frac{1}{N}\sum_i r_i^2}Interpretable units; scalar-only (won’t work with residual-array optimisers)
iws.costs.MAE()1Niri\frac{1}{N}\sum_i \lvert r_i \rvertRobust to outliers
iws.costs.Wasserstein()1Niy~model,iy~data,i\frac{1}{N}\sum_i \lvert \tilde y_{\text{model},i} - \tilde y_{\text{data},i} \rvertMatch distributions (sorted samples) rather than point-wise time series. Set position_variable and weight_variable for weighted point-cloud mode
For MLE, see iws.costs.GaussianLogLikelihood — it accepts per-variable noise standard deviations or can estimate them alongside the fitting parameters. It produces a Gaussian negative log-likelihood suitable for Bayesian and MAP estimation.

Wiring a cost into a fit

import ionworks_schema as iws

fit = iws.DataFit(
    objectives={
        "1C": iws.objectives.CurrentDriven(
            data_input="file:.../1C.csv",
            options={"model": {"type": "SPMe"}},
        ),
    },
    parameters={
        "Negative particle diffusivity [m2.s-1]": iws.Parameter(
            "Negative particle diffusivity [m2.s-1]",
            initial_value=2e-14,
            bounds=(1e-14, 1e-13),
        ),
    },
    cost=iws.costs.RMSE(),
)
If cost is omitted, the optimizer’s default cost function is used (typically a least-squares form).

Wasserstein weighted point-cloud mode

By default iws.costs.Wasserstein() compares the model and data samples for each objective variable with uniform weights (sorted point-wise comparison). Set both position_variable and weight_variable to switch to weighted point-cloud mode: one variable supplies the positions, the other supplies the (sign-stripped, renormalised) weights, and a single Wasserstein-1 distance is computed per objective. Use this when you want to match a density by position rather than sample-by-sample values — for example, lining up dQ/dV peaks in voltage rather than penalising every dQ/dV residual.
import ionworks_schema as iws

fit = iws.DataFit(
    objectives={
        "ocp": iws.objectives.MSMRFullCell(
            data_input="file:.../ocp.csv",
            options={
                "model": {"type": "MSMR"},
                "objective variables": [
                    "Differential capacity [Ah/V]",
                    "Voltage [V] (dQdU)",
                ],
            },
        ),
    },
    parameters={...},
    cost=iws.costs.Wasserstein(
        position_variable="Voltage [V] (dQdU)",
        weight_variable="Differential capacity [Ah/V]",
    ),
)
position_variable and weight_variable must be set together — providing only one raises a validation error. Weights are taken as absolute values and renormalised internally, so sign conventions on dQ/dV don’t matter. Residual-array output is not available in this mode.

Available objectives

SchemaUse for
iws.objectives.CurrentDriven(data_input=..., options={...})Time-series voltage vs. current loads (drive cycles, custom loads)
iws.objectives.Pulse(data_input=..., options={...})Pulse experiments — GITT, HPPC, ICI — with optional feature-extraction variants
iws.objectives.OCPHalfCell(electrode=..., data_input=...)Half-cell OCP curves
iws.objectives.MSMRHalfCell(...)Fit MSMR parameters to half-cell data
iws.objectives.MSMRFullCell(...)Fit MSMR parameters to full-cell data. Supports Differential voltage [V/Ah] and Differential capacity [Ah/V] as objective variables
iws.objectives.ElectrodeBalancing(...)Stoichiometry windows from full-cell discharge
iws.objectives.EIS(...)Electrochemical impedance spectra
iws.objectives.Resistance(...)DC resistance extracted from pulse data
iws.objectives.CalendarAgeing(...) / iws.objectives.CycleAgeing(...)Ageing curves
Combine several by passing a dict[str, objective] to DataFit.objectives.

Tuning the auto-built solver

Simulation-backed objectives (CurrentDriven, Pulse, CalendarAgeing, CycleAgeing, MSMRFullCell, …) build an IonworksSolver for you when no explicit solver is provided. Pass solver_kwargs inside simulation_kwargs to override individual pieces of that default without restating the rest:
  • Nested options are merged over the default IDAKLU options. For example, {"options": {"compile": True}} flips on model compilation but keeps every other tuned option.
  • Other top-level keys (atol, rtol, on_extrapolation, …) override the corresponding default solver kwargs.
solver_kwargs is ignored (with a warning) when an explicit solver is supplied — configure those on the solver instance directly. It is also ignored when the model’s default solver isn’t IDAKLU-based.
import ionworks_schema as iws

fit = iws.DataFit(
    objectives={
        "1C": iws.objectives.CurrentDriven(
            data_input="file:.../1C.csv",
            options={
                "model": {"type": "SPMe"},
                "simulation_kwargs": {
                    "solver_kwargs": {
                        "options": {"compile": True},
                        "atol": 1e-8,
                    },
                },
            },
        ),
    },
    parameters={...},
)
Enabling compile ahead of time ({"options": {"compile": True}}) trades a one-off compilation cost for faster repeated evaluations — useful when the same objective is solved many times during a fit or sweep.
For most optimisers, SSE is the safest choice — it has both a residual-array form and a scalar form, so it’s compatible with every algorithm. Use MSE or RMSE when you need scale-independent reporting.

Objective Functions (theory)

Residual vs. canonical form, MLE interpretation.

Data Fitting overview

Putting objectives, parameters, and optimisers together.