> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ionworks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Regularization

> Stabilise parameter fits with priors on single parameters, and penalties or constraints on expressions that relate several.

Regularization in `iws.DataFit` takes two forms. A **prior** pairs one fit parameter with a distribution from `iws.stats`. A **penalty** or **constraint** is a pybamm expression over any of the fit parameters, attached to an objective, for statements a distribution on one parameter cannot make. For why regularization is needed and how to choose prior strengths, see the [Regularization Guide](/guide/data-fitting/regularization).

## Distributions

| Schema | Use for |
| - | - |
| `iws.stats.Normal(mean=..., std=...)` | Gaussian prior, additive scale |
| `iws.stats.LogNormal(mean=..., std=...)` | Strictly-positive parameter spanning orders of magnitude (e.g. solid-phase diffusivities) |
| `iws.stats.Uniform(lb=..., ub=...)` | Hard support; equivalent to bounds with constant density |
| `iws.stats.MultivariateNormal(...)` | Correlated priors across multiple parameters |

## A regularized fit

```python theme={null}
import pybamm
import ionworks_schema as iws
from ionworks import Ionworks

parameters = {
    "Positive particle diffusivity [m2.s-1]": iws.Parameter(
        "Positive particle diffusivity [m2.s-1]",
        initial_value=1e-14,
        bounds=(1e-16, 1e-12),
    ),
    "Negative particle diffusivity [m2.s-1]": iws.Parameter(
        "Negative particle diffusivity [m2.s-1]",
        initial_value=3e-14,
        bounds=(1e-16, 1e-12),
    ),
}

priors = {
    "Positive particle diffusivity [m2.s-1]": iws.priors.Prior(
        "Positive particle diffusivity [m2.s-1]",
        iws.stats.LogNormal(mean=-32.2, std=1.0),  # log-mean, log-std
    ),
    "Negative particle diffusivity [m2.s-1]": iws.priors.Prior(
        "Negative particle diffusivity [m2.s-1]",
        iws.stats.LogNormal(mean=-31.1, std=1.0),
    ),
}

fit = iws.DataFit(
    objectives={
        "1C": iws.objectives.CurrentDriven(
            data_input="file:.../1C.csv",
            options={"model": pybamm.lithium_ion.SPMe()},
        ),
    },
    parameters=parameters,
    priors=priors,
    optimizer=iws.optimizers.ScipyLeastSquares(),
)

pipeline = iws.Pipeline({"fit": fit})

client = Ionworks()
submission = client.pipeline.create(pipeline)
```

The `priors` dict adds a regularization term to the cost function so deviations from the prior mean are penalised in proportion to the prior's inverse variance.

## Penalties and constraints on expressions

A prior acts on one parameter. When the physics is a relation between several, write it as a pybamm expression over `pybamm.InputParameter` symbols named after the fit parameters and attach it to an objective.

| Schema | Cost contribution |
| - | - |
| `iws.objective_functions.Penalty(fun=expr, regularizer_weight=w)` | `w * expr`, added to the objective's cost in the cost's own units |
| `iws.objective_functions.Constraint(fun=lhs <= rhs)` | Inequality: `max(0, lhs - rhs)`, scaled by the constraint-set weight (default 1000) |
| `iws.objective_functions.Constraint(fun=expr)` | Equality: `expr` must evaluate to zero |

The example keeps a cathode's remaining capacity at cut-off, its *headroom*, at or above 0.15 Ah with a one-sided quadratic penalty. It is zero wherever the headroom is comfortable and grows only where the fit would otherwise let the cathode fill up exactly as the anode empties.

