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

# Universal Cycler Protocol

> Full reference for UCP, Ionworks' YAML-based language for defining battery cycling experiments and protocol steps

The Universal Cycler Protocol (UCP) is a flexible YAML-based language for defining battery cycling experiments. It allows you to create complex experimental sequences with programmatic control.

<Tip>
  You can create and edit UCP protocols visually using the
  [Protocol Builder](/simulate/protocol-builder), or by converting a
  [commercial protocol](/simulate/simulating-commercial-protocols) into UCP
  format. This page is the full reference for the underlying UCP file format.
</Tip>

## Global Configuration

You can define global settings for the entire protocol under the optional `global` key.

```yaml theme={null}
global:
  initial_temperature: 25
  initial_state_type: soc_percentage # or "voltage"
  initial_state_value: 100 # percent if soc_percentage, volts if voltage
  resolution:
    time: 60
```

* `initial_temperature`: The starting temperature for the experiment in degrees Celsius. Defaults to `25`. When used in a [design optimization](/optimize/running-optimization#define-the-experiment), this value is automatically converted to Kelvin and applied as both the initial and ambient temperature for the simulation.
* `initial_state_type`: How to define the initial state of the cell. Allowed values:
  * `soc_percentage`: Interpret `initial_state_value` as a percentage of state of charge (0–100).
  * `voltage`: Interpret `initial_state_value` as a starting voltage in Volts.
* `initial_state_value`: The initial value for the selected type.
* `resolution`: The time resolution for the simulation output in seconds. Defaults to `60`.

## Safety Limits

You can define optional safety limits for the simulation. If any of these are breached, the simulation jumps to a recovery step (`goto`) or, if no `goto` is supplied, ends the test.

Each limit may be given as a bare number or as an object with an explicit `goto` so that different fault conditions can route to different recovery steps:

```yaml theme={null}
safety_limits:
  # Object form — this limit has its own dedicated recovery step.
  voltage_max:
    value: 4.525
    goto: Pause_Voltage_Fault
    delay: 3 # Optional debounce in seconds. The safety only trips when
             # ``Voltage > 4.525 V`` AND the step has been running for more
             # than 3 s, mirroring the Maccor ``VOLT>=4.525&STIME>3`` idiom.
  # Bare-number form — falls back to the protocol-level `goto` below.
  voltage_min: 2.5
  temperature_min: -20
  temperature_max:
    value: 60
    goto: Pause_Thermal_Fault
  charge_current_max: 10 # Max charge current in Amps (positive value)
  discharge_current_max: 15 # Max discharge current in Amps (positive value)
  # Optional fallback target for any limit that doesn't specify its own `goto`.
  goto: Pause_Generic_Fault
```

Resolution order when a limit triggers:

1. If the triggering limit has its own `goto`, jump there.
2. Otherwise, if `safety_limits.goto` is set, jump there.
3. Otherwise, the test ends.

<Note>
  If a step's termination condition (see below) has the same value as a global
  `safety_limit`, the safety limit takes precedence.
</Note>

## Simulation Steps

Each simulation step is defined as a dictionary with a single key that defines the step's **direction**. The value of this key is a dictionary containing the step's parameters.

The primary step directions are:

* `Rest`: The cell is at rest.
* `Charge`: The cell is being charged.
* `Discharge`: The cell is being discharged.
* `Drive`: A drive cycle step (see below).
* `EIS`: An Electrochemical Impedance Spectroscopy step (see below).

### Step Parameters

* `mode`: The control mode for the step (e.g., `C-rate`, `Current`, `Power`, `Voltage`). Required unless the step is `Rest`.
* `value`: The setpoint for the control mode. This can be a fixed number or a dynamic expression.
* `duration`: The maximum duration of the step in seconds.
* `ends`: A list of one or more cut-off conditions. A step must have either a `duration` or at least one `ends` condition.
* `temperature`: (Optional) The ambient temperature for this specific step in Celsius, overriding the global setting.
* `resolution`: (Optional) The time resolution for this specific step, overriding the global setting.
* `set_variable`: (Optional) A list of variables to calculate and set *after* the step completes.
* `note`: (Optional) A string for informational purposes.

### Parameterized Steps with `input['...']`

You can use `input['...']` syntax in `value`, `duration`, and `ends` fields to create parameterized protocols. When running the protocol, the actual values are provided at runtime.

```yaml theme={null}
steps:
  # Parameterized step value
  - Discharge:
      mode: C-rate
      value: input["C-rate"]
      ends:
        - Voltage < input["Cut-off voltage [V]"]

  # Parameterized duration
  - Rest:
      duration: input["Rest duration [s]"]

  # Parameterized duration in ends
  - Discharge:
      mode: Power
      value: 2
      ends:
        - "Duration > input['Cruise duration [s]']"
        - Voltage < 3.0

  # Expressions with inputs
  - Rest:
      duration: input["Duration [s]"] / 2
```

Inputs can also be used in `resolution`, `global` settings, and `set_variable` expressions. See [Experiment Templates](/simulate/experiment-templates) for complete examples.

### Termination Conditions (`ends`)

The `ends` conditions define when a step should terminate.

```yaml theme={null}
# Simple termination
ends:
  - "Voltage > 4.2"

# Derivative-based termination
ends:
  - "d/dt(Voltage) < 0.001"

# Termination with a `goto` to another step block
ends:
  - "Voltage < 2.7":
      goto: My_Next_Step

# Termination that ends the entire test
ends:
  - "Voltage < 2.5":
      goto: End Test
```

* **Supported Types:** `Voltage`, `Current`, `C-rate`, `Capacity`, `Temperature` (case-insensitive).
* **Supported Operators:** `<` and `>`.
* **Sign Convention**: For `Current`, `C-rate`, and `Capacity` terminations, always provide a positive value. The engine automatically handles the internal sign convention (e.g., negative current for charge). For `Voltage` and `Temperature`, signs are respected as written.
* **Derivative Terminations**: To terminate based on the rate of change of a variable, use the format `"d/dt(Type) operator value"`.

<Note>
  If a step's termination condition is met at the very beginning (e.g., trying
  to charge a battery that is already at 4.2V), the step will be skipped. Any
  `goto` transitions on that termination condition will **not** be executed.
  The step's `set_variable` entries **are** still evaluated, so accumulators and
  reference values you compute at the end of a step remain consistent whether
  the step ran or was skipped.
