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

# Simulating Commercial Protocols

> Upload protocol files from Maccor, Neware, Arbin, BioLogic, and Novonix cyclers and run physics-based simulations

Have you ever started a multi-day battery test, only to realize later that the protocol you used was incorrect? The Battery Cycler Simulator helps you prevent this by allowing you to upload a protocol file and run a quick physics-based simulation to verify its behavior before you start a real-world experiment.

The Battery Cycler Simulator lives inside a [project](/core-concepts/projects-studies) — open a project and select **Simulate → Protocol Simulator** in the sidebar to get started. You launch and run it from within a project, and it can select parameterized models from that project. Uploaded protocols and Quick models are shared across your organization, so they remain available to other projects in the same organization rather than being isolated to a single project.

## How It Works

The process is simple: upload your protocol, configure your cell model, and run the simulation.

1. **Upload:** Upload your protocol file in the raw format (e.g. XML or JSON). The system automatically detects the format and parses it.
2. **Configure:** Pick a cell model — either a **Quick model** built on the fly from a chemistry preset, or a full [parameterized model](/build/parameterized-models) from your project.
3. **Simulate:** Run the simulation and analyze the results.

## Step 1: Upload Your Protocol File

You can start by uploading a protocol file from your computer.

We currently support files from:

* Arbin
* BioLogic (.mps, .bttest)
* Maccor (.xml, .csv)
* Neware
* Novonix (.pro2)
* PyBaMM experiment strings (.txt)

Once you upload a file, the system immediately parses it and displays the translated steps.

