Health Check

Stanza provides a comprehensive suite of automated tools for checking the initial status of quantum dot devices. We group these tools into a routine called Health Check. Health Check follows a similar workflow to other algorithms that you may be familiar with for quantum devices such as BATIS [1]. The Health Check routine systematically tests and characterizes your device to establish baseline operating parameters such as checking for leaking gates and threshold voltages before running more complex experiments.

Overview

Health Check is the critical first step in quantum dot tuning. Before you can measure charge stability diagrams or manipulate individual quantum dots, you need to:

  1. Verify that gates are not shorted or leaking
  2. Determine the global turn-on voltage for the device
  3. Characterize individual reservoir gates
  4. Characterize individual finger gates (plungers and barriers)

Stanza’s Health Check routine automates this entire workflow, producing actionable parameters that subsequent tune-up sequences can use.

Quick Start

Configure the Health Check routine in your device YAML file:

1name: "My Quantum Device"
2
3gates:
4 R1: {type: RESERVOIR, control_channel: 1, measure_channel: 1, v_lower_bound: -3.0, v_upper_bound: 3.0}
5 R2: {type: RESERVOIR, control_channel: 2, measure_channel: 2, v_lower_bound: -3.0, v_upper_bound: 3.0}
6 B1: {type: BARRIER, control_channel: 3, measure_channel: 3, v_lower_bound: -3.0, v_upper_bound: 3.0}
7 P1: {type: PLUNGER, control_channel: 4, measure_channel: 4, v_lower_bound: -3.0, v_upper_bound: 3.0}
8
9contacts:
10 SOURCE: {type: SOURCE, control_channel: 5, measure_channel: 5, v_lower_bound: -3.0, v_upper_bound: 3.0}
11 DRAIN: {type: DRAIN, control_channel: 6, measure_channel: 6, v_lower_bound: -3.0, v_upper_bound: 3.0}
12
13routines:
14 - name: device_health_check
15 routines:
16 - name: noise_floor_measurement
17 parameters:
18 measure_electrode: DRAIN
19 num_points: 100
20
21 - name: leakage_test
22 parameters:
23 leakage_threshold_resistance: 50e6 # 50 MOhm
24 leakage_threshold_count: 0
25 num_points: 10
26
27 - name: global_accumulation
28 parameters:
29 measure_electrode: DRAIN
30 step_size: 0.01 # 10 mV steps
31 bias_gate: SOURCE
32 bias_voltage: 0.01
33
34 - name: reservoir_characterization
35 parameters:
36 measure_electrode: DRAIN
37 step_size: 0.01
38 bias_gate: SOURCE
39 bias_voltage: 0.01
40
41 - name: finger_gate_characterization
42 parameters:
43 measure_electrode: DRAIN
44 step_size: 0.01
45 bias_gate: SOURCE
46 bias_voltage: 0.01
47
48instruments:
49 - name: qdac2
50 type: GENERAL
51 driver: qdac2
52 ip_addr: 192.168.1.100
53 nplc: 10
54 sample_time: 10e-6
55 slew_rate: 100.0

Then run the full Health Check sequence:

1from stanza.utils import load_device_config
2from stanza.routines import RoutineRunner
3import stanza.routines.builtins
4
5# Load device configuration
6config = load_device_config("device.yaml")
7
8# Create runner
9runner = RoutineRunner(configs=[config])
10
11# Run the Health Check routine
12results = runner.run_all(parent_routine="device_health_check")
13
14# Access individual results
15noise_floor = results["noise_floor_measurement"]
16print(f"Noise floor: {noise_floor['current_std']:.2e} A")
17
18global_turn_on = results["global_accumulation"]
19print(f"Global turn-on voltage: {global_turn_on['global_turn_on_voltage']:.3f} V")
20
21reservoir_cutoff_voltages = results["reservoir_characterization"]
22for reservoir, cutoff_voltages in reservoir_cutoff_voltages.items():
23 print(f"{reservoir} pinch_off: {cutoff_voltages:.3f} V")

