> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.conductorquantum.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.conductorquantum.com/_mcp/server.

# Routines

Routines are Python functions that orchestrate your device measurements. Think of them as recipes - they take a device, perform a sequence of operations, and return results. The `@routine` decorator handles parameter management, logging, and result storage automatically.

## Your First Routine

Let's write a simple routine to sweep a gate and measure current:

```python
import numpy as np
from stanza.routines import routine

@routine
def pinchoff_sweep(ctx, gate, v_start, v_stop, n_points, contact, session=None):
    """Sweep a gate to find the pinchoff voltage."""
    device = ctx.resources.device

    # Generate voltage points
    voltages = np.linspace(v_start, v_stop, n_points)

    # Sweep and measure (automatically logged if session provided)
    v_data, i_data = device.sweep_1d(
        gate,
        voltages.tolist(),
        contact,
        session=session
    )

    # Find pinchoff (where current drops below threshold)
    threshold = 1e-12  # 1 pA
    pinchoff_idx = np.where(np.abs(i_data) < threshold)[0]
    pinchoff_voltage = v_data[pinchoff_idx[0]] if len(pinchoff_idx) > 0 else None

    return {
        "voltages": v_data,
        "currents": i_data,
        "pinchoff_voltage": pinchoff_voltage
    }
```

Run it:

```python
from stanza.routines import RoutineRunner
from stanza.models import DeviceConfig

config = DeviceConfig.from_yaml("device.yaml")
runner = RoutineRunner(configs=[config])

result = runner.run(
    "pinchoff_sweep",
    gate="LEFT_BARRIER",
    v_start=-2.0,
    v_stop=0.0,
    n_points=100,
    contact="DRAIN"
)

print(f"Pinchoff voltage: {result['pinchoff_voltage']:.3f} V")
```

## Understanding the Context

The `ctx` parameter gives you access to everything your routine needs:

### Accessing Your Device

```python
@routine
def check_voltages(ctx):
    """Read current voltage on all gates."""
    device = ctx.resources.device

    voltages = {}
    for gate in device.gates:
        voltages[gate] = device.check(gate)

    return {"voltages": voltages}
```

### Using Previous Results

Routines can access results from earlier routines:

```python
@routine
def find_pinchoff(ctx, gate, contact):
    """Find pinchoff voltage."""
    device = ctx.resources.device

    voltages = np.linspace(-2.0, 0.0, 100)
    v_data, i_data = device.sweep_1d(gate, voltages.tolist(), contact)

    # Find where current drops below 1 pA
    threshold_idx = np.where(np.abs(i_data) < 1e-12)[0]
    pinchoff = v_data[threshold_idx[0]] if len(threshold_idx) > 0 else None

    return {"gate": gate, "pinchoff_voltage": pinchoff}

@routine
def set_to_pinchoff(ctx, gate):
    """Set gate to its pinchoff voltage."""
    device = ctx.resources.device

    # Get result from find_pinchoff routine
    pinchoff_result = ctx.results.get("find_pinchoff")

    if pinchoff_result and pinchoff_result["pinchoff_voltage"]:
        voltage = pinchoff_result["pinchoff_voltage"]
        device.jump({gate: voltage})
        return {"status": "success", "voltage": voltage}
    else:
        return {"status": "failed", "reason": "no pinchoff found"}
```

Run them in sequence:

```python
runner.run("find_pinchoff", gate="LEFT_BARRIER", contact="DRAIN")
runner.run("set_to_pinchoff", gate="LEFT_BARRIER")
```

## Pre-configuring Parameters

Instead of passing parameters every time, configure them in your device YAML:

```yaml
routines:
  - name: pinchoff_sweep
    parameters:
      gate: LEFT_BARRIER
      v_start: -2.0
      v_stop: 0.0
      n_points: 100
      contact: DRAIN
```

Now run without arguments:

```python
# Uses parameters from YAML
runner.run("pinchoff_sweep")

# Override specific parameters
runner.run("pinchoff_sweep", n_points=200)
```

## Example: Building a 2D Charge Stability Diagram