```python theme={null}
import pybamm
import ionworks_schema as iws

f_p = pybamm.InputParameter("Positive electrode active volume fraction of solid")
f_n = pybamm.InputParameter("Negative electrode active volume fraction of solid")
c_n = pybamm.InputParameter("Initial concentration in negative electrode [mol.m-3]")

headroom = f_p * 4.69 * (1 - 0.22) - f_n * 3.46 * (c_n / 18738 - 0.993) - 3.33  # Ah
penalty = iws.objective_functions.Penalty(
    fun=pybamm.maximum(0.0, 0.15 - headroom) ** 2,  # V, added to a voltage RMSE
    regularizer_weight=1.0,
)

objective = iws.objectives.CurrentDriven(
    data_input="db:<measurement id>",
    options={"model": pybamm.lithium_ion.DFN()},
    penalties=[penalty],
)
```

Pick `regularizer_weight` so the penalty is comparable to the cost where it should bite: here a 0.1 Ah shortfall costs 10 mV against a voltage RMSE of about 9 mV. A symbol `fun` serialises to pybamm's JSON form in `to_config()` and is rebuilt on the server, so the same object works locally and through `client.pipeline.create`.

## Attaching priors via `Parameter`

Priors can also be attached directly to a `Parameter` rather than passed as a separate dict — useful when the prior is intrinsic to that parameter:

```python theme={null}
diffusivity = iws.Parameter(
    "Positive particle diffusivity [m2.s-1]",
    initial_value=1e-14,
    bounds=(1e-16, 1e-12),
    prior=iws.stats.LogNormal(mean=-32.2, std=1.0),
)
```

## Dict-form priors

Schema configs can also be written as plain dicts — useful when a config is loaded from JSON or YAML. Two equivalent forms are accepted in the `priors` mapping:

```python theme={null}
# Flat form: distribution name as a string, params inline
priors = {
    "Positive particle diffusivity [m2.s-1]": {
        "distribution": "LogNormal",
        "mean": -32.2,
        "std": 1.0,
    },
}

# Nested form: distribution as its own dict
priors = {
    "Positive particle diffusivity [m2.s-1]": {
        "distribution": {"distribution": "LogNormal", "mean": -32.2, "std": 1.0},
        "regularizer_weight": 2.0,
    },
}
```

The mapping key is the authoritative parameter name. Any `name` embedded inside the prior dict is ignored — including when the parameter itself is literally named `"distribution"`.

## Strict validation

Priors, distributions, and samplers are validated against discriminated unions at submission time. This catches typos and stale configs before a run starts, rather than letting them surface as opaque runtime crashes.

Mistakes that are now rejected with a `ValidationError`:

* An unknown distribution name (e.g. `{"distribution": "Guassian", ...}`).
* A stray or misspelled key in a distribution, prior, or sampler config.
* A `type` discriminator that doesn't match the field (e.g. `{"type": "Penalty", ...}` placed under `priors`).
* A bare scalar or list passed where a prior, distribution, or sampler is expected.

The legacy `type` alias is still accepted in place of the `distribution` discriminator **inside a distribution config** — e.g. the nested form's inner dict `{"type": "Normal", "mean": 3.0, "std": 0.2}` resolves to a `Normal` — so existing serialized configs round-trip unchanged. Note that `type` only aliases `distribution` at the distribution level: at the top level of a prior mapping, `type` is the prior discriminator (it must be `"Prior"`), so a flat-form prior must name its distribution with `distribution`, not `type`.

## Why `LogNormal` for diffusivities

Solid-phase diffusivities span many orders of magnitude (often $10^{-16}$ to $10^{-10}$ m²/s). A `Normal` prior on the raw value is hard to specify — mean ± std doesn't reflect order-of-magnitude uncertainty. A `LogNormal` prior treats the parameter on a log scale, so "mean ± 1 std" corresponds to a factor of $e$ — much more natural. The `mean=-32.2` in the example above is the natural log of $\sim 10^{-14}$, so the prior is centred on a typical particle diffusivity.

<CardGroup cols={2}>
  <Card title="Regularization (theory)" icon="scale-balanced" href="/guide/data-fitting/regularization">
    Ridge regression, MAP estimation, bias–variance tradeoff.
  </Card>

  <Card title="Data Fitting overview" icon="chart-line" href="/build/parameterize/data-fitting/overview">
    Putting priors together with objectives and optimisers.
  </Card>
</CardGroup>