Health Check Routine

1. Noise Floor Measurement

Purpose: Establish baseline measurement noise and offset to distinguish real signals from noise.

The noise floor measurement performs repeated current measurements on a specified electrode to characterize the statistical properties of measurement noise and offset. The resulting standard deviation is used as a threshold in subsequent routines.

1from stanza.routines.builtins.health_check import noise_floor_measurement
2
3result = runner.run("noise_floor_measurement")
4# Returns: {"current_mean": <float>, "current_std": <float>}

Parameters:

  • measure_electrode (str): Electrode (or contact) to measure current from (e.g., “DRAIN”)
  • num_points (int, default=100): Number of measurements for statistics
  • session (LoggerSession, optional): Session for logging measurements

Returns:

  • current_mean (float): Mean measured current in Amperes
  • current_std (float): Standard deviation of current in Amperes

Usage Notes:

  • All gates and contacts are automatically set to 0V before measurements using device.zero(PadType.ALL)
  • Results automatically logged if session provided
  • The current_std value is typically used in leakage_test as min_current_threshold
Noise floor measurement
Noise floor measurement showing the distribution of currents at zero bias, with mean (μ) and standard deviation (σ) used to set detection thresholds.

2. Leakage Test

Purpose: Detect unwanted electrical leakage or shorts between gate electrodes and the device’s conductive channel (or substrate), ensuring gate isolation integrity.

The leakage test systematically tests for electrical leakage between all pairs of control gates by sweeping each gate through its voltage range while measuring current on other gates. Leakage is quantified as inter-gate resistance (dV/dI).

1from stanza.routines.builtins.health_check import leakage_test
2
3result = runner.run("leakage_test")
4# Returns: {"max_safe_voltage_bound": <float>, "min_safe_voltage_bound": <float>}

Parameters:

  • leakage_threshold_resistance (int): Maximum acceptable resistance (Ohms) between gate pairs. Lower values indicate more stringent requirements. Typical: 50e6 (50 MOhm)
  • leakage_threshold_count (int, default=0): Maximum number of leaky gate pairs allowed. Default 0 means any leakage causes failure
  • num_points (int, default=10): Number of voltage steps per direction
  • session (LoggerSession, optional): Session for logging leakage matrices

Returns:

  • max_safe_voltage_bound (float): Voltage offset tested for positive bound
  • min_safe_voltage_bound (float): Voltage offset tested for negative bound

How It Works:

  1. Sweeps both positive (max_voltage_bound) and negative (min_voltage_bound) directions from initial voltages
  2. For each gate, measures current response on all other gates
  3. Calculates resistance matrix: R[i,j] = |ΔV / ΔI|
  4. Flags gate pairs where resistance < leakage_threshold_resistance
  5. Device always returned to initial voltages in finally block

Usage Notes:

  • Uses ctx.results.get("current_std") from noise_floor_measurement if available, otherwise defaults to 1e-10 A
  • Both voltage bounds tested independently regardless of failures
  • Current differences below min_current_threshold are skipped to avoid noise
  • Leakage matrix is symmetric; only upper triangle checked
Leakage test matrix
Inter-gate leakage resistance matrix showing resistance (R = |ΔV/ΔI|) between all gate pairs. High resistance (dark blue) indicates proper isolation, while low resistance (light blue) indicates potential shorts.

3. Global Accumulation

Purpose: Determine the global turn-on voltage by sweeping all gate voltages simultaneously to induce channel conduction.

This routine sweeps all control gates together from minimum to maximum voltage, identifying the voltage where the device transitions from depletion to accumulation. This establishes a baseline operating point for subsequent characterization.

1from stanza.routines.builtins.health_check import global_accumulation
2
3result = runner.run("global_accumulation")
4# Returns: {"global_turn_on_voltage": <float>}

