Skip to content
Test Data & Analytics

Set Measurement Limits from Prod Data

Learn how to derive statistically sound measurement limits from production data using the 3-sigma method, detect distribution anomalies, and configure.

JJulien Buteau
intermediate10 min readMarch 14, 2026

Datasheet limits are starting points, not production limits. Real boards have tolerances, assembly variation, and environmental drift that datasheets cannot capture. This guide shows you how to collect measurement data from your production line, apply the 3-sigma method to derive statistically sound limits, and configure those limits in OpenHTF with TofuPilot tracking.

Prerequisites

  • TofuPilot account with production run data
  • Python 3.8+
  • numpy and scipy installed
  • At least 30 test samples (100+ recommended)

Why Datasheet Limits Are Not Enough

Source of VariationExample
Component toleranceResistor +/-5% shifts output voltage
PCB trace impedanceLayout differences across panel positions
Assembly variationSolder joint resistance changes
Environmental driftTemperature at test station vs. field
Measurement noiseProbe contact, cable length

A 3.3V rail with a datasheet range of +/-5% (3.135V-3.465V) may show actual production distribution centered at 3.31V with a std of 0.012V. Setting limits from the datasheet alone misses that your process is drifting high.

Step 1: Collect Baseline Samples

You need a minimum of 30 samples from boards you have already validated as good. Use 100+ for stable sigma estimates.

collect_samples.py
import numpy as np

# Simulated collection from known-good boards
# In practice, export from TofuPilot or accumulate from live runs
vdd_samples = [
    3.312, 3.308, 3.315, 3.310, 3.307,
    3.319, 3.311, 3.314, 3.309, 3.316,
    3.313, 3.308, 3.312, 3.317, 3.310,
    3.306, 3.315, 3.311, 3.313, 3.309,
    3.318, 3.310, 3.312, 3.315, 3.308,
    3.311, 3.314, 3.307, 3.316, 3.313,
    3.310, 3.308, 3.315, 3.312, 3.311,
    3.309, 3.317, 3.314, 3.310, 3.313,
]

n = len(vdd_samples)
mean = np.mean(vdd_samples)
std = np.std(vdd_samples, ddof=1)  # sample std, not population

print(f"N={n}  mean={mean:.4f}V  std={std:.4f}V")
# N=40  mean=3.3119V  std=0.0031V

Use ddof=1 for sample standard deviation when your sample size is less than the full population.

Step 2: Check for Outliers and Bimodal Distributions

Before computing limits, inspect the distribution. A bimodal distribution signals two distinct populations.

check_distribution.py
import numpy as np
from scipy import stats

vdd_samples = [
    3.312, 3.308, 3.315, 3.310, 3.307,
    3.319, 3.311, 3.314, 3.309, 3.316,
    3.313, 3.308, 3.312, 3.317, 3.310,
    3.306, 3.315, 3.311, 3.313, 3.309,
    3.318, 3.310, 3.312, 3.315, 3.308,
    3.311, 3.314, 3.307, 3.316, 3.313,
    3.310, 3.308, 3.315, 3.312, 3.311,
    3.309, 3.317, 3.314, 3.310, 3.313,
]

# Z-score outlier detection
z_scores = np.abs(stats.zscore(vdd_samples))
outliers = [v for v, z in zip(vdd_samples, z_scores) if z > 3]
print(f"Outliers (|z| > 3): {outliers}")

# Shapiro-Wilk normality test
stat, p = stats.shapiro(vdd_samples)
print(f"Shapiro-Wilk p={p:.4f} ({'normal' if p > 0.05 else 'NOT normal'})")

If Shapiro-Wilk returns p < 0.05, investigate whether your samples came from two assembly batches or two component reels.

Step 3: Compute 3-Sigma Limits

The 3-sigma rule covers 99.73% of a normal distribution (roughly 2700 DPMO at the limit edge).

compute_limits.py
import numpy as np

