Measurements
Last updated on May 21, 2026
A measurement is a typed value a Phase collects: voltage, temperature, firmware version, waveform. You declare each one on the phase in procedure.yaml and assign its value from the Python phase function, and TofuPilot validates the measurement on assignment so a failing validator fails the phase immediately.
Declaration
The example below pairs a YAML declaration with the matching Python assignment, so you can see the contract on both sides.
main:
- name: Measure Voltage
python: phases.measure_voltage
measurements:
- name: Output Voltage
unit: V
validators:
- operator: ">="
expected_value: 4.8
- operator: "<="
expected_value: 5.2def measure_voltage(measurements, multimeter):
measurements.output_voltage = multimeter.read_voltage()The key defaults to snake_case(name), and that key is the attribute name on the injected measurements object inside Python.
Fields
The table below covers every field you can set on a measurement, so you can spot the right knob without digging through the schema.
| Field | Purpose |
|---|---|
name | Display name. Up to 100 characters. Required. |
key | Python identifier for assignment. Defaults to snake_case(name). |
unit | Unit symbol (V, A, °C). Up to 50 characters. |
title | Chart and dashboard label override. Up to 200 characters. |
description | Long-form description on the run page. |
validators | Rules evaluated against the assigned value. |
aggregations | Computed statistics on arrays. |
x_axis | X-axis spec for multi-dimensional measurements. |
y_axis | Y-axis specs for multi-dimensional measurements. |
Types
Measurements come in four types, and each type supports a different set of operators because the comparisons that make sense for a number do not all map to a string.
| Type | Value | Operators |
|---|---|---|
number | float | >=, <=, >, <, ==, !=, in, not in |
string | string | ==, !=, in, not in, matches |
boolean | bool | ==, != |
| multi-dim | X array + one or more Y arrays | ==, != per-point (length-matched) |
Numeric
Numeric measurements cover voltages, temperatures, counts, and anything else with a unit, so you reach for range validators on continuous specs and in on discrete setpoints.
measurements:
- name: Voltage
unit: V
validators:
- operator: ">="
expected_value: 4.8
- operator: "<="
expected_value: 5.2
- name: Setpoint
validators:
- operator: in
expected_value: [5.0, 12.0, 24.0]String
String measurements cover firmware versions, status codes, and serial-format checks, so you use matches when you need a regex and in when the value must be one of a fixed set.
measurements:
- name: Firmware Version
validators:
- operator: matches
expected_value: "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
- name: Status
validators:
- operator: in
expected_value: ["OK", "READY", "IDLE"]Boolean
Boolean measurements cover flags and presence checks, so equality is the only operator that applies.
measurements:
- name: Device Connected
validators:
- operator: "=="
expected_value: trueMulti-dimensional
Multi-dimensional measurements cover waveforms, sweeps, and time-series, so you declare an x_axis and one or more y_axis entries, and each axis carries its own unit, legend, and optional aggregations.
measurements:
- name: Output Voltage
x_axis:
legend: Time
unit: ms
y_axis:
- legend: Voltage
unit: V
key: voltage
aggregations:
- type: mean
validators:
- operator: ">="
expected_value: 2.9
- operator: "<="
expected_value: 3.1def measure_voltage(measurements, scope):
times, voltages = scope.capture()
measurements.output_voltage.x_axis = times
measurements.output_voltage.y_axis.voltage = voltagesPer-point validators accept == and != with a literal array of the same length, so when you need range checks on multi-dimensional data you reach for aggregations instead.
Validators
A validator pairs an operator with an expected_value, and when you stack multiple validators they combine with AND logic so every one has to pass for the measurement to pass.
| Outcome | When |
|---|---|
PASS | Every validator passes. |
FAIL | At least one validator fails. |
UNSET | No validators defined. |
UNSET measurements record their value but do not affect the phase outcome, which is the right setup when you want to capture data for analytics without pinning a spec.
Aggregations
Aggregations attach computed statistics (mean, min, max, std, custom names) to a numeric array or multi-dimensional axis, and each aggregation can carry its own validators so you score the summary instead of every point.
measurements:
- name: Temperature
unit: °C
aggregations:
- type: mean
validators:
- operator: ">="
expected_value: 20
- operator: "<="
expected_value: 80
- type: std
validators:
- operator: "<="
expected_value: 2.0import numpy as np
def measure_temperature(measurements, sensor):
samples = [sensor.read() for _ in range(60)]
measurements.temperature = samples
measurements.temperature.aggregations.mean = float(np.mean(samples))
measurements.temperature.aggregations.std = float(np.std(samples))The type field is free-form, so common conventions are mean, std, min, max, sum, count, pass_count, and p2p. Pick names your analytics queries can rely on, because the dashboard groups across runs by exact name.
How is this guide?
Phases
Learn how to declare TofuPilot phases that run Python or shell commands, contribute measurements, logs, and attachments, and produce a typed outcome.
Operator UI
Learn how to declare per-phase components that collect operator input and stream live values, with the same render in the CLI and on a Station.