Parameters:

  • measure_electrode (str): Electrode to measure current from
  • step_size (float): Voltage increment (V) between sweep points. Smaller = higher resolution but longer measurement time. Typical: 0.01 V (10 mV)
  • bias_gate (str): Name of gate to apply bias voltage to during measurement
  • bias_voltage (float): Bias voltage (V) to apply to bias_gate
  • session (LoggerSession, optional): Session for logging sweep data

Returns:

  • global_turn_on_voltage (float): Cut-off voltage (cutoff_voltage) where device turns on, in Volts

How It Works:

  1. Requires max_voltage_bound and min_voltage_bound from prior leakage_test
  2. Sweeps all control gates together from min to max voltage
  3. Measures current at each voltage step
  4. Fits data using pinch-off heuristic to extract turn-on voltage (cutoff_voltage)
  5. Automatically sets all gates to cutoff_voltage after analysis

Usage Notes:

  • step_size converted to num_points based on voltage range
  • Device automatically set to cutoff_voltage after successful analysis
  • The cutoff_voltage value used by subsequent steps in the Health Check routine
  • Raises RoutineError if step_size ≤ 0
Global accumulation

Global accumulation sweep with all control gates swept simultaneously. The hyperbolic tangent fit extracts the global turn-on voltage (cutoff_voltage) from the depletion to accumulation transition.

4. Reservoir Characterization

Purpose: Characterize individual reservoir gates to determine their cut-off voltages.

This routine determines the turn-on voltage for each reservoir gate individually by sweeping each reservoir while holding others in accumulation. This isolates each reservoir’s behavior.

1from stanza.routines.builtins.health_check import reservoir_characterization
2
3result = runner.run("reservoir_characterization")
4# Returns: {"reservoir_characterization": {"R1": <float>, "R2": <float>, ...}}

Parameters:

  • measure_electrode (str): Electrode to measure current from
  • step_size (float): Voltage increment (V) between sweep points. Typical: 0.01 V
  • bias_gate (str): Name of gate to apply bias voltage to during measurement
  • bias_voltage (float): Bias voltage (V) to apply to bias_gate
  • session (LoggerSession, optional): Session for logging sweep data

Returns:

  • reservoir_characterization (dict): Maps each reservoir name to its turn-on voltage cutoff_voltage in Volts

How It Works:

  1. Requires voltage bounds and global_turn_on_voltage from prior routines
  2. For each reservoir:
    • Sets other reservoirs to min(1.2 × global_turn_on_voltage, max_voltage_bound)
    • Sets target reservoir to 0 V with 10s settling time
    • Sweeps target reservoir from min to max voltage
    • Fits data using pinch-off heuristic to extract cutoff_voltage
  3. Tests each reservoir sequentially

Usage Notes:

  • Each reservoir tested sequentially (not parallel)
  • 10 second settling time between voltage changes
  • Other reservoirs are set to 120% of the global turn-on voltage to ensure full conduction
  • May raise ValueError if curve fit fails
Reservoir characterization
Individual reservoir gate characterization with each gate swept independently while others are held in accumulation. Pinch-off analysis extracts the cut-off voltage for each reservoir.

5. Finger Gate Characterization

Purpose: Characterize individual finger gates (plungers and barriers) to determine their cut-off voltages.

This routine determines the cut-off voltage for each finger gate (plungers and barriers) individually by sweeping each gate while holding others in accumulation.

1from stanza.routines.builtins.health_check import finger_gate_characterization
2
3result = runner.run("finger_gate_characterization")
4# Returns: {"finger_gate_characterization": {"P1": <float>, "B1": <float>, ...}}

Parameters:

  • measure_electrode (str): Electrode to measure current from
  • step_size (float): Voltage increment (V) between sweep points. Typical: 0.01 V
  • bias_gate (str): Name of gate to apply bias voltage to during measurement
  • bias_voltage (float): Bias voltage (V) to apply to bias_gate
  • session (LoggerSession, optional): Session for logging sweep data

