Device Configuration

Think of device configuration as your lab notebook for defining a quantum device. Instead of hardcoding voltages and channel numbers in your Python scripts, you describe your device topology once in a YAML file. This makes it easy to work with different devices, share configurations with collaborators, and keep your experimental code clean.

Your First Device Configuration

Let’s configure a simple single quantum dot device. Create a file called device.yaml:

1name: "Single Quantum Dot"
2
3gates:
4 PLUNGER:
5 type: PLUNGER
6 control_channel: 1
7 v_lower_bound: -2.0
8 v_upper_bound: 0.0
9
10 BARRIER:
11 type: BARRIER
12 control_channel: 2
13 v_lower_bound: -1.5
14 v_upper_bound: 0.0
15
16contacts:
17 SOURCE:
18 type: SOURCE
19 measure_channel: 1
20 control_channel: 3
21 v_lower_bound: -0.01
22 v_upper_bound: 0.01
23
24 DRAIN:
25 type: DRAIN
26 measure_channel: 2
27 control_channel: 4
28 v_lower_bound: -0.01
29 v_upper_bound: 0.01
30
31instruments:
32 - name: qdac
33 type: GENERAL
34 driver: qdac2
35 ip_addr: 192.168.1.100
36 slew_rate: 0.5
37
38routines:
39 - name: pinchoff_sweep
40 parameters:
41 gate: BARRIER
42 v_start: -1.5
43 v_stop: 0.0
44 n_points: 100
45 contact: DRAIN

Now you can control your device using gate names instead of channel numbers:

1from stanza.utils import device_from_yaml
2
3device = device_from_yaml("device.yaml")
4
5# Set voltages using descriptive names
6device.jump({"PLUNGER": -1.2, "BARRIER": -0.8})
7
8# Measure current
9current = device.measure("DRAIN")

Understanding Gates

Gates control the electrostatic landscape of your device. Stanza supports four gate types:

  • PLUNGER: Controls charge occupation in quantum dots
  • BARRIER: Controls tunnel coupling between regions
  • RESERVOIR: Connects to electron reservoirs
  • SCREEN: Provides electrostatic screening

Example: Double Quantum Dot

Here’s how you might configure a double quantum dot device:

1gates:
2 # Plunger gates control chemical potential
3 LEFT_PLUNGER:
4 type: PLUNGER
5 control_channel: 1
6 v_lower_bound: -2.0
7 v_upper_bound: 0.0
8
9 RIGHT_PLUNGER:
10 type: PLUNGER
11 control_channel: 2
12 v_lower_bound: -2.0
13 v_upper_bound: 0.0
14
15 # Barrier gates control tunnel coupling
16 LEFT_BARRIER:
17 type: BARRIER
18 control_channel: 3
19 v_lower_bound: -1.5
20 v_upper_bound: 0.0
21
22 CENTER_BARRIER:
23 type: BARRIER
24 control_channel: 4
25 v_lower_bound: -1.5
26 v_upper_bound: 0.0
27
28 RIGHT_BARRIER:
29 type: BARRIER
30 control_channel: 5
31 v_lower_bound: -1.5
32 v_upper_bound: 0.0

The voltage bounds act as safety limits - if your code tries to exceed them, Stanza raises an error before touching the hardware.

Working with Gate Groups

Filter gates by type for common operations:

1# Get all plunger gates
2plungers = device.get_gates_by_type("PLUNGER")
3# ["LEFT_PLUNGER", "RIGHT_PLUNGER"]
4
5# Sweep all barriers together
6barriers = device.get_gates_by_type("BARRIER")
7voltages = [-1.5, -1.0, -0.5, 0.0]
8device.sweep_nd(barriers, voltages, contact="DRAIN")

Contacts: Where You Measure

Contacts are where current flows in and out of your device. Stanza supports SOURCE and DRAIN contact types for DC transport measurements.

Example: DC Transport with Bias

Configure contacts that can both measure current and apply bias voltage:

1contacts:
2 LEFT_RESERVOIR:
3 type: SOURCE
4 measure_channel: 1
5 control_channel: 6
6 v_lower_bound: -0.01 # 10 mV bias range
7 v_upper_bound: 0.01
8
9 RIGHT_RESERVOIR:
10 type: DRAIN
11 measure_channel: 2
12 control_channel: 7
13 v_lower_bound: -0.01
14 v_upper_bound: 0.01

Sweep bias voltage to measure Coulomb diamonds:

1# Apply 10 mV bias
2device.jump({"LEFT_RESERVOIR": 0.005, "RIGHT_RESERVOIR": -0.005})
3
4# Measure conductance
5current = device.measure("RIGHT_RESERVOIR")
6conductance = current / 0.010

Example: Measurement-Only Contact

For contacts that only measure without voltage control:

1contacts:
2 LEFT_RESERVOIR:
3 type: SOURCE
4 measure_channel: 1
5 control_channel: 6
6 v_lower_bound: -0.01
7 v_upper_bound: 0.01
8
9 RIGHT_RESERVOIR:
10 type: DRAIN
11 measure_channel: 2
12 control_channel: 7
13 v_lower_bound: -0.01
14 v_upper_bound: 0.01
15
16 SENSOR_DRAIN:
17 type: DRAIN
18 measure_channel: 3 # No voltage control needed

Use measurement-only contacts for fast current measurements during sweeps:

1import numpy as np
2
3# Sweep and measure at sensor
4voltages = np.linspace(-2.0, 0.0, 100)
5v_data, i_data = device.sweep_1d(
6 "LEFT_PLUNGER",
7 voltages.tolist(),
8 "SENSOR_DRAIN"
9)

Connecting to Hardware

Instruments bridge your device configuration and actual hardware. The most common setup uses a QDAC-II for voltage control and current measurement.

Example: Simple Setup

Use a single QDAC-II for everything:

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 # V/s
7 measurement_duration: 10e-3 # 10 ms
8 sample_time: 100e-6 # 100 μs

Example: Separate Control and Measurement

For more flexibility, split control and measurement:

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 # QDAC for DC current measurement
10 - name: qdac-measure
11 type: MEASUREMENT
12 driver: qdac2
13 ip_addr: 192.168.1.100
14 measurement_duration: 10e-3
15 sample_time: 100e-6
16 current_range: "LOW" # For pA-nA currents

The device automatically routes operations to the appropriate instrument based on control/measure channels.

Testing Without Hardware

Enable simulation mode for developing routines 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"

Pre-configuring Routine Parameters

The routines section lets you pre-configure parameters for common measurements:

1routines:
2 - name: barrier_sweep
3 parameters:
4 gate: LEFT_BARRIER
5 v_start: -1.5
6 v_stop: 0.0
7 n_points: 100
8 contact: DRAIN
9
10 - name: charge_stability
11 parameters:
12 gate_x: LEFT_PLUNGER
13 gate_y: RIGHT_PLUNGER
14 v_x_start: -2.0
15 v_x_stop: -0.5
16 v_y_start: -2.0
17 v_y_stop: -0.5
18 n_points_x: 100
19 n_points_y: 100
20 contact: DRAIN

Run routines without repeating parameters:

1from stanza.routines import RoutineRunner
2from stanza.models import DeviceConfig
3
4config = DeviceConfig.from_yaml("device.yaml")
5runner = RoutineRunner(configs=[config])
6
7# Parameters come from YAML
8runner.run("barrier_sweep")
9
10# Override parameters at runtime
11runner.run("barrier_sweep", v_start=-2.0, n_points=200)

Organizing Complex Workflows

Use nested routines to organize multi-step tune-ups:

