A measurement is a named value with units and limits, recorded against a unit. Getting this right is the single highest-leverage decision in a test system, because every analysis you will ever want (yield, capability, drift, Pareto) is computed from measurements and impossible without them.
The Shape of a Measurement
main: - name: Power Rails python: phases.power_rails measurements: - name: Rail 3V3 unit: V description: TP-100 rev B section 3.1 validators: - operator: ">=" expected_value: 3.2 - operator: "<=" expected_value: 3.4def power_rails(measurements, dmm): measurements.rail_3v3 = dmm.read_voltage()Four parts, each doing a job:
| Field | Purpose |
|---|---|
name | Display name, and the source of the Python key |
unit | Displayed everywhere, and prevents ambiguity between mV and V |
description | Where the spec paragraph reference goes |
validators | The limits, stored as data |
The Python key is derived from the name in snake_case: Rail 3V3 becomes rail_3v3. You can set key explicitly if you want a different one.
Numeric Limits
A two-sided range is two validators:
- name: Output Voltage unit: V validators: - operator: ">=" expected_value: 4.75 - operator: "<=" expected_value: 5.25The available numeric operators:
| Operator | Meaning |
|---|---|
== | Equal to |
!= | Not equal to |
> | Greater than |
>= | Greater than or equal |
< | Less than |
<= | Less than or equal |
One-Sided Limits
Plenty of measurements have only one meaningful bound. Leakage current has a maximum; insulation resistance has a minimum. Declare exactly one validator:
- name: Leakage Current unit: uA validators: - operator: "<=" expected_value: 500 - name: Insulation Resistance unit: "MΩ" validators: - operator: ">=" expected_value: 100Do not invent a second limit to make the measurement look symmetric. A fabricated lower bound on leakage current produces a Cpk figure that describes nothing. See CPL and CPU.
String Measurements
- name: Firmware Version validators: - operator: "==" expected_value: "2.1.0"String operators include set membership and regex, which is what you want for serial formats and multi-version acceptance:
| Operator | Expected value | Example |
|---|---|---|
== | String | "PASS" |
!= | String | "ERROR" |
in | Array of strings | ["v1.0.0", "v1.0.1"] |
not in | Array of strings | ["FAIL", "ERROR"] |
matches | Regex | "^SN[0-9]{8}$" |
- name: Accepted Firmware validators: - operator: "in" expected_value: ["2.1.0", "2.1.1", "2.2.0"] - name: Serial Format validators: - operator: "matches" expected_value: "^SN[0-9]{8}$"Boolean Measurements
- name: Lid Closed validators: - operator: "==" expected_value: truedef safety(measurements, fixture): measurements.lid_closed = fixture.lid_closed()Booleans are the right type for genuinely binary facts: an interlock, a link established, a self-test result. They are the wrong type for something that was measured and then thresholded in Python, which is the most common measurement mistake.
Record the Value, Not Your Verdict
def power_rails(measurements, dmm): v = dmm.read_voltage() measurements.rail_ok = 3.2 <= v <= 3.4 # The number is gonedef power_rails(measurements, dmm): measurements.rail_3v3 = dmm.read_voltage() # Keep the numberThe boolean version passes and fails correctly and is worthless a month later. It cannot tell you the rail has drifted from 3.31 to 3.38, cannot produce a Cpk, and cannot justify a limit change. Let the validators do the comparison.
Measurements Without Limits
A measurement with no validators is recorded and reported without a verdict. That is correct for context you want to keep but not judge:
- name: Ambient Temperature unit: "°C" - name: Chamber Setpoint unit: "°C"Useful for HALT steps, environmental context, and any diagnostic value whose acceptable range you do not yet know. Recording them now is what lets you set limits from data later.
Several Measurements in One Phase
A phase records as many measurements as it needs:
procedure.yaml27 lines
- name: Power Checks python: phases.power_checks measurements: - name: Rail 3V3 unit: V validators: - operator: ">=" expected_value: 3.2 - operator: "<=" expected_value: 3.4 - name: Rail 5V0 unit: V validators: - operator: ">=" expected_value: 4.8 - operator: "<=" expected_value: 5.2 - name: Quiescent Current unit: mA validators: - operator: "<=" expected_value: 45 - name: Boot Time unit: ms validators: - operator: "<=" expected_value: 2000def power_checks(measurements, dmm, dut): measurements.rail_3v3 = dmm.dc_voltage(1) measurements.rail_5v0 = dmm.dc_voltage(2) measurements.quiescent_current = dmm.dc_current_ma() measurements.boot_time = dut.boot_time_ms()The phase fails if any of its measurements fail, and the failure message names which one.
Measurements From Operator Input
A value an operator types is still a measurement. Bind the UI component directly to it:
- name: Torque Check python: phases.torque_check ui: components: - key: torque type: number_input label: "Screw torque (Nm)" required: true bind: measurements.screw_torque measurements: - name: Screw Torque unit: Nm validators: - operator: ">=" expected_value: 0.45 - operator: "<=" expected_value: 0.55A manually recorded torque then trends on a control chart like any instrument reading.
Naming Is Permanent
The name is the identity of the measurement across every run you will ever take. Renaming splits the history and breaks every trend spanning the change.
| Poor name | Better | Why |
|---|---|---|
voltage | rail_3v3 | Which voltage? |
current1 | quiescent_current | Meaningless in six months |
test2 | boot_time | Describes nothing |
temp | chamber_temperature | Ambient, chamber, or die? |
Specific names also survive the test growing a second, third and fourth voltage.
If You Already Run OpenHTF
The same concepts, expressed with decorators:
fct.py26 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures( htf.Measurement("rail_3v3") .in_range(3.2, 3.4) .with_units(units.VOLT) .doc("TP-100 rev B section 3.1"), htf.Measurement("leakage_current_uA") .in_range(maximum=500) .with_units(units.MICROAMPERE), htf.Measurement("firmware_version").equals("2.1.0"), htf.Measurement("lid_closed").equals(True),)def phase_checks(test): test.measurements.rail_3v3 = 3.31 test.measurements.leakage_current_uA = 85.0 test.measurements.firmware_version = "2.1.0" test.measurements.lid_closed = Truetest = htf.Test(phase_checks, part_number="PCBA-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan serial: "))Install with pip install "tofupilot[openhtf]", or run it with tofupilot run.
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Thresholding in Python into a boolean | The number is lost; no Cpk, no drift | Record the value, declare validators |
| Inventing a second limit for symmetry | Meaningless capability figures | One validator for one-sided limits |
| Generic names | Ambiguous in six months, unscalable | Specific names chosen once |
| Renaming later | History splits, trends break | Treat names as permanent |
| Omitting units | mV and V confusion on a real bench | Always set unit |
| Discarding operator-entered values | Manual readings never trend | bind: measurements.<key> |
No description | Nobody can find the governing spec paragraph | Reference the document section |
