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

* **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.
</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>

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

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

```yaml theme={null}
- US06 Drive Cycle:
    - Drive:
        mode: Current
        value: US06
```

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