```python
@routine
def charge_stability(ctx, gate_x, gate_y, v_x_start, v_x_stop, n_x,
                      v_y_start, v_y_stop, n_y, contact, session=None):
    """Measure a 2D charge stability diagram."""
    device = ctx.resources.device

    # Create voltage arrays
    v_x = np.linspace(v_x_start, v_x_stop, n_x)
    v_y = np.linspace(v_y_start, v_y_stop, n_y)

    # 2D sweep with automatic logging
    v_data, signal = device.sweep_2d(
        gate_x, v_x.tolist(),
        gate_y, v_y.tolist(),
        contact,
        session=session
    )

    # Analyze: find charge transitions
    grad_x = np.gradient(signal, axis=0)
    grad_y = np.gradient(signal, axis=1)
    edges = np.sqrt(grad_x**2 + grad_y**2)

    return {
        "voltages_x": v_data[0],
        "voltages_y": v_data[1],
        "signal": signal,
        "edges": edges
    }
```

Configure in YAML:

```yaml
routines:
  - name: charge_stability
    parameters:
      gate_x: LEFT_PLUNGER
      gate_y: RIGHT_PLUNGER
      v_x_start: -2.0
      v_x_stop: -0.5
      n_x: 150
      v_y_start: -2.0
      v_y_stop: -0.5
      n_y: 150
      contact: DRAIN
```

Run it:

```python
result = runner.run("charge_stability")

# Plot the diagram
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 8))
plt.pcolormesh(
    result["voltages_x"],
    result["voltages_y"],
    result["signal"].T,
    cmap="RdBu_r"
)
plt.xlabel("Left Plunger (V)")
plt.ylabel("Right Plunger (V)")
plt.colorbar(label="Signal")
plt.title("Charge Stability Diagram")
plt.show()
```

## Example: Iterative Tuning with Feedback

Routines can iterate until they find what they're looking for:

```python
@routine
def optimize_coupling(ctx, barrier_gate, plunger_gates, target_current,
                       max_iterations=20):
    """Iteratively tune barrier gate to achieve target interdot coupling."""
    device = ctx.resources.device

    v_barrier = -1.5  # Starting voltage
    step_size = 0.05  # Initial step

    for iteration in range(max_iterations):
        # Set barrier voltage
        device.jump({barrier_gate: v_barrier})

        # Measure charge diagram to estimate coupling
        v_plunger = np.linspace(-2.0, -0.5, 50)
        v_data, i_data = device.sweep_2d(
            plunger_gates[0], v_plunger.tolist(),
            plunger_gates[1], v_plunger.tolist(),
            "DRAIN"
        )

        # Estimate coupling from gradient
        gradient = np.gradient(i_data)
        coupling_strength = np.max(np.abs(gradient))

        # Check if we hit target
        if np.abs(coupling_strength - target_current) < target_current * 0.1:
            return {
                "converged": True,
                "barrier_voltage": v_barrier,
                "coupling_strength": coupling_strength,
                "iterations": iteration + 1
            }

        # Adjust barrier voltage
        if coupling_strength < target_current:
            v_barrier += step_size  # Increase coupling
        else:
            v_barrier -= step_size  # Decrease coupling

        step_size *= 0.9  # Reduce step size

    return {
        "converged": False,
        "barrier_voltage": v_barrier,
        "coupling_strength": coupling_strength,
        "iterations": max_iterations
    }
```

## Example: Conditional Workflows

Build intelligent workflows that adapt based on measurements:

```python
@routine
def check_leakage(ctx, threshold=1e-9):
    """Check if leakage current is acceptable."""
    device = ctx.resources.device

    current = device.measure("DRAIN")
    passed = np.abs(current) < threshold

    return {"current": current, "passed": passed, "threshold": threshold}

@routine
def conditional_tuneup(ctx):
    """Only proceed with tuneup if leakage is acceptable."""
    # Check if leakage test passed
    leakage_result = ctx.results.get("check_leakage")

    if not leakage_result or not leakage_result["passed"]:
        return {
            "status": "aborted",
            "reason": f"Leakage too high: {leakage_result['current']:.2e} A"
        }

    # Proceed with characterization
    device = ctx.resources.device
    voltages = np.linspace(-2.0, 0.0, 100)
    v_data, i_data = device.sweep_1d("LEFT_BARRIER", voltages.tolist(), "DRAIN")

    return {
        "status": "complete",
        "voltages": v_data,
        "currents": i_data
    }
```