</Note>

### Example: Derivative Termination

Derivative-based terminations are useful for ending a step based on the stability of a signal. For example, a constant-voltage charge phase can be terminated when the current stops changing, which indicates the battery is full.

```yaml theme={null}
- CV Charge with Current Stability Check:
    - Charge:
        mode: Voltage
        value: 4.2 # Constant Voltage phase
        ends:
          - "d/dt(Current) < 0.001"
```

### Example: CCCV Charge

```yaml theme={null}
- CC Charge:
    - Charge:
        mode: C-rate
        value: 1
        ends:
          - "Voltage > 4.2"
- CV Charge:
    - Charge:
        mode: Voltage
        value: 4.2
        ends:
          - "Current < 0.05"
```

### Ambient Temperature Steps

The `Ambient Temperature` step changes the ambient (chamber) temperature mid-protocol. It is an instantaneous step — it sets the chamber setpoint and immediately advances to the next step; the simulation continues at the new ambient temperature until another `Ambient Temperature` step is reached.

* `temperature_setpoint`: Required. The new ambient temperature in degrees Celsius. Must be greater than absolute zero.
* `temperature_ramp_rate`: (Optional) Ramp rate in °C/min. Defaults to `2.0`.

```yaml theme={null}
- Soak at 45 C:
    - Ambient Temperature:
        temperature_setpoint: 45
    - Rest:
        duration: 3600
- Cool to 25 C:
    - Ambient Temperature:
        temperature_setpoint: 25
    - Rest:
        duration: 1800
```

Use this step for thermal-soak segments, RPT diagnostics that run at a fixed reference temperature, or any protocol whose chamber setpoint changes during the test.