vdd_samples = [
    3.312, 3.308, 3.315, 3.310, 3.307,
    3.319, 3.311, 3.314, 3.309, 3.316,
    3.313, 3.308, 3.312, 3.317, 3.310,
    3.306, 3.315, 3.311, 3.313, 3.309,
    3.318, 3.310, 3.312, 3.315, 3.308,
    3.311, 3.314, 3.307, 3.316, 3.313,
    3.310, 3.308, 3.315, 3.312, 3.311,
    3.309, 3.317, 3.314, 3.310, 3.313,
]

mean = np.mean(vdd_samples)
std = np.std(vdd_samples, ddof=1)

sigma_levels = {
    "3s (99.73%)": 3,
    "4s (99.9937%)": 4,
    "6s (99.99966%)": 6,
}

print(f"Mean:  {mean:.4f} V")
print(f"Std:   {std:.4f} V
")

for label, k in sigma_levels.items():
    lo = mean - k * std
    hi = mean + k * std
    print(f"{label:25s}  low={lo:.4f}V  high={hi:.4f}V")
Sigma LevelCoverageHard limits (VDD example)
3-sigma99.73%3.3026V to 3.3212V
4-sigma99.9937%3.2995V to 3.3243V
6-sigma99.99966%3.2933V to 3.3305V

Step 4: Add Marginal Limits (Warning Zone)

Marginal limits create a warning zone between the soft limit and the hard fail. A board in the marginal zone passes but gets flagged for monitoring.

ZoneRangeResult
Passmean +/- 2-sigmaNormal pass
Marginalmean +/- 2-sigma to mean +/- 3-sigmaPass with warning
Failoutside mean +/- 3-sigmaHard fail

Step 5: Configure Limits in OpenHTF with TofuPilot

vdd_test.py
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot

# Derived from 40 production samples: mean=3.3119V, std=0.0031V
VDD_HARD_LOW      = 3.3026  # mean - 3-sigma
VDD_HARD_HIGH     = 3.3212  # mean + 3-sigma
VDD_MARGINAL_LOW  = 3.3057  # mean - 2-sigma
VDD_MARGINAL_HIGH = 3.3181  # mean + 2-sigma


@htf.measures(
    htf.Measurement("vdd_rail_voltage")
    .in_range(
        minimum=VDD_HARD_LOW,
        maximum=VDD_HARD_HIGH,
        marginal_minimum=VDD_MARGINAL_LOW,
        marginal_maximum=VDD_MARGINAL_HIGH,
    )
    .with_units(units.VOLT)
    .doc("3.3V rail with marginal zone [2-sigma, 3-sigma] for drift monitoring.")
)
def measure_vdd(test):
    voltage = read_voltage_at_tp12()
    test.measurements.vdd_rail_voltage = voltage


def main():
    test = htf.Test(
        measure_vdd,
        test_name="VDD Rail Validation",
    )
    with TofuPilot(test):
        test.execute(test_start=lambda: "SN-001")


if __name__ == "__main__":
    main()

OpenHTF marginal outcome maps to MARGINAL_PASS in TofuPilot. You can filter by this outcome in the dashboard to track drift trends without stopping the line.

Step 6: Track Limit Drift in TofuPilot

Re-run the sigma analysis monthly and compare:

refresh_limits.py
import numpy as np
from datetime import datetime


def compute_sigma_limits(samples: list[float], k: float = 3.0) -> dict:
    arr = np.array(samples)
    mean = np.mean(arr)
    std = np.std(arr, ddof=1)
    return {
        "n": len(arr),
        "mean": round(mean, 6),
        "std": round(std, 6),
        "low": round(mean - k * std, 6),
        "high": round(mean + k * std, 6),
        "sigma": k,
        "computed_at": datetime.utcnow().isoformat(),
    }

Compare new limits to current production limits before deploying. A shift in mean greater than 1-sigma warrants a process investigation.

Review TriggerAction
Mean shift > 1-sigmaInvestigate root cause before updating
Std increase > 20%Check component reel change or station calibration
Marginal rate > 5%Tighten limits or improve process
New sample N > 500Re-evaluate sigma level (consider 4-sigma)

More Guides

Put this guide into practice