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.

procedure.yaml
main:
  - name: Measure Voltage
    python: phases.measure_voltage
    measurements:
      - name: Output Voltage
        unit: V
        validators:
          - operator: ">="
            expected_value: 4.8
          - operator: "<="
            expected_value: 5.2
phases/measure_voltage.py
def 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.

FieldPurpose
nameDisplay name. Up to 100 characters. Required.
keyPython identifier for assignment. Defaults to snake_case(name).
unitUnit symbol (V, A, °C). Up to 50 characters.
titleChart and dashboard label override. Up to 200 characters.
descriptionLong-form description on the run page.
validatorsRules evaluated against the assigned value.
aggregationsComputed statistics on arrays.
x_axisX-axis spec for multi-dimensional measurements.
y_axisY-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.

TypeValueOperators
numberfloat>=, <=, >, <, ==, !=, in, not in
stringstring==, !=, in, not in, matches
booleanbool==, !=
multi-dimX 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.

procedure.yaml
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.

procedure.yaml
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.

procedure.yaml
measurements:
  - name: Device Connected
    validators:
      - operator: "=="
        expected_value: true

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

procedure.yaml
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.1
phases/measure_voltage.py
def measure_voltage(measurements, scope):
    times, voltages = scope.capture()
    measurements.output_voltage.x_axis = times
    measurements.output_voltage.y_axis.voltage = voltages

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

OutcomeWhen
PASSEvery validator passes.
FAILAt least one validator fails.
UNSETNo 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.

procedure.yaml
measurements:
  - name: Temperature
    unit: °C
    aggregations:
      - type: mean
        validators:
          - operator: ">="
            expected_value: 20
          - operator: "<="
            expected_value: 80
      - type: std
        validators:
          - operator: "<="
            expected_value: 2.0
phases/measure_temperature.py
import 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.

Aggregations are TofuPilot Framework only. OpenHTF, Pytest, and Robot ignore them.

How is this guide?

On this page