<Note>
  An `Ambient Temperature` step overrides the `global.initial_temperature`
  setting (and any prior step's `temperature` override) from that point in the
  protocol onward.
</Note>

### EIS Steps

The `EIS` step performs an Electrochemical Impedance Spectroscopy measurement at the current state of the cell.

* `lower_frequency`: The lower frequency bound for the EIS sweep in Hz.
* `upper_frequency`: The upper frequency bound for the EIS sweep in Hz.

Both frequency parameters can be a fixed number or a dynamic expression.

<Note>
  EIS steps are not supported in [design optimization](/optimize/running-optimization#define-the-experiment) experiments. Use EIS steps in standalone [simulations](/simulate/simulations) instead.
</Note>

EIS steps run against both physics-based models (using `pybamm.EISSimulation` under the hood) and Equivalent Circuit Models. For an ECM the impedance is computed analytically from the series resistance parameter `R0 [Ohm]` and each indexed `R_rc (i) [Ohm]` / `C_rc (i) [F]` parallel branch — so the Battery Cycler Simulator, which uses an ECM, can run EIS steps from uploaded protocols (e.g. BioLogic PEIS/GEIS techniques) without any extra configuration. Analytic ECM EIS requires the resistance and capacitance parameters to be constants; SOC- or current-dependent ECM parameters are rejected with a clear error.

In the simulation results view, each EIS step is marked with a vertical line on the time-domain plots at the instant the impedance sweep was taken; hovering the marker shows an **EIS step N** tooltip so you can correlate the Nyquist trace with its position in the cycling history.

#### Example: Basic EIS

Perform an EIS measurement after a 30-minute rest period.

```yaml theme={null}
- Rest:
    - Rest:
        duration: 1800
- Impedance Measurement:
    - EIS:
        lower_frequency: 0.1
        upper_frequency: 1000
```

#### Example: EIS with Dynamic Frequencies

The frequencies can be calculated dynamically using variables.

```yaml theme={null}
- Set Up Frequencies:
    - Control:
        set_variable:
          - name: VAR_UPPER_FREQ
            eval: 10 * input['Frequency Multiplier']
- Perform EIS:
    - EIS:
        lower_frequency: VAR_UPPER_FREQ / 1000
        upper_frequency: VAR_UPPER_FREQ
```

## Control Steps

Control steps are used for programmatic logic and do not run a simulation. They are ideal for setting variables or creating loops with `goto`.

```yaml theme={null}
- Setup and Jump:
    - Control:
        set_variable:
          - name: VAR_C_RATE
            eval: 1.5
    - "Increment cycle number"
```

### Control step parameters

* `set_variable`: (Optional) A list of variables to set (see [dynamic behavior with variables](#dynamic-behavior-with-variables)).
* `goto`: (Optional) The name of the step block to jump to after executing this control step. This enables non-sequential control flow, such as branching to different steps based on variable values.

```yaml theme={null}
- Assign C-rate and Continue:
    - Control:
        set_variable:
          - name: VAR_C_RATE
            eval: 0.33
        goto: Start_Discharge
```

Special string commands are also available:

* `"Increment cycle number"`: Increments the internal cycle counter.
* `"End"` or `"Pause"`: Terminates the entire simulation.

### Reserved `goto` targets

In addition to jumping to a named step block, `goto` accepts the reserved target `End Test`, which stops the simulation immediately. This works from both step termination conditions (`ends`) and from Control step `goto` fields, and is also valid inside subschedules — jumping to `End Test` from a subschedule ends the entire test, not just the subschedule.

A Control step's `goto` runs **unconditionally** every time the step is
entered, so use it to terminate a branch you have already routed into:

```yaml theme={null}
# Unconditional: entering this Control step always ends the test
- Stop Here:
    - Control:
        goto: End Test
```

To terminate **conditionally**, attach the `goto` to a step termination
condition instead — the jump fires only when that condition is met:

```yaml theme={null}
# Conditional: only jumps to End Test when the voltage drops below 2.8 V
- Discharge until fault:
    Discharge:
      C-rate: 0.33
    ends:
      - "Voltage < 2.8":
          goto: End Test
```

Use `End Test` when you want a fault condition or logic branch to terminate the run explicitly rather than falling through to the next step.

## Dynamic Behavior with Variables

You can define variables using `set_variable` and use them to create dynamic, responsive protocols.

* `name`: The name of the variable. Must start with `VAR_`.
* `eval`: A Python expression to be evaluated. The result must be a number.

The `eval` expression can use:

* **User Inputs:** `input['...']`
* **Other Variables:** Any `VAR_` variable already defined.
* **Special Variables:** `t` (step-relative time in seconds), `Cycle` (0-indexed cycle counter).
* **Simulation Results:** `Voltage`, `Current`, etc. from the previous step.
* **Helper Functions:** `first()`, `last()`, `mean()`, `abs()`, `sign()`, `min()`, `max()`, `ifelse()`.

### Initialising variables

Expressions are evaluated eagerly — `ifelse(condition, A, B)` evaluates **both** branches before picking one — so a variable referenced anywhere in an expression must already exist. Use a `Control` step at the top of the protocol to seed every variable you'll write to:

```yaml theme={null}
- Initialize Variables:
    - Control:
        set_variable:
          - { name: VAR_REFERENCE_CAPACITY, eval: "0" }
          - { name: VAR_PEAK_TEMP, eval: "25" }
        note: Seed accumulator variables before the cycling loop.
```

This matters most for the common "set on first cycle, carry over otherwise" idiom:

```yaml theme={null}
set_variable:
  - name: VAR_REFERENCE_CAPACITY
    # Without an init Control step above, VAR_REFERENCE_CAPACITY is undefined
    # the first time this expression runs and the simulation will fail.
    eval: ifelse(Cycle == 0, last(Capacity), VAR_REFERENCE_CAPACITY)
```

<Note>
  Protocols imported from a Maccor `.000` file automatically gain a leading `Initialize Variables` `Control` block that seeds every referenced `VARn` to 0 — this mirrors the way real Maccor cyclers default user variables in hardware. You'll see it at the top of the parsed UCP YAML.
</Note>

### Example: Time-Varying Control

Linearly ramp the C-rate from 0.1 to 1.1 over one hour.

```yaml theme={null}
- Ramp Discharge:
    - Discharge:
        mode: C-rate
        value: 0.1 + t / 3600
        duration: 3600
```

Time-varying values can also change sign within a step — for example, a piecewise current waveform built with `sign()` that charges for the first 30 s and then discharges for the next 30 s:

```yaml theme={null}
- Piecewise Current:
    - Charge:
        mode: Current
        value: sign(30 - t) * 1.0
        duration: 60
        ends:
          - Voltage > input["Upper voltage cut-off [V]"]
          - Voltage < input["Lower voltage cut-off [V]"]
```

<Note>
  When a step value is an explicit function of `t`, the waveform may legitimately drive the cell in both directions during the single step. The usual rule that a `Charge` step can only carry an upper voltage cut-off (and a `Discharge` step a lower one) is relaxed for time-varying values, so you can attach safety cut-offs on both sides.
</Note>

### Example: Conditional Logic

Use the `ifelse` helper to create conditional logic. This example defines a subsequent step's direction based on the final voltage of the previous step.

```yaml theme={null}
- Discharge and Check:
    - Discharge:
        mode: C-rate
        value: 1
        duration: 600
        set_variable:
          - name: VAR_NEEDS_CHARGE
            eval: ifelse(last(Voltage) < 3.5, 1, 0)

- Conditional Step:
    - Direction[ifelse(VAR_NEEDS_CHARGE == 1, "Charge", "Rest")]:
        mode: C-rate
        value: 1
        ends:
          - "Voltage > input['Upper voltage cut-off [V]']"
```

## Step Blocks

Steps can be grouped into named blocks. This is essential for `goto` targets and for repeating a sequence of steps using the `repeat` keyword.

```yaml theme={null}
- Ten Pulses:
    - Discharge:
        mode: C-rate
        value: 5
        duration: 1
    - Rest:
        duration: 1
  repeat: 10
```

<Note>
  A block's name cannot be one of the reserved step types (`Charge`,
  `Discharge`, `Rest`, `Control`, etc.). The reserved `goto` target `End Test`
  is allowed as a jump destination but cannot be used as a block name.
</Note>

## Drive Cycles

You can use a drive cycle for complex profiles by passing data to the `solve_protocol` function in your Python code and referencing it in the YAML.

* **In Python:** Pass a `drive_cycles` dictionary where values are 2-column NumPy arrays (time, value).
* **In YAML:** Use the `Drive` direction. The `value` is the name of the drive cycle from the dictionary. The duration is defined by the time column in the data.
* **`mode`:** Controls how the second data column is interpreted. Use `Current` (default, amps) for a current profile, or `Power` (watts) for a power profile. Voltage cut-offs can be attached to either mode via `ends`.

```yaml theme={null}
# Current-mode drive cycle (values in amps)
- US06 Drive Cycle:
    - Drive:
        mode: Current
        value: US06

# Power-mode drive cycle (values in watts)
- Power Profile:
    - Drive:
        mode: Power
        value: LoadProfile
        ends:
          - Voltage < input['Lower voltage cut-off [V]']
          - Voltage > input['Upper voltage cut-off [V]']
```

## Subroutines

Subroutines are reusable sequences of steps defined in your Python code and called from the YAML protocol. This is useful for standard procedures like a CCCV charge.

* **In Python:** Pass a `subroutines` dictionary to `solve_protocol`. The values are lists of steps.
* **In YAML:** Use the `Subroutine` step type with the name of the subroutine to execute.

```yaml theme={null}
# In your main protocol.yaml file
steps:
  - Initial Rest:
      - Rest:
          duration: 1800

  - "Increment cycle number"
  - Subroutine: CCCV # This calls the "CCCV" subroutine defined in Python
```

## Output

The protocol solver returns a pandas DataFrame containing the results of the simulation, including time, voltage, current, temperature, cycle number, and any custom variables defined with `set_variable`.

For EIS steps, the output DataFrame includes frequency-domain impedance data instead of time-domain data:

| Column           | Description                                                    |
| ---------------- | -------------------------------------------------------------- |
| `Frequency [Hz]` | Excitation frequency                                           |
| `Z_Re [Ohm]`     | Real part of impedance                                         |
| `Z_Im [Ohm]`     | Imaginary part of impedance (negative for capacitive behavior) |

During EIS steps, time-domain columns (`Time [s]`, `Voltage [V]`, `Current [A]`) are present but contain `NaN` values. The `Step count` column identifies which step each row belongs to, allowing you to separate EIS data from cycling data.