<Note>
  Gamry `.dta` files are not protocol files and cannot be uploaded here. To
  import EIS measurement data from Gamry instruments, use the
  [`ionworksdata`](https://data.docs.ionworks.com/) library — see
  [Data format](/data/format#eis-and-impedance-data) for details.
</Note>

### Required Additional Files

Some protocols reference external files for complex steps, such as custom drive cycle waveforms or reusable subroutines. If your protocol requires such files, the simulator will detect this and prompt you to upload them.

For Arbin schedules this includes `.subsdx` subschedule files referenced by `SubSchedule` steps. Each subschedule maps to a UCP [`Subroutine`](/simulate/universal-cycler-protocol#subroutines) step and can itself reference further subschedules — those nested requirements are surfaced once you attach the parent file, so the upload prompt cascades one level per attachment until every dependency is resolved.

## Step 2: Review and Configure

After parsing, you can review the protocol and set up the simulation.

### Protocol Steps

The simulator displays the parsed protocol in three different formats, accessible via tabs:

* **Human-Readable:** A simplified, easy-to-read summary of the steps in your protocol.
* **UCP (YAML):** The full protocol translated into our [Universal Cycler Protocol](/simulate/universal-cycler-protocol) format. This shows the detailed underlying structure that will be executed.
* **Raw:** The raw text content of the file you uploaded.

### Cell Configuration

To run a meaningful simulation, you need to tell the simulator which cell model to use. You have two options:

* **Quick model** — build a cell model on the fly from a chemistry preset and a few basic inputs. Best for a fast sanity check when you don't yet have a fitted model for the cell.
* **Project parameterized model** — select an existing [parameterized model](/build/parameterized-models) from the current project. Best when you want the protocol to run against the same physics-based model you use elsewhere in the project.

#### Quick model

The Quick model configures the underlying [Equivalent Circuit Model (ECM)](/build/models#ionworks-models) with one RC pair, using OCV and resistance parameters matched to a chemistry preset. Use it when you want to validate a protocol against a representative cell without first fitting a full parameterized model.

* **Chemistry:** Select the cell chemistry from a list of pre-configured options. Full-cell chemistries (e.g., `NMC/Graphite`, `LFP/Graphite`) and Li-metal half cells (e.g., `NMC/Li metal`, `Graphite/Li metal`, `LFP/Li metal`) are available. This determines the OCV curve, anode/cathode open-circuit potentials, and resistance values used by the model.
* **Cell capacity (Ah):** The nominal capacity of your cell. The ECM parameters are scaled to match this capacity.
* **Resistance scale (%):** Adjusts the model's internal resistance relative to the default value for the selected chemistry. The default is `100%` (no change). For example, set this to `200` to double the resistance or `50` to halve it. Useful for approximating cells with higher or lower impedance than the chemistry default.
* **Initial SOC (%):** The State of Charge of the cell at the beginning of the simulation.
* **Temperature (°C):** The ambient temperature for the simulation.

#### Project parameterized model

If you already have a [parameterized model](/build/parameterized-models) in the current project, pick it from the model selector instead of building a Quick model. The simulator runs the uploaded protocol against that model with its full parameter set, so results are directly comparable to other simulations in the project. Only Initial SOC and Temperature need to be supplied on top of the model.

<Warning>
  Check carefully that the cell model matches the cell for which the protocol
  was designed. If the model is a poor fit — wrong chemistry, wrong capacity,
  or wrong voltage window — the simulation may fail to run or produce
  misleading results.
</Warning>

### Advanced configuration

Under the **Advanced** section, you can configure optional rules that modify simulation behavior at runtime.

#### Termination conditions

Termination conditions let you stop a simulation early when a variable reaches a target value. This is useful for long cycling protocols where you only need to simulate a limited number of cycles or a specific amount of time.

Each condition specifies a **variable**, a **comparison operator** (`==`, `!=`, `>`, `<`, `>=`, `<=`), and a **value**. The simulation stops as soon as any condition is met.

The variable dropdown is organized into two groups:

* **Built-in** — variables automatically provided by the simulation engine regardless of the protocol
* **Protocol variables** — variables defined in and extracted from the parsed protocol

##### Total time

**Total time** is a built-in variable that tracks the cumulative elapsed simulation time in seconds. Use it to cap how long a simulation runs, independent of the protocol's own step logic.

When you select **Total time**, a unit picker appears next to the value field so you can enter the threshold in **seconds**, **minutes**, **hours**, or **days**. The value is automatically converted to seconds for the simulation engine.

For example, to stop a long cycling protocol after 2 hours, add a termination condition where **Total time >= 2 hours**.

<Note>
  If the protocol defines its own variable named `total_time`, the protocol's variable takes precedence and the built-in value is not injected.
</Note>

##### Protocol variables

Protocol variables are extracted from your uploaded protocol file. For example, to stop an Arbin protocol after 3 cycles, you can add a condition where `PV_CHAN_Cycle_Index >= 4`. Arbin increments `PV_CHAN_Cycle_Index` at the start of each cycle, so the index reaches 4 only when the 4th cycle begins — meaning 3 full cycles have already completed.

When a termination condition triggers, the reason is displayed in the simulation metrics using the human-readable form (e.g., "Early termination reason: Total time >= 2 hours").

#### Variable callback rules

Variable callback rules let you dynamically update protocol variables during the simulation based on conditions. Each rule specifies a condition (variable, operator, value) and a set of variable updates to apply when the condition is met.

For example, you could create a rule that increases the C-rate after a certain number of cycles: when `PV_CHAN_Cycle_Index >= 3`, update `Current(A)` to a higher value. This lets you simulate multi-phase protocols where charging or discharging parameters change at specific points during cycling.

## Step 3: Run Simulation & Analyze Results

Once everything is configured, click **Run Simulation**. The simulator will execute the protocol against the configured cell model.

You can cancel a running simulation at any time by clicking the **Cancel** button on the simulation page. This is useful for long-running protocols where you can already see the results you need.

### Simulation results

#### Plots

The primary output is an interactive plot of your simulation data over time. By default, **Voltage** and **Current** are shown, but you can configure exactly which variables appear.

Click **Configure Plot** to open the plot settings drawer. Under **Time Series**, toggle any of the available variables on or off:

| Variable                  | Unit | Description                           |
| ------------------------- | ---- | ------------------------------------- |
| Voltage                   | V    | Cell terminal voltage                 |
| Current                   | A    | Applied current                       |
| Temperature               | °C   | Cell temperature (when available)     |
| Charge capacity           | Ah   | Cumulative charge capacity            |
| Discharge capacity        | Ah   | Cumulative discharge capacity         |
| State of charge           | %    | Cell state of charge (when available) |
| Step count                |      | Overall step index                    |
| Cycle count               |      | Current cycle number                  |
| Step count (within cycle) |      | Step number within the current cycle  |

Each enabled variable is displayed in its own subplot, stacked vertically and sharing a common time axis. Charge capacity and discharge capacity are grouped into a single subplot for easy comparison.

<Tip>
  If your protocol defines numeric variables (e.g., C-rate, temperature setpoints), you can also plot these as time series. In the **Configure Plot** drawer, use the **Additional Variables** dropdown to select any numeric protocol variable. The selected variables appear as additional subplots alongside the built-in time series.
</Tip>

#### Key metrics

Below the plot, you will find key performance indicators calculated from the simulation, including:

* Total Time
* Charge Throughput (Ah)
* Energy Throughput (Wh)
* Early termination reason (if a termination condition was triggered)
* Stop reason (set to `Reached pause step` or `Reached end step` when the protocol's control flow stopped on an auxiliary step instead of running to the end)

#### Fullscreen mode

Click the fullscreen icon in the top-right corner of the plot to expand it to fill the screen. The fullscreen view includes the same **Configure Plot** button, so you can adjust which variables are shown without leaving fullscreen.

#### Download CSV

Click **Download CSV** next to **Configure Plot** to export the full simulation time series as a CSV file (`simulation_data.csv`). The download always contains the complete simulation, even when you've zoomed into a window on the plot.

The file includes every time-series variable produced by the simulation (Voltage, Current, Temperature, capacities, Step count, Cycle count, and any additional protocol variables), plus step-level columns expanded to match each time point. Use it to bring results into Excel, pandas, MATLAB, or your own analysis tooling without re-running the simulation.

By simulating your protocols, you can catch errors, validate your experimental design, and gain confidence before committing time and resources to a real test.

## Exporting UCP to a vendor protocol file

The conversion also runs in reverse: you can take a protocol authored in [UCP](/simulate/universal-cycler-protocol) and export it as a native file for Maccor, Neware, Arbin, BioLogic, or Novonix. This is useful when you've designed and validated a protocol in Ionworks and want to run it on physical hardware.

Use the [`client.protocol.convert`](/simulate/api#converting-ucp-to-a-vendor-protocol-file) method in the Python API to perform the conversion programmatically.

Because vendor formats vary in expressiveness, not every UCP construct round-trips perfectly. See [Differences between commercial protocols](#differences-between-commercial-protocols) below for the major gotchas, and [Export-time validation](#export-time-validation) for the specific UCP features each target format rejects upfront. We recommend re-importing the converted file using the upload flow above to confirm it parses back into the steps you expect before running it on a cycler.

### Export-time validation

When you convert a UCP protocol to Arbin or Maccor, the converter walks the protocol first and rejects features that can't be faithfully represented in the target format. You get a `ValueError` naming the offending step rather than a file that silently misbehaves on the cycler.

**Arbin** rejects:

* `EIS` steps — Arbin schedules have no impedance-sweep step type.
* `Subroutine` steps — there's no equivalent step type.
* Compound `And` / `Or` end conditions — Arbin step limits accept a single equation per limit.
* Maccor user variables (`VAR1`, `VAR2`, …) referenced in step values or `VariableEnd` expressions — Arbin has no runtime variable namespace for them.

**Maccor** rejects:

* Zero-duration `Rest` steps — Maccor requires every step to have at least one nonzero termination. (This is most commonly hit when round-tripping an Arbin "Internal Resistance" step through UCP.)

If you hit one of these, restructure the protocol to avoid the feature, or export to a different target format that supports it.

#### Cross-cycler variable translation

When a UCP protocol was originally parsed from one vendor and is exported to another, the converter rewrites variable names and cycle-index comparisons so the semantics survive translation:

* `PV_CHAN_Cycle_Index`, `PV_CHAN_Voltage`, `PV_CHAN_Current`, `PV_CHAN_Charge_Capacity` / `PV_CHAN_Discharge_Capacity`, `PV_CHAN_Step_Time` (Arbin) ↔ `CYCLE`, `VOLTAGE`, `CURRENT`, `CAPACITY`, `STIME` (Maccor). Both Arbin capacity variants map to Maccor's single `CAPACITY` token; the reverse path (Maccor → Arbin) emits `PV_CHAN_Capacity`.
* Arbin's `PV_CHAN_Cycle_Index` is 1-indexed while Maccor's `CYCLE` is 0-indexed. Numeric cycle-index comparisons are shifted by 1 in either direction so the same set of cycles is matched. For example, an Arbin end `PV_CHAN_Cycle_Index <= 2` becomes a Maccor User Def end `CYCLE <= 1`.
* UCP-canonical short names (`Capacity`, `Voltage`, `Current`, `Duration`, `Energy`) used in `set_variable` expressions are expanded back to Arbin's `PV_CHAN_*` tokens on Arbin export so the round-trip parser resolves them correctly.

Compound `And` / `Or` ends and `VariableEnd` expressions exported to Maccor are emitted as a single `User Def` end with `&` / `|` operators.

## Differences between Commercial Protocols

Much of the challenge in converting between the different protocols is not the syntax, but the fundamental differences in how the cyclers define the logic to follow the same sequence of steps.

In this section, we'll cover some implementation differences between the Ionworks Universal Cycler Protocol, and the protocols from the cyclers we support.

### CCCV steps

Most cyclers define CCCV steps as one step with voltage as a "limit" field:

* Maccor: Uses a constant current step, with voltage as a "limit". Maccor also supports a native single-step `Chg Func CCCV` / `Dis Func CCCV` step type that combines the CC and CV phases (with the CV cutoff specified as a current limit).
* Neware: Specifies both current and voltage as a "limit"
* Novonix: Specifies both current and voltage as fields, and uses step type to differentiate between CC (voltage is a cutoff) and CCCV (transition to voltage and hold until a current cutoff)

In UCP, for maximum modularity, we use a [step block](/simulate/universal-cycler-protocol#step-blocks) with two steps, CC with a voltage cut-off and CV with a current cut-off, with total duration and any other end conditions defined at the step block level. When parsing a Maccor `Chg Func CCCV` / `Dis Func CCCV` step, the parser expands it into the equivalent two-step UCP block automatically.

### Header metadata

Some formats (e.g., Novonix `.pro2`) include top-level header data (`Version`, `LastUpdated`, `Charger`). This metadata is preserved in UCP under `header` and is used for round-trips when converting back to the original format.

### Functional expressions

Maccor supports functional expressions (e.g., `VAR1*0.5`) for step values and end conditions. These are carried through UCP as strings and round-tripped using vendor-specific constructs (e.g., Maccor "User Def" end entries), so the meaning is preserved even if another format lacks an exact equivalent.

### Nested loops

For formats that use Do/Loop constructs (Maccor), nested loops are numbered (Do 1/Loop 1, Do 2/Loop 2, …) to reflect structure. UCP expresses loops with `repeat` on a block; when converting back to Maccor, Do/Loop numbering is generated from the block nesting depth.

### Report/Record/Save data/Resolution

This is the field that defines how often the cycler will save data to the output time series.

* Maccor uses the "Report" field and allows time, current, voltage, and temperature
* Neware uses the "Record" field and allows time, current, and voltage
* Novonix uses `StepConditions` entries with `ConditionType: "Save data"` and allows `Δt`, `ΔV`, and `ΔI`.
* UCP uses the "Resolution" field, which can be set globally and overridden for each step, and currently only supports time

### Loops

For loops, there are two fundamental approaches:

1. **Nested Steps:** Define the loop as a step block with a nested step, with a `repeat` parameter specifying the number of times to repeat the loop. This is the more modern approach, similar to how loops are defined in modern programming languages such as Python.

The following protocols use this approach:

* UCP (using [step blocks](/simulate/universal-cycler-protocol#step-blocks))
* Novonix (using `ChildProtocolStepList` with `TimesToLoop`).

### Increment cycle number

* Novonix uses `StepType = 6` to increment the cycle counter. In UCP this maps to an auxiliary `Increment cycle number` step.
* Arbin uses the built-in `PV_CHAN_Cycle_Index` variable to track the current cycle. When this variable is incremented via a `Set Variable(s)` step, it maps to both a `set_variable` action and an `Increment cycle number` auxiliary step in UCP.

2. **Goto/State machine:** Define special steps like "start loop" and "end loop". This is the more legacy approach, similar to older programming languages such as Fortran.

The following protocols use this approach:

* Maccor (using a `Do` step to start the loop, and a `Loop` step to end the loop)
* Neware (using a special step type to loop back to a specified previous step a certain number of times)
* Arbin (using limit conditions with goto targets to jump between steps, and `Set Variable(s)` steps to manage counters)

### Cycle-index branching (Arbin)

Arbin protocols commonly use `PV_CHAN_Cycle_Index` to implement cycle-dependent branching, where different parameters are applied on different cycles. When parsed into UCP, this pattern is represented using [control steps](/simulate/universal-cycler-protocol#control-steps) with `set_variable` actions and `goto` targets.

### Time-varying advanced formulas (Arbin)

Arbin schedules can drive a step with an **advanced formula** (an `AF_*` expression) whose value changes over the course of the step — for example a `SIGN(...)`-based piecewise current waveform that alternates polarity at fixed elapsed times. In Arbin, these formulas read the elapsed step time from the built-in process variable `PV_CHAN_Step_Time`.

When such a formula is used as a step's control value, the parser translates `PV_CHAN_Step_Time` to the UCP step-time variable [`t`](/simulate/universal-cycler-protocol#example-time-varying-control) and inlines the formula directly into the step value, so the simulator evaluates it as a function of time. The `SIGN(...)` Arbin function maps to UCP's [`sign()`](/simulate/universal-cycler-protocol#dynamic-behavior-with-variables) helper.

For example, an Arbin advanced formula equivalent to "+1 A for the first 30 s of the step, then –1 A for the next 30 s" becomes the following UCP step:

```yaml theme={null}
- SMC Sign Current Waveform:
    - Charge:
        mode: Current
        value: sign(30 - t) * 1.0
        duration: 60
```

A few details to be aware of:

* The waveform carries its own polarity in the expression, so the parser leaves the sign untouched and reports the step as a nominal `Charge` direction regardless of how many times the value flips during the step.
* Voltage cut-offs on both sides of the operating window can be attached as `ends`, since the value may push the cell in either direction within a single step.
* `PV_CHAN_Step_Time` appearing inside a **termination** equation still means a Duration end and is handled separately — only its use inside the control-value formula maps to `t`.
* Advanced formulas that reference only Arbin `MV_UD*` user variables (and don't depend on step time) continue to be seeded as scalar UCP variables at protocol start, exactly as before.

### Last-step read-outs (Arbin)

Arbin schedules can log the value a channel had **at the end of the previous step** by reading an `LS_CHAN_*` ("last step") variable inside a `Set Variable(s)` step — for example, capturing the discharge capacity of the discharge that just finished into a user variable for later use:

```ini theme={null}
[Schedule_Step1]
m_szStepCtrlType=SetValue(s)
m_szCtrlValue=MV_UD1
m_szExtCtrlValue1=LS_CHAN_Discharge_Capacity
m_szLabel=Log Capacity
```

When parsed into UCP, the `Set Variable(s)` step becomes a [control step](/simulate/universal-cycler-protocol#control-steps) whose `set_variable` action is evaluated against the previous step's data — so `LS_CHAN_*` resolves to that step's final value:

```yaml theme={null}
- Log Capacity:
    - Control:
        set_variable:
          - name: MV_UD1
            eval: LastStepDischargeCapacity
```

The supported read-outs are:

* `LS_CHAN_Discharge_Capacity` → `LastStepDischargeCapacity`
* `LS_CHAN_Charge_Capacity` → `LastStepChargeCapacity`

Charge and discharge stay distinct — the mapped context variables carry the previous step's directional capacity, not a net signed value.

A few details to be aware of:

* `LS_CHAN_*` is only valid **directly** in a `Set Variable(s)` source expression, including inside arithmetic (`LS_CHAN_Discharge_Capacity * 0.25`). Using it in a control value, end-condition equation, or a named formula body — even one that a `Set Variable(s)` step later consumes — is rejected at parse time, because in those places it would bind to the wrong step's data.
* Energy variants (`LS_CHAN_Discharge_Energy`, `LS_CHAN_Charge_Energy`) are recognised but currently rejected: the simulated per-step data exposes capacity but not energy. If you hit this, use a capacity read-out or file a request.

### Power Simulation drive cycles (Arbin)

Arbin schedules can drive a step with a **Power Simulation** waveform — a `.txt` profile referenced by the step whose second column is interpreted as power (watts) rather than current. This is the power-mode counterpart of the existing **Current Simulation** step type; both are parsed into a UCP [`Drive`](/simulate/universal-cycler-protocol#drive-cycles) step, with the step's `mode` set to match:

* `Current Simulation` → `Drive` with `mode: Current` (values are amps)
* `Power Simulation` → `Drive` with `mode: Power` (values are watts)

The referenced `.txt` waveform is uploaded alongside the schedule via the **Additional Files** prompt, exactly like a Current Simulation profile. Voltage step limits carry across unchanged and become `ends` on the `Drive` step.

```yaml theme={null}
- Load Cycle:
    - Drive:
        mode: Power
        value: highway_power_profile.txt
        ends:
          - Voltage < input['Lower voltage cut-off [V]']
```

### Cumulative capacity and energy counters (Arbin)

Arbin's `PV_CHAN_Discharge_Capacity`, `PV_CHAN_Charge_Capacity`, `PV_CHAN_Discharge_Energy`, and `PV_CHAN_Charge_Energy` channels accumulate charge and energy throughput across steps until an explicit `Set Variable(s)` reset zeroes them. When an Arbin protocol is parsed into UCP, these four counters are kept as distinct runtime variables (rather than collapsed into a net `Capacity` / `Energy`) so that expressions comparing them evaluate correctly.

The counters are:

* **Seeded to 0** at the top of the protocol.
* **Incremented after every step** by that step's discharge or charge throughput.
* **Reset to 0** whenever a `Set Variable(s)` step includes the counter in its reset mask.

This lets protocols branch on **net** throughput. For example, a schedule that alternates charge and discharge until the net capacity swings past a target can be written using the difference of the two counters in a `set_variable` expression or a `VariableEnd` condition:

```yaml theme={null}
- Log Net Capacity:
    - Control:
        set_variable:
          - name: VAR_NET_CAPACITY
            eval: PV_CHAN_Discharge_Capacity - PV_CHAN_Charge_Capacity
```

When a **capacity counter** appears on the left side of a step limit (e.g. `Charge to PV_CHAN_Charge_Capacity >= MV_UD1`), the parser rewrites it to a per-step `Capacity` end — protocols typically reset the counter just before such a step, so the counter's value within the step equals that step's own throughput. Directional energy counters can be used in variable expressions, but are not currently supported on the left side of step limits.

<Note>
  Round-tripping is preserved: exporting a UCP protocol back to Arbin recognises `PV_CHAN_(Charge|Discharge)_(Capacity|Energy) = 0` set-variable actions and encodes them as reset-mask entries on a `Set Variable(s)` step.
</Note>

### Drive cycles (BioLogic)

BioLogic `.mps` protocols use **User Profile** steps to apply drive cycles (for example, a driving current profile). The waveform is embedded directly in the `.mps` file as an **Urban Profile Table** appended after the technique block, so no additional upload is required. When parsed into UCP, a User Profile step maps to a [`Drive`](/simulate/universal-cycler-protocol#drive-cycles) step: the table's time column defines the step duration, and the value column drives the cell in the corresponding control mode (e.g., `Current`).

A `.mps` file with multiple techniques can contain several User Profile steps that share an internal `Ns` index. Each drive cycle is named `UP_tech<N>_step_<Ns>` (where `<N>` is the technique number) so cycles from different techniques don't collide on import.

#### Overriding an embedded drive cycle

EC-Lab embeds an **event-compressed** copy of each drive-cycle waveform inside the `.mps` file. The compressed waveform simulates as-is, but if you have the original full-resolution `.txt` profile you can attach it to swap in higher fidelity before simulating.

After uploading a `.mps` that contains drive cycles, the **Additional Files** panel shows a **Drive cycle waveforms (optional)** section listing each embedded cycle by name. Drop a file onto any slot to override that cycle for the simulation; leave a slot empty to keep the embedded waveform. Overrides are matched by drive-cycle name, not file name, so you can attach any file you like to each slot.

### Battery Capacity Determination (BioLogic)

EC-Lab's **Battery Capacity Determination (BCD)** technique runs a fixed capacity-check sequence — a CC leg toward the first voltage bound (`EM1` by default, since `I1 sign` defaults to charge), an optional CV hold, a single rest (`tR`), then a CC leg toward the opposite bound (`EM2`, since `I2 sign` defaults to discharge) — and is imported as a UCP step block with the corresponding CC, CV, and Rest steps. The direction of each CC leg follows its `I1 sign` / `I2 sign` field, so a discharge-first sequence is equally supported. Both halves of the technique (`Is1`/`N1` and `Is2`/`N2`) support `Set I/C` modes `I` (constant current with `A`, `mA`, `µA` units), `C / N` (C-rate computed from a user-supplied `N` divisor), and `C x N` (C-rate multiplied by `N`), and the `tR` rest duration is parsed from EC-Lab's `h:m:s` string (a duration of `0:00:0.0000` means the rest is disabled and is dropped from the block).

### EIS (BioLogic)

Both potentiostatic (**PEIS**) and galvanostatic (**GEIS**) impedance techniques are recognised and converted to a UCP [`EIS`](/simulate/universal-cycler-protocol#eis-steps) step using the frequency bounds from the technique's `ctrl2_val` / `ctrl3_val` fields. The same UCP `EIS` step represents both — the cycler simulator runs an impedance sweep against the configured cell model and returns a Nyquist trace in the results.

### 3-electrode vs 2-electrode cells (BioLogic)

How `Ewe` and `Ece` step limits are interpreted depends on the **Potential control** mode set in the `.mps` file header:

* `Ewe-Ece` (3-electrode): `Ewe` is the working-electrode (cathode) potential and maps to a `CathodePotential` end condition; `Ece` is the counter-electrode (anode) potential and maps to `AnodePotential`.
* Any other mode — `Ewe`, `Ecell`, or no entry (2-electrode full cell): `Ewe` is the terminal voltage and maps to a `Voltage` end condition. `Ece` has no terminal-voltage equivalent and is dropped, because mapping a counter-electrode safety guard to terminal voltage would create a spurious cutoff that often fires at the very start of the step.

If a 2-electrode `.mps` parses with no voltage cutoffs where you expected them, check that the header's potential control is set explicitly — leaving it blank is treated as 2-electrode.

### Loop counts (BioLogic)

EC-Lab's `Loop` technique stores the number of additional iterations in `nt` (i.e. `nt = 0` runs the loop body once, `nt = 3` runs it four times). UCP's [`repeat`](/simulate/universal-cycler-protocol#step-blocks) is the **total** number of executions, so the parser converts BioLogic `nt` to UCP `repeat = nt + 1`. Loops imported before this change ran one iteration short — re-import the `.mps` to pick up the corrected count.

## PyBaMM experiment strings

You can upload a plain text file containing [PyBaMM experiment strings](https://docs.pybamm.org/en/latest/source/api/experiment/index.html) directly into the Battery Cycler Simulator. The system auto-detects the format and converts the steps into UCP, with list repetition blocks mapping to [step blocks](/simulate/universal-cycler-protocol#step-blocks) with a `repeat` count.

### Basic syntax

For plain steps, each line in the file is a single PyBaMM step string. The supported step types are:

* `Charge at <value> <unit>` — constant current, C-rate, or power charge
* `Discharge at <value> <unit>` — constant current, C-rate, or power discharge
* `Hold at <value> V` — constant voltage hold
* `Rest for <duration>` — open-circuit rest period

Steps can include termination conditions with `until` and duration constraints with `for`:

```text theme={null}
Charge at 1C until 4.2V
Discharge at 0.5C for 1 hour
Hold at 4.2V until C/50
Rest for 10 minutes
```

### List repetition

To repeat a sequence of steps multiple times, wrap them in square brackets and multiply with `* N`. This is equivalent to Python's list repetition syntax.

```text theme={null}
["Charge at 1C until 4.2V", "Discharge at 1C until 2.5V"] * 100
```

This produces a single repeated block in UCP with `repeat: 100`, rather than duplicating the steps 100 times. You can mix repeated blocks with plain steps:

```text theme={null}
Charge at 1C until 4.2V
Hold at 4.2V until C/50
["Discharge at 0.5C until 3.0V", "Rest for 10 minutes"] * 50
Rest for 1 hour
```

#### Nested repetition

Repeated blocks can be nested for more complex protocols:

```text theme={null}
[["Charge at 1C until 4.2V", "Rest for 5 minutes"] * 2, "Discharge at 1C until 2.5V"] * 30
```

This creates an outer block that repeats 30 times, where each iteration runs the charge-rest pair twice followed by a single discharge.

#### Cycle groups

Use parentheses inside a list to mark a group of steps as a **cycle**. This automatically inserts an "Increment cycle number" step at the end of each repetition, so cycle-level metrics are tracked correctly:

```text theme={null}
[("Charge at 1C until 4.2V", "Discharge at 1C until 2.5V")] * 100
```

<Note>
  Tuple cycle groups must always be inside a list. Use `[(...)] * N` — not `(...) * N`.
</Note>

#### Multiline format

For readability, you can split a repeated block across multiple lines:

```text theme={null}
[
    "Charge at 1C until 4.2V",
    "Hold at 4.2V until C/50",
    "Discharge at 1C until 2.5V",
] * 50
```
