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

# Drivers

Drivers are the translators between Stanza's high-level device operations and the specific commands your hardware understands. Stanza includes drivers for common instruments (QDAC-II, OPX) and makes it straightforward to add support for custom hardware.

## Built-in Drivers

### QDAC-II: Voltage Control and DC Measurement

The QDAC-II is a 24-channel DAC with built-in current measurement. It's perfect for DC gate control and low-noise current measurements.

**Basic setup for voltage control:**

```yaml
instruments:
  - name: qdac-control
    type: CONTROL
    driver: qdac2
    ip_addr: 192.168.1.100
    slew_rate: 0.5  # V/s - safe ramp rate
```

**Adding current measurement:**

```yaml
instruments:
  - name: qdac-control
    type: CONTROL
    driver: qdac2
    ip_addr: 192.168.1.100
    slew_rate: 0.5

  - name: qdac-measure
    type: MEASUREMENT
    driver: qdac2
    ip_addr: 192.168.1.100
    measurement_duration: 10e-3  # 10 ms integration
    sample_time: 100e-6  # 100 μs per sample
    current_range: "LOW"  # For pA-nA currents
```

**Or use one instrument for both:**

```yaml
instruments:
  - name: qdac
    type: GENERAL  # Both control and measurement
    driver: qdac2
    ip_addr: 192.168.1.100
    slew_rate: 0.5
    measurement_duration: 10e-3
    sample_time: 100e-6
```

**Key parameters:**

* `slew_rate`: How fast voltages ramp (V/s). Lower is safer but slower.
* `measurement_duration`: Total time to measure (seconds). Longer = less noise.
* `sample_time`: Integration time per sample. Smaller = more samples.
* `current_range`: `"LOW"` (±1 μA) or `"HIGH"` (±100 μA)

### OPX: Fast DC Measurements

The OPX driver provides DC current measurement with pause-based synchronization. It's useful when you need faster measurements than QDAC can provide:

```yaml
instruments:
  # QDAC for voltage control
  - name: qdac-control
    type: CONTROL
    driver: qdac2
    ip_addr: 192.168.1.100
    slew_rate: 0.5

  # OPX for DC current measurement
  - name: opx
    type: MEASUREMENT
    driver: opx
    ip_addr: 192.168.1.200
    measurement_duration: 1e-3
    sample_time: 100e-6
    measurement_channels: [1, 2]
    machine_type: "OPX1000"
```

The OPX driver automatically generates QUA programs for pause-based measurements. During sweeps, it pauses between voltage steps, measures current, and streams data back.

**Example sweep with OPX:**

```python
@routine
def fast_charge_diagram(ctx, gate_x, gate_y, contact, session=None):
    """Fast charge diagram with OPX current measurement."""
    device = ctx.resources.device

    vx = np.linspace(-2.0, -0.5, 200)
    vy = np.linspace(-2.0, -0.5, 200)

    # OPX measures DC current at the specified contact
    v_data, i_data = device.sweep_2d(
        gate_x, vx.tolist(),
        gate_y, vy.tolist(),
        contact,  # Must be a contact configured with OPX measure_channel
        session=session
    )

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

### QSwitch: Digital Breakout Box

QSwitch is a programmable relay matrix for routing signals between instruments and device pads. Each QSwitch channel has 10 relays: relay 0 is ground, relays 1-9 connect to different instruments or buses. This lets you reconfigure measurement setups without manual cable swapping.

**Basic configuration:**

```yaml
gates:
  G1: {type: BARRIER, control_channel: 1, breakout_channel: 1, v_lower_bound: -3.0, v_upper_bound: 3.0}
  G2: {type: PLUNGER, control_channel: 2, breakout_channel: 2, v_lower_bound: -3.0, v_upper_bound: 3.0}

contacts:
  DRAIN: {type: DRAIN, control_channel: 3, measure_channel: 1, breakout_channel: 3, v_lower_bound: -3.0, v_upper_bound: 3.0}

instruments:
  - name: qdac2
    type: GENERAL
    driver: qdac2
    ip_addr: 192.168.1.100
    port: 5025
    slew_rate: 100.0
    breakout_line: 9  # This instrument connects through relay 9

  - name: qswitch
    type: BREAKOUT_BOX
    driver: qswitch
    ip_addr: 192.168.1.102
    port: 5025
