Sensor Calibration at Scale with TofuPilot
Calibrating 10 sensors is a manual job. Calibrating 10,000 requires a system. You need multi-point reference measurements, pass/fail against tolerance bands, calibration certificates, and traceability back to reference standards.
TofuPilot handles the data side so you can focus on the calibration procedure itself.
What Sensor Calibration Involves
Production sensor calibration typically follows this flow:
- Apply known reference stimuli (temperature, pressure, force, etc.)
- Read the sensor output at each reference point
- Calculate error, linearity, and hysteresis
- Apply correction factors if needed
- Verify corrected output meets specifications
- Generate a calibration certificate
Prerequisites
- Python 3.8+ with
openhtfandtofupilotinstalled - A reference standard (calibrated source or reference sensor)
- A data acquisition system or instrument to read sensor output
Step 1: Define Multi-Point Calibration Measurements
A typical calibration uses 5 to 11 reference points across the sensor's range. Define measurements for each point:
import openhtf as htf
from openhtf.util import units
CAL_POINTS = [0, 25, 50, 75, 100] # Percent of full scale
measures = []
for pct in CAL_POINTS:
measures.append(
htf.Measurement(f"error_at_{pct}pct")
.with_units(units.PERCENT)
.in_range(-0.5, 0.5)
.doc(f"Measurement error at {pct}% of full scale")
)
measures.append(
htf.Measurement("max_linearity_error")
.with_units(units.PERCENT)
.at_most(0.25)
.doc("Maximum linearity deviation from best-fit line")
)
measures.append(
htf.Measurement("hysteresis")
.with_units(units.PERCENT)
.at_most(0.1)
.doc("Maximum hysteresis between up and down sweep")
)
@htf.measures(*measures)
def calibration_test(test, reference_source, sensor_reader):
"""Run multi-point calibration with up and down sweep."""
full_scale = 100.0 # Adjust for your sensor range
up_readings = {}
down_readings = {}
# Up sweep
for pct in CAL_POINTS:
ref_value = full_scale * pct / 100.0
reference_source.set_output(ref_value)
time.sleep(2.0) # Settle time
reading = sensor_reader.read()
up_readings[pct] = reading
error = (reading - ref_value) / full_scale * 100
setattr(test.measurements, f"error_at_{pct}pct", error)
# Down sweep for hysteresis
for pct in reversed(CAL_POINTS):
ref_value = full_scale * pct / 100.0
reference_source.set_output(ref_value)
time.sleep(2.0)
reading = sensor_reader.read()
down_readings[pct] = reading
# Hysteresis: max difference between up and down readings
max_hyst = max(
abs(up_readings[p] - down_readings[p]) / full_scale * 100
for p in CAL_POINTS
)
test.measurements.hysteresis = max_hyst
# Linearity: deviation from best-fit line
import numpy as np
ref_vals = [full_scale * p / 100.0 for p in CAL_POINTS]
read_vals = [up_readings[p] for p in CAL_POINTS]
coeffs = np.polyfit(ref_vals, read_vals, 1)
fit_vals = np.polyval(coeffs, ref_vals)
linearity_errors = [(r - f) / full_scale * 100 for r, f in zip(read_vals, fit_vals)]
test.measurements.max_linearity_error = max(abs(e) for e in linearity_errors)Step 2: Store Calibration Curves as Multi-Dimensional Data
Capture the full calibration curve, not just pass/fail:
import openhtf as htf
from openhtf.util import units
@htf.measures(
htf.Measurement("calibration_curve")
.with_dimensions(units.PERCENT)
.doc("Full calibration curve: reference input vs sensor output"),
htf.Measurement("correction_curve")
.with_dimensions(units.PERCENT)
.doc("Correction factors at each calibration point"),
)
def capture_calibration_curve(test, reference_source, sensor_reader):
"""Capture dense calibration curve for post-processing."""
full_scale = 100.0
for pct in range(0, 101, 5): # 5% increments
ref_value = full_scale * pct / 100.0
reference_source.set_output(ref_value)
time.sleep(1.0)
reading = sensor_reader.read()
test.measurements.calibration_curve[pct] = reading
# Correction factor: what to add to get the true value
correction = ref_value - reading
test.measurements.correction_curve[pct] = correctionTofuPilot stores the full curve. You can compare curves across sensors, detect batch variations, and track calibration drift over time.
Step 3: Track Reference Standard Traceability
Every calibration is only as good as its reference. Track which reference standard was used:
from tofupilot import TofuPilotClient
client = TofuPilotClient()
result = client.create_run(
procedure_id="pressure-sensor-cal",
unit_under_test={
"serial_number": sensor_sn,
"part_number": "PS-500-A",
},
run_passed=True,
properties={
"reference_standard": "FLUKE-8845A-SN12345",
"reference_cal_date": "2026-01-15",
"reference_cal_due": "2027-01-15",
"reference_cert_number": "CAL-2026-0042",
"ambient_temp_c": 23.1,
"ambient_humidity_pct": 45,
"operator": "OP-003",
},
)When an auditor asks about your traceability chain, every calibration run links to the reference standard, its own calibration certificate, and the environmental conditions during the procedure.
Step 4: Manage Recalibration Schedules
Sensors drift. Track when each unit was last calibrated and when it's due:
from tofupilot import TofuPilotClient
from datetime import date, timedelta
client = TofuPilotClient()
# Get all calibration runs for a sensor type
cal_runs = client.get_runs(
procedure_id="pressure-sensor-cal",
limit=5000,
)
# Find latest calibration per serial number
latest_cal = {}
for run in cal_runs:
sn = run.unit.serial_number
if sn not in latest_cal or run.started_at > latest_cal[sn].started_at:
latest_cal[sn] = run
# Check for overdue recalibrations (12-month interval)
cal_interval = timedelta(days=365)
today = date.today()
overdue = []
upcoming = []
for sn, run in latest_cal.items():
cal_date = run.started_at.date()
due_date = cal_date + cal_interval
if due_date < today:
overdue.append((sn, due_date))
elif due_date < today + timedelta(days=30):
upcoming.append((sn, due_date))
print(f"Overdue: {len(overdue)} sensors")
print(f"Due within 30 days: {len(upcoming)} sensors")Step 5: Detect Calibration Drift
Compare calibration results over time to catch sensors that are drifting toward their tolerance limits:
from tofupilot import TofuPilotClient
client = TofuPilotClient()
# Get calibration history for a specific sensor
cal_history = client.get_runs(
procedure_id="pressure-sensor-cal",
unit_serial_number="PS-500-A-0042",
limit=10,
)
print(f"Calibration history for PS-500-A-0042:")
print(f"{'Date':<12} {'Error@50%':<12} {'Linearity':<12} {'Status'}")
print("-" * 48)
for run in reversed(cal_history):
error_50 = run.measurements.get("error_at_50pct", {}).get("value", "N/A")
linearity = run.measurements.get("max_linearity_error", {}).get("value", "N/A")
status = "PASS" if run.passed else "FAIL"
print(f"{run.started_at.date()!s:<12} {error_50:<12} {linearity:<12} {status}")If the error at 50% is 0.1% this year and was 0.05% last year, you know which direction it's heading. Replace or adjust before it fails.