> ## 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.

# ECM parameterization

> Fit OCV, R0, and RC-pair parameters of an equivalent circuit model from experimental cycling data, in-browser

The **ECM Parameterization** tool fits an equivalent circuit model (ECM) to experimental battery cycling data directly in the browser. Fit a measurement that you've already uploaded to a [project](/core-concepts/projects-studies), upload your own file, or use one of the built-in example datasets to extract cell capacity, OCV, R0, and RC-pair parameters as functions of state of charge (SOC).

There are two ways to launch the tool:

* **Inside a project** — open the **ECM Fitting** page from the project sidebar. You can pick a measurement that's already attached to one of the project's cells and the result is saved back to the project.
* **Standalone demo** — visit [studio.ionworks.com/ecm-demo](https://studio.ionworks.com/ecm-demo) directly. No login is required, and you can experiment with example datasets or upload a one-off file without saving anything.

## How it works

The tool fits a circuit consisting of an open-circuit voltage (OCV) source, a series resistance (R0), and one or more RC pairs to time-series voltage and current data. The fitting process extracts parameters as smooth functions of SOC, and co-optimises the cell's usable capacity together with the RC parameters using the fitted OCV(SOC) curve as a constraint.

The circuit structure looks like this:

```
  ┌───[ R0 ]───┬───[ R_rc1 ]───┬───[ R_rc2 ]───┐
  │             │               │               │
 OCV         [ C_rc1 ]       [ C_rc2 ]         V_terminal
  │             │               │               │
  └─────────────┴───────────────┴───────────────┘
```

Each RC pair captures a different timescale of dynamic polarization behavior. More RC pairs produce a more accurate fit but also increase model complexity.

### Capacity co-optimisation

Cycling data often doesn't reflect the cell's exact usable capacity — measurements are taken at different temperatures, C-rates, and ages, and small errors in the assumed capacity bias every SOC-dependent parameter that follows. To avoid this, the fit co-optimises the cell's capacity alongside the RC parameters, using the OCV(SOC) curve as a self-consistency constraint: SOC at any time is computed from the integrated current divided by the fitted capacity, and the OCV at that SOC must agree with the rest-voltage segments of the data.

The fitted capacity is returned in the fit response, so you can use it directly when building a [parameterized model](/build/parameterized-models) without having to estimate cell capacity separately.

## Fitting a measurement in a project

Use this workflow when you want the fit to be associated with a specific [project](/core-concepts/projects-studies) and [cell](/core-concepts/cells), and to use a measurement you've already [uploaded](/data/uploading).

<Steps>
  <Step title="Open ECM Fitting from your project">
    Open the project, then click **ECM Fitting** in the sidebar. The page lists the cells in the project and the measurements attached to each one.
  </Step>

  <Step title="Pick one or more measurements">
    Select the cell measurements you want to fit. A preview plot of the voltage and current traces is shown so you can confirm it's the right data before running the fit.

    Only `time_series` measurements with voltage, current, and time data can be fit. Properties and file-type measurements aren't shown in the picker.

    You can select **multiple measurements** to fit them jointly as a single ECM. This is useful when each measurement covers a different part of the operating window (for example, separate pulse trains at different SOCs or on different cell instances). The fit treats each measurement as its own segment and shares a single set of SOC-dependent parameters across all of them.
  </Step>

  <Step title="Configure and run the fit">
    Set the number of RC pairs (0–5) and toggle **Fit OCV** as described in [Configure the fit](#using-the-tool).

    When you select two or more measurements, you must enter an **initial SOC (0–1) for each measurement** in the configuration card. This tells the fitter where each segment starts on the SOC axis so it can stitch them together. Capacity remains a single value shared across all selected measurements (leave blank to estimate from the data). For a single measurement, the initial SOC field stays optional and is estimated automatically when left blank.

    Click **Parameterize ECM** to start.
  </Step>

  <Step title="Save the result to the project">
    When the fit completes, review the results and click **Save** to attach the fitted parameter set to the project's cell. Saved fits appear in the cell's measurement history and can be used as a starting point when [creating a parameterized model](/build/parameterized-models#creating-a-new-parameterized-model).
  </Step>
</Steps>

## Using the tool

The standalone demo at [studio.ionworks.com/ecm-demo](https://studio.ionworks.com/ecm-demo) — and the fit configuration step inside a project — share the same controls.

<Steps>
  <Step title="Select your data">
    Choose from built-in example datasets or upload your own cycling data file. (Inside a project, you instead pick a measurement attached to one of the project's cells, as described above.)

    **Built-in examples** include cells from published literature (Chen 2020, Ecker 2015, Prada 2013, and others) as well as drive-cycle profiles (UDDS, mixed current). Each example shows a recommended number of RC pairs.

    **Uploaded files** are automatically detected and parsed. The tool supports common cycler formats including CSV, Excel, and formats from BaSyTec, Maccor, and Biologic. Your file must contain time, voltage, and current columns. If an `Open-circuit voltage [V]` column is present, you can use it directly instead of fitting OCV.
  </Step>

  <Step title="Configure the fit">
    Set the number of **RC pairs** (0–5). More RC pairs capture faster dynamics but increase complexity. The recommended value depends on your data — example datasets show a suggested count.

    Toggle **Fit OCV** on or off. When your data includes a measured OCV column, you can disable OCV fitting to use the provided values directly and only fit R0 and RC parameters.
  </Step>

  <Step title="View results">
    After fitting, you see:

    * **Model vs. data** voltage comparison plot and RMSE
    * **OCV(SOC)** and **R0(SOC)** parameter curves
    * **R\_rc(SOC)**, **C\_rc(SOC)**, and **τ\_rc(SOC)** curves for each RC pair

    The fitted cell capacity, co-optimised from the OCV(SOC) curve, is returned in the fit response.

    <Note>
      Full RC-pair parameters require ECM results access to be enabled for your organization. Contact [info@ionworks.com](mailto:info@ionworks.com) to request access.
    </Note>
  </Step>
</Steps>

## Downloading results as CSV

After a fit completes, click the **Download CSV** button in the results header to export the fitted parameters.

The CSV contains 200 interpolated SOC points with these columns:

| Column          | Description                    |
| --------------- | ------------------------------ |
| `SOC`           | State of charge (0 to 1)       |
| `OCV [V]`       | Open-circuit voltage           |
| `R0 [mOhm]`     | Series resistance in milliohms |
| `R_rc_N [mOhm]` | RC pair N resistance           |
| `C_rc_N [F]`    | RC pair N capacitance          |
| `tau_N [s]`     | RC pair N time constant        |

Columns for each fitted RC pair follow this pattern (e.g. `R_rc_1`, `R_rc_2`, …).

<Note>
  RC-pair columns are included only when ECM results access is enabled for your organization. Otherwise the CSV contains SOC, OCV, and R0 only. Contact [info@ionworks.com](mailto:info@ionworks.com) to request access.
</Note>

## Data requirements

Your cycling data must include:

* **Time \[s]** — time in seconds
* **Voltage \[V]** — terminal voltage
* **Current \[A]** — applied current

Optionally, include **Open-circuit voltage \[V]** to skip OCV fitting and use measured OCV directly.

<Tip>
  The tool uses [ionworksdata](https://data.docs.ionworks.com/) for format detection, so data exported from common cyclers is usually recognized automatically.
</Tip>

## Fitting from Python

The same fits available in the UI can be submitted programmatically through `client.ecm` in the [Python API client](/api-client). Authenticated fits run as **background jobs** — each `fit_*` call returns immediately with a job handle, and `wait_for_completion` blocks until the result is ready (typically 10–60 s).

Three input modes are supported:

| Method                                  | Source                              | Auth     | Returns                    |
| --------------------------------------- | ----------------------------------- | -------- | -------------------------- |
| `client.ecm.fit_from_example(...)`      | Built-in demo dataset               | optional | `FitResults` (synchronous) |
| `client.ecm.fit_from_file(...)`         | Local file upload                   | required | `EcmFitJob` (async)        |
| `client.ecm.fit_from_measurements(...)` | Measurements stored in the platform | required | `EcmFitJob` (async)        |

### Fit from stored measurements

```python theme={null}
from ionworks import Ionworks

client = Ionworks()

fit_job = client.ecm.fit_from_measurements({
    "measurements": [
        {"id": "meas-id-1"},
        {"id": "meas-id-2", "start_step": 5, "end_step": 50, "initial_soc": 0.95},
    ],
    "ecm_options": {
        "num_rcs": 2,       # 0–5 RC pairs (default 2)
        "fit_ocv": True,    # set False when the data already has an OCV column
    },
})

result = client.ecm.wait_for_completion(fit_job, timeout=300)
print(f"RMSE: {result.rmse_mV:.2f} mV")
```

`result` is a `FitResults` model with `time`, `data_voltage`, `model_voltage` traces, `soc`/`ocv`/`r0` parameter grids, and `rc_pairs[i].r/.c/.tau` curves for each RC pair.

### Fit from a local file

Accepts CSV, parquet, and any cycler format that `ionworksdata` can detect:

```python theme={null}
fit_job = client.ecm.fit_from_file(
    "data/pulse_test.csv",
    num_rcs=2,
    fit_ocv=True,
    initial_soc=0.95,   # optional
    capacity=4.85,      # optional, in Ah
)
result = client.ecm.wait_for_completion(fit_job)
```

To preview a file before fitting (auto-detect format, return its time-series without running a fit), call `client.ecm.detect_and_read(file)`.

### Multi-measurement fits with per-segment SOC seeds

When you fit multiple measurements jointly, each segment can carry its own `initial_soc` so the fitter starts each segment from the correct state of charge:

```python theme={null}
fit_job = client.ecm.fit_from_measurements({
    "measurements": [
        {"id": "meas-fully-charged",     "initial_soc": 1.00},
        {"id": "meas-partial-discharge", "initial_soc": 0.80},
    ],
    "ecm_options": {"num_rcs": 2, "capacity": 4.85},
})
```

When `initial_soc` is omitted and an `ocv_soc_curve` is provided in `ecm_options`, the service auto-seeds `soc0` by inverting `V[s] = OCV(soc0) − I[s]·R0(soc0)` after a warm-up fit. Without a curve, single-measurement runs fall back to coulomb-counting.

### Per-measurement capacity

When each measurement was recorded on a cell with a different known capacity — e.g. measurements taken at different ages, or on different physical cells you're fitting jointly — attach the capacity directly to each measurement dict:

```python theme={null}
fit_job = client.ecm.fit_from_measurements({
    "measurements": [
        {"id": "meas-fresh-cell", "initial_soc": 1.00, "capacity": 4.85},
        {"id": "meas-aged-cell",  "initial_soc": 1.00, "capacity": 4.50},
    ],
    "ecm_options": {"num_rcs": 2},
})
```

Rules:

* Per-measurement `capacity` is **all-or-none** — either every measurement supplies one, or none of them do. Mixed configurations are rejected at submission time.
* Each capacity must be `> 0` (Ah).
* When per-measurement capacities are provided, the shared `ecm_options.capacity` is ignored. When they're not, the shared value applies to every segment (or capacity is estimated / fit against `bounds_capacity` when the shared value is also omitted).
* The returned `FitResults.capacity_Ah` is a list with one entry per measurement segment. When a single cell-wide capacity was used (estimated, fitted, or supplied via `ecm_options.capacity`), that value is repeated across segments so the shape stays consistent.

<Note>
  Fitting capacity (via `bounds_capacity`) still produces a single cell-wide value shared across segments. Per-measurement `capacity` is for cases where you already **know** each segment's capacity and want to hold each one fixed.
</Note>

### Smoothness regularization

`ecm_options.regularization` applies a Gaussian smoothness prior to the R0 / RC parameter curves (never to OCV). Increase it to damp oscillations in the fitted SOC-dependent parameters when your data doesn't tightly constrain them — for example, noisy pulse data or short traces that only cover a narrow SOC window.

```python theme={null}
fit_job = client.ecm.fit_from_measurements({
    "measurements": [{"id": "meas-id-1"}],
    "ecm_options": {
        "num_rcs": 2,
        "regularization": 1.0,   # 0 disables (default); larger = smoother curves
    },
})
```

For `regularization > 0` the value maps internally to `scale = 5 / regularization`, so `1.0` is a modest prior and larger values apply a stronger smoothness penalty. `0.0` (the default) is special-cased to disable the prior entirely — no smoothness penalty is added and the `scale = 5 / regularization` formula is not evaluated (so there is no division by zero). The same option is available on `fit_from_file(..., regularization=...)`.

### Fitting capacity from a known OCV(SoC) curve

If you have an OCV curve from a separate slow-rate characterisation, pass it via `ecm_options.ocv_soc_curve` to skip OCV fitting and (optionally) co-optimise capacity within explicit bounds:

```python theme={null}
fit_job = client.ecm.fit_from_measurements({
    "measurements": [{"id": "meas-id-1"}],
    "ecm_options": {
        "num_rcs": 2,
        "ocv_soc_curve": {
            "soc": [0.0, 0.1, 0.2, 0.5, 0.8, 1.0],
            "ocv": [3.0, 3.2, 3.4, 3.7, 4.0, 4.2],
        },
        "bounds_capacity": {"lo": 4.0, "hi": 6.0},
    },
})
```

Constraints validated locally before the request hits the wire:

* `ocv_soc_curve.soc` must be strictly increasing and lie in `[0, 1]`.
* `ocv_soc_curve.soc` and `.ocv` must have the same length (≥ 2).
* `bounds_capacity` is only consulted when `capacity` is `None`; it requires `hi > lo`.
* Mutually exclusive with input data that already carries an `Open-circuit voltage [V]` column — pass one or the other.

### Tuning knot resolution

For challenging traces (long relaxations, multiple time scales), bump the SOC-knot resolution via `ecm_options`:

```python theme={null}
"ecm_options": {
    "num_rcs": 3,
    "num_knots": 21,                    # total SOC knots for RC-pair parameters
    "num_knots_r0": 7,                  # SOC knots for R0(SoC)
    "knot_schedule": [3, 5, 9, 21],     # multi-resolution refinement schedule
    "clamp_boundary_knots": True,       # default; relax for symbolic R_rc=alpha/beta
    "clamp_max_ratio": 10.0,
}
```

`knot_schedule` must be a strictly increasing list of positive ints ending at `num_knots`. Defaults are auto-derived when omitted.

### Save a fit as a Parameterized Model

Once a fit completes, persist it inside a project so it can be used in simulations:

```python theme={null}
saved = client.ecm.save_to_project(
    name="ECM 2RC — pulse test",
    cell_spec_id="cell-spec-id",
    fit_results=result,
    description="Fitted from May 2026 pulse test",
)

print(saved.parameterized_model_id)
```

The returned `parameterized_model_id` can be used as the `parameterized_model` in `client.simulation.protocol(...)`. See [Parameterized Models](/build/parameterized-models) for more.

### Fit a built-in example without auth

`fit_from_example` is rate-limited (60/min) and synchronous — no job polling needed:

```python theme={null}
examples = client.ecm.list_examples()
result = client.ecm.fit_from_example(examples[0]["id"], num_rcs=2)
```

RC-pair parameters are only included for authenticated callers with ECM results access enabled.

## Validating on held-out data

Once you have a fitted ECM, use `client.ecm.validate(...)` from the [Python API client](/api-client) to check how well it reproduces a **held-out** load case — a measurement, rate, or drive cycle the fit did *not* see. The call re-simulates the fitted model forward on the held-out current trace using the same engine the fit uses internally and returns aligned model-vs-data traces plus error metrics.

The call is **synchronous** (no job, no [pipeline](/pipelines/overview)) and is the right tool for ECM held-out validation — don't route an ECM through a validation pipeline for this.

### When to use it

* After a fit, to gate whether the model is accurate enough for the application.
* To compare a saved parameterized model against a new measurement at a different rate or drive cycle.
* To produce a model-vs-data overlay and residual plot for a report.

### Inputs

Provide exactly one **held-out source** and exactly one **model source**:

| Argument                 | Description                                                                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `measurement_id`         | Held-out cell measurement (not used in the fit). Mutually exclusive with `example_id`.                                                      |
| `example_id`             | Built-in example dataset to validate against.                                                                                               |
| `fit_results`            | In-memory `FitResults` returned by `wait_for_completion(...)` or `fit_from_example(...)`. Mutually exclusive with `parameterized_model_id`. |
| `parameterized_model_id` | A saved ECM parameterized model (e.g. from `save_to_project`).                                                                              |

Optional arguments:

* `start_step`, `end_step` — inclusive step bounds applied to the held-out measurement.
* `initial_soc` — known SOC (0–1) at the start of the held-out trace. When omitted it is recovered from the trace's first voltage via the fitted OCV(SOC) curve (assumes the trace starts near rest); pass it explicitly if the trace starts mid-load.
* `capacity` — cell capacity \[Ah] used for SOC integration. Defaults to the fit/model capacity (the SOC reference the curves were fitted against); it is never re-estimated from the held-out trace.

### Example

```python theme={null}
# `result` is the FitResults from an ECM fit, e.g.:
#   job = client.ecm.fit_from_example(...)
#   result = client.ecm.wait_for_completion(job)

# Validate the in-memory fit against a held-out measurement
val = client.ecm.validate(
    measurement_id="held-out-measurement-id",   # a load case NOT used in the fit
    fit_results=result,
)

print(f"RMSE: {val.rmse_mV:.2f} mV "
      f"(MAE {val.mae_mV:.2f} mV, Max {val.max_mV:.2f} mV)")

# Or validate a previously saved Parameterized Model. `save_to_project`
# returns a response whose `parameterized_model_id` identifies the saved model:
#   saved = client.ecm.save_to_project(name=..., cell_spec_id=..., fit_results=result)
val = client.ecm.validate(
    measurement_id="held-out-measurement-id",
    parameterized_model_id=saved.parameterized_model_id,
)
```

### Results

The returned `ValidationResults` object carries error metrics plus aligned, downsampled traces ready to plot as a two-panel (overlay + residual) figure:

| Field                         | Description                                      |
| ----------------------------- | ------------------------------------------------ |
| `rmse_mV`, `mae_mV`, `max_mV` | Voltage error metrics in millivolts.             |
| `time`                        | Downsampled time grid \[s].                      |
| `data_voltage`                | Held-out measured voltage \[V].                  |
| `model_voltage`               | Re-simulated model voltage \[V].                 |
| `residual_mV`                 | Model − data residual \[mV].                     |
| `initial_soc`, `capacity_Ah`  | Values used for the SOC integration.             |
| `num_rcs`, `model_source`     | RC-pair count and origin of the validated model. |

A typical plot overlays `model_voltage` and `data_voltage` against `time` in one row, with `residual_mV` against `time` in a second row sharing the x-axis:

```python theme={null}
import matplotlib.pyplot as plt

fig, (ax_v, ax_e) = plt.subplots(2, 1, sharex=True, figsize=(9, 6))
ax_v.plot(val.time, val.data_voltage, label="data")
ax_v.plot(val.time, val.model_voltage, label="model", linestyle="--")
ax_v.set_ylabel("Voltage [V]")
ax_v.legend()
ax_v.set_title(f"Held-out validation — RMSE {val.rmse_mV:.1f} mV")
ax_e.plot(val.time, val.residual_mV)
ax_e.axhline(0, color="k", linewidth=0.8)
ax_e.set_ylabel("Error [mV]")
ax_e.set_xlabel("Time [s]")
fig.tight_layout()
```

## Next steps

* [Upload measurements](/data/uploading) to a project so you can fit them in place
* Create a [Parameterized Model](/build/parameterized-models) with your fitted parameters
* Learn about [ECM and other model types](/build/models) available in Ionworks Studio
* Explore the [data format requirements](/data/format) for uploading cycling data