```

The `breakout_channel` on each pad maps to a QSwitch channel. The `breakout_line` on each instrument indicates which relay connects to that instrument.

**High-level device operations:**

```python
@routine
def initialize_breakout(ctx):
    device = ctx.resources.device

    # Ground all channels for safety
    device.ground_breakout_lines()

    # Unground channels to enable control
    device.unground_breakout_lines()

    # Connect all pads to their configured instruments
    device.connect_breakout_lines()

    # Disconnect when done
    device.disconnect_breakout_lines()
```

**Direct instrument access for fine-grained control:**

```python
from stanza.utils import device_from_yaml

# Load device and get QSwitch instance
device = device_from_yaml("device.yaml")
qswitch = device.breakout_box_instrument

# Single channel operations
qswitch.set_grounded("G1")              # Connect G1 to ground (relay 0)
qswitch.set_ungrounded("G1")            # Disconnect G1 from ground
qswitch.set_connected("G1", 9)          # Connect G1 to relay 9 (QDAC)
qswitch.set_disconnected("G1", 9)       # Disconnect G1 from relay 9

# Check state
is_grounded = qswitch.get_grounded("G1")
is_connected = qswitch.get_connected("G1", 9)

# Multi-channel operations (faster than looping)
qswitch.set_grounded(["G1", "G2", "DRAIN"])
qswitch.set_connected(["G1", "G2"], 9)

# Query multiple channels - Get the connection status of G1 and G2 to line 9
states = qswitch.get_connected(["G1", "G2"], 9)  # Returns [True, False]
```

**Safety pattern:**

```python
@routine
def safe_measurement(ctx, gate, voltages, contact):
    device = ctx.resources.device
    qswitch = device.breakout_box_instrument

    try:
        # Connect measurement path
        qswitch.set_ungrounded([gate, contact])
        device.connect_breakout_lines()

        # Run measurement
        v_data, i_data = device.sweep_1d(gate, voltages, contact)

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

    finally:
        # Set all pads to zero volts
        device.zero()
        # Always ground channels when done
        device.disconnect_breakout_lines()
        device.ground_breakout_lines()
```

**Key methods:**

* `set_grounded(channel)` / `set_ungrounded(channel)`: Control relay 0 (ground)
* `set_connected(channel, line)` / `set_disconnected(channel, line)`: Control specific relay
* `get_grounded(channel)` / `get_connected(channel, line)`: Query relay state
* All methods accept single channel strings or lists for batch operations

The QSwitch uses SCPI commands over TCP/IP. Each channel-relay pair is addressed as `channel!relay` in SCPI notation (e.g., `1!9` for channel 1, relay 9).

## Testing Without Hardware

Enable simulation mode to develop code before hardware access:

```yaml
instruments:
  - name: qdac-sim
    type: GENERAL
    driver: qdac2
    ip_addr: 127.0.0.1
    is_simulation: true
    sim_file: "./sim_config.yaml"
```

Create `sim_config.yaml` to define simulated responses:

```yaml
# Simulation configuration
gates:
  LEFT_BARRIER:
    pinchoff_voltage: -0.85
    leakage_current: 1e-13

  RIGHT_BARRIER:
    pinchoff_voltage: -0.92
    leakage_current: 2e-13

contacts:
  DRAIN:
    noise_level: 1e-12
```

Now your code runs without hardware, using these simulated characteristics.

## Writing a Custom Driver

Need to support custom hardware? Here's how to write a driver.

### Step 1: Understand What You Need

Every driver implements one or both protocols:

* **ControlInstrument**: For setting/reading voltages
* **MeasurementInstrument**: For measuring currents

### Step 2: Create Your Driver File

Create `stanza/drivers/mycustom.py`:

```python
from stanza.base.instruments import BaseInstrument
from stanza.base.channels import ControlChannel, MeasurementChannel, ChannelConfig
import socket

