Data Logging

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

  • 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