Part I: Foundations of Sensory AI
Chapter 2: Sensor Physics and Measurement Models

Bias, drift, hysteresis, saturation

"Noise I can average away. Bias just lies to me politely, drift lies a little more each day, hysteresis lies about where it has been, and saturation stops answering the question entirely."

A Weary AI Agent

The big picture

The previous section treated the error term \(\eta\) as random noise, the kind that shrinks when you average. This section is about the errors that do not shrink. Bias, drift, hysteresis, and saturation are systematic distortions: they are baked into the sensor response \(h\) itself, not sprinkled on top of it. Averaging a thousand readings from a biased sensor gives you a very precise wrong answer. These four failure modes are the reason a model that scored perfectly in the lab can quietly rot in the field, and recognizing them by their fingerprints is a core skill for anyone who ships perception on real hardware.

Recall the measurement model from Chapter 1, \(x = h(s) + \eta\). Section 2.3 put all sensor imperfection into \(\eta\) and showed how to beat it down with signal-to-noise ratio. That framing hides a trap: it suggests every error is zero-mean and independent, so more data always helps. It is not, and it does not. The four phenomena in this section are deformations of \(h\) with structure and memory. You need only functions, a little calculus, and the probability intuitions that Chapter 4 makes rigorous. We take each in turn, name its fingerprint, and say what to do about it.

Bias: a constant lie

What it is. Bias (also called offset) is a constant additive error. The response is really \(x = h(s) + b + \eta\), where \(b\) does not depend on the state \(s\) or on time. A scale that reads 0.3 kg with nothing on it has a bias of 0.3 kg; every reading is inflated by the same amount.

Why it earns its own name. Because it survives everything you would normally do to clean a signal. It is not zero-mean, so time-averaging preserves it exactly; it is not high-frequency, so a low-pass filter passes it straight through. Bias attacks accuracy (closeness to truth) while leaving precision (repeatability) untouched, so a biased sensor can look reassuringly stable and be reliably wrong. In estimation terms it inflates the estimator bias while doing nothing to variance, which is exactly the axis that more samples cannot fix.

How to handle it. Measure it and subtract it. A one-time calibration under a known reference (a zeroing step, a tare) yields \(\hat b\), and you report \(x - \hat b\). The whole reason zeroing a scale before weighing works is that bias is stationary: what you measure at reference time still holds at measurement time.

Drift: bias that will not sit still

What it is. Drift is bias that changes slowly with time, temperature, or age: \(b \to b(t)\). Two flavors dominate. Deterministic drift follows a trend, often roughly linear, \(b(t) \approx b_0 + \beta t\), as a chemical electrode ages or a component warms up. Stochastic drift is a random walk, \(b(t{+}1) = b(t) + w_t\) with small independent steps \(w_t\); it has no trend to extrapolate, yet it wanders unboundedly far given enough time.

Why it is worse than bias. A one-time calibration expires. Whatever \(\hat b\) you measured this morning is stale by afternoon, and the staleness grows without bound for the random-walk kind. This is the phenomenon that makes recalibration schedules a line item and makes uncorrected inertial navigation hopeless: integrating a drifting gyroscope bias turns a tiny rate error into an angle error that grows linearly, then a position error that grows as time squared.

In practice: the gyroscope that flew the drone into a wall

A delivery drone holds heading between GPS fixes using a MEMS gyroscope, integrating angular rate into orientation. On the bench the gyro reads a steady \(0.6^\circ/\text{s}\) with the drone sitting perfectly still: a pure bias. The flight team zeroes it at power-on and forgets it. But the bias drifts with the electronics warming in flight, adding perhaps \(0.4^\circ/\text{s}\) over ten minutes. Integrated, that unmodeled drift accrues into tens of degrees of heading error, and dead reckoning walks the estimated position steadily away from the true one until the next GPS fix snaps it back, or until there is no next fix indoors and the drone clips a loading-dock pillar. The fix is not a better one-time calibration; it is to estimate the bias online as part of the state, which is precisely what the augmented-state filters of Chapter 24 do.

Key insight

Bias is a stationary offset you calibrate once; drift is a non-stationary offset you must track. The moment an error term has memory, the right home for it is not a preprocessing constant but a slot in the estimator's state vector, where a recursive filter can chase it. This is the conceptual bridge from static calibration to the Kalman family: model the drift as a slow random walk and let the filter separate it from the signal you actually want.

Hysteresis: the sensor remembers where it came from

What it is. Hysteresis is path dependence: the output at a given state \(s\) differs depending on whether \(s\) was approached from below or from above. Plot output against a slowly rising-then-falling input and you get a loop, not a line. Ferromagnetic sensors, many humidity sensors, mechanical strain gauges, and any component with internal friction or material relaxation show it. The gap between the rising and falling curves, expressed as a fraction of full scale, is the hysteresis error.

Why it breaks naive correction. Bias and even drift are functions the current input can undo, but hysteresis makes the output a function of the input's history. There is no single number, and no memoryless calibration curve \(g(x)\), that inverts it, because two different true states legitimately produce the same reading. A humidity sensor climbing from 30% to 60% and one falling from 90% to 60% can both display 58%. Any correction has to know the direction of travel, which means it needs memory, which pushes you again toward a stateful model rather than a lookup table.

Common Misconception

