> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.conductorquantum.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.conductorquantum.com/_mcp/server.

# Health Check

> Automated Health Check routine for quantum dot devices

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\]](#ref-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:

```yaml
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:

```python
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.

```python
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](/_fern-img/613f7c99189deb1cbb379f81c0bcf4c118815f5964dac117d410553e1644f870.webp)

### 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).

```python
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](/_fern-img/1fc06d7d6d2caf776f1f5401f1db110f6a2d8b54f28184ed52c98495b1b5d7fb.webp)

### 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.

```python
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](/_fern-img/85ade90438f23ffc580db5bf2d26518b77d262ff3393a8c2907a35156c59b235.webp)

### 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.

```python
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](/_fern-img/b60cdbc1cbab5bda885d9b380450fc9543d002713f3e5b67d4a7172f6822d965.webp)

### 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.

```python
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](/_fern-img/01d1c11f5b68ed46e049da68db05668077536faa8d4c04f9306e800a0c45019b.webp)

## 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\]](#ref-2).

## Configuration Guide

### Device Configuration

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

```yaml
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**:

```yaml
- 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**:

```yaml
- 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:

```yaml
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:

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

Or run individual routines:

```python
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\]](#ref-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**

```yaml
# 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}
```

```python
# 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:

```python
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:

```yaml
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:

```python
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:

```python
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:

```yaml
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:

```python
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:

```python
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:

```bash
./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](https://arxiv.org/abs/2412.07676) (2024).

\[2] Darulová, J. et al. Autonomous tuning and charge state detection of gate defined quantum dots. [*Physical Review Applied* **13**, 054005](https://doi.org/10.1103/PhysRevApplied.13.054005) (2020).