Skip to content
Test Data & Analytics

Time-Series Test Data Analysis with TofuPilot

Learn how to capture, store, and analyze time-series measurement data from hardware tests using TofuPilot's multi-dimensional arrays.

JJulien Buteau
intermediate10 min readMarch 14, 2026

Time-Series Test Data Analysis with TofuPilot

Hardware tests produce waveforms, not just single numbers. A power supply ripple test captures thousands of voltage samples over time. A vibration test records acceleration spectra across frequency bands. TofuPilot stores these time-series measurements natively, so you can trend and compare waveforms across your production.

Single Values vs. Time-Series

Most test systems store one number per measurement: "output voltage = 3.31V." But the full story is in the waveform. That 3.31V might be a clean DC signal or a noisy mess that happens to average out to 3.31V.

Data typeExampleWhat it reveals
Single valueVout = 3.31VAverage output level
Time series1000 samples over 10msRipple, noise, transient behavior
Frequency spectrumFFT of output voltageSwitching noise frequency content
Multi-axisX/Y/Z accelerationVibration in all directions

TofuPilot handles all of these through multi-dimensional measurement arrays.

Capturing Time-Series Data

With OpenHTF

Use dimensioned measurements to store time-series data.

ripple_test_openhtf.py
import openhtf as htf
from tofupilot.openhtf import TofuPilotClient
import numpy as np

@htf.measures(
    htf.Measurement("output_ripple_mv")
        .with_dimensions("time_us")
        .with_units("mV"),
    htf.Measurement("ripple_pk_pk_mv")
        .in_range(0, 50)
        .with_units("mV"),
)
def ripple_test(test):
    # Capture 1000 samples at 100kHz (10us spacing)
    waveform = capture_oscilloscope(channel=1, samples=1000, rate_hz=100000)

    for i, sample in enumerate(waveform):
        test.measurements.output_ripple_mv[i * 10] = sample  # time in microseconds

    # Also store the scalar summary
    test.measurements.ripple_pk_pk_mv = max(waveform) - min(waveform)

def main():
    test = htf.Test(ripple_test)
    test.add_output_callbacks(TofuPilotClient())
    test.execute(lambda: "PSU-2025-0099")

With the Python Client

Pass lists or arrays directly.

ripple_test_client.py
from tofupilot import TofuPilotClient
import numpy as np

client = TofuPilotClient()

# Capture waveform from oscilloscope
waveform = capture_oscilloscope(channel=1, samples=1000, rate_hz=100000)
pk_pk = float(np.max(waveform) - np.min(waveform))

client.create_run(
    procedure_id="PSU-RIPPLE-TEST",
    unit_under_test={"serial_number": "PSU-2025-0099"},
    run_passed=pk_pk < 50,
    steps=[{
        "name": "Output Ripple",
        "step_type": "measurement",
        "status": pk_pk < 50,
        "measurements": [
            {
                "name": "ripple_waveform_mv",
                "value": waveform.tolist(),
                "unit": "mV",
            },
            {
                "name": "ripple_pk_pk_mv",
                "value": pk_pk,
                "unit": "mV",
                "limit_high": 50,
            },
        ],
    }],
)

Types of Time-Series Test Data

Voltage/Current Waveforms

Captured from oscilloscopes or DAQs during power-on sequences, ripple tests, or transient response tests. Store the raw waveform plus scalar summaries (peak-to-peak, RMS, rise time).

Temperature Profiles

Recorded during thermal cycling, burn-in, or heat dissipation tests. Multiple sensors produce multi-channel time-series data over minutes or hours.

thermal_profile.py
# Thermal test with 4 temperature sensors sampled every second for 30 minutes
sensors = ["junction", "ambient", "heatsink", "case"]
duration_s = 1800
samples_per_sensor = duration_s  # 1 sample/second

for sensor_name in sensors:
    readings = read_thermal_sensor(sensor_name, samples=samples_per_sensor)
    measurements.append({
        "name": f"temp_{sensor_name}",
        "value": readings,  # list of 1800 values
        "unit": "°C",
    })

Vibration Spectra

FFT data from accelerometers during vibration testing. Frequency on one axis, amplitude on the other.

Pressure/Flow Curves

Time-series pressure and flow measurements during leak tests, pneumatic tests, or hydraulic validation.

Analyzing Time-Series Across Production

The power of storing waveforms (not just scalars) is comparison across units.

Waveform Overlay

Compare ripple waveforms from 100 units. If 99 look the same and one has an extra spike, that unit has a problem that the peak-to-peak measurement alone might not catch.

Statistical Bounds

Calculate the mean and standard deviation of your waveform at each time point across all units. This gives you an envelope of normal behavior. Any unit whose waveform falls outside the envelope is flagged.

waveform_statistics.py
import numpy as np

# All waveforms from production (each is a list of 1000 samples)
all_waveforms = np.array(waveforms_from_tofupilot)  # shape: (N_units, 1000)

mean_waveform = np.mean(all_waveforms, axis=0)
std_waveform = np.std(all_waveforms, axis=0)

upper_bound = mean_waveform + 3 * std_waveform
lower_bound = mean_waveform - 3 * std_waveform

# Check if a new unit's waveform is within bounds
new_waveform = np.array(new_unit_data)
is_anomalous = np.any(new_waveform > upper_bound) or np.any(new_waveform < lower_bound)

Trend Analysis on Waveform Features

Extract features from each waveform (rise time, settling time, overshoot, RMS) and trend them over production. A gradual increase in rise time across units suggests a component or process drift.

Best Practices

PracticeWhy
Store both raw waveform and scalar summariesWaveforms for deep analysis, scalars for dashboards and trending
Use consistent sample ratesComparing waveforms requires the same number of points and timing
Include time axis metadataSample rate or time stamps so the waveform can be reconstructed
Set limits on scalar summariesScalars drive pass/fail, waveforms drive root cause analysis
Limit waveform sizeKeep arrays under 10,000 points per measurement for practical storage

More Guides

Put this guide into practice