For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
OverviewCodaControlAPI ReferenceChangelog
OverviewCodaControlAPI ReferenceChangelog
  • Getting Started
    • Overview
    • Quickstart
    • API Keys
  • Models
    • Overview
    • Coulomb Blockade
    • Coulomb Diamond
    • Charge Stability Diagram
    • Electron Unload
    • Anticrossing
    • Turn-on
    • Pinch-off
    • Resonator Dip Finder
    • QEC Decoding
  • Agents
    • NVIDIA Ising Calibration
  • Stanza
    • Overview
    • Quickstart
      • Device Configuration
      • Device Groups
      • Drivers
      • Data Logging
      • Live Plotting
      • Jupyter Integration
    • Cookbooks
LogoLogo
On this page
  • Quick Start
  • What Gets Logged Automatically
  • File Organization
  • Reading Your Data Back
  • Reading HDF5 Files
  • Reading JSONL Files
  • Logging Custom Data
  • Example: 2D Charge Stability Diagram
  • Compression Options
  • Working with Multiple Routines
  • Batch Processing Logged Data
  • Tips and Best Practices
  • Next Steps
StanzaCore Concepts

Data Logging

Was this page helpful?
Built with

Stanza automatically logs your measurement data in both HDF5 and JSON formats. You don’t need to write separate logging code for each routine - just pass a DataLogger to RoutineRunner and your sweep data, parameters, and analysis results are saved automatically.

Quick Start

Enable logging in three steps:

1from stanza.logger import DataLogger
2from stanza.routines import RoutineRunner
3from stanza.models import DeviceConfig
4
5# 1. Create a logger
6logger = DataLogger(
7 name="logger",
8 routine_name="characterization",
9 base_dir="./data",
10 formats=["hdf5", "jsonl"]
11)
12
13# 2. Pass it to RoutineRunner
14config = DeviceConfig.from_yaml("device.yaml")
15runner = RoutineRunner(configs=[config], logger=logger)
16
17# 3. Run routines - data logs automatically
18runner.run("charge_stability")

Your data is saved to ./data/characterization/<session_id>/ with both HDF5 and JSONL files.

What Gets Logged Automatically

When you use device.sweep_1d() or device.sweep_2d() with a session parameter, Stanza logs:

  • Sweep data: Voltage arrays and measured signals
  • Labels: Axis labels for plotting
  • Metadata: Gate names, contact information, timestamps
  • Parameters: Routine parameters from YAML or runtime

Example - this routine automatically logs everything:

1@routine
2def barrier_sweep(ctx, gate, v_start, v_stop, n_points, contact, session=None):
3 """Sweep a barrier gate."""
4 device = ctx.resources.device
5
6 voltages = np.linspace(v_start, v_stop, n_points)
7
8 # Session parameter enables automatic logging
9 v_data, i_data = device.sweep_1d(
10 gate,
11 voltages.tolist(),
12 contact,
13 session=session # ← This enables logging
14 )
15
16 return {"voltages": v_data, "currents": i_data}

When you run this:

1runner.run("barrier_sweep", gate="LEFT_BARRIER", v_start=-2.0, v_stop=0.0,
2 n_points=100, contact="DRAIN")

Stanza automatically logs:

  • The voltage sweep (v_data)
  • The measured currents (i_data)
  • Parameters: gate="LEFT_BARRIER", v_start=-2.0, etc.
  • Metadata: timestamp, gate name, contact name

File Organization

Data is organized by routine and session:

data/
characterization/
20241003_143022_barrier_sweep/
session.h5 # HDF5 format (for analysis)
sweep.jsonl # JSON Lines (human-readable)
measurement.jsonl
session_metadata.json # Session info

Each time you run a routine, a new session directory is created with a timestamp.

Reading Your Data Back

Reading HDF5 Files

HDF5 is great for numerical analysis:

1import h5py
2import matplotlib.pyplot as plt
3
4# Open the file
5with h5py.File("data/characterization/20241003_143022/session.h5", "r") as f:
6 # Read sweep data
7 voltages = f["sweeps"]["LEFT_BARRIER_sweep_DRAIN"]["x_data"][:]
8 currents = f["sweeps"]["LEFT_BARRIER_sweep_DRAIN"]["y_data"][:]
9
10 # Read metadata
11 gate = f["sweeps"]["LEFT_BARRIER_sweep_DRAIN"]["metadata"]["gate"][()].decode()
12 contact = f["sweeps"]["LEFT_BARRIER_sweep_DRAIN"]["metadata"]["contact"][()].decode()
13
14# Plot
15plt.plot(voltages, currents * 1e9) # Convert to nA
16plt.xlabel("Voltage (V)")
17plt.ylabel("Current (nA)")
18plt.title(f"{gate} Pinchoff ({contact})")
19plt.show()

Reading JSONL Files

JSON Lines are easier for quick inspection:

1import json
2
3# Read sweep data
4sweeps = []
5with open("data/characterization/20241003_143022/sweep.jsonl", "r") as f:
6 for line in f:
7 sweeps.append(json.loads(line))
8
9# Access the data
10first_sweep = sweeps[0]
11print(f"Data name: {first_sweep['data_name']}")
12print(f"Gate: {first_sweep['metadata']['gate']}")
13print(f"Points: {len(first_sweep['x_data'])}")

Logging Custom Data

For measurements beyond simple sweeps, log custom data manually:

1@routine
2def find_pinchoff(ctx, gate, contact, session=None):
3 """Find pinchoff and log the analysis."""
4 device = ctx.resources.device
5
6 # Perform sweep with auto-logging
7 voltages = np.linspace(-2.0, 0.0, 100)
8 v_data, i_data = device.sweep_1d(gate, voltages.tolist(), contact, session=session)
9
10 # Analyze data
11 threshold_idx = np.where(np.abs(i_data) < 1e-12)[0]
12 pinchoff_voltage = v_data[threshold_idx[0]] if len(threshold_idx) > 0 else None
13
14 # Manually log analysis results
15 if session:
16 session.log_analysis(
17 f"{gate}_pinchoff_analysis",
18 {
19 "pinchoff_voltage": pinchoff_voltage,
20 "threshold": 1e-12,
21 "gate": gate,
22 "contact": contact
23 }
24 )
25
26 return {
27 "pinchoff_voltage": pinchoff_voltage,
28 "voltages": v_data,
29 "currents": i_data
30 }

The analysis results are saved alongside the sweep data.

Example: 2D Charge Stability Diagram

Here’s a complete example of measuring and logging a 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 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 - automatically logs
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 # Count number of visible charge transitions
25 n_transitions = np.sum(edges > np.percentile(edges, 95))
26
27 # Log analysis
28 if session:
29 session.log_analysis(
30 "charge_stability_analysis",
31 {
32 "n_transitions": int(n_transitions),
33 "max_gradient": float(np.max(edges)),
34 "mean_signal": float(np.mean(signal)),
35 "gates": [gate_x, gate_y]
36 }
37 )
38
39 return {
40 "voltages_x": v_data[0],
41 "voltages_y": v_data[1],
42 "signal": signal,
43 "edges": edges,
44 "n_transitions": n_transitions
45 }

Run and analyze:

1# Configure logger
2logger = DataLogger(
3 name="logger",
4 routine_name="charge_diagrams",
5 base_dir="./data",
6 formats=["hdf5", "jsonl"],
7 compression="gzip" # Compress to save space
8)
9
10runner = RoutineRunner(configs=[config], logger=logger)
11
12# Run measurement
13result = runner.run("charge_stability")
14
15print(f"Found {result['n_transitions']} charge transitions")
16
17# Plot
18plt.figure(figsize=(12, 5))
19
20plt.subplot(1, 2, 1)
21plt.pcolormesh(result["voltages_x"], result["voltages_y"],
22 result["signal"].T, cmap="RdBu_r")
23plt.xlabel("Left Plunger (V)")
24plt.ylabel("Right Plunger (V)")
25plt.title("Charge Stability")
26plt.colorbar(label="Signal")
27
28plt.subplot(1, 2, 2)
29plt.pcolormesh(result["voltages_x"], result["voltages_y"],
30 result["edges"].T, cmap="viridis")
31plt.xlabel("Left Plunger (V)")
32plt.ylabel("Right Plunger (V)")
33plt.title("Charge Transitions")
34plt.colorbar(label="Gradient")
35
36plt.tight_layout()
37plt.show()