Returns:

  • finger_gate_characterization (dict): Maps each finger gate name to its cut-off voltage in Volts

How It Works:

  1. Requires voltage bounds and global_turn_on_voltage from prior routines
  2. For each finger gate (plunger or barrier):
    • Sets other finger gates to min(1.2 × global_turn_on_voltage, max_voltage_bound)
    • Sets target gate to 0 V with 10s settling time
    • Sweeps target gate from min to max voltage
    • Fits data using pinch-off heuristic to extract cutoff_voltage
  3. Tests each gate sequentially

Usage Notes:

  • Processes both PLUNGER and BARRIER gate types
  • Each gate tested sequentially (not parallel)
  • 10 second settling time after setting target gate to 0 V
  • Other gates biased at 120% of global turn-on
  • May raise ValueError if curve fit fails
Finger gate characterization
Individual finger gate characterization for plungers and barriers. Each gate is swept independently with pinch-off analysis extracting the cut-off voltage for each gate.

Pinch-off Analysis

All gate characterizations in the Health Check routine uses a pinch-off curve fitting approach to extract turn-on voltages. The fitting uses a smooth hyperbolic tangent model:

I(V) = a * (1 + tanh(b * V + c))

Where:

  • a: Amplitude parameter
  • b: Slope parameter
  • c: Offset parameter

The analysis extracts three characteristic voltages from derivative extrema:

  • v_cut_off (cut-off): Where device approaches near-zero current (minimum of second derivative)
  • v_transition (transition): Midpoint of transition from cut-off to saturation (maximum of first derivative)
  • v_saturation (saturation): Where device approaches saturated current (maximum of second derivative)

The Health Check routine returns the pinch-off analysis characteristics [2].

Configuration Guide

Device Configuration

The Health Check routine requires proper gate type annotations in your device config:

1gates:
2 # Reservoir gates - connect to electron/hole reservoirs
3 R1: {type: RESERVOIR, control_channel: 1, measure_channel: 1, v_lower_bound: -3.0, v_upper_bound: 3.0}
4 R2: {type: RESERVOIR, control_channel: 2, measure_channel: 2, v_lower_bound: -3.0, v_upper_bound: 3.0}
5
6 # Barrier gates - control tunneling between dots and reservoirs
7 B1: {type: BARRIER, control_channel: 3, measure_channel: 3, v_lower_bound: -3.0, v_upper_bound: 3.0}
8 B2: {type: BARRIER, control_channel: 4, measure_channel: 4, v_lower_bound: -3.0, v_upper_bound: 3.0}
9
10 # Plunger gates - control dot chemical potential
11 P1: {type: PLUNGER, control_channel: 5, measure_channel: 5, v_lower_bound: -3.0, v_upper_bound: 3.0}
12 P2: {type: PLUNGER, control_channel: 6, measure_channel: 6, v_lower_bound: -3.0, v_upper_bound: 3.0}

Routine Parameters

Key parameters to tune for your device:

Leakage Test:

1- name: leakage_test
2 parameters:
3 leakage_threshold_resistance: 50e6 # 50 MOhm - adjust based on device quality
4 leakage_threshold_count: 0 # 0 = fail on any leakage, >0 = allow some leakage
5 num_points: 10 # More points = finer resolution but slower

Sweep Step Size:

1- name: global_accumulation
2 parameters:
3 step_size: 0.01 # 10 mV - smaller = better resolution but slower

Trade-offs:

  • Smaller step_size: Better voltage resolution, longer measurement time
  • Larger step_size: Faster measurement, may miss narrow features
  • More num_points: Better leakage detection, longer test time

Nested Routine Execution

Stanza supports nested routines for organizing complex workflows:

1routines:
2 - name: health_check
3 routines:
4 # These run in sequence when you call runner.run_all("health_check")
5 - name: noise_floor_measurement
6 parameters: {...}
7 - name: leakage_test
8 parameters: {...}
9 - name: global_accumulation
10 parameters: {...}
11 - name: reservoir_characterization
12 parameters: {...}
13 - name: finger_gate_characterization
14 parameters: {...}

Execute the entire tree with:

1results = runner.run_all(parent_routine="health_check")

Or run individual routines:

1noise_result = runner.run("noise_floor_measurement")
2leakage_result = runner.run("leakage_test")

Stanza Improvements

Stanza’s Health Check routine follows a similar workflow to the BATIS framework [1], with several key improvements:

1. Configuration-First Design

  • All parameters defined in YAML configuration files
  • Benefit: Change characterization parameters without modifying code. Easier collaboration and version control.

2. Modular Architecture

  • Decorator-based routines that work standalone or in sequences
  • Benefit: Use individual characterization routines independently. Mix and match with custom routines.

3. Extensible Framework

  • Driver abstraction layer supporting multiple instrument backends (QDAC2, OPX+, custom)
  • Benefit: Same Health Check code works across different hardware platforms.

4. Automatic Data Logging

  • Built-in session-based logging to HDF5/JSONL with automatic metadata
  • Benefit: All measurements and analysis results automatically saved with timestamps and parameters.

5. Result Chaining

  • Automatic result registry (ctx.results) passes data between routines
  • Benefit: Downstream routines automatically access upstream results. No manual plumbing.

6. Type Safety

  • Pydantic models with compile-time type checking
  • Benefit: Catch configuration errors before running experiments. Better IDE support.

Example: Stanza

1# device.yaml - all parameters in config
2routines:
3 - name: health_check
4 routines:
5 - name: noise_floor_measurement
6 parameters: {num_points: 100}
7 - name: leakage_test
8 parameters: {leakage_threshold_resistance: 50e6}
9 - name: global_accumulation
10 parameters: {step_size: 0.01}
11 - name: reservoir_characterization
12 parameters: {step_size: 0.01}
1# Run all, automatic logging and result chaining
2runner = RoutineRunner(configs=[config])
3results = runner.run_all(parent_routine="health_check")
4
5# Data automatically saved to ./data/ with timestamps

Advanced Usage

Custom Analysis Functions

You can write custom routines that use Health Check results:

1from stanza.routines import routine
2
3@routine
4def analyze_asymmetry(ctx):
5 """Analyze asymmetry in reservoir cut-off voltages."""
6 reservoir_cutoffs = ctx.results.get("reservoir_characterization")["reservoir_characterization"]
7
8 vcs = list(reservoir_cutoffs.values())
9 asymmetry = max(vcs) - min(vcs)
10
11 return {
12 "asymmetry_voltage": asymmetry,
13 "max_vc": max(vcs),
14 "min_vc": min(vcs)
15 }

Add to your config:

1routines:
2 - name: health_check_analysis
3 routines:
4 - name: reservoir_characterization
5 parameters: {step_size: 0.01}
6 - name: analyze_asymmetry # Runs after reservoir_characterization

Error Handling

The Health Check routine raises RoutineError for configuration issues and ValueError for fit failures:

1from stanza.exceptions import RoutineError
2
3try:
4 result = runner.run("global_accumulation")
5except RoutineError as e:
6 print(f"Configuration error: {e}")
7except ValueError as e:
8 print(f"Curve fit failed: {e}")

Common failure modes:

  • Curve fit failure: Data too noisy or doesn’t follow cut-off model. Try adjusting step_size or check device connectivity.
  • Leakage test failure: Inter-gate resistance below threshold. Check for shorts or reduce threshold.
  • Missing results: Routine depends on previous routine results. Ensure routines run in correct order.

Session Logging

All components of the Health Check routine support optional session logging:

