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:

1import numpy as np
2from stanza.routines import routine
3
4@routine
5def pinchoff_sweep(ctx, gate, v_start, v_stop, n_points, contact, session=None):
6 """Sweep a gate to find the pinchoff voltage."""
7 device = ctx.resources.device
8
9 # Generate voltage points
10 voltages = np.linspace(v_start, v_stop, n_points)
11
12 # Sweep and measure (automatically logged if session provided)
13 v_data, i_data = device.sweep_1d(
14 gate,
15 voltages.tolist(),
16 contact,
17 session=session
18 )
19
20 # Find pinchoff (where current drops below threshold)
21 threshold = 1e-12 # 1 pA
22 pinchoff_idx = np.where(np.abs(i_data) < threshold)[0]
23 pinchoff_voltage = v_data[pinchoff_idx[0]] if len(pinchoff_idx) > 0 else None
24
25 return {
26 "voltages": v_data,
27 "currents": i_data,
28 "pinchoff_voltage": pinchoff_voltage
29 }

Run it:

1from stanza.routines import RoutineRunner
2from stanza.models import DeviceConfig
3
4config = DeviceConfig.from_yaml("device.yaml")
5runner = RoutineRunner(configs=[config])
6
7result = runner.run(
8 "pinchoff_sweep",
9 gate="LEFT_BARRIER",
10 v_start=-2.0,
11 v_stop=0.0,
12 n_points=100,
13 contact="DRAIN"
14)
15
16print(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

1@routine
2def check_voltages(ctx):
3 """Read current voltage on all gates."""
4 device = ctx.resources.device
5
6 voltages = {}
7 for gate in device.gates:
8 voltages[gate] = device.check(gate)
9
10 return {"voltages": voltages}

Using Previous Results

Routines can access results from earlier routines:

1@routine
2def find_pinchoff(ctx, gate, contact):
3 """Find pinchoff voltage."""
4 device = ctx.resources.device
5
6 voltages = np.linspace(-2.0, 0.0, 100)
7 v_data, i_data = device.sweep_1d(gate, voltages.tolist(), contact)
8
9 # Find where current drops below 1 pA
10 threshold_idx = np.where(np.abs(i_data) < 1e-12)[0]
11 pinchoff = v_data[threshold_idx[0]] if len(threshold_idx) > 0 else None
12
13 return {"gate": gate, "pinchoff_voltage": pinchoff}
14
15@routine
16def set_to_pinchoff(ctx, gate):
17 """Set gate to its pinchoff voltage."""
18 device = ctx.resources.device
19
20 # Get result from find_pinchoff routine
21 pinchoff_result = ctx.results.get("find_pinchoff")
22
23 if pinchoff_result and pinchoff_result["pinchoff_voltage"]:
24 voltage = pinchoff_result["pinchoff_voltage"]
25 device.jump({gate: voltage})
26 return {"status": "success", "voltage": voltage}
27 else:
28 return {"status": "failed", "reason": "no pinchoff found"}

Run them in sequence:

1runner.run("find_pinchoff", gate="LEFT_BARRIER", contact="DRAIN")
2runner.run("set_to_pinchoff", gate="LEFT_BARRIER")

Pre-configuring Parameters

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

1routines:
2 - name: pinchoff_sweep
3 parameters:
4 gate: LEFT_BARRIER
5 v_start: -2.0
6 v_stop: 0.0
7 n_points: 100
8 contact: DRAIN

Now run without arguments:

1# Uses parameters from YAML
2runner.run("pinchoff_sweep")
3
4# Override specific parameters
5runner.run("pinchoff_sweep", n_points=200)

Example: Building a 2D Charge Stability Diagram

1@routine
2def charge_stability(ctx, gate_x, gate_y, v_x_start, v_x_stop, n_x,
3 v_y_start, v_y_stop, n_y, contact, session=None):
4 """Measure a 2D charge stability diagram."""
5 device = ctx.resources.device
6
7 # Create voltage arrays
8 v_x = np.linspace(v_x_start, v_x_stop, n_x)
9 v_y = np.linspace(v_y_start, v_y_stop, n_y)
10
11 # 2D sweep with automatic logging
12 v_data, signal = device.sweep_2d(
13 gate_x, v_x.tolist(),
14 gate_y, v_y.tolist(),
15 contact,
16 session=session
17 )
18
19 # Analyze: find charge transitions
20 grad_x = np.gradient(signal, axis=0)
21 grad_y = np.gradient(signal, axis=1)
22 edges = np.sqrt(grad_x**2 + grad_y**2)
23
24 return {
25 "voltages_x": v_data[0],
26 "voltages_y": v_data[1],
27 "signal": signal,
28 "edges": edges
29 }

Configure in YAML:

1routines:
2 - name: charge_stability
3 parameters:
4 gate_x: LEFT_PLUNGER
5 gate_y: RIGHT_PLUNGER
6 v_x_start: -2.0
7 v_x_stop: -0.5
8 n_x: 150
9 v_y_start: -2.0
10 v_y_stop: -0.5
11 n_y: 150
12 contact: DRAIN

Run it:

1result = runner.run("charge_stability")
2
3# Plot the diagram
4import matplotlib.pyplot as plt
5
6plt.figure(figsize=(10, 8))
7plt.pcolormesh(
8 result["voltages_x"],
9 result["voltages_y"],
10 result["signal"].T,
11 cmap="RdBu_r"
12)
13plt.xlabel("Left Plunger (V)")
14plt.ylabel("Right Plunger (V)")
15plt.colorbar(label="Signal")
16plt.title("Charge Stability Diagram")
17plt.show()

Example: Iterative Tuning with Feedback

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

1@routine
2def optimize_coupling(ctx, barrier_gate, plunger_gates, target_current,
3 max_iterations=20):
4 """Iteratively tune barrier gate to achieve target interdot coupling."""
5 device = ctx.resources.device
6
7 v_barrier = -1.5 # Starting voltage
8 step_size = 0.05 # Initial step
9
10 for iteration in range(max_iterations):
11 # Set barrier voltage
12 device.jump({barrier_gate: v_barrier})
13
14 # Measure charge diagram to estimate coupling
15 v_plunger = np.linspace(-2.0, -0.5, 50)
16 v_data, i_data = device.sweep_2d(
17 plunger_gates[0], v_plunger.tolist(),
18 plunger_gates[1], v_plunger.tolist(),
19 "DRAIN"
20 )
21
22 # Estimate coupling from gradient
23 gradient = np.gradient(i_data)
24 coupling_strength = np.max(np.abs(gradient))
25
26 # Check if we hit target
27 if np.abs(coupling_strength - target_current) < target_current * 0.1:
28 return {
29 "converged": True,
30 "barrier_voltage": v_barrier,
31 "coupling_strength": coupling_strength,
32 "iterations": iteration + 1
33 }
34
35 # Adjust barrier voltage
36 if coupling_strength < target_current:
37 v_barrier += step_size # Increase coupling
38 else:
39 v_barrier -= step_size # Decrease coupling
40
41 step_size *= 0.9 # Reduce step size
42
43 return {
44 "converged": False,
45 "barrier_voltage": v_barrier,
46 "coupling_strength": coupling_strength,
47 "iterations": max_iterations
48 }

Example: Conditional Workflows

Build intelligent workflows that adapt based on measurements:

1@routine
2def check_leakage(ctx, threshold=1e-9):
3 """Check if leakage current is acceptable."""
4 device = ctx.resources.device
5
6 current = device.measure("DRAIN")
7 passed = np.abs(current) < threshold
8
9 return {"current": current, "passed": passed, "threshold": threshold}
10
11@routine
12def conditional_tuneup(ctx):
13 """Only proceed with tuneup if leakage is acceptable."""
14 # Check if leakage test passed
15 leakage_result = ctx.results.get("check_leakage")
16
17 if not leakage_result or not leakage_result["passed"]:
18 return {
19 "status": "aborted",
20 "reason": f"Leakage too high: {leakage_result['current']:.2e} A"
21 }
22
23 # Proceed with characterization
24 device = ctx.resources.device
25 voltages = np.linspace(-2.0, 0.0, 100)
26 v_data, i_data = device.sweep_1d("LEFT_BARRIER", voltages.tolist(), "DRAIN")
27
28 return {
29 "status": "complete",
30 "voltages": v_data,
31 "currents": i_data
32 }

Run the workflow:

1# First check leakage
2runner.run("check_leakage", threshold=1e-9)
3
4# Then run conditional tuneup (only proceeds if leakage OK)
5result = runner.run("conditional_tuneup")
6
7if result["status"] == "aborted":
8 print(f"Tuneup aborted: {result['reason']}")
9else:
10 print("Tuneup completed successfully")

Organizing Complex Tune-ups

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

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

Run the entire workflow:

1# Run all routines under "full_characterization"
2results = runner.run_all(parent_routine="full_characterization")
3
4# Access individual results
5leakage = results["check_leakage"]
6pinchoffs = results["find_all_pinchoffs"]
7charge_diagram = results["charge_stability"]

Automatic Data Logging

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

1from stanza.logger import DataLogger
2
3logger = DataLogger(
4 name="logger",
5 routine_name="characterization",
6 base_dir="./data",
7 formats=["hdf5", "jsonl"],
8 compression="gzip"
9)
10
11runner = RoutineRunner(configs=[config], logger=logger)
12
13# This automatically logs all sweep data
14runner.run("charge_stability")
15
16# Data saved to: ./data/characterization/<session_id>/

Your routine receives a session parameter automatically:

1@routine
2def custom_measurement(ctx, session=None):
3 """Routine with custom logging."""
4 device = ctx.resources.device
5
6 # Perform measurement
7 data = do_something_custom(device)
8
9 # Manually log specific data
10 if session:
11 session.log_measurement(
12 "custom_data",
13 data={
14 "measured_value": data["value"],
15 "timestamp": datetime.now()
16 },
17 metadata={"algorithm": "custom_v2"}
18 )
19
20 return data

Error Handling

Handle errors gracefully to prevent tune-up interruption:

1from stanza.exceptions import DeviceError, InstrumentError
2
3@routine
4def safe_sweep(ctx, gate, v_start, v_stop, n_points, contact):
5 """Sweep with robust error handling."""
6 device = ctx.resources.device
7
8 try:
9 voltages = np.linspace(v_start, v_stop, n_points)
10 v_data, i_data = device.sweep_1d(gate, voltages.tolist(), contact)
11
12 return {
13 "status": "success",
14 "voltages": v_data,
15 "currents": i_data
16 }
17
18 except InstrumentError as e:
19 # Instrument communication failed
20 return {
21 "status": "instrument_error",
22 "error": str(e),
23 "gate": gate
24 }
25
26 except DeviceError as e:
27 # Device-level error (e.g., voltage out of bounds)
28 return {
29 "status": "device_error",
30 "error": str(e),
31 "gate": gate
32 }
33
34 finally:
35 # Always return to safe voltage
36 device.jump({gate: 0.0})

Complete Example: Automated Tuneup Workflow

Here’s a complete example combining multiple routines:

1import numpy as np
2from stanza.routines import routine, RoutineRunner
3from stanza.models import DeviceConfig
4from stanza.logger import DataLogger
5
6@routine
7def initialize_device(ctx, safe_voltages=None):
8 """Initialize device to safe voltages."""
9 device = ctx.resources.device
10
11 if safe_voltages is None:
12 safe_voltages = {gate: 0.0 for gate in device.gates}
13
14 device.jump(safe_voltages)
15 return {"status": "initialized", "voltages": safe_voltages}
16
17@routine
18def check_leakage(ctx, contact="DRAIN", threshold=1e-9):
19 """Verify leakage is below threshold."""
20 device = ctx.resources.device
21
22 current = device.measure(contact)
23 passed = np.abs(current) < threshold
24
25 return {
26 "current": current,
27 "threshold": threshold,
28 "passed": passed
29 }
30
31@routine
32def find_pinchoff(ctx, gate, v_start, v_stop, n_points, contact,
33 threshold=1e-12, session=None):
34 """Find pinchoff voltage for a gate."""
35 device = ctx.resources.device
36
37 voltages = np.linspace(v_start, v_stop, n_points)
38 v_data, i_data = device.sweep_1d(gate, voltages.tolist(), contact, session=session)
39
40 # Find pinchoff
41 below_threshold = np.where(np.abs(i_data) < threshold)[0]
42 pinchoff_voltage = v_data[below_threshold[0]] if len(below_threshold) > 0 else None
43
44 # Log analysis
45 if session:
46 session.log_analysis(
47 f"{gate}_pinchoff",
48 {
49 "pinchoff_voltage": pinchoff_voltage,
50 "threshold": threshold,
51 "gate": gate
52 }
53 )
54
55 return {
56 "gate": gate,
57 "pinchoff_voltage": pinchoff_voltage,
58 "voltages": v_data,
59 "currents": i_data
60 }
61
62@routine
63def measure_charge_diagram(ctx, gate_x, gate_y, v_x_start, v_x_stop, n_x,
64 v_y_start, v_y_stop, n_y, contact, session=None):
65 """Measure 2D charge stability diagram."""
66 device = ctx.resources.device
67
68 v_x = np.linspace(v_x_start, v_x_stop, n_x)
69 v_y = np.linspace(v_y_start, v_y_stop, n_y)
70
71 v_data, signal = device.sweep_2d(
72 gate_x, v_x.tolist(),
73 gate_y, v_y.tolist(),
74 contact,
75 session=session
76 )
77
78 return {
79 "gate_x": gate_x,
80 "gate_y": gate_y,
81 "voltages_x": v_data[0],
82 "voltages_y": v_data[1],
83 "signal": signal
84 }
85
86# Main execution
87if __name__ == "__main__":
88 # Setup
89 config = DeviceConfig.from_yaml("device.yaml")
90 logger = DataLogger(
91 name="logger",
92 routine_name="automated_tuneup",
93 base_dir="./data",
94 formats=["hdf5", "jsonl"]
95 )
96 runner = RoutineRunner(configs=[config], logger=logger)
97
98 # Execute workflow
99 runner.run("initialize_device")
100
101 leakage = runner.run("check_leakage")
102 if not leakage["passed"]:
103 print(f"Leakage too high: {leakage['current']:.2e} A")
104 exit(1)
105
106 # Find pinchoffs for all barriers
107 for gate in ["LEFT_BARRIER", "CENTER_BARRIER", "RIGHT_BARRIER"]:
108 result = runner.run(
109 "find_pinchoff",
110 gate=gate,
111 v_start=-2.0,
112 v_stop=0.0,
113 n_points=100,
114 contact="DRAIN"
115 )
116 print(f"{gate} pinchoff: {result['pinchoff_voltage']:.3f} V")
117
118 # Measure charge stability diagram
119 diagram = runner.run("measure_charge_diagram")
120
121 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:

1from typing import Dict, List, Optional
2
3@routine
4def my_routine(ctx, gate: str, voltages: List[float]) -> Dict[str, any]:
5 ...

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:

1try:
2 # measurement code
3finally:
4 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 - Learn how to log and retrieve measurement data
  • Device Configuration - Configure your device for use with routines
  • Drivers - Understand how device operations communicate with hardware