Each Litmus run produces one Parquet file. Every row carries an explicit record_type discriminator with one of three values:
record_type = 'run' — exactly one row per file. Carries run-level identity, timing, outcome, plus DUT / station / project / git / environment context.
record_type = 'step' — one row per (step_path, vector_index) execution. Step identity, timing, outcome, dynamic in_* / out_* columns. Measurement columns are NULL.
record_type = 'measurement' — one row per recorded measurement. Carries the measurement payload plus the same denormalized step + run + DUT + station + fixture context as the corresponding step row.
Step and measurement rows share grain (run_id, step_path, vector_index); measurement rows are further keyed by measurement_name. A step that records N measurements emits 1 step row + N measurement rows.
The canonical schema lives at src/litmus/data/schemas.py (RUN_ROW_SCHEMA); this page is a human-readable mirror of it.
<data_dir>/runs/{date}/├── {timestamp}_{serial}.parquet # Run row + all step + measurement rows for one run├── {timestamp}.parquet # Same shape, no DUT serial (dev runs)└── {timestamp}_{serial}_ref/ # Reference data (waveforms, images, files) ├── {vector_id}_scope_waveform.npz ├── {vector_id}_camera_image.png └── ...
Timestamps are UTC and sort naturally. DuckDB / Spark / Polars / Pandas all read the file directly with read_parquet.
Where — instruments (dynamic step_instruments_*) #
Per-step instrument identity, captured from the pytest fixtures the test actually used. All columns are list[string] (one entry per instrument) and arrays stay in parallel order.
Column
Type
Description
step_instruments_name
list[string]
Role names (e.g. ["dmm", "psu"])
step_instruments_id
list[string]
Instrument file IDs
step_instruments_driver
list[string]
Driver class paths
step_instruments_resource
list[string]
VISA addresses
step_instruments_protocol
list[string]
Protocols ("visa", "daqmx", …)
step_instruments_manufacturer
list[string]
From *IDN? or YAML config
step_instruments_model
list[string]
Model number
step_instruments_serial
list[string]
Serial number
step_instruments_firmware
list[string]
Firmware version
step_instruments_cal_due
list[string]
Calibration due date (ISO 8601)
step_instruments_cal_last
list[string]
Last cal date (ISO 8601)
step_instruments_cal_certificate
list[string]
Cal certificate number
step_instruments_cal_lab
list[string]
Cal lab name
step_instruments_mocked
list[bool]
True if the instrument ran in mock mode
For real hardware, identity comes from *IDN? at session start. For mock instruments, identity comes from the instrument YAML configs.
-- DuckDB: unnest parallel arrays for per-instrument queriesSELECT step_name, unnest(step_instruments_name) AS instrument, unnest(step_instruments_serial) AS serial, unnest(step_instruments_cal_due) AS cal_dueFROM read_parquet('data/runs/**/*.parquet')WHERE record_type = 'step';
Characteristic ID from the product YAML (e.g. "output_voltage")
spec_ref
string
Human-readable reference with conditions (e.g. "Table 4.2 @ temp=25")
-- Yield by characteristic across all productsSELECT characteristic_id, product_id, AVG(CASE WHEN measurement_outcome='passed' THEN 1.0 ELSE 0.0 END) AS yieldFROM read_parquet('data/runs/**/*.parquet')WHERE record_type = 'measurement'GROUP BY characteristic_id, product_id;
Filter to the final execution with WHERE vector_retry = (SELECT MAX(vector_retry) ...) or use the daemon's runs view, which already rolls retry_count per (run_id, step_path, vector_index).
SELECT instrument_name, instrument_resource, COUNT(*) AS failuresFROM read_parquet('data/runs/**/*.parquet')WHERE record_type = 'measurement' AND measurement_outcome = 'failed'GROUP BY 1, 2ORDER BY failures DESC;
SELECT step_name, AVG(EPOCH(step_ended_at) - EPOCH(step_started_at)) AS avg_seconds, COUNT(*) AS runsFROM read_parquet('data/runs/**/*.parquet')WHERE record_type = 'step' AND step_started_at IS NOT NULLGROUP BY step_nameORDER BY avg_seconds DESC;