1routines:
2 - name: full_tuneup
3 parameters:
4 threshold: 1e-9
5 routines:
6 - name: leakage_test
7 parameters:
8 gates: [LEFT_BARRIER, CENTER_BARRIER, RIGHT_BARRIER]
9 contact: DRAIN
10
11 - name: pinchoff_all
12 parameters:
13 gates: [LEFT_BARRIER, CENTER_BARRIER, RIGHT_BARRIER]
14 v_start: -1.5
15 v_stop: 0.0
16 n_points: 50
17
18 - name: charge_stability
19 parameters:
20 gate_x: LEFT_PLUNGER
21 gate_y: RIGHT_PLUNGER

Run the entire workflow:

1# Execute all nested routines
2results = runner.run_all(parent_routine="full_tuneup")

Complete Example

Here’s a full configuration for a double quantum dot device:

1name: "Double Quantum Dot - Device 42"
2
3gates:
4 LP:
5 type: PLUNGER
6 control_channel: 1
7 v_lower_bound: -3.0
8 v_upper_bound: 0.0
9
10 RP:
11 type: PLUNGER
12 control_channel: 2
13 v_lower_bound: -3.0
14 v_upper_bound: 0.0
15
16 LB:
17 type: BARRIER
18 control_channel: 3
19 v_lower_bound: -2.0
20 v_upper_bound: 0.0
21
22 CB:
23 type: BARRIER
24 control_channel: 4
25 v_lower_bound: -2.0
26 v_upper_bound: 0.0
27
28 RB:
29 type: BARRIER
30 control_channel: 5
31 v_lower_bound: -2.0
32 v_upper_bound: 0.0
33
34contacts:
35 LEFT:
36 type: SOURCE
37 control_channel: 6
38 measure_channel: 1
39 v_lower_bound: -0.01
40 v_upper_bound: 0.01
41
42 RIGHT:
43 type: DRAIN
44 control_channel: 7
45 measure_channel: 2
46 v_lower_bound: -0.01
47 v_upper_bound: 0.01
48
49routines:
50 - name: quick_pinchoff
51 parameters:
52 gate: LB
53 v_start: -2.0
54 v_stop: 0.0
55 n_points: 100
56 contact: RIGHT
57
58 - name: charge_diagram
59 parameters:
60 gate_x: LP
61 gate_y: RP
62 v_x_start: -2.5
63 v_x_stop: -0.5
64 v_y_start: -2.5
65 v_y_stop: -0.5
66 n_points_x: 150
67 n_points_y: 150
68 contact: RIGHT
69
70instruments:
71 - name: qdac-control
72 type: CONTROL
73 driver: qdac2
74 ip_addr: 192.168.1.100
75 slew_rate: 0.5
76
77 - name: qdac-measure
78 type: MEASUREMENT
79 driver: qdac2
80 ip_addr: 192.168.1.100
81 measurement_duration: 10e-3
82 sample_time: 100e-6
83 current_range: "LOW"

Use it in your code:

1from stanza.utils import device_from_yaml
2from stanza.routines import RoutineRunner
3from stanza.models import DeviceConfig
4
5# Direct device usage
6device = device_from_yaml("device.yaml")
7device.jump({"LP": -1.5, "RP": -1.2})
8current = device.measure("RIGHT")
9
10# Or use with routines
11config = DeviceConfig.from_yaml("device.yaml")
12runner = RoutineRunner(configs=[config])
13runner.run("quick_pinchoff")
14runner.run("charge_diagram")

Tips and Best Practices

Use descriptive names: Names like LEFT_PLUNGER and RIGHT_BARRIER are clearer than G1 and G2.

Start with conservative voltage bounds: You can always widen them later. Better safe than sorry.

Test with simulation first: Use is_simulation: true to verify your configuration before connecting to hardware.

One config per device: Don’t try to reuse configurations across different physical devices. Channel mappings will differ.

Version control your configs: Keep them in git alongside your routines. Document what changed and why.

Comment your YAML: Future you will thank you for explaining why certain parameters were chosen.

Next Steps

  • Routines - Learn how to write measurement routines using your configured device
  • Drivers - Understand how instruments communicate with hardware
  • Data Logging - Automatically log data from your measurements