"The sensor did not lie to me. It simply kept reporting the last true thing it ever knew, and I believed it for six more hours."
A Trusting AI Agent
Prerequisites
This section assumes the sensor measurement model \(z = h(x) + b + \varepsilon\), noise, bias, and transduction, from Chapter 2, the sampling, timestamping, and synchronization vocabulary of Chapter 3, and the residual and change-detection machinery of Chapter 12. We treat a working perception model as given. This section is not about the environment shifting under a healthy sensor (that is Chapter 66); it is about the sensor itself becoming an unreliable narrator, and about naming the failure precisely enough that a monitor can catch it before the model does something irreversible.
The Big Picture
A perception stack is only as trustworthy as its worst input, and inputs fail in structured, nameable ways. The dangerous failures are not the loud ones. A sensor that returns zeros or NaNs is easy to catch; a sensor that returns plausible values that are quietly wrong is the one that propagates into a control action. This section builds a taxonomy of sensor fault modes, separates abrupt failures (dropout, stuck-at, saturation) from slow degradation (bias drift, gain loss, noise inflation), and ties each to the term of the measurement equation it corrupts. That mapping is what makes a fault detectable: once you know a stuck accelerometer freezes the innovation of your filter and a drifting gyro biases it, you know exactly which statistic to monitor. Everything later in this chapter, redundancy, runtime monitors, safety cases, presupposes this vocabulary.
A taxonomy grounded in the measurement equation
Start from the Chapter 2 model: a sensor reports \(z_t = g \cdot h(x_t) + b_t + \varepsilon_t\), where \(h\) is the transduction of the true state \(x_t\), \(g\) is a gain, \(b_t\) a bias, and \(\varepsilon_t\) zero-mean noise. Every fault mode is a specific corruption of one of these terms, and organizing faults this way is not pedantry: it tells you which residual will move.
- Dropout / erasure: \(z_t\) is missing or a sentinel (NaN, zero, last-value-held). The transduction \(h\) is severed. Cause: cable fault, packet loss on a CAN or MIPI bus, a MEMS die crack, power brown-out.
- Stuck-at (frozen): \(z_t = z_{t^\*}\) for all \(t > t^\*\), the value is constant regardless of \(x_t\). This is the treacherous one, because a frozen value inside the normal range passes every range check. Cause: a hung sensor microcontroller that keeps re-serving its last register, a firmware deadlock, an ADC latch.
- Bias / offset fault: \(b_t\) jumps or ramps away from zero. A step bias is a calibration loss; a ramp is drift. Cause: thermal expansion, MEMS charge accumulation, aging of a reference voltage.
- Gain / scale fault: \(g\) departs from unity, so the sensor under- or over-reports proportionally. Cause: aging photodiode responsivity, lens transmission loss, membrane fatigue in a pressure sensor.
- Noise inflation: \(\mathrm{Var}(\varepsilon_t)\) grows. The mean is right but precision collapses. Cause: loose connector, EMI, a failing pre-amp, thermal noise near end-of-life.
- Saturation / clipping: \(z_t\) is pinned at a rail because \(h(x_t)\) exceeded the dynamic range. Cause: an accelerometer set to \(\pm 2g\) during an \(8g\) impact, an over-exposed pixel well.
- Quantization and timing faults: dropped bits, or timestamps that are late, jittered, or duplicated, corrupting the \(t\) index itself. These break the synchronization assumptions of Chapter 3 and are invisible to any check that looks only at \(z\), never at when \(z\) arrived.
The single most useful axis across this list is abrupt versus incipient. Abrupt faults (dropout, stuck-at, saturation) arrive between one sample and the next and are, once you know to look, statistically obvious. Incipient faults (bias drift, gain loss, noise inflation) develop over hours to months and hide inside the noise band until they don't. Different detectors catch each: a per-sample residual gate catches the abrupt, a slow cumulative statistic catches the incipient.
Key Insight
A stuck sensor is more dangerous than a dead one. A dead sensor announces itself, zeros, NaNs, a lost heartbeat, and your fault logic fires immediately. A stuck sensor keeps serving a value that is in-range, self-consistent, and low-variance, which is exactly what a naive health check calls "healthy." The tell is not the value, it is the absence of variation where variation is expected: the innovation of a Kalman filter fed a frozen measurement collapses toward a constant, and the signal's short-window variance drops to the noise floor even when the platform is clearly moving. Monitor for a sensor that is too quiet, not only for one that is out of range.
Modeling degradation over time
Incipient faults deserve a model, because their whole hazard is that they are gradual. A useful and standard first-order picture treats a slowly corrupting parameter, say the bias \(b_t\), as a drift-plus-diffusion process:
$$ b_t = b_0 + r\,t + \sigma_w W_t, $$where \(r\) is a deterministic drift rate (units per hour), \(W_t\) is a random walk capturing the stochastic wander of the fault, and \(\sigma_w\) its volatility. This is exactly the "bias instability" you find on a MEMS gyro datasheet, and it is why an uncorrected inertial sensor's dead-reckoned position error grows super-linearly (the orientation and dead-reckoning error budget of Chapter 24). Two summary numbers govern how long a sensor stays usable: the drift rate \(r\), which sets when the bias exceeds a tolerance \(\tau\) in the mean (at time \(t \approx \tau / r\)), and the signal-to-noise trend. A convenient health scalar is the ratio of fault magnitude to noise scale,
$$ \mathrm{SNR}_{\text{fault}}(t) = \frac{|b_t|}{\sigma_\varepsilon}, $$which starts below one (the fault is buried in noise, undetectable and, usefully, still harmless) and crosses a detection threshold and then a safety threshold as the fault grows. The gap between those two crossings is your warning budget. Prognostics, estimating the remaining time before a sensor crosses the safety threshold, is the subject of predictive maintenance in Chapter 36; here we care that the model exists and tells us which statistic to watch.
Step-Through: the automotive radar that faded before it failed
An automated-driving stack fuses camera, lidar, and a front radar for adaptive cruise. Over one winter, road grime and a hairline crack in the radome slowly attenuate the radar's return: a gain fault, \(g\) drifting downward. Nothing trips a range alarm, the radar still returns targets, just weaker, so its detections thin out at the edges of range first. The fusion layer, trusting each modality equally, begins dropping distant vehicles that only the radar had been confirming. The failure is invisible per-frame; it shows up only as a slow rise in the disagreement between radar range and lidar range on shared targets, a cross-modal residual. The team's fix was not a better radar model but a degradation monitor: track the running median ratio of radar-confirmed to lidar-confirmed detections, and when it falls below a learned band, downweight radar in the fuser and raise a maintenance flag. That cross-sensor consistency check is the natural detector for gain and bias faults, and it is why missing-modality robustness (Chapter 50) and this section are two halves of the same discipline.
Detecting and injecting faults
Detection follows directly from the taxonomy: pick the statistic the fault is guaranteed to move. Range and rate-of-change gates catch saturation and impossible jumps. A short-window variance floor catches stuck-at and, inverted, catches noise inflation. A cross-sensor or model-residual test catches bias and gain, because those leave the single sensor self-consistent and only reveal themselves against a second opinion. The code below implements the three cheapest, highest-value gates as a single reusable checker, the kind of guard that belongs at the very front of any sensor pipeline.
import numpy as np
def sensor_health(window, lo, hi, max_step, stuck_var, sat_frac=0.02):
"""Fault flags for one channel over a recent sample window.
window: 1-D array of recent samples (oldest to newest).
Returns a dict of boolean fault indicators. Each maps to one
corruption of z = g*h(x) + b + eps."""
w = np.asarray(window, dtype=float)
flags = {}
flags["dropout"] = bool(np.isnan(w).any()) # erasure / missing h
finite = w[np.isfinite(w)]
if finite.size < 2:
flags["stuck"] = flags["saturated"] = flags["out_of_range"] = True
flags["jump"] = False
return flags
flags["out_of_range"] = bool((finite < lo).any() or (finite > hi).any())
flags["jump"] = bool(np.abs(np.diff(finite)).max() > max_step) # impossible slew
flags["stuck"] = bool(np.var(finite) < stuck_var) # too quiet -> frozen
rail_hits = (np.abs(finite - lo) < 1e-6) | (np.abs(finite - hi) < 1e-6)
flags["saturated"] = bool(rail_hits.mean() > sat_frac) # pinned at a rail
return flags
# A gyro channel that has silently frozen at a plausible in-range value.
frozen = np.full(64, 0.031) + 1e-9 * np.random.randn(64)
print(sensor_health(frozen, lo=-4.0, hi=4.0, max_step=0.5, stuck_var=1e-4))
# -> {'dropout': False, 'out_of_range': False, 'jump': False,
# 'stuck': True, 'saturated': False}
dropout and out_of_range are range checks, jump is a rate-of-change (slew) gate for impossible transitions, stuck is the "too quiet" test that catches a frozen sensor a range check misses, and saturated counts rail hits. The example shows the case the introduction warned about: an in-range, self-consistent, frozen gyro that only the variance-floor test catches.Code 68.1.1 makes the key insight operational: the stuck flag is the only one that fires on the frozen gyro, precisely because its value is in-range and its jumps are zero. But you cannot trust a detector you have never seen trip, which is why the other half of this discipline is fault injection: deliberately corrupting healthy recordings with each mode, dropout, bias ramps, stuck-at, saturation, timing jitter, and confirming both that the monitor fires and that the downstream model degrades gracefully. This is exactly what Lab 68 builds. Fault injection turns the taxonomy from a checklist into a test suite, and it is the only honest way to know your safety envelope before the field finds the hole for you (leakage-safe evaluation discipline from Chapter 5 applies: inject on held-out recordings, never on training data).
Right Tool: fault streams without the plumbing
Hand-rolling a full fault-injection harness, per-mode corruptions, configurable onset times and rates, reproducible seeding, and a logger that records which fault was injected when, is roughly 150 lines of careful, easy-to-get-subtly-wrong code. Robustness-testing libraries such as imagecorruptions (the ImageNet-C corruption set) for vision, and time-series toolkits like tsaug or the perturbation utilities in aeon and river for streams, provide parameterized corruptions and drift injectors out of the box, collapsing the harness to about 15 lines: choose a corruption, set severity, map over the stream. That is a roughly 150-to-15 reduction, with reproducible severity levels and standard corruption names you can cite in a safety case (Section 68.6). Keep the from-scratch injector only for domain-specific faults, a MIPI line stall, a CAN timestamp reorder, that no general library models.
Research Frontier
Two frontiers are active. First, learned degradation detection: instead of hand-set thresholds, self-supervised models learn a sensor's healthy signature and flag deviations as incipient faults, extending the anomaly-detection methods of Chapter 37 to the sensor itself rather than the machine it watches. The hard part is separating a degrading sensor from a changing world, which is the concept-shift problem of Chapter 66 wearing a different hat. Second, standardized robustness benchmarks: ImageNet-C and its 3D and lidar successors (Robo3D, and corruption suites for point clouds and radar) let researchers report accuracy under a fixed grid of corruption types and severities, turning "robust to sensor faults" from a claim into a measured number. The open question is a benchmark that scores graceful degradation and timely detection jointly, rewarding a system that notices it is failing over one that merely fails a little less.
Exercise
You own a wrist-worn PPG heart-rate sensor. Consider four field faults: (a) the optical window fogs with sweat, slowly attenuating the signal; (b) the ADC latches and returns a constant 0.42 for minutes at a time; (c) a loose band lets motion inject high-frequency noise; (d) firmware occasionally stamps two samples with the same timestamp. For each, name the fault mode, write which term of \(z = g\,h(x) + b + \varepsilon\) (or the time index) it corrupts, and state which flag in Code 68.1.1 would catch it, or explain why none would and what statistic you would add. Then extend the checker with a timing-consistency flag that catches case (d).
Self-Check
- Explain why a stuck-at fault at an in-range value defeats a range check but not a variance-floor check, and write the one-line statistic each uses.
- Given a bias drift \(b_t = r t\) with rate \(r\) and noise scale \(\sigma_\varepsilon\), at roughly what time does the fault become detectable versus dangerous, and why is the gap between those times the quantity you actually care about?
- Why is a cross-sensor residual the natural detector for a gain fault, when no single-channel gate fires on it? Tie your answer to the automotive radar step-through.
What's Next
In Section 68.2, we turn from faults that happen to faults that are made to happen: an adversary who spoofs lidar returns, injects phantom radar targets (MadRadar), forges GPS, or slips inaudible commands into a microphone. The taxonomy here is the foundation, because a good attack is engineered to look exactly like a benign degradation and slip past the very health gates we just built.