class MyCustomDriver(BaseInstrument):
    """Driver for my custom instrument."""

    def __init__(self, name: str, ip_addr: str, port: int = 5025,
                 slew_rate: float = 1.0):
        super().__init__(name=name)

        self.ip_addr = ip_addr
        self.port = port
        self.default_slew_rate = slew_rate

        # Connect to instrument
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((ip_addr, port))

    def set_voltage(self, pad: str, voltage: float) -> None:
        """Set voltage on a pad."""
        channel = self.get_channel(pad)

        # Validate bounds
        v_min, v_max = channel.config.voltage_range
        if not (v_min <= voltage <= v_max):
            raise ValueError(f"Voltage {voltage} out of bounds [{v_min}, {v_max}]")

        # Send to hardware
        cmd = f"SOUR{channel.control_channel}:VOLT {voltage}\n"
        self.sock.sendall(cmd.encode())

        # Update parameter
        channel.set_parameter("voltage", voltage)

    def get_voltage(self, pad: str) -> float:
        """Get current voltage on a pad."""
        channel = self.get_channel(pad)

        # Query hardware
        cmd = f"SOUR{channel.control_channel}:VOLT?\n"
        self.sock.sendall(cmd.encode())
        response = self.sock.recv(1024).decode().strip()

        voltage = float(response)
        channel.set_parameter("voltage", voltage)

        return voltage

    def get_slew_rate(self, pad: str) -> float:
        """Get slew rate for a pad."""
        channel = self.get_channel(pad)
        return channel.get_parameter_value("slew_rate")

    def measure(self, pad: str) -> float:
        """Measure current at a pad."""
        channel = self.get_channel(pad)

        # Trigger measurement
        cmd = f"MEAS{channel.measure_channel}:CURR\n"
        self.sock.sendall(cmd.encode())
        response = self.sock.recv(1024).decode().strip()

        return float(response)

    def close(self) -> None:
        """Close connection."""
        self.sock.close()
```

### Step 3: Use Your Driver

Add it to your device configuration:

```yaml
instruments:
  - name: my-instrument
    type: GENERAL
    driver: mycustom  # Matches filename (mycustom.py)
    ip_addr: 192.168.1.200
    port: 5025
    slew_rate: 0.5
```

Stanza automatically discovers and instantiates your driver.

## Example: Simple Serial Instrument

Here's a complete driver for a serial DAC:

```python
from stanza.base.instruments import BaseControlInstrument
import serial

class SerialDAC(BaseControlInstrument):
    """Driver for a simple serial DAC."""

    def __init__(self, name: str, port: str, baudrate: int = 9600,
                 slew_rate: float = 1.0):
        super().__init__(name=name)

        self.default_slew_rate = slew_rate

        # Open serial port
        self.serial = serial.Serial(
            port=port,
            baudrate=baudrate,
            timeout=1.0
        )

    def set_voltage(self, pad: str, voltage: float) -> None:
        """Set voltage on a channel."""
        channel = self.get_channel(pad)

        # Validate
        v_min, v_max = channel.config.voltage_range
        if not (v_min <= voltage <= v_max):
            raise ValueError(f"Voltage out of bounds")

        # Send command (example protocol)
        cmd = f"SET {channel.control_channel} {voltage}\n"
        self.serial.write(cmd.encode())

        # Wait for acknowledgment
        response = self.serial.readline().decode().strip()
        if response != "OK":
            raise RuntimeError(f"DAC error: {response}")

        channel.set_parameter("voltage", voltage)

    def get_voltage(self, pad: str) -> float:
        """Read voltage from a channel."""
        channel = self.get_channel(pad)

        cmd = f"GET {channel.control_channel}\n"
        self.serial.write(cmd.encode())

        response = self.serial.readline().decode().strip()
        voltage = float(response)

        channel.set_parameter("voltage", voltage)
        return voltage

    def get_slew_rate(self, pad: str) -> float:
        """Get slew rate."""
        return self.default_slew_rate

    def close(self) -> None:
        """Close serial port."""
        self.serial.close()
```

Use it:

```yaml
instruments:
  - name: serial-dac
    type: CONTROL
    driver: serial_dac  # Filename: serial_dac.py
    port: "/dev/ttyUSB0"
    baudrate: 115200
    slew_rate: 0.5
```

## How Sweeps Work

When you call `device.sweep_1d()`, here's what happens:

1. **Stanza generates voltage points** from your start/stop/n_points
2. **For each voltage:**
   * Calls `control_instrument.set_voltage(gate, voltage)`
   * Waits for voltage to settle (based on slew rate)
   * Calls `measurement_instrument.measure(contact)`
3. **Returns arrays** of voltages and measurements
4. **If session provided**, logs the data automatically

Your driver just needs to implement `set_voltage()` and `measure()`. Stanza handles the rest.

## Driver Best Practices

**Validate inputs**: Check voltage bounds before sending to hardware.

```python
def set_voltage(self, pad: str, voltage: float) -> None:
    channel = self.get_channel(pad)
    v_min, v_max = channel.config.voltage_range

    if not (v_min <= voltage <= v_max):
        raise ValueError(f"Voltage {voltage} out of range [{v_min}, {v_max}]")

    # Now safe to send to hardware
    ...
