Skip to navigation

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:

name: "My Quantum Device"
gates:
R1: {type: RESERVOIR, control_channel: 1, measure_channel: 1, v_lower_bound: -3.0, v_upper_bound: 3.0}
R2: {type: RESERVOIR, control_channel: 2, measure_channel: 2, v_lower_bound: -3.0, v_upper_bound: 3.0}
B1: {type: BARRIER, control_channel: 3, measure_channel: 3, v_lower_bound: -3.0, v_upper_bound: 3.0}
P1: {type: PLUNGER, control_channel: 4, measure_channel: 4, v_lower_bound: -3.0, v_upper_bound: 3.0}
contacts:
SOURCE: {type: SOURCE, control_channel: 5, measure_channel: 5, v_lower_bound: -3.0, v_upper_bound: 3.0}
DRAIN: {type: DRAIN, control_channel: 6, measure_channel: 6, v_lower_bound: -3.0, v_upper_bound: 3.0}
routines:
- name: device_health_check
parameters:
charge_carrier_type: HOLE # The mobile charge carrier type, either "ELECTRON" or "HOLE"
routines:
- name: noise_floor_measurement
parameters:
measure_electrode: DRAIN
num_points: 100
- name: leakage_test
parameters:
leakage_threshold_resistance: 50e6 # 50 MOhm
leakage_threshold_count: 0
num_points: 10
- name: global_accumulation
parameters:
measure_electrode: DRAIN
step_size: 0.01 # 10 mV steps
bias_gate: SOURCE
bias_voltage: 0.01
- name: reservoir_characterization
parameters:
measure_electrode: DRAIN
step_size: 0.01
bias_gate: SOURCE
bias_voltage: 0.01
- name: finger_gate_characterization
parameters:
measure_electrode: DRAIN
step_size: 0.01
bias_gate: SOURCE
bias_voltage: 0.01
instruments:
- name: qdac2
type: GENERAL
driver: qdac2
ip_addr: 192.168.1.100
nplc: 10
sample_time: 10e-6
slew_rate: 100.0

Then run the full Health Check sequence:

from stanza.utils import load_device_config
from stanza.routines import RoutineRunner
import stanza.routines.builtins
# Load device configuration
config = load_device_config("device.yaml")
# Create runner
runner = RoutineRunner(configs=[config])
# Run the Health Check routine
results = runner.run_all(parent_routine="device_health_check")
# Access individual results
noise_floor = results["noise_floor_measurement"]
print(f"Noise floor: {noise_floor['current_std']:.2e} A")
global_turn_on = results["global_accumulation"]
print(f"Global turn-on voltage: {global_turn_on['global_turn_on_voltage']:.3f} V")
reservoir_cutoff_voltages = results["reservoir_characterization"]
for reservoir, cutoff_voltages in reservoir_cutoff_voltages.items():
print(f"{reservoir} pinch_off: {cutoff_voltages:.3f} V")

Voltage Bounds and Carrier Type Conventions

The safe voltage bounds (max_safe_voltage_bound and min_safe_voltage_bound) represent the extreme voltages that can be safely applied to gates without causing device damage or unwanted leakage. The interpretation of these bounds depends on the charge carrier type:

For electron-based devices (n-channel, charge_carrier_type=“electron”):

  • Accumulation occurs at POSITIVE gate voltages, which attract electrons to form a conductive channel
  • max_safe_voltage_bound is POSITIVE and represents the maximum safe positive voltage
  • min_safe_voltage_bound is NEGATIVE and represents the most negative voltage (depletion)
  • Current INCREASES as gate voltage sweeps from negative (depleted) to positive (accumulated)

For hole-based devices (p-channel, charge_carrier_type=“hole”):

  • Accumulation occurs at NEGATIVE gate voltages, which attract holes to form a conductive channel
  • max_safe_voltage_bound is POSITIVE and represents the most positive voltage (depletion)
  • min_safe_voltage_bound is NEGATIVE and represents the maximum safe negative voltage
  • Current INCREASES as gate voltage sweeps from positive (depleted) to negative (accumulated)

In both cases, the routines sweep from depletion (low current) toward accumulation (high current), but the voltage polarity is reversed between electron and hole devices. The charge_carrier_type parameter determines which voltage bound is used for accumulation sweeps.

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.

from stanza.routines.builtins.health_check import noise_floor_measurement
result = runner.run("noise_floor_measurement")
# 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).

