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

> Run UCP simulations and submit parameterization pipelines programmatically with the ionworks-api Python client

The [`ionworks-api`](https://github.com/ionworks/ionworks-api) Python package lets you run simulations and submit parameterization pipelines programmatically. For installation and authentication, see the [Python API client](/api-client) page.

## Running simulations

Use `client.simulation` to run simulations. A simulation requires a [parameterized model](/build/parameterized-models) and a protocol in [UCP format](/simulate/universal-cycler-protocol).

### Single simulation

```python theme={null}
response = client.simulation.protocol({
    "parameterized_model": "your-parameterized-model-id",
    "protocol_experiment": {
        "protocol": """
global:
  initial_soc: 1
  temperature: 25
steps:
  - direction: Discharge
    mode: C-rate
    value: 1
    ends:
      - variable: Voltage [V]
        value: 2.5
""",
        "name": "1C Discharge",
    },
})

print(f"Simulation ID: {response.simulation_id}")
print(f"Job ID: {response.job_id}")
```

You can also pass experiment parameters and design parameters:

```python theme={null}
response = client.simulation.protocol({
    "parameterized_model": "your-parameterized-model-id",
    "protocol_experiment": {
        "protocol": """
global:
  initial_soc: input["Initial SOC"]
steps:
  - direction: Discharge
    mode: C-rate
    value: input["C-rate"]
    ends:
      - variable: Voltage [V]
        value: 2.5
""",
        "name": "Parameterized Discharge",
    },
    "experiment_parameters": {
        "Initial SOC": 1.0,
        "C-rate": 0.5,
    },
    "design_parameters": {
        "Positive electrode thickness [m]": 75e-6,
    },
})
```

`design_parameters` is a single-simulation convenience field on `protocol()`. Pass a flat
`dict[str, float]` of parameter overrides — the client translates them internally to a
one-row discrete DOE before submission. Use it when you want to vary one or more design
parameters for a single run without writing out the full DOE schema.

<Note>
  In `protocol()`, `design_parameters` and `design_parameters_doe` are mutually exclusive,
  and any DOE you supply must resolve to exactly one simulation. Passing both, or a DOE
  that would expand to multiple simulations, raises `ValueError` — use
  [`protocol_batch`](#batch-simulations-with-design-of-experiments) for multi-simulation
  sweeps instead.
</Note>

### Waiting for results

Use `wait_for_completion` to poll until the simulation finishes. The method detects failed and canceled jobs immediately rather than waiting for the timeout.

```python theme={null}
result = client.simulation.wait_for_completion(
    response.simulation_id,
    timeout=120,        # seconds (default: 60)
    poll_interval=2,    # seconds between polls (default: 2)
    verbose=True,       # print status updates (default: True)
)
```

Set `raise_on_failure=False` to get the result dict instead of raising an exception when a simulation fails:

```python theme={null}
result = client.simulation.wait_for_completion(
    response.simulation_id,
    raise_on_failure=False,
)
```

### Batch simulations with design of experiments

Run multiple simulations across a parameter sweep using `protocol_batch`:

```python theme={null}
responses = client.simulation.protocol_batch({
    "parameterized_model": "your-parameterized-model-id",
    "protocol_experiment": {
        "protocol": "...",
        "name": "C-rate Sweep",
    },
    "design_parameters_doe": {
        "sampling": "grid",
        "rows": [
            {
                "type": "discrete",
                "name": "Positive electrode thickness [m]",
                "values": [50e-6, 75e-6, 100e-6],
            },
        ],
    },
})

# Wait for all simulations
results = client.simulation.wait_for_completion(
    [r.simulation_id for r in responses],
    timeout=300,
)
```

Supported DOE row types:

| Type       | Fields                 | Description                              |
| ---------- | ---------------------- | ---------------------------------------- |
| `discrete` | `values`               | Specific values to test                  |
| `range`    | `min`, `max`, `count`  | Evenly spaced values between min and max |
| `normal`   | `mean`, `std`, `count` | Values sampled around the mean           |

Sampling strategies: `grid` (all combinations), `random`, `latin_hypercube`.

### Retrieving simulation data

```python theme={null}
# List all simulations
simulations = client.simulation.list()

# Get a specific simulation
simulation = client.simulation.get(simulation_id)

# Get result data (time series, steps, metrics)
result = client.simulation.get_result(simulation_id)
```

`get_result` returns a typed `SimulationResult` dataclass with three fields:

| Field                | Type             | Description                                                                                             |
| -------------------- | ---------------- | ------------------------------------------------------------------------------------------------------- |
| `result.time_series` | `DataFrame`      | One row per time point; columns are signal names (e.g. `"Time [s]"`, `"Voltage [V]"`, `"Current [A]"`). |
| `result.steps`       | `DataFrame`      | One row per protocol step (e.g. `"Step count"`, `"Step type"`, `"Duration [s]"`).                       |
| `result.metrics`     | `dict[str, Any]` | Scalar metrics computed over the run (e.g. cycle-level summaries).                                      |

`time_series` and `steps` are returned as polars DataFrames by default. Call [`set_dataframe_backend("pandas")`](/api-client#dataframe-backend) once at session start to receive pandas DataFrames instead.

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

client = Ionworks()
simulation_id = "your-simulation-id"  # e.g. response.simulation_id from a submitted run
result = client.simulation.get_result(simulation_id)

# Plot voltage vs time. With the default polars backend, convert to pandas first;
# with the pandas backend (set_dataframe_backend("pandas")), use result.time_series directly.
ts = result.time_series.to_pandas()  # drop .to_pandas() on the pandas backend
ts.plot(x="Time [s]", y="Voltage [V]")

# Inspect protocol steps
print(result.steps)

# Read scalar metrics
print(result.metrics)
```

<Note>
  `Discharge capacity [A.h]` and `Charge capacity [A.h]` in `time_series`
  reset to 0 at each step boundary. Use `"Step count"` to join `time_series`
  to `steps`, or accumulate per-step end values if you need a continuous
  cumulative capacity trace.
</Note>

## Running pipelines

Pipelines combine data fitting, calculations, and validation steps for battery model parameterization. Use `client.pipeline` to submit and manage pipelines.

<Note>
  Pipelines require a `project_id`. Set the `IONWORKS_PROJECT_ID`
  environment variable (or pass `project_id=` to `Ionworks(...)`) to
  configure a [default project](/api-client#default-project), or include
  `project_id` in the pipeline config explicitly. The deprecated
  `PROJECT_ID` env var is still accepted as a fallback.
</Note>

### Submitting a pipeline

```python theme={null}
# project_id is auto-injected from IONWORKS_PROJECT_ID or the client default;
# pass it explicitly in the config to override.
pipeline = client.pipeline.create({
    "name": "NMC622 Parameterization",
    "elements": {
        "entry": {
            "element_type": "entry",
            "values": {
                "model": {"type": "SPM"},
                "data": "db:your-measurement-id",
            },
        },
        "data_fit": {
            "element_type": "data_fit",
            "objectives": {"voltage": {"data": "entry.data"}},
            "parameters": {
                "Negative electrode diffusivity [m2.s-1]": {
                    "bounds": [1e-16, 1e-12],
                    "initial_value": 3.3e-14,
                }
            },
        },
    },
})

print(f"Pipeline ID: {pipeline.id}")
print(f"Status: {pipeline.status}")
```

Pipeline elements must be a dictionary (not a list). Each key is the element name and the value is its configuration.

<Tip>
  Prefer the typed [`iws.Pipeline` builder](/pipelines/api) for
  construction-time validation and IDE autocomplete — it serializes to the
  same config shown here. Raw dicts (as above) are accepted too.
</Tip>

### Waiting for pipeline completion

```python theme={null}
result = client.pipeline.wait_for_completion(
    pipeline.id,
    timeout=600,        # seconds (default: 600)
    poll_interval=2,    # seconds between polls (default: 2)
    verbose=True,
)

print(f"Final status: {result.status}")
```

### Getting pipeline results

```python theme={null}
pipeline_result = client.pipeline.result(pipeline.id)

# Access fitted parameter values
for name, element in pipeline_result.element_results.items():
    print(f"{name}: {element}")
```

### Data references in pipelines

Use these prefixes to reference data sources in pipeline configs:

| Prefix    | Example               | Description                             |
| --------- | --------------------- | --------------------------------------- |
| `db:`     | `"db:measurement-id"` | Reference an uploaded measurement by ID |
| `file:`   | `"file:data.csv"`     | Load a local CSV file                   |
| `folder:` | `"folder:data_dir/"`  | Load from a local directory             |

<Tip>
  For inline DataFrames in pipeline configs, there is a 1,000-row limit.
  Upload larger datasets as measurements first, then reference them with
  `db:measurement-id`.
</Tip>

<Note>
  The `folder:` scheme expects a directory containing `time_series` and
  `steps` files. Both `.parquet` and `.csv` are supported, and parquet is
  preferred when both are present. For example, a folder with
  `time_series.parquet` and `steps.parquet` (or `.csv`) loads correctly.
</Note>

### PyBaMM model support

Pipeline configs accept PyBaMM model objects directly. The client auto-serializes them before sending:

```python theme={null}
import pybamm

pipeline = client.pipeline.create({
    "elements": {
        "entry": {
            "element_type": "entry",
            "values": {
                "model": pybamm.lithium_ion.SPM(),
            },
        },
    },
})
```

## Running simple pipelines

For pipelines that contain a single data fit or a single validation step,
use `client.simple_pipeline` instead of `client.pipeline`. A simple pipeline
is a lightweight, fire-and-forget alternative: you submit one config and the
server runs it end-to-end as a single job, returning a flat
`parameter_values` result.

### When to use simple pipelines

| Use `client.simple_pipeline` when                               | Use `client.pipeline` when                                          |
| --------------------------------------------------------------- | ------------------------------------------------------------------- |
| Your config has at most one `data_fit` or `validation` element  | Your config chains multiple data fits, calculations, or validations |
| You want fire-and-forget execution with a single result payload | You need per-element status tracking and intermediate results       |
| You want a flat `parameter_values` dict back                    | You need cumulative parameter threading across elements             |

<Note>
  Simple pipelines require a `project_id`. Set the `IONWORKS_PROJECT_ID`
  environment variable (or pass `project_id=` to `Ionworks(...)`) to
  configure a [default project](/api-client#default-project), or pass
  `project_id=` explicitly on each call.
</Note>

### Submitting a simple pipeline

```python theme={null}
config = {
    "elements": {
        "initial_params": {
            "element_type": "entry",
            "values": {"Negative particle diffusivity [m2.s-1]": 2e-14},
        },
        "fit": {
            "element_type": "data_fit",
            "objectives": {"voltage": {"data": "db:your-measurement-id"}},
            "parameters": {
                "Negative particle diffusivity [m2.s-1]": {
                    "bounds": [1e-14, 1e-13],
                    "initial_value": 2e-14,
                }
            },
            "cost": {"type": "RMSE"},
            "optimizer": {"type": "DifferentialEvolution", "max_iterations": 10},
        },
    }
}

sp = client.simple_pipeline.create(
    config,
    name="NMC622 diffusivity fit",
    description="Single-parameter fit on the May 14 dataset",
)

print(sp.id, sp.status)  # status starts as "pending"
```

<Tip>
  Prefer the typed [`iws.SimplePipeline` form](/guide/pipelines/simple-pipelines)
  for construction-time validation and IDE autocomplete (e.g.
  `cost=iws.costs.RMSE()`). It serializes to the same config shown here.
</Tip>

The `elements` dict may contain at most one `data_fit` or `validation`
element. Helper entries such as `entry` elements are allowed and are
evaluated before the fit.

#### Accepted `element_type` values

Each element's `element_type` accepts the canonical wire values below.
For backwards compatibility, configs authored against earlier versions of
the app may also use the legacy display labels in parentheses — the
server normalizes them to the canonical value before running the job.

| Canonical value | Legacy alias              | Use for                                                    |
| --------------- | ------------------------- | ---------------------------------------------------------- |
| `entry`         | `"Direct Entry"`          | Seeding parameter values or model selection before the fit |
| `data_fit`      | `"Data Fit"`, `"datafit"` | The single fitting step                                    |
| `calculation`   | `"Calculation"`           | Derived parameter calculations                             |
| `validation`    | `"Validation"`            | A single validation step (in place of a `data_fit`)        |

New configs should use the canonical values. An unrecognized
`element_type` causes the job to fail with a `ValueError` listing the
accepted values.

<Note>
  `data_fit` elements in simple pipelines are evaluated in parallel using
  the same distributed worker pool as regular pipelines. No extra
  configuration is required — set `optimizer.population_size` as usual
  and the server fans the population evaluations out across workers.
</Note>

### Execution options

Pass an `options` dict to `create` to control runtime execution behavior
for the submitted pipeline. Options are submission metadata — they affect
how the server runs the job but are not stored as part of the pipeline
config.

| Option                  | Type           | Default                                                   | Effect                                                                                                                                                                                   |
| ----------------------- | -------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `live_progress_updates` | `bool \| None` | `None` (worker picks a sensible default for the job type) | When `True`, the worker writes checkpoint progress to the database during execution so you can poll intermediate progress. When `False`, checkpoints are skipped for better performance. |

```python theme={null}
sp = client.simple_pipeline.create(
    config,
    name="NMC622 diffusivity fit",
    options={"live_progress_updates": False},  # skip checkpointing for speed
)
```

You can also embed `options` (along with `project_id`, `name`, or
`description`) directly in the config dict — `create` lifts them out of
the config before submission. Arguments passed explicitly to `create`
take precedence over values found in the config.

```python theme={null}
config = {
    "options": {"live_progress_updates": True},
    "elements": { ... },
}

sp = client.simple_pipeline.create(config)
```

### Waiting for completion

`wait_for_completion` polls until the pipeline reaches a terminal status
(`completed`, `failed`, or `canceled`) and returns the final record.

```python theme={null}
completed = client.simple_pipeline.wait_for_completion(
    sp.id,
    timeout=600,        # seconds (default: 600)
    poll_interval=2,    # seconds between polls (default: 2)
    verbose=True,
    raise_on_failure=True,
)

# Fitted parameter values are returned as a flat dict
print(completed.result["parameter_values"])
# {"Negative particle diffusivity [m2.s-1]": 5.3e-14}
```

For validation runs, the result also contains a `summary_stats` block
alongside `parameter_values`.

If the pipeline does not finish within `timeout`, a `TimeoutError` is
raised. If it ends in `failed` and `raise_on_failure=True` (the default),
an `IonworksError` is raised with the server-side error message.

### Listing, filtering, and sorting

`list` returns a paginated response with `items`, `count`, and `total`.
String filters accept either an exact value or
[Supabase operator syntax](https://supabase.com/docs/guides/api/rest/generated-api#filtering)
such as `ilike.%foo%` or `in.(completed,failed)`.

```python theme={null}
# Newest 25 simple pipelines in the default project
page = client.simple_pipeline.list(limit=25)

# Only currently active runs
active = client.simple_pipeline.list(status="in.(pending,running)")

# Name contains "diffevo" (case-insensitive)
matches = client.simple_pipeline.list(name="ilike.%diffevo%")

# Created in the last week, oldest first
recent = client.simple_pipeline.list(
    created_at_gt="2026-05-08",
    created_at_lt="2026-05-15",
    order_by="created_at",
    order="asc",
)
```

### Updating, cancelling, and deleting

```python theme={null}
# Rename or add a description (PATCH — at least one field is required)
client.simple_pipeline.update(sp.id, name="Renamed", description="notes")

# Cancel a running pipeline
client.simple_pipeline.cancel(sp.id)

# Permanently delete the pipeline, its job, and stored config
client.simple_pipeline.delete(sp.id)
```

### Handling errors

```python theme={null}
from ionworks.errors import IonworksError

try:
    result = client.simple_pipeline.wait_for_completion(sp.id)
except TimeoutError:
    # Still running after the timeout — fetch the latest status to decide
    current = client.simple_pipeline.get(sp.id)
    print(f"Still {current.status}")
except IonworksError as e:
    # Pipeline ended in "failed" — the message includes the server error
    print(e)
```

## Managing studies

Use `client.study` to create, list, update, and delete [studies](/simulate/studies). Studies are scoped to a project.

All `client.study.*` methods accept `project_id` as an optional keyword
argument. When omitted, they use the [default project](/api-client#default-project)
configured on the client (or resolved from `IONWORKS_PROJECT_ID`). Pass
`project_id=` explicitly to override on a per-call basis.

### Listing studies

```python theme={null}
# Uses the default project from the client
studies = client.study.list()
for study in studies:
    print(f"{study.name} (ID: {study.id})")

# Filter by name
studies = client.study.list(name="Discharge")

# Paginate results
studies = client.study.list(limit=10, offset=0)

# Override the project for a single call
studies = client.study.list(project_id="other-project-id")
```

Supported filters: `name`, `name_exact`, `order_by`, `order`.

### Getting a study

```python theme={null}
study = client.study.get("your-study-id")
```

### Creating a study

```python theme={null}
study = client.study.create({
    "name": "1C Discharge Sweep",
    "description": "Comparing discharge curves across temperatures",
})
```

### Updating a study

```python theme={null}
study = client.study.update("your-study-id", {
    "name": "1C Discharge Sweep v2",
})
```

### Assigning simulations and measurements

```python theme={null}
# Assign a simulation to a study
client.study.assign_simulation("your-study-id", "simulation-id")

# Remove a simulation from a study
client.study.remove_simulation("your-study-id", "simulation-id")

# Assign a measurement to a study
client.study.assign_measurement("your-study-id", "measurement-id")

# List measurements in a study
measurements = client.study.list_measurements("your-study-id")

# Remove a measurement from a study
client.study.remove_measurement("your-study-id", "measurement-id")
```

### Deleting a study

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

## Managing protocols

Use `client.protocol` to validate [UCP protocols](/simulate/universal-cycler-protocol).

### Validating a protocol

```python theme={null}
result = client.protocol.validate("""
global:
  initial_soc: 1
  temperature: 25
steps:
  - direction: Discharge
    mode: C-rate
    value: 1
    ends:
      - variable: Voltage [V]
        value: 2.5
""")
print(result["valid"])  # True or False
if not result["valid"]:
    print(result["error"])
```

### Finding input references

Find `input[...]` placeholders in a protocol string, useful for building experiment parameter forms.

```python theme={null}
refs = client.protocol.find_input_references("""
global:
  initial_soc: input["Initial SOC"]
steps:
  - direction: Discharge
    mode: C-rate
    value: input["C-rate"]
""")
print(refs)  # ["Initial SOC", "C-rate"]
```

### Converting UCP to a vendor protocol file

Use `client.protocol.convert` to translate a UCP YAML protocol into the native file format used by a commercial cycler. This is the reverse of the [commercial protocol upload flow](/simulate/simulating-commercial-protocols) — start from a protocol designed in Ionworks and produce a file you can run on hardware.

Supported targets: `maccor`, `arbin`, `neware`, `biologic_bttest`, `novonix`.

```python theme={null}
result = client.protocol.convert(
    protocol="""
global:
  initial_temperature: 25
  initial_state_type: soc_percentage
  initial_state_value: 100
steps:
  - CC Charge:
      - Charge:
          mode: C-rate
          value: 1
          ends:
            - "Voltage > 4.2"
  - CV Charge:
      - Charge:
          mode: Voltage
          value: 4.2
          ends:
            - "Current < 0.05"
  - Discharge:
      - Discharge:
          mode: C-rate
          value: 1
          ends:
            - "Voltage < 2.7"
""",
    target="maccor",
)

# Inspect the converted protocol as text
print(result.text())

# Or write every output file (primary protocol plus any sidecars) to disk
paths = result.save("./converted")
print(paths)  # [PosixPath('converted/protocol.000'), ...]
```

`ConvertResult` exposes:

* `primary_bytes` — raw bytes of the primary protocol file.
* `text(encoding="utf-8")` — decode `primary_bytes` to a string.
* `save(dir)` — write every output file (primary plus any sidecars) into `dir` and return the list of paths written.

<Note>
  Maccor protocols that include drive-cycle steps produce one or more `.MWF`
  waveform files alongside the primary `.000` file. Use `result.save(dir)` so
  every sidecar lands next to the primary protocol — handling only
  `primary_bytes` drops the waveform files and the protocol will not run on
  the cycler.
</Note>

<Note>
  Some UCP features map cleanly across all formats, but each vendor has its own
  syntax and limitations (see [Differences between commercial
  protocols](/simulate/simulating-commercial-protocols#differences-between-commercial-protocols)).
  If a UCP construct can't be expressed in the target format, the conversion
  returns an error naming the unsupported step (see [Export-time
  validation](/simulate/simulating-commercial-protocols#export-time-validation)
  for the specific features each target format rejects). Validate the output
  by reuploading it through the [commercial protocol
  flow](/simulate/simulating-commercial-protocols) before running it on a real
  cycler.
</Note>

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

## Next steps

<CardGroup cols={2}>
  <Card title="Simulations" icon="play" href="/simulate/simulations">
    Learn about running simulations in Ionworks Studio.
  </Card>

  <Card title="Protocol reference" icon="scroll" href="/simulate/universal-cycler-protocol">
    Full reference for the Universal Cycler Protocol format.
  </Card>

  <Card title="Uploading data" icon="database" href="/data/uploading">
    Upload and manage cell data via the Python API.
  </Card>

  <Card title="Build API" icon="wrench" href="/build/api">
    List and retrieve models and parameterized models.
  </Card>
</CardGroup>