1from stanza.logger.data_logger import DataLogger
2
3# Create logger
4data_logger = DataLogger(
5 name="logger",
6 routine_name="health_check",
7 base_dir="./experiment_data"
8)
9
10# Register with runner
11runner = RoutineRunner(resources=[device, data_logger])
12
13# Logging happens automatically
14results = runner.run_all(parent_routine="health_check")
15
16# Data saved to ./experiment_data/health_check/{routine_name}/

Each routine session contains:

  • Measurements: Raw sweep data (voltages, currents, leakage matrices)
  • Analysis: Fit parameters, turn-on voltages, statistics
  • Metadata: Timestamps, device config, routine parameters

Best Practices

1. Always Run Noise Floor First

The noise floor measurement provides the current threshold for leakage testing:

1routines:
2 - name: health_check
3 routines:
4 - name: noise_floor_measurement # Always first
5 - name: leakage_test # Uses noise_floor result
6 # ... other routines

2. Check Leakage Before Health Check

Running gate characterization on a leaky device can damage gates. Always verify leakage test passes before proceeding:

1leakage_result = runner.run("leakage_test")
2# If no exception raised, device passed leakage test
3
4# Now safe to run Health Check
5global_result = runner.run("global_accumulation")

3. Use Appropriate Step Sizes

Balance resolution vs measurement time:

  • Coarse sweep (50-100 mV): Initial device testing, fast sanity checks
  • Medium sweep (10-20 mV): Standard Health Check, good balance
  • Fine sweep (1-5 mV): High-resolution Health Check, detailed features

4. Monitor Fit Quality

Check covariance matrices for poor fits:

1from stanza.analysis.criterion import pcov_fail_criterion
2
3result = runner.run("global_accumulation")
4# Routine already checks internally, but you can validate:
5
6# Access fit quality from session logs if needed

5. Save Configuration with Data

Always version control your device YAML alongside data:

$./experiment_data/
>└── health_check/
> ├── noise_floor_measurement/
> │ ├── noise_floor_measurement.h5
> │ ├── session_metadata.json
> │ ├── measurement.jsonl
> │ └── analysis.jsonl
> ├── leakage_test/
> │ ├── leakage_test.h5
> │ ├── session_metadata.json
> │ ├── measurement.jsonl
> │ └── analysis.jsonl
> └── global_accumulation/
> ├── global_accumulation.h5
> ├── session_metadata.json
> ├── sweep.jsonl
> └── analysis.jsonl

Troubleshooting

Curve Fit Failures

Symptom: ValueError: Curve fit covariance matrix indicates poor fit

Causes:

  • Data too noisy
  • Device not responding to voltage sweeps
  • Insufficient voltage range
  • Wrong measure electrode

Solutions:

  1. Check device connectivity and grounding
  2. Verify measure_electrode in config matches physical setup
  3. Increase voltage range (v_lower_bound, v_upper_bound)
  4. Reduce step_size for better resolution
  5. Check for instrument calibration issues

Leakage Test Failures

Symptom: Leakage test failed: Found N leaky connections

Causes:

  • Actual gate shorts or crosstalk
  • Threshold too stringent
  • Measurement noise mistaken for leakage

Solutions:

  1. Check for physical shorts on device
  2. Increase leakage_threshold_resistance if device has known crosstalk
  3. Set leakage_threshold_count > 0 to allow some leakage
  4. Verify cable connections and shielding

Missing Results

Symptom: KeyError: 'global_turn_on_voltage' or similar

Causes:

  • Routines run out of order
  • Previous routine failed without raising exception

Solutions:

  1. Use nested routine structure to enforce ordering
  2. Check that all prerequisite routines completed successfully
  3. Use runner.run_all() instead of individual run() calls

References

[1] Kovach, T. et al. BATIS: Bootstrapping, Autonomous Testing, and Initialization System for Si/SiGe Multi-quantum Dot Devices. arXiv:2412.07676 (2024).

[2] Darulová, J. et al. Autonomous tuning and charge state detection of gate defined quantum dots. Physical Review Applied 13, 054005 (2020).