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

# Python API

> Create, list, and manage models and parameterized models programmatically with the ionworks-api Python client

The [`ionworks-api`](https://github.com/ionworks/ionworks-api) Python package provides sub-clients for managing [models](/build/models) and [parameterized models](/build/parameterized-models) programmatically. For installation and authentication, see the [Python API client](/api-client) page.

## Models

Use `client.model` to create, list, update, and delete models.

### Listing models

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

client = Ionworks()

# List all models
models = client.model.list()
for model in models:
    print(f"{model.name} ({model.id})")

# Filter by name
models = client.model.list(name="SPM")

# Paginate results
models = client.model.list(limit=10, offset=0)
```

Supported filters: `name`, `name_exact`, `created_by_email`, `created_after`, `created_before`, `updated_after`, `updated_before`, `order_by`, `order`.

### Getting a model

```python theme={null}
model = client.model.get("your-model-id")
print(model.config)
# {"type": "SPMe"}
```

The `config` field (e.g. `{"type": "SPMe"}`) is included on `get` responses.
It may be `None` on the `create` response — re-fetch with `get` if you need it.

### Creating a model

```python theme={null}
model = client.model.create({
    "name": "Custom SPM",
    "config": {"type": "SPM"},
    "description": "Single Particle Model with custom variables",
})
```

The `type` in `config` is the PyBaMM model class name (`SPM`, `SPMe`, `DFN`).

<Tip>
  You only need to create a model when you want custom variables. For a standard
  model, reference a built-in system model by name (e.g. `"SPMe (Full Cell)"`) when
  building a parameterized model — no model creation required. See [Models](/build/models).
</Tip>

### Updating a model

```python theme={null}
model = client.model.update("your-model-id", {
    "name": "Custom SPM v2",
    "description": "Updated description",
})
```

### Adding a custom variable

```python theme={null}
model = client.model.add_custom_variable("your-model-id", {
    "name": "Total energy [W.h]",
    "expression": "Voltage [V] * Current [A] * Time [s] / 3600",
})
```

### Deleting a model

```python theme={null}
client.model.delete("your-model-id")
```

### Downloading an Ionworks model as a PyBaMM model

Use `client.model.download()` to fetch an [Ionworks model](/build/models#ionworks-models)
(`ECM`, `LumpedSPMR`, `LumpedSPMeR`, the MSMR models, `GITTModel`, ...) as a
ready-to-use PyBaMM model. The server constructs the model from the licensed
`ionworkspipeline` package and returns its serialized form, so you can run it
locally with only `pybamm` installed — no `ionworkspipeline` license required.

```python theme={null}
import pybamm

model = client.model.download("ECM")
# model is a pybamm.BaseModel — pass it straight to pybamm.Simulation
sim = pybamm.Simulation(model)
```

Pass model constructor options through `options=`, and optionally write the
serialized JSON to disk with `path=` so you can reload it later or re-upload
it as a [custom model](/build/models#creating-a-model):

```python theme={null}
model = client.model.download(
    "LumpedSPMR",
    options={"thermal": "lumped", "surface temperature": "ambient"},
    path="lumped_spmr.json",
)
```

The available model names match the entries listed under `"ionworks_models"`
by `client.pybamm_models()`. Standard PyBaMM models (`SPM`, `SPMe`,
`DFN`, ...) are not served by this endpoint — instantiate them directly with
`pybamm` instead.

<Note>
  Serialization captures the model's mathematical structure (rhs, algebraic,
  variables, events, initial conditions) but not Python helper methods such
  as `set_initial_state` or classmethods.

  When `path` is given, the file may contain bare `Infinity`/`NaN` tokens
  (PyBaMM uses infinite bounds and event thresholds). Python's `json` and
  `Serialise.load_custom_model` read these fine, but strict parsers
  (`JSON.parse`, `jq`, ...) will reject the file.
</Note>

If you only need the raw serialized document — for example, to save it,
re-upload it, or inspect it — use `client.model.serialize()`, which returns
the dict without loading it through PyBaMM:

```python theme={null}
model_json = client.model.serialize("GITTModel")
```

#### Geometry and mesh are preserved

Downloaded models carry the serialized `geometry`, `var_pts`,
`spatial_methods`, and `submesh_types` that the original Ionworks model was
built with. When you re-upload one as a custom model — or feed it into a
pipeline that goes through `parse_model` — those values are restored as the
model's `default_geometry`, `default_var_pts`, `default_spatial_methods`, and
`default_submesh_types`, so a downstream `pybamm.Simulation` discretises
against the correct mesh instead of falling back to empty defaults. You do
not need to reconstruct the geometry by hand.

## Parameterized models

Use `client.parameterized_model` to create, list, and update parameterized models. Parameterized models are scoped to a [cell specification](/core-concepts/cells).

### Listing parameterized models

You can list parameterized models scoped to a single cell specification or
across every cell specification in a project.

```python theme={null}
# List parameterized models for a single cell specification
param_models = client.parameterized_model.list_by_cell_specification("your-cell-spec-id")
for pm in param_models:
    print(f"{pm.name} (ID: {pm.id})")

# Paginate results
param_models = client.parameterized_model.list_by_cell_specification(
    "your-cell-spec-id", limit=10, offset=0
)
```

To list every parameterized model linked to any cell specification in a
project, use `list_by_project`. When `project_id` is omitted it defaults to
the `project_id` configured on the client (see [API client](/api-client)).

```python theme={null}
# All parameterized models in the client's default project
param_models = client.parameterized_model.list_by_project()

# Explicit project, paginated
param_models = client.parameterized_model.list_by_project(
    project_id="your-project-id", limit=100, offset=0
)

# Narrow a project-scoped query to one cell specification
param_models = client.parameterized_model.list_by_project(
    project_id="your-project-id", cell_spec_id="your-cell-spec-id"
)
```

<Note>
  `list_by_project` accepts `limit` values up to 1000, so you can load every
  model for a project in a single request when populating UI selectors or
  bulk-processing models.
</Note>

### Getting a parameterized model

```python theme={null}
param_model = client.parameterized_model.get("your-parameterized-model-id")
```

### Creating a parameterized model

```python theme={null}
param_model = client.parameterized_model.create("your-cell-spec-id", {
    "name": "NMC622 Fitted Parameters",
    "model_id": "your-model-id",
    "description": "Parameters from 1C discharge fitting",
    "parameters": {
        "Negative electrode diffusivity [m2.s-1]": 3.3e-14,
    },
})
```

### Creating or getting a parameterized model

Use `create_or_get` to make setup scripts safely re-runnable. If a
parameterized model with the same name already exists for the cell
specification, the client returns the existing one instead of raising a
`409 Conflict` error.

```python theme={null}
param_model = client.parameterized_model.create_or_get("your-cell-spec-id", {
    "name": "NMC622 Fitted Parameters",
    "model_id": "your-model-id",
    "parameters": {
        "Negative electrode diffusivity [m2.s-1]": 3.3e-14,
    },
})
```

This mirrors the `create_or_get` behaviour already available on
`client.cell_spec`, `client.cell_instance`, and `client.cell_measurement` —
see [idempotent uploads](/data/uploading#idempotent-uploads-with-create_or_get)
for the same pattern applied to cell data.

### Updating a parameterized model

```python theme={null}
param_model = client.parameterized_model.update(
    "your-cell-spec-id",
    "your-parameterized-model-id",
    {"name": "NMC622 Fitted Parameters v2"},
)
```

### Getting parameter values

Retrieve all parameter values as a dictionary, useful as baseline parameters for data fitting or optimization workflows.

```python theme={null}
params = client.parameterized_model.get_parameter_values("your-parameterized-model-id")
print(params)
# {"Negative electrode diffusivity [m2.s-1]": 3.3e-14, ...}
```

### Getting variable names

List the scalar variable names available from a parameterized model.

```python theme={null}
variables = client.parameterized_model.get_variable_names("your-parameterized-model-id")
print(variables)
# ["Terminal voltage [V]", "Current [A]", ...]
```

### Persisting simulation settings

A parameterized model can carry its own **simulation settings** — the
mesh (`var_pts`, `submesh_types`), spatial discretisation (`spatial_methods`),
and solver configuration to use whenever this model is simulated. Persisting
settings ensures the model simulates the same way everywhere it's used — a
DataFit, a validation run, a downstream sweep — without you having to
reconfigure the mesh and solver in each caller.

Build a `SimulationSettings` from live PyBaMM objects with
`iws.models.SimulationSettings(...)`, then pass it under the `simulation_settings`
key when creating or updating a parameterized model:

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

model = pybamm.lithium_ion.DFN()

settings = iws.models.SimulationSettings(
    var_pts={"x_n": 20, "x_s": 10, "x_p": 20, "r_n": 20, "r_p": 20},
    submesh_types=model.default_submesh_types,
    spatial_methods=model.default_spatial_methods,
    solver=pybamm.IDAKLUSolver(rtol=1e-6, atol=1e-6),
)

param_model = client.parameterized_model.create("your-cell-spec-id", {
    "name": "NMC622 — fine mesh",
    "model_id": "your-model-id",
    "parameters": {...},
    "simulation_settings": settings,
})
```

The stored settings come back on the parameterized model's `simulation_settings`
field. Omit `simulation_settings` on create and the model uses the built-in
defaults for its underlying model type.

<Warning>
  Two write-time limits are worth knowing before you build a settings block:

  * **`geometry` is rejected.** Persisting a geometry override returns
    `400 BAD_REQUEST`; persist `var_pts` / `submesh_types` / `spatial_methods` /
    `solver` instead and let the geometry come from the model.
  * **Solvers are allowlisted.** Only `IDAKLUSolver`, `AlgebraicSolver`, and
    `NonlinearSolver` may be stored. The deprecated `CasadiSolver` and
    `ScipySolver` are rejected.

  `var_pts` keys, submesh classes, and spatial-method classes are validated
  against allowlists too, so an unrecognized value fails the write rather than
  the later simulation.
</Warning>

<Tip>
  You can find the ID for any resource from the Ionworks Studio web app. The
  ID is displayed in the URL when you navigate to a resource's detail page.
</Tip>

## ECM parameterization

Use `client.ecm` to fit an Equivalent Circuit Model (R0 + N RC pairs, plus optional OCV) to cycling data and persist the result as a [Parameterized Model](/build/parameterized-models). Authenticated fits run as background jobs — `fit_from_measurements` and `fit_from_file` return an `EcmFitJob` handle immediately, and `wait_for_completion` blocks until the worker finishes (typically 10–60 s).

See [ECM parameterization](/build/ecm-parameterization#fitting-from-python) for the full guide, including `ocv_soc_curve` co-capacity fits, per-segment SOC seeds, and knot-resolution tuning.

### Fitting 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, "fit_ocv": True},
})

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

### Fitting from a local file

```python theme={null}
fit_job = client.ecm.fit_from_file("data/pulse_test.csv", num_rcs=2, capacity=4.85)
result = client.ecm.wait_for_completion(fit_job)
```

`fit_from_file` accepts CSV, parquet, and any cycler format that `ionworksdata` can detect. Use `client.ecm.detect_and_read(file)` to preview a file before fitting.

### Saving a fit as a Parameterized Model

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

print(saved.parameterized_model_id)
```

The returned `parameterized_model_id` can be used as `parameterized_model` in `client.simulation.protocol(...)`.

### Fitting a built-in example (no auth)

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

The demo endpoint is rate-limited (60/min) and synchronous. RC-pair parameters are only included for authenticated callers whose organization has ECM results access enabled.