from stanza.routines.builtins.health_check import leakage_test
result = runner.run("leakage_test")
# 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.

from stanza.routines.builtins.health_check import global_accumulation
result = runner.run("global_accumulation")
# 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
  • charge_carrier_type (str): The mobile charge particle type. Must be “electron” or “hole”.
  • 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 depleted to accumulated voltage, depending on the charge carrier type
  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.

from stanza.routines.builtins.health_check import reservoir_characterization
result = runner.run("reservoir_characterization")
# 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
  • charge_carrier_type (str): The mobile charge particle type. Must be “electron” or “hole”.
  • 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, voltage_bound)
    • Sets target reservoir to 0 V with 10s settling time
    • Sweeps target reservoir from depleted to accumulated voltage, depending on the charge carrier type
    • 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.

from stanza.routines.builtins.health_check import finger_gate_characterization
result = runner.run("finger_gate_characterization")
# 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
  • charge_carrier_type (str): The mobile charge particle type. Must be “electron” or “hole”.
  • 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, voltage_bound)
    • Sets target gate to the left voltage bound with 10s settling time
    • Sweeps target gate from depleted to accumulated voltage, depending on the charge carrier type
    • 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:

gates:
# Reservoir gates - connect to electron/hole reservoirs
R1: {type: RESERVOIR, control_channel: 1, measure_channel: 1, v_lower_bound: -3.0, v_upper_bound: 3.0}
R2: {type: RESERVOIR, control_channel: 2, measure_channel: 2, v_lower_bound: -3.0, v_upper_bound: 3.0}
# Barrier gates - control tunneling between dots and reservoirs
B1: {type: BARRIER, control_channel: 3, measure_channel: 3, v_lower_bound: -3.0, v_upper_bound: 3.0}
B2: {type: BARRIER, control_channel: 4, measure_channel: 4, v_lower_bound: -3.0, v_upper_bound: 3.0}
# Plunger gates - control dot chemical potential
P1: {type: PLUNGER, control_channel: 5, measure_channel: 5, v_lower_bound: -3.0, v_upper_bound: 3.0}
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:

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

Sweep Step Size:

- name: global_accumulation
parameters:
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:

routines:
- name: health_check
routines:
# These run in sequence when you call runner.run_all("health_check")
- name: noise_floor_measurement
parameters: {...}
- name: leakage_test
parameters: {...}
- name: global_accumulation
parameters: {...}
- name: reservoir_characterization
parameters: {...}
- name: finger_gate_characterization
parameters: {...}

Execute the entire tree with:

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

Or run individual routines:

noise_result = runner.run("noise_floor_measurement")
leakage_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

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

Advanced Usage

Custom Analysis Functions

You can write custom routines that use Health Check results:

from stanza.routines import routine
@routine
def analyze_asymmetry(ctx):
"""Analyze asymmetry in reservoir cut-off voltages."""
reservoir_cutoffs = ctx.results.get("reservoir_characterization")["reservoir_characterization"]
vcs = list(reservoir_cutoffs.values())
asymmetry = max(vcs) - min(vcs)
return {
"asymmetry_voltage": asymmetry,
"max_vc": max(vcs),
"min_vc": min(vcs)
}

Add to your config:

routines:
- name: health_check_analysis
routines:
- name: reservoir_characterization
parameters: {step_size: 0.01}
- name: analyze_asymmetry # Runs after reservoir_characterization

Error Handling

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

from stanza.exceptions import RoutineError
try:
result = runner.run("global_accumulation")
except RoutineError as e:
print(f"Configuration error: {e}")
except ValueError as e:
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:

from stanza.logger.data_logger import DataLogger
# Create logger
data_logger = DataLogger(
name="logger",
routine_name="health_check",
base_dir="./experiment_data"
)
# Register with runner
runner = RoutineRunner(resources=[device, data_logger])
# Logging happens automatically
results = runner.run_all(parent_routine="health_check")
# 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:

routines:
- name: health_check
routines:
- name: noise_floor_measurement # Always first
- name: leakage_test # Uses noise_floor result
# ... 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:

leakage_result = runner.run("leakage_test")
# If no exception raised, device passed leakage test
# Now safe to run Health Check
global_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:

from stanza.analysis.criterion import pcov_fail_criterion
result = runner.run("global_accumulation")
# Routine already checks internally, but you can validate:
# 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).