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 type | Example | What it reveals |
|---|---|---|
| Single value | Vout = 3.31V | Average output level |
| Time series | 1000 samples over 10ms | Ripple, noise, transient behavior |
| Frequency spectrum | FFT of output voltage | Switching noise frequency content |
| Multi-axis | X/Y/Z acceleration | Vibration 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.
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.
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 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.
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
| Practice | Why |
|---|---|
| Store both raw waveform and scalar summaries | Waveforms for deep analysis, scalars for dashboards and trending |
| Use consistent sample rates | Comparing waveforms requires the same number of points and timing |
| Include time axis metadata | Sample rate or time stamps so the waveform can be reconstructed |
| Set limits on scalar summaries | Scalars drive pass/fail, waveforms drive root cause analysis |
| Limit waveform size | Keep arrays under 10,000 points per measurement for practical storage |