"A filter that never doubts itself is not confident. It is broken, and it will take you down with it."
A Self-Auditing AI Agent
Prerequisites
This section assumes the Gaussian innovation, its covariance, and the Mahalanobis distance from Section 49.1, and the predict-update loop of the multi-sensor Kalman filter from Section 49.2 and Chapter 9. You will need the chi-square distribution and hypothesis-testing basics from the uncertainty primer in Chapter 4. The change-detection statistics used here (CUSUM, likelihood ratios) are developed from scratch in Chapter 12; we apply them to filter residuals rather than raw signals.
The Big Picture
A Bayesian filter always produces an answer, even when a sensor has died, drifted, or is being spoofed. The estimate keeps flowing and the covariance keeps shrinking, projecting calm confidence over garbage. Consistency checking is how a fusion system audits itself at runtime: it compares the errors the filter actually makes against the errors its own covariance predicted it should make. When those two disagree, the model is wrong, and the same statistic that flags the disagreement tells you which sensor to distrust and by how much. This is the difference between a filter that merely runs and one you can put in a car, a pacemaker, or a flight controller. It turns a silent, overconfident estimator into a system that raises its hand and says: do not trust me right now.
The innovation is the filter's confession
The one quantity you can check at every timestep is the innovation \(\nu_k = z_k - \hat{z}_k\), the gap between the measurement that arrived and the measurement the filter predicted. The what: the innovation is the only place where the model meets reality before it is absorbed, so it carries every discrepancy the filter has not yet explained away. The why it is checkable is that a correctly modeled Gaussian filter makes a hard, testable promise about it. The innovation should be zero-mean, and its spread should match the innovation covariance \(S_k = H_k P_k^- H_k^\top + R_k\), which the filter computes anyway during the update: \(H_k P_k^- H_k^\top\) is the predicted-state uncertainty seen through the measurement, and \(R_k\) is the sensor noise. If the real innovations are consistently larger than \(S_k\) allows, the filter is overconfident (its covariance is a lie, the dangerous direction); consistently smaller and it is needlessly conservative.
To make "larger than \(S_k\) allows" precise, collapse the vector innovation to a scalar with the Mahalanobis form from Section 49.1, giving the Normalized Innovation Squared (NIS):
\[ \epsilon_k = \nu_k^\top S_k^{-1} \nu_k. \]The how of the test is a theorem: if the filter is consistent and the true noise is Gaussian, \(\epsilon_k\) follows a chi-square distribution with \(m\) degrees of freedom, where \(m\) is the measurement dimension. That distribution has a known mean (\(m\)) and known tail quantiles, so a single measurement gives you a calibrated yes/no: does this reading fall inside the ellipsoid my covariance drew for it?
Key Insight
NIS is checkable online without ground truth. Its cousin NEES (Normalized Estimation Error Squared, \(\tilde{x}_k^\top P_k^{-1} \tilde{x}_k\)) is the sharper consistency test because it uses the true state error \(\tilde{x}_k\), but you only know the true state in simulation or on a rig. In the field the innovation is all you have, and that is enough: a filter whose NIS statistic stays inside its chi-square band is certifying, timestep by timestep, that its reported uncertainty is honest. Tune your filter's process and measurement noise on a rig with NEES; then police it in production with NIS. Same theorem, two vantage points.
Gating: reject the outlier before it poisons the estimate
The first job of the NIS statistic is defensive. Before folding a measurement into the state, test it: if \(\epsilon_k\) exceeds a threshold \(\gamma\) drawn from the chi-square inverse CDF at a chosen confidence (say the 99th percentile), the reading is gated out and the update is skipped for that sensor this step. This is the same validation gate that guards data association in Section 49.4, used here for fault rejection rather than track assignment. The why gating matters: a single wild measurement (a multipath GPS jump, a lidar return off a mirror, a wireless outlier) enters the Kalman update multiplied by the gain, and because the filter believes it, the bad value drags the mean and, worse, shrinks the covariance around the wrong place. One ungated outlier can corrupt an estimate for many subsequent steps. The gate is a chi-square bouncer at the door.
Gating on a single sample, though, is a coin flip against slow faults. A sensor that drifts by a few millimeters per second produces innovations that each sit comfortably inside the gate while their sum marches steadily off. The fix is to accumulate. Averaging NIS over a sliding window of \(N\) steps gives a statistic distributed as chi-square with \(mN\) degrees of freedom (scaled), which tightens the band around the mean and exposes a persistent bias that any single sample would hide. For a step change of unknown onset, the sequential tools of Chapter 12, CUSUM and the generalized likelihood ratio, run directly on the innovation sequence and detect the change far faster than a fixed-window average, at the price of a tuned false-alarm rate.
import numpy as np
from scipy.stats import chi2
def nis(nu, S):
"""Normalized Innovation Squared for one measurement update."""
return float(nu.T @ np.linalg.solve(S, nu))
m = 2 # measurement dimension (e.g. x, y)
gate = chi2.ppf(0.99, df=m) # single-step 99% gate ~ 9.21
rng = np.random.default_rng(0)
S = np.array([[0.20, 0.05], [0.05, 0.30]]) # innovation covariance from the filter
L = np.linalg.cholesky(S)
# Healthy stream: innovations really are N(0, S)
healthy = [nis(L @ rng.standard_normal(m), S) for _ in range(500)]
# Faulted stream: sensor develops a slow bias after step 250
faulted, bias = [], np.zeros(m)
for k in range(500):
if k > 250:
bias += np.array([0.004, 0.0]) # creeping drift, invisible per-sample
faulted.append(nis(L @ rng.standard_normal(m) + bias, S))
print(f"healthy mean NIS {np.mean(healthy):.2f} (expect ~{m})")
print(f"healthy gate viol {np.mean(np.array(healthy) > gate)*100:.1f}% (expect ~1%)")
print(f"faulted mean NIS {np.mean(faulted):.2f}")
# A 20-step moving average crosses the fault long before any single sample trips the gate
mov = np.convolve(faulted, np.ones(20)/20, mode="valid")
alarm = np.argmax(mov > chi2.ppf(0.99, df=m*20)/20) + 20
print(f"windowed alarm at step {alarm} (true onset 250)")
As Listing 49.7 shows, the healthy filter certifies its own honesty (mean NIS at \(m\), violations near the design rate), while the same statistic, accumulated, surfaces a creeping bias that no individual reading betrays. That gap between per-sample and windowed detection is exactly the tradeoff you tune for your latency and false-alarm budget.
In Practice: an automotive GNSS/IMU stack detects a spoofed satellite fix
A self-driving shuttle fuses inertial dead reckoning with GNSS in an extended Kalman filter. Entering a tunnel-mouth urban canyon, the GNSS receiver locks onto a multipath reflection, then later onto a low-power spoofing transmitter, both of which slide the reported position sideways by half a lane over several seconds. The raw fixes look plausible: correct format, tight reported accuracy, smooth motion. What betrays them is the innovation against the IMU-propagated state. The GNSS NIS climbs and stays above its chi-square gate while the IMU-only prediction remains self-consistent, so the fusion layer isolates the fault to the GNSS channel, gates those fixes out, and coasts on inertial propagation with a visibly (and honestly) growing covariance until clean fixes return. The vehicle degrades to dead reckoning instead of confidently steering into the next lane. This residual-based fault isolation is the runtime backbone of the sensor-spoofing defenses in Chapter 68.
From detection to isolation: which sensor lied?
Detecting that something is wrong is half the job; a safety case needs to know which source failed so the estimator can drop it and keep running. This is fault detection and isolation (FDI), and Bayesian fusion offers two complementary handles. The first is analytical redundancy: run a bank of filters, each excluding one sensor, and compare their innovation histories. If dropping sensor \(j\) restores every other channel's NIS to its healthy band, sensor \(j\) is the culprit. This is the residual-structuring idea behind the receiver autonomous integrity monitoring (RAIM) used in aviation GNSS, generalized to arbitrary sensor sets. The second handle is hardware redundancy read through the same statistic: with three or more sources measuring one quantity, a per-channel innovation cross-check performs a probabilistic majority vote, outvoting the deviant even when you cannot say a priori which one it is.
The when to reach for which: analytical redundancy is cheap in sensors but costly in computation and modeling (you must trust every excluded-filter model), so it fits mass-market systems that cannot afford triplicated hardware. Hardware redundancy is expensive in parts but robust and simple to certify, so it dominates aerospace and medical devices where a missed fault is catastrophic. Most serious stacks blend the two, and both ultimately rest on the same claim this section has built: a residual that violates its own predicted covariance is evidence, and the pattern of which residuals violate is the diagnosis.
The Right Tool
Hand-rolled consistency monitoring means maintaining the innovation and its covariance, forming the Mahalanobis quadratic with a stable solve (never an explicit inverse), looking up chi-square quantiles for your dimension and confidence, and wiring the windowed accumulator, easily fifty lines with a numerics pitfall at every step. A modern filtering stack folds gating into the update itself:
from filterpy.kalman import KalmanFilter
from filterpy.stats import mahalanobis
import scipy.stats as st
kf = KalmanFilter(dim_x=4, dim_z=2) # set F, H, Q, R, P as usual
kf.predict()
d2 = mahalanobis(z, kf.H @ kf.x, kf.S) ** 2 # == NIS, S computed by the filter
if d2 <= st.chi2.ppf(0.99, df=2):
kf.update(z) # accept: consistent measurement
# else: gate out this reading, coast on the prediction
filterpy, the innovation covariance kf.S and the Mahalanobis helper collapse a page of consistency plumbing into three lines, turning gate-or-coast into a one-line branch. The library computes \(S_k\) and the stable solve; the engineer still owns the confidence level, the window length, and the isolation logic, because no library knows your fault model.As Listing 49.8 shows, the library removes the linear algebra and the quantile lookup, roughly a 15-to-3 line reduction, but not the judgment: choosing the false-alarm rate and deciding what "coast" means for your application stay with you.
Research Frontier: learned and adaptive consistency
Classical NIS/NEES assumes Gaussian noise and a known \(R_k\); real sensors violate both under weather, occlusion, and adversarial interference. The current frontier makes the check itself adaptive. Adaptive Kalman filtering estimates \(R_k\) and \(Q_k\) online from the innovation sequence so the consistency band tracks changing noise, and variational Bayes filters treat the noise covariance as a latent variable inferred jointly with the state. On the learning side, deep state estimators now output calibrated covariances that feed the same chi-square machinery, and conformal-prediction wrappers (see Chapter 18) give distribution-free residual bands with finite-sample coverage guarantees, replacing the Gaussian assumption entirely. The open problem is fast, provable fault isolation when the noise model is itself being learned, a live concern for the functional-safety cases of Chapter 68.
Exercise
Extend Listing 49.7 with a bias fault instead of a drift: at step 300, add a constant offset of \(1.5\sigma\) to one innovation channel and keep it there. First, measure how often a single-step 99% gate catches it (the detection probability per sample). Then implement a CUSUM detector on the innovation and report its mean detection delay. Finally, sweep the offset from \(0.5\sigma\) to \(3\sigma\) and plot detection delay against fault magnitude for both detectors. Which detector wins for small faults, and what does that cost you in false alarms on the healthy segment?
Self-Check
1. Your filter's mean NIS over a long run is 0.6 when the measurement dimension is 3. Is the filter overconfident or underconfident, and which way should you adjust the noise covariances?
2. Why can NIS be monitored in a deployed vehicle while NEES generally cannot? What does each one need that the other does not?
3. A slow sensor drift passes every single-step chi-square gate yet the estimate is clearly diverging. Name two statistics that would catch it and explain what property of the drift each one exploits.
Lab 49
fuse two noisy sensors with different error profiles; visualize posterior uncertainty and detect an injected fault.
What's Next
In Chapter 50, we leave the Gaussian world where consistency has a closed-form chi-square test and enter deep multimodal fusion, where a network blends camera, lidar, and audio through learned encoders. The auditing question sharpens rather than disappears: when a modality drops out or turns adversarial, how does a neural fusion model notice, and how does it degrade gracefully instead of hallucinating confidence? The consistency instinct you built here, comparing predicted uncertainty against realized error, becomes the design principle for missing-modality robustness.
Bibliography
Filter consistency and estimation theory
The canonical treatment of NIS, NEES, and chi-square gating for tracking filters; the source most engineers cite for filter-consistency tests.
The origin of the innovation and its covariance, the two quantities every consistency check in this section is built on.
Fault detection, isolation, and integrity monitoring
The definitive reference on CUSUM and generalized likelihood ratio detectors applied to residual sequences, the workhorses for catching slow and abrupt filter faults.
Introduces RAIM, residual-based fault detection and exclusion for GNSS, the aviation ancestor of the isolation logic used in modern GNSS/IMU fusion.
Systematic coverage of analytical versus hardware redundancy and residual structuring, the FDI framework this section applies to sensor fusion.
Robust and adaptive fusion
Covariance Intersection, the provably consistent fusion rule for unknown cross-correlation that keeps a fused estimate from becoming overconfident.
A variational-Bayes filter that infers the measurement-noise covariance online, letting the consistency band adapt when sensor noise changes.