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:

1instruments:
2 - name: qdac-control
3 type: CONTROL
4 driver: qdac2
5 ip_addr: 192.168.1.100
6 slew_rate: 0.5 # V/s - safe ramp rate

Adding current measurement:

1instruments:
2 - name: qdac-control
3 type: CONTROL
4 driver: qdac2
5 ip_addr: 192.168.1.100
6 slew_rate: 0.5
7
8 - name: qdac-measure
9 type: MEASUREMENT
10 driver: qdac2
11 ip_addr: 192.168.1.100
12 measurement_duration: 10e-3 # 10 ms integration
13 sample_time: 100e-6 # 100 μs per sample
14 current_range: "LOW" # For pA-nA currents

Or use one instrument for both:

1instruments:
2 - name: qdac
3 type: GENERAL # Both control and measurement
4 driver: qdac2
5 ip_addr: 192.168.1.100
6 slew_rate: 0.5
7 measurement_duration: 10e-3
8 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:

1instruments:
2 # QDAC for voltage control
3 - name: qdac-control
4 type: CONTROL
5 driver: qdac2
6 ip_addr: 192.168.1.100
7 slew_rate: 0.5
8
9 # OPX for DC current measurement
10 - name: opx
11 type: MEASUREMENT
12 driver: opx
13 ip_addr: 192.168.1.200
14 measurement_duration: 1e-3
15 sample_time: 100e-6
16 measurement_channels: [1, 2]
17 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:

1@routine
2def fast_charge_diagram(ctx, gate_x, gate_y, contact, session=None):
3 """Fast charge diagram with OPX current measurement."""
4 device = ctx.resources.device
5
6 vx = np.linspace(-2.0, -0.5, 200)
7 vy = np.linspace(-2.0, -0.5, 200)
8
9 # OPX measures DC current at the specified contact
10 v_data, i_data = device.sweep_2d(
11 gate_x, vx.tolist(),
12 gate_y, vy.tolist(),
13 contact, # Must be a contact configured with OPX measure_channel
14 session=session
15 )
16
17 return {"voltages": v_data, "currents": i_data}

Testing Without Hardware

Enable simulation mode to develop code before hardware access:

1instruments:
2 - name: qdac-sim
3 type: GENERAL
4 driver: qdac2
5 ip_addr: 127.0.0.1
6 is_simulation: true
7 sim_file: "./sim_config.yaml"

Create sim_config.yaml to define simulated responses:

1# Simulation configuration
2gates:
3 LEFT_BARRIER:
4 pinchoff_voltage: -0.85
5 leakage_current: 1e-13
6
7 RIGHT_BARRIER:
8 pinchoff_voltage: -0.92
9 leakage_current: 2e-13
10
11contacts:
12 DRAIN:
13 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:

1from stanza.base.instruments import BaseInstrument
2from stanza.base.channels import ControlChannel, MeasurementChannel, ChannelConfig
3import socket
4
5class MyCustomDriver(BaseInstrument):
6 """Driver for my custom instrument."""
7
8 def __init__(self, name: str, ip_addr: str, port: int = 5025,
9 slew_rate: float = 1.0):
10 super().__init__(name=name)
11
12 self.ip_addr = ip_addr
13 self.port = port
14 self.default_slew_rate = slew_rate
15
16 # Connect to instrument
17 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
18 self.sock.connect((ip_addr, port))
19
20 def set_voltage(self, pad: str, voltage: float) -> None:
21 """Set voltage on a pad."""
22 channel = self.get_channel(pad)
23
24 # Validate bounds
25 v_min, v_max = channel.config.voltage_range
26 if not (v_min <= voltage <= v_max):
27 raise ValueError(f"Voltage {voltage} out of bounds [{v_min}, {v_max}]")
28
29 # Send to hardware
30 cmd = f"SOUR{channel.control_channel}:VOLT {voltage}\n"
31 self.sock.sendall(cmd.encode())
32
33 # Update parameter
34 channel.set_parameter("voltage", voltage)
35
36 def get_voltage(self, pad: str) -> float:
37 """Get current voltage on a pad."""
38 channel = self.get_channel(pad)
39
40 # Query hardware
41 cmd = f"SOUR{channel.control_channel}:VOLT?\n"
42 self.sock.sendall(cmd.encode())
43 response = self.sock.recv(1024).decode().strip()
44
45 voltage = float(response)
46 channel.set_parameter("voltage", voltage)
47
48 return voltage
49
50 def get_slew_rate(self, pad: str) -> float:
51 """Get slew rate for a pad."""
52 channel = self.get_channel(pad)
53 return channel.get_parameter_value("slew_rate")
54
55 def measure(self, pad: str) -> float:
56 """Measure current at a pad."""
57 channel = self.get_channel(pad)
58
59 # Trigger measurement
60 cmd = f"MEAS{channel.measure_channel}:CURR\n"
61 self.sock.sendall(cmd.encode())
62 response = self.sock.recv(1024).decode().strip()
63
64 return float(response)
65
66 def close(self) -> None:
67 """Close connection."""
68 self.sock.close()

Step 3: Use Your Driver

Add it to your device configuration:

1instruments:
2 - name: my-instrument
3 type: GENERAL
4 driver: mycustom # Matches filename (mycustom.py)
5 ip_addr: 192.168.1.200
6 port: 5025
7 slew_rate: 0.5

Stanza automatically discovers and instantiates your driver.

Example: Simple Serial Instrument

Here’s a complete driver for a serial DAC:

1from stanza.base.instruments import BaseControlInstrument
2import serial
3
4class SerialDAC(BaseControlInstrument):
5 """Driver for a simple serial DAC."""
6
7 def __init__(self, name: str, port: str, baudrate: int = 9600,
8 slew_rate: float = 1.0):
9 super().__init__(name=name)
10
11 self.default_slew_rate = slew_rate
12
13 # Open serial port
14 self.serial = serial.Serial(
15 port=port,
16 baudrate=baudrate,
17 timeout=1.0
18 )
19
20 def set_voltage(self, pad: str, voltage: float) -> None:
21 """Set voltage on a channel."""
22 channel = self.get_channel(pad)
23
24 # Validate
25 v_min, v_max = channel.config.voltage_range
26 if not (v_min <= voltage <= v_max):
27 raise ValueError(f"Voltage out of bounds")
28
29 # Send command (example protocol)
30 cmd = f"SET {channel.control_channel} {voltage}\n"
31 self.serial.write(cmd.encode())
32
33 # Wait for acknowledgment
34 response = self.serial.readline().decode().strip()
35 if response != "OK":
36 raise RuntimeError(f"DAC error: {response}")
37
38 channel.set_parameter("voltage", voltage)
39
40 def get_voltage(self, pad: str) -> float:
41 """Read voltage from a channel."""
42 channel = self.get_channel(pad)
43
44 cmd = f"GET {channel.control_channel}\n"
45 self.serial.write(cmd.encode())
46
47 response = self.serial.readline().decode().strip()
48 voltage = float(response)
49
50 channel.set_parameter("voltage", voltage)
51 return voltage
52
53 def get_slew_rate(self, pad: str) -> float:
54 """Get slew rate."""
55 return self.default_slew_rate
56
57 def close(self) -> None:
58 """Close serial port."""
59 self.serial.close()

Use it:

1instruments:
2 - name: serial-dac
3 type: CONTROL
4 driver: serial_dac # Filename: serial_dac.py
5 port: "/dev/ttyUSB0"
6 baudrate: 115200
7 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.

1def set_voltage(self, pad: str, voltage: float) -> None:
2 channel = self.get_channel(pad)
3 v_min, v_max = channel.config.voltage_range
4
5 if not (v_min <= voltage <= v_max):
6 raise ValueError(f"Voltage {voltage} out of range [{v_min}, {v_max}]")
7
8 # Now safe to send to hardware
9 ...

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

1from stanza.exceptions import InstrumentError
2
3def measure(self, pad: str) -> float:
4 try:
5 # Talk to hardware
6 ...
7 except socket.timeout:
8 raise InstrumentError(f"Timeout measuring {pad}")
9 except Exception as e:
10 raise InstrumentError(f"Error measuring {pad}: {e}")

Implement close(): Clean up resources.

1def close(self) -> None:
2 """Clean up connections."""
3 if hasattr(self, 'sock') and self.sock:
4 self.sock.close()
5 if hasattr(self, 'serial') and self.serial:
6 self.serial.close()

Support simulation: Add a simulation flag for testing.

1def __init__(self, name: str, ip_addr: str, is_simulation: bool = False):
2 super().__init__(name=name)
3
4 self.is_simulation = is_simulation
5
6 if not is_simulation:
7 # Connect to real hardware
8 self.sock = socket.socket(...)
9 else:
10 # Simulation mode
11 self.simulated_voltages = {}
12
13def set_voltage(self, pad: str, voltage: float) -> None:
14 if self.is_simulation:
15 self.simulated_voltages[pad] = voltage
16 else:
17 # Real hardware command
18 ...

Add logging for debugging: Use Python’s logging module.

1import logging
2
3logger = logging.getLogger(__name__)
4
5def set_voltage(self, pad: str, voltage: float) -> None:
6 logger.debug(f"Setting {pad} to {voltage:.3f} V")
7 ...
8 logger.debug(f"Set complete")

Testing Your Driver

Write tests to verify your driver works:

1import pytest
2from stanza.drivers.mycustom import MyCustomDriver
3from stanza.base.channels import ControlChannel, ChannelConfig
4
5def test_set_get_voltage():
6 """Test voltage setting and reading."""
7 driver = MyCustomDriver(
8 name="test",
9 ip_addr="127.0.0.1",
10 is_simulation=True
11 )
12
13 # Add a channel
14 config = ChannelConfig(
15 name="G1",
16 voltage_range=(-3.0, 3.0),
17 control_channel=1
18 )
19 channel = ControlChannel(name="G1", config=config, control_channel=1)
20 driver.add_channel("G1", channel)
21
22 # Test
23 driver.set_voltage("G1", -1.5)
24 voltage = driver.get_voltage("G1")
25
26 assert voltage == pytest.approx(-1.5)
27 driver.close()

Test with actual device configuration:

1from stanza.utils import device_from_yaml
2
3def test_driver_in_device():
4 """Test driver with full device configuration."""
5 device = device_from_yaml("test_device.yaml")
6
7 # Test jump
8 device.jump({"G1": -1.5, "G2": -0.8})
9
10 # Test check
11 v = device.check("G1")
12 assert v == pytest.approx(-1.5)
13
14 # Test measure
15 i = device.measure("DRAIN")
16 assert isinstance(i, float)
17
18 # Test sweep
19 voltages = [-2.0, -1.0, 0.0]
20 v_data, i_data = device.sweep_1d("G1", voltages, "DRAIN")
21 assert len(v_data) == 3
22 assert len(i_data) == 3

Troubleshooting

“Cannot connect to instrument”: Check IP address and network connection.

1import socket
2
3# Test if you can reach the instrument
4sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
5sock.settimeout(2.0)
6try:
7 sock.connect(("192.168.1.100", 5025))
8 print("Connection successful")
9except socket.timeout:
10 print("Connection timeout - check IP address")
11except socket.error as e:
12 print(f"Connection failed: {e}")
13finally:
14 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