Compression Options

For large datasets, enable compression:

1logger = DataLogger(
2 name="logger",
3 routine_name="test",
4 base_dir="./data",
5 compression="gzip" # Options: "gzip", "lzf", None
6)

gzip: Best compression (slower) lzf: Fast compression (larger files) None: No compression (fastest)

For a 200×200 charge diagram:

  • Uncompressed: ~640 KB
  • LZF: ~250 KB
  • GZIP: ~180 KB

Working with Multiple Routines

Log data from multiple routines in the same session:

1@routine
2def full_characterization(ctx, session=None):
3 """Run multiple measurements in one session."""
4 device = ctx.resources.device
5
6 # Barrier pinchoff
7 v = np.linspace(-2.0, 0.0, 100)
8 v_data, i_data = device.sweep_1d("LEFT_BARRIER", v.tolist(), "DRAIN", session=session)
9
10 # Plunger sweep
11 v = np.linspace(-2.5, -0.5, 150)
12 v_data, i_data = device.sweep_1d("LEFT_PLUNGER", v.tolist(), "DRAIN", session=session)
13
14 # Charge stability
15 vx = np.linspace(-2.5, -0.5, 100)
16 vy = np.linspace(-2.5, -0.5, 100)
17 v_data, signal = device.sweep_2d(
18 "LEFT_PLUNGER", vx.tolist(),
19 "RIGHT_PLUNGER", vy.tolist(),
20 "DRAIN",
21 session=session
22 )
23
24 return {"status": "complete"}

All three measurements are saved in the same session directory.

Batch Processing Logged Data

Process multiple sessions at once:

1import h5py
2from pathlib import Path
3import numpy as np
4
5def analyze_all_sessions(routine_dir):
6 """Analyze all logged sessions for a routine."""
7 results = []
8
9 # Find all session directories
10 for session_dir in Path(routine_dir).iterdir():
11 if not session_dir.is_dir():
12 continue
13
14 h5_file = session_dir / "session.h5"
15 if not h5_file.exists():
16 continue
17
18 # Read sweep data
19 with h5py.File(h5_file, "r") as f:
20 for sweep_name in f["sweeps"].keys():
21 voltages = f["sweeps"][sweep_name]["x_data"][:]
22 currents = f["sweeps"][sweep_name]["y_data"][:]
23
24 # Analyze
25 max_current = np.max(np.abs(currents))
26
27 results.append({
28 "session": session_dir.name,
29 "sweep": sweep_name,
30 "max_current": max_current
31 })
32
33 return results
34
35# Analyze all barrier sweeps
36results = analyze_all_sessions("./data/characterization")
37
38for r in results:
39 print(f"{r['session']}: {r['sweep']} -> {r['max_current']:.2e} A")

Tips and Best Practices

Use both HDF5 and JSONL: HDF5 for analysis scripts, JSONL for quick inspection.

Enable compression for large sweeps: A 200×200 charge diagram compresses 3-4x.

Log analysis results: Don’t just save raw data - save what you learned from it.

Use descriptive routine names: routine_name becomes your top-level directory. Use names like "device_42_characterization" rather than generic names like "test".

Add metadata to analysis: Include information that will help you interpret results later:

1session.log_analysis(
2 "pinchoff_analysis",
3 data={"pinchoff_voltage": -0.85},
4 metadata={
5 "temperature": 10e-3, # 10 mK
6 "magnetic_field": 0.0,
7 "operator": "alice",
8 "notes": "After cooldown #15"
9 }
10)

Flush periodically for long measurements: Force data to disk during long routines:

1@routine
2def long_measurement(ctx, session=None):
3 """Long measurement with periodic flushing."""
4 device = ctx.resources.device
5
6 for i in range(100):
7 # Measure
8 v_data, i_data = device.sweep_1d(...)
9
10 # Flush every 10 sweeps
11 if i % 10 == 0 and session:
12 session.flush()
13
14 return {"status": "complete"}

Check disk space: HDF5 files can get large. A full day of measurements might generate several GB.

Next Steps

  • Live Plotting - Visualize your logged data in real-time as routines execute
  • Routines - Learn how to write routines that log data automatically
  • Device Configuration - Configure your device for use with logging
  • Drivers - Understand how measurements are performed