Multi-Dimensional
Last updated on October 28, 2026
Multi-dimensional Measurement: Output Voltage
X axis: Time (ms), 0..200
Y axis: Voltage (V), rises 0->~3.1, holds ~3.1, falls ->0 (pulse waveform)
Outcome: PASSYou can create a multi-dimensional measurement with measurements:
measurements:
- name: Output Voltage
title: Output Voltage
x_axis:
legend: Time
unit: ms
y_axis:
- legend: Voltage
unit: VYou can set the measurement value in your Python phase using the builder pattern:
def measure_voltage(measurements, dmm):
times = []
voltages = []
for t in range(0, 201, 5):
times.append(t)
voltages.append(dmm.read_voltage())
measurements.output_voltage.x_axis = times
measurements.output_voltage.y_axis.voltage = voltages Definition
Measurement
You can configure the measurement name and metadata.
| Property | Type | Required | Description |
|---|---|---|---|
key | string | No | Unique identifier for the measurement within the phase |
name | string | Yes | Display name (1-100 characters) |
description | string | No | Detailed description (max 50,000 characters) |
title | string | No | Chart title |
x_axis | object | Yes | X-axis specification |
y_axis | array | Yes | Array of Y-axis specifications (one or more) |
TofuPilot auto-generates the key from name (snake_case) if not specified, and uses it for Python access (e.g., measurements.frequency_response).
Axis
Each axis (x_axis or y_axis item) has the following properties:
| Property | Type | Required | Description |
|---|---|---|---|
key | string | No | Python identifier for axis (auto-generated from legend if not set) |
legend | string | No | Axis label for chart legend |
unit | string | No | Axis unit (e.g., "Hz", "V", "dB") |
description | string | No | Axis description |
validators | array | No | Validators for axis data |
aggregations | array | No | Aggregations for axis data |
TofuPilot matches the y axes your phase records to the declared ones by key. Series without a key are matched by position, when none of the recorded series has one. Mixing keyed and keyless series is reported in the phase log and nothing is matched: the declared validators don't apply, each series is checked only against the validators it carries itself.
A declared multi-dimensional measurement the phase never records is uploaded as an empty measurement. The limits declared on its axes and their aggregations are carried on the measurement itself, FAIL, so the run page shows which limit had no value to meet. The same goes for a single declared axis the phase left unrecorded: an unrecorded y axis keeps its failed limits beside the x axis, and an unrecorded x axis carries them on the measurement.
Validators
You can add validators to each axis to check individual data points.
y_axis:
- legend: Voltage
unit: V
validators:
- operator: "=="
expected_value: [3.0, 3.1, 3.0, 3.1] Axis validators only support the == and != operators with an array of expected values matching the data length. TofuPilot will compare each data point against the corresponding expected value and set the outcome to FAIL if any point fails.
Any other operator (<=, >, in, …) can't judge a series. The validator is left UNSET, the measurement with it, and the phase log names the operator. Put that kind of limit on an aggregation instead.
Aggregations
You can add aggregations to each axis to validate statistical properties.
y_axis:
- legend: Voltage
unit: V
aggregations:
- type: mean
validators:
- operator: ">="
expected_value: 2.9
- operator: "<="
expected_value: 3.1TofuPilot will validate the aggregation value against the specified criteria. You must set the aggregation value from Python. See Aggregations for details.
The measurement outcome comes from the validators on every axis and on their aggregations: FAIL if any fails or has no data to check, PASS if all passed or there are none on recorded data, UNSET if one couldn't run or no axis has data. See Outcome.
Examples
Multi-Channel Waveform
We will capture waveforms from multiple channels simultaneously:
name: Oscilloscope Capture Test
plugs:
- key: scope
name: Oscilloscope
python: plugs.scope:Scope
main:
- name: Waveform Capture
python: capture_waveform
measurements:
- name: Output Waveform
description: Dual-channel oscilloscope capture
title: Output Waveform
x_axis:
legend: Time
unit: ms
y_axis:
- legend: Channel 1
key: channel_1
unit: V
- legend: Channel 2
key: channel_2
unit: Vdef capture_waveform(measurements, log, scope):
log.info("Capturing waveform")
scope.trigger()
data = scope.get_waveform()
measurements.output_waveform.x_axis = data['time']
measurements.output_waveform.y_axis.channel_1 = data['channel1']
measurements.output_waveform.y_axis.channel_2 = data['channel2']
log.info(f"Captured {len(data['time'])} samples")Combining Validators and Aggregations
We will measure voltage over time and validate both individual readings and overall statistics:
name: Power Supply Test
plugs:
- key: dmm
name: Digital Multimeter
python: plugs.dmm:Dmm
main:
- name: Voltage Monitoring
python: measure_voltage
measurements:
- name: Output Voltage
description: Voltage monitoring with range and statistical validation
title: Output Voltage
x_axis:
legend: Time
unit: ms
y_axis:
- legend: Voltage
unit: V
aggregations:
- type: min
validators:
- operator: ">="
expected_value: 0
- type: max
validators:
- operator: "<="
expected_value: 5
- type: mean
validators:
- operator: ">="
expected_value: 2.9
- operator: "<="
expected_value: 3.1def measure_voltage(measurements, log, dmm):
log.info("Monitoring voltage over time")
times = []
voltages = []
for t in range(0, 201, 5):
times.append(t)
voltages.append(dmm.read_voltage())
measurements.output_voltage.x_axis = times
measurements.output_voltage.y_axis.voltage = voltages
aggs = measurements.output_voltage.y_axis.voltage.aggregations
aggs.min = min(voltages)
aggs.max = max(voltages)
aggs.mean = sum(voltages) / len(voltages)
log.info(f"Captured {len(times)} samples")Range validation (0-5V) is done via min/max aggregations since axis validators only support == and !=. The mean aggregation validates overall statistics (2.9-3.1V). The measurement fails if any aggregation validator fails.
How is this guide?