```

**Handle errors gracefully**: Catch and re-raise with context.

```python
from stanza.exceptions import InstrumentError

def measure(self, pad: str) -> float:
    try:
        # Talk to hardware
        ...
    except socket.timeout:
        raise InstrumentError(f"Timeout measuring {pad}")
    except Exception as e:
        raise InstrumentError(f"Error measuring {pad}: {e}")
```

**Implement close()**: Clean up resources.

```python
def close(self) -> None:
    """Clean up connections."""
    if hasattr(self, 'sock') and self.sock:
        self.sock.close()
    if hasattr(self, 'serial') and self.serial:
        self.serial.close()
```

**Support simulation**: Add a simulation flag for testing.

```python
def __init__(self, name: str, ip_addr: str, is_simulation: bool = False):
    super().__init__(name=name)

    self.is_simulation = is_simulation

    if not is_simulation:
        # Connect to real hardware
        self.sock = socket.socket(...)
    else:
        # Simulation mode
        self.simulated_voltages = {}

def set_voltage(self, pad: str, voltage: float) -> None:
    if self.is_simulation:
        self.simulated_voltages[pad] = voltage
    else:
        # Real hardware command
        ...
```

**Add logging for debugging**: Use Python's logging module.

```python
import logging

logger = logging.getLogger(__name__)

def set_voltage(self, pad: str, voltage: float) -> None:
    logger.debug(f"Setting {pad} to {voltage:.3f} V")
    ...
    logger.debug(f"Set complete")
```

## Testing Your Driver

Write tests to verify your driver works:

```python
import pytest
from stanza.drivers.mycustom import MyCustomDriver
from stanza.base.channels import ControlChannel, ChannelConfig

def test_set_get_voltage():
    """Test voltage setting and reading."""
    driver = MyCustomDriver(
        name="test",
        ip_addr="127.0.0.1",
        is_simulation=True
    )

    # Add a channel
    config = ChannelConfig(
        name="G1",
        voltage_range=(-3.0, 3.0),
        control_channel=1
    )
    channel = ControlChannel(name="G1", config=config, control_channel=1)
    driver.add_channel("G1", channel)

    # Test
    driver.set_voltage("G1", -1.5)
    voltage = driver.get_voltage("G1")

    assert voltage == pytest.approx(-1.5)
    driver.close()
```

Test with actual device configuration:

```python
from stanza.utils import device_from_yaml

def test_driver_in_device():
    """Test driver with full device configuration."""
    device = device_from_yaml("test_device.yaml")

    # Test jump
    device.jump({"G1": -1.5, "G2": -0.8})

    # Test check
    v = device.check("G1")
    assert v == pytest.approx(-1.5)

    # Test measure
    i = device.measure("DRAIN")
    assert isinstance(i, float)

    # Test sweep
    voltages = [-2.0, -1.0, 0.0]
    v_data, i_data = device.sweep_1d("G1", voltages, "DRAIN")
    assert len(v_data) == 3
    assert len(i_data) == 3
```

## Troubleshooting

**"Cannot connect to instrument"**: Check IP address and network connection.

```python
import socket

# Test if you can reach the instrument
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2.0)
try:
    sock.connect(("192.168.1.100", 5025))
    print("Connection successful")
except socket.timeout:
    print("Connection timeout - check IP address")
except socket.error as e:
    print(f"Connection failed: {e}")
finally:
    sock.close()
```

**"Voltage out of bounds"**: Check your device configuration voltage limits match your hardware.

**"No response from instrument"**: Verify your command syntax matches the instrument's protocol. Check the manual or use the instrument's native software to test commands.

**"Channel not found"**: Make sure channel numbers in your YAML match the driver's expectations.

## Next Steps

* [Device Configuration](/control/stanza/core-concepts/device-configuration) - Configure instruments in your device YAML
* [Routines](/control/stanza/core-concepts/routines/overview) - Use devices with drivers in your measurements
* [Data Logging](/control/stanza/core-concepts/data-logging) - Automatically log data from driver measurements