Run the workflow:

```python
# First check leakage
runner.run("check_leakage", threshold=1e-9)

# Then run conditional tuneup (only proceeds if leakage OK)
result = runner.run("conditional_tuneup")

if result["status"] == "aborted":
    print(f"Tuneup aborted: {result['reason']}")
else:
    print("Tuneup completed successfully")
```

## Organizing Complex Tune-ups

Use nested routines in your YAML to organize multi-step processes:

```yaml
routines:
  - name: full_characterization
    parameters:
      threshold: 1e-9
    routines:
      - name: check_leakage
        parameters:
          threshold: 1e-9

      - name: find_all_pinchoffs
        parameters:
          gates: [LEFT_BARRIER, CENTER_BARRIER, RIGHT_BARRIER]
          v_start: -2.0
          v_stop: 0.0
          n_points: 100
          contact: DRAIN

      - name: charge_stability
        parameters:
          gate_x: LEFT_PLUNGER
          gate_y: RIGHT_PLUNGER
```

Run the entire workflow:

```python
# Run all routines under "full_characterization"
results = runner.run_all(parent_routine="full_characterization")

# Access individual results
leakage = results["check_leakage"]
pinchoffs = results["find_all_pinchoffs"]
charge_diagram = results["charge_stability"]
```

## Automatic Data Logging

When you provide a `logger` to `RoutineRunner`, data is automatically logged:

```python
from stanza.logger import DataLogger

logger = DataLogger(
    name="logger",
    routine_name="characterization",
    base_dir="./data",
    formats=["hdf5", "jsonl"],
    compression="gzip"
)

runner = RoutineRunner(configs=[config], logger=logger)

# This automatically logs all sweep data
runner.run("charge_stability")

# Data saved to: ./data/characterization/<session_id>/
```

Your routine receives a `session` parameter automatically:

```python
@routine
def custom_measurement(ctx, session=None):
    """Routine with custom logging."""
    device = ctx.resources.device

    # Perform measurement
    data = do_something_custom(device)

    # Manually log specific data
    if session:
        session.log_measurement(
            "custom_data",
            data={
                "measured_value": data["value"],
                "timestamp": datetime.now()
            },
            metadata={"algorithm": "custom_v2"}
        )

    return data
```

## Error Handling

Handle errors gracefully to prevent tune-up interruption:

```python
from stanza.exceptions import DeviceError, InstrumentError

@routine
def safe_sweep(ctx, gate, v_start, v_stop, n_points, contact):
    """Sweep with robust error handling."""
    device = ctx.resources.device

    try:
        voltages = np.linspace(v_start, v_stop, n_points)
        v_data, i_data = device.sweep_1d(gate, voltages.tolist(), contact)

        return {
            "status": "success",
            "voltages": v_data,
            "currents": i_data
        }

    except InstrumentError as e:
        # Instrument communication failed
        return {
            "status": "instrument_error",
            "error": str(e),
            "gate": gate
        }

    except DeviceError as e:
        # Device-level error (e.g., voltage out of bounds)
        return {
            "status": "device_error",
            "error": str(e),
            "gate": gate
        }

    finally:
        # Always return to safe voltage
        device.jump({gate: 0.0})
```

## Complete Example: Automated Tuneup Workflow

Here's a complete example combining multiple routines:

```python
import numpy as np
from stanza.routines import routine, RoutineRunner
from stanza.models import DeviceConfig
from stanza.logger import DataLogger

@routine
def initialize_device(ctx, safe_voltages=None):
    """Initialize device to safe voltages."""
    device = ctx.resources.device

    if safe_voltages is None:
        safe_voltages = {gate: 0.0 for gate in device.gates}

    device.jump(safe_voltages)
    return {"status": "initialized", "voltages": safe_voltages}

@routine
def check_leakage(ctx, contact="DRAIN", threshold=1e-9):
    """Verify leakage is below threshold."""
    device = ctx.resources.device

    current = device.measure(contact)
    passed = np.abs(current) < threshold

    return {
        "current": current,
        "threshold": threshold,
        "passed": passed
    }

@routine
def find_pinchoff(ctx, gate, v_start, v_stop, n_points, contact,
                   threshold=1e-12, session=None):
    """Find pinchoff voltage for a gate."""
    device = ctx.resources.device

    voltages = np.linspace(v_start, v_stop, n_points)
    v_data, i_data = device.sweep_1d(gate, voltages.tolist(), contact, session=session)

    # Find pinchoff
    below_threshold = np.where(np.abs(i_data) < threshold)[0]
    pinchoff_voltage = v_data[below_threshold[0]] if len(below_threshold) > 0 else None

    # Log analysis
    if session:
        session.log_analysis(
            f"{gate}_pinchoff",
            {
                "pinchoff_voltage": pinchoff_voltage,
                "threshold": threshold,
                "gate": gate
            }
        )

    return {
        "gate": gate,
        "pinchoff_voltage": pinchoff_voltage,
        "voltages": v_data,
        "currents": i_data
    }

@routine
def measure_charge_diagram(ctx, gate_x, gate_y, v_x_start, v_x_stop, n_x,
                            v_y_start, v_y_stop, n_y, contact, session=None):
    """Measure 2D charge stability diagram."""
    device = ctx.resources.device

    v_x = np.linspace(v_x_start, v_x_stop, n_x)
    v_y = np.linspace(v_y_start, v_y_stop, n_y)

    v_data, signal = device.sweep_2d(
        gate_x, v_x.tolist(),
        gate_y, v_y.tolist(),
        contact,
        session=session
    )

    return {
        "gate_x": gate_x,
        "gate_y": gate_y,
        "voltages_x": v_data[0],
        "voltages_y": v_data[1],
        "signal": signal
    }

# Main execution
if __name__ == "__main__":
    # Setup
    config = DeviceConfig.from_yaml("device.yaml")
    logger = DataLogger(
        name="logger",
        routine_name="automated_tuneup",
        base_dir="./data",
        formats=["hdf5", "jsonl"]
    )
    runner = RoutineRunner(configs=[config], logger=logger)

    # Execute workflow
    runner.run("initialize_device")

    leakage = runner.run("check_leakage")
    if not leakage["passed"]:
        print(f"Leakage too high: {leakage['current']:.2e} A")
        exit(1)

    # Find pinchoffs for all barriers
    for gate in ["LEFT_BARRIER", "CENTER_BARRIER", "RIGHT_BARRIER"]:
        result = runner.run(
            "find_pinchoff",
            gate=gate,
            v_start=-2.0,
            v_stop=0.0,
            n_points=100,
            contact="DRAIN"
        )
        print(f"{gate} pinchoff: {result['pinchoff_voltage']:.3f} V")

    # Measure charge stability diagram
    diagram = runner.run("measure_charge_diagram")

    print(f"Data saved to: ./data/automated_tuneup/")
```

## Tips and Best Practices

**Always return dictionaries**: Makes results easy to access and extend later.

**Use type hints**: Helps with IDE autocomplete and documentation:

```python
from typing import Dict, List, Optional

@routine
def my_routine(ctx, gate: str, voltages: List[float]) -> Dict[str, any]:
    ...
```

**Document your routines**: Write clear docstrings explaining what the routine does and what parameters it expects.

**Keep routines focused**: Each routine should do one thing well. Compose complex workflows from simple routines.

**Check for previous results**: Always check if `ctx.results.get()` returns None before using results.

**Use finally blocks**: Ensure cleanup code runs even if errors occur:

```python
try:
    # measurement code
finally:
    device.jump(safe_voltages)  # Always return to safe state
```

**Test incrementally**: Test each routine individually before chaining them together.

**Version your code**: Keep routines in git and document changes.

## Next Steps

* [Data Logging](/control/stanza/core-concepts/data-logging) - Learn how to log and retrieve measurement data
* [Live Plotting](/control/stanza/core-concepts/live-plotting) - Visualize routine data in real-time
* [Device Configuration](/control/stanza/core-concepts/device-configuration) - Configure your device for use with routines
* [Drivers](/control/stanza/core-concepts/drivers) - Understand how device operations communicate with hardware