"I fit a calibration polynomial, so the sensor is corrected." A memoryless polynomial \(g(x)\) can undo a nonlinear but single-valued response, and it can undo a fixed bias, but it cannot undo hysteresis, because hysteresis is not a function of the current reading at all. Fitting one curve through a hysteresis loop splits the difference and leaves a direction-dependent residual that changes sign depending on whether the measurand is rising or falling. That sign flip is the tell.

Saturation: when the sensor stops listening

What it is. Every real sensor has a finite range (revisit Section 2.2 on dynamic range). Past the top or bottom of that range the response flattens: the output clips at \(x_{\max}\) no matter how large the true state grows. Formally \(x = \min(h(s), x_{\max})\) at the top rail, with the analogous floor at the bottom. An accelerometer rated to \(\pm 16g\) reports \(16g\) for a \(40g\) impact, and reports it with perfect, useless steadiness.

Why it is uniquely dangerous. Saturation destroys information rather than distorting it, and it does so silently. A clipped reading is not merely inaccurate; it is censored, an inequality (\(s \ge s_{\text{sat}}\)) masquerading as an equality. Worse, saturation is most likely exactly when the state is most extreme, which is often exactly when it matters most: the crash sensor rails out during the crash, the microphone clips during the gunshot, the current sensor pins during the fault. A model trained on nicely-in-range lab data has never seen the flat top and will read it as a genuine plateau. The honest response is to flag saturated samples as censored and let downstream inference treat them as bounds, a theme picked up when we discuss missing and corrupted data in Chapter 5.

Seeing all four in one signal

These distortions rarely arrive alone. The short simulation below stacks bias, linear-plus-random-walk drift, and hard saturation onto a clean sinusoid so you can watch each fingerprint appear. It is the seed of Lab 2, where you will quantify how each one degrades a classifier and an anomaly detector.

import numpy as np
rng = np.random.default_rng(0)
t = np.linspace(0, 20, 2000)
s = 3.0 * np.sin(0.5 * t)              # clean measurand h(s)

bias  = 0.8                            # constant offset b
drift = 0.05 * t                       # deterministic ramp
drift += np.cumsum(rng.normal(0, 0.01, t.size))   # random-walk drift
noise = rng.normal(0, 0.10, t.size)    # ordinary zero-mean noise

x = s + bias + drift + noise           # systematic errors do NOT average away
x_sat = np.clip(x, -3.5, 3.5)          # saturation: censors the extremes
censored = (x_sat != x)                # flag saturated samples as bounds

print(f"true mean {s.mean():+.3f}   observed mean {x_sat.mean():+.3f}")
print(f"samples lost to saturation: {censored.sum()} of {t.size}")
Injecting bias, compound drift, and saturation onto a sinusoid. The printed means show that even after ideal averaging the observed mean is offset from the truth (bias and drift do not cancel), and the censored mask records which samples are inequalities rather than measurements. Feed x_sat and censored into Lab 2 to measure the accuracy each error mode costs.

The right tool: online drift estimation without hand-rolling a filter

Tracking a random-walk bias by hand means writing the predict/update recursion, tuning process and measurement variances, and validating the whole thing: roughly fifty lines before it works. A maintained filtering library collapses that to a few:

from filterpy.kalman import KalmanFilter
kf = KalmanFilter(dim_x=2, dim_z=1)    # state = [signal, slow bias]
# ... set F, H, Q (small on bias), R once ...
for z in x_sat:                        # skip z where censored[k] is True
    kf.predict(); kf.update(z)         # kf.x[1] is the tracked drift estimate
Estimating slowly drifting bias online with filterpy. Modeling the bias as a second state with tiny process noise lets the filter separate the wandering offset from the signal. The library supplies the numerically-stable recursion and covariance bookkeeping you would otherwise write and debug; you provide only the model matrices. About fifty lines become five.

Exercise

Take the simulation above and, for each of the four phenomena, design a one-line diagnostic that detects it from data alone:

  1. Bias: what statistic of the residual against a known reference reveals a nonzero constant offset?
  2. Drift: how would you test whether the residual has a trend rather than a fixed offset? (Hint: split the record in half.)
  3. Hysteresis: sweep the input up then down over the same range; what comparison between the two passes exposes the loop?
  4. Saturation: what pattern in the histogram of \(x\) betrays a clipped rail?
Hint

Saturation piles probability mass into a single bin at the rail value, so the histogram shows an anomalous spike at \(x_{\max}\). Drift shows up as a difference in mean residual between the first and second halves of the record; a pure bias shows the same nonzero residual in both halves.

Self-check

  1. You average ten thousand readings and the estimate is still off by a fixed amount. Which of the four phenomena is ruled out, and which two are the leading suspects?
  2. Why can a memoryless calibration curve \(g(x)\) correct a fixed bias but never correct hysteresis?
  3. A crash sensor reads exactly \(16g\) for a full 20 ms during an impact. Why is treating that as "the acceleration was 16g" a dangerous mistake, and what should the value be treated as instead?

What's Next

In Section 2.5, we formalize the response \(h\) itself as a transfer function and characterize how fast a sensor reacts: rise time, settling time, and the lag that turns a sharp real event into a smeared reading. Bias, drift, hysteresis, and saturation deform what the sensor reports; response time governs when it reports it, and the two together complete the static-plus-dynamic picture of \(h\) that the rest of the book inverts.