Skip to the content
FixTheRobot Explore Robotics / UAV / Module 3 / M3-T15

UAV Systems Engineering · Module 3

M3-T15: Normalized Estimation Error Squared

NIS needs no ground truth and tests the measurements; NEES needs ground truth and tests the state estimate itself. Together they tell you whether a filter is honest.

  • Lesson 15 of 16
  • Sensor Fusion and State Estimation
  • Established engineering
  • About 6 minutes to read

A filter that is accurate but overconfident is dangerous: a planner or monitor trusting its covariance will take risks the data do not support. NEES is the test for that, and simulation is where you can run it.

Learning objectives

After this lesson, you should be able to:

  • Define NEES and the distribution it follows for a consistent filter.
  • Run a Monte Carlo consistency test and compute average NEES with chi-square bounds.
  • Interpret values above and below the band and relate them to QQ, RR and model error.
  • Explain why NEES is a simulation tool and what replaces it in flight.

Conceptual explanation

The normalised estimation error squared compares the actual error of the state estimate with the covariance the filter reports: NEES = (x−x^)⊤ P−1(x−x^)(x-\hat{x})^{\top}\,P^{-1}(x-\hat{x}). If the filter is consistent, the error is zero-mean Gaussian with covariance PP and NEES is chi-square with nxn_{x} degrees of freedom (the state dimension), so its mean is nxn_{x}.

NEES Monte CarloMany independent simulation runs each produce a NEES time series. Averaging across runs at each time gives the average NEES, which must stay inside chi-square bounds scaled by the number of runs. Above the band means overconfident, below means conservative.n_x average NEES over N runsinside the band: consistentabove the band: overconfident (orange)
Average NEES across independent runs, with its acceptance band.

It needs the true state, so it is a simulation and test-range tool. Onboard, where truth is unavailable, NIS (lesson M3-T14) and cross-checks between sensors take its place.

Animation of the average normalised estimation error squared over 50 Monte Carlo runs for two filters on the same problem. The filter with the correct process noise has an average NEES near 2, the number of states, inside the 95 percent chi-square band. The filter with a process noise one hundred times too small becomes overconfident, its covariance shrinks below its real error, and its average NEES climbs far above the upper bound.
NEES = (x−x^)⊤ P−1(x−x^)(x-\hat{x})^{\top}\,P^{-1}(x-\hat{x}) averaged over NN runs should equal the state dimension (2) with chi-square bounds from χ2(N⋅2)/N\chi ^{2}(N\cdot 2)/N. Above the band the filter is overconfident (covariance too small), below it pessimistic. Simulated constant-velocity filter; 50 runs. (Looping, about 6 seconds; drawn and computed for this course with illustrative values.)

Engineering intuition

The covariance is a promise: “my error is probably within this ellipse”. NEES counts how often and by how much the promise is broken. A mean far above nxn_{x} means the errors are larger than promised (overconfident): the covariance is too small, which usually means QQ or RR too small, an unmodelled error, or a linearisation problem. A mean far below nxn_{x} means the filter is needlessly pessimistic. One run proves little because a single trajectory is a sample of one; averaging NN independent runs reduces the variance of the test by NN.

Mathematical formulation

NEES
εk=xk−x^k\boldsymbol\varepsilon_k = \mathbf x_k - \hat{\mathbf x}_k
NEESk=εk⊤Pk−1εk∼χnx2\mathrm{NEES}_k = \boldsymbol\varepsilon_k^{\top}\mathbf P_k^{-1}\boldsymbol\varepsilon_k \sim \chi^{2}_{n_x}

For NN independent runs, the average NEES at time kk has N nxN\,n_{x} degrees of freedom after scaling:

Monte Carlo consistency test (Bar-Shalom)
NEES‾k=1N∑iNEESk(i),N NEES‾k∼χNnx2\overline{\mathrm{NEES}}_k = \frac{1}{N}\sum_i \mathrm{NEES}_k^{(i)}, \qquad N\,\overline{\mathrm{NEES}}_k \sim \chi^{2}_{N n_x}
Accept consistency at time k if
χNnx2(α/2)N≤NEES‾k≤χNnx2(1−α/2)N\frac{\chi^{2}_{N n_x}(\alpha/2)}{N} \le \overline{\mathrm{NEES}}_k \le \frac{\chi^{2}_{N n_x}(1-\alpha/2)}{N}
nxn_{x}
dimension of the state (or of the sub-state tested, for example position only).
NN
number of independent Monte Carlo runs with different noise seeds.
α\alpha
significance level, typically 0.05 for a 95% two-sided band.

For large degrees of freedom the chi-square quantile is well approximated by the Wilson–Hilferty formula χν2(p)≈ν(1−2/(9ν)+zp2/(9ν))3\chi ^{2}_{\nu }(p)\approx \nu (1-2/(9\nu )+z_{p}\sqrt{2/(9\nu )})^{3}, with zpz_{p} the standard normal quantile (±1.96 for 95%). Test position and velocity sub-blocks separately as well: a consistent total NEES can hide an inconsistent sub-block.

Architecture and data flow

Simulation harness: generate truth → simulate sensors with exactly the noise the filter assumes → run the filter NN times with different seeds → record error and covariance per step → compute the average NEES and its bounds → plot. First do this with perfectly matched models (it must pass: otherwise the filter or the test is wrong), then with realistic mismatch (the interesting result is how far outside the band it goes).

Real-world UAV applications

  • Validating a new estimator before flight.
  • Tuning QQ and RR against truth in simulation, then confirming with NIS on logs.
  • Regression tests that fail when a code change makes a filter overconfident.
  • Justifying that a reported covariance can be used for safety margins (lesson M2-T16).

Implementation example

This runs as shown: 100 Monte Carlo runs of a constant-velocity Kalman filter, with a matched model (passes) and a filter whose process noise is 100 times too small (fails).

import numpy as np
dt, n, runs = 0.1, 200, 100
F = np.array([[1, dt], [0, 1]]); H = np.array([[1.0, 0.0]])
q_true, r_std = 0.5, 2.0
Q = lambda q: q * np.array([[dt**3 / 3, dt**2 / 2], [dt**2 / 2, dt]]); R = np.array([[r_std**2]])

def mc_nees(q_filter, seed0=0):
    acc = np.zeros(n)
    for i in range(runs):
        rng = np.random.default_rng(seed0 + i); L = np.linalg.cholesky(Q(q_true))
        x = np.array([0.0, 5.0]); xh = x.copy(); P = np.diag([4.0, 4.0])      # filter starts with truthful uncertainty
        for k in range(n):
            x = F @ x + L @ rng.standard_normal(2)
            z = x[0] + r_std * rng.standard_normal()
            xh = F @ xh; P = F @ P @ F.T + Q(q_filter)
            S = H @ P @ H.T + R; K = P @ H.T @ np.linalg.inv(S)
            xh = xh + (K @ (z - H @ xh)).ravel(); I_KH = np.eye(2) - K @ H; P = I_KH @ P @ I_KH.T + K @ R @ K.T
            e = x - xh; acc[k] += e @ np.linalg.inv(P) @ e
    return acc / runs

def chi2_bounds(dof, z=1.96):                                           # Wilson-Hilferty approximation of the 95% interval
    f = lambda zz: dof * (1 - 2 / (9 * dof) + zz * np.sqrt(2 / (9 * dof))) ** 3
    return f(-z), f(z)

lo, hi = (b / runs for b in chi2_bounds(runs * 2))
good, bad = mc_nees(q_true)[50:], mc_nees(0.01 * q_true)[50:]
print(f"band [{lo:.2f}, {hi:.2f}] (expected 2); matched: mean {good.mean():.2f}, {100 * np.mean((good >= lo) & (good <= hi)):.0f}% in band; "
      f"Q 100x too small: mean {bad.mean():.2f}, {100 * np.mean((bad >= lo) & (bad <= hi)):.0f}% in band")
assert np.mean((good >= lo) & (good <= hi)) > 0.8 and good.mean() < 2.2
assert bad.mean() > 3 * good.mean() and np.mean(bad > hi) > 0.9
Monte Carlo NEES with chi-square bounds: a matched filter passes, an overconfident one fails.

Common engineering mistakes

Running one trajectory

A single run has a huge variance; use at least 20–50 runs with different seeds.

Testing with noise the filter does not assume

Unmatched noise tests a different question. First pass the matched case.

Looking only at total NEES

Test position and velocity (and attitude) blocks separately.

Using NEES onboard

Truth is not available in flight; use NIS and cross-checks there.

Tuning only to get NEES inside the band

Inflating PP blindly passes the test and wastes information; tune QQ and RR from physics and logs.

Failure modes

Failure modes, symptoms and causes
Failure modeHow it shows upLikely cause
Overconfident filterAverage NEES far above nxn_{x}QQ or RR too small, linearisation error, unmodelled bias
Conservative filterAverage NEES below the bandQQ or RR too large or double-counted noise
Inconsistent attitude blockPosition fine, orientation NEES highUnobservable yaw drift not reflected in PP
Initial transient outside bandHigh NEES in the first secondsInitial covariance too small for the initial error

Debugging approach

  1. Plot average NEES versus time for each block with the band.
  2. Check the matched case first; if it fails, suspect the Jacobian, the discretisation of QQ or a covariance-update bug.
  3. Plot error against ±3σ3\sigma for the worst block.
  4. Repeat with the sensor and process noise you believe real and see how far the result moves.

Practical exercises

  1. Compute the 95% band for nx=9n_{x}=9 and N=50N=50 runs.
  2. Run the example with RR mis-scaled by 4 in both directions and report NEES.
  3. Test position alone (nx=1)(n_{x}=1) in the example: is its average NEES consistent?
  4. Apply the harness to the Sensor Fusion simulator’s VIO covariance.

Interview questions

Write an answer in your own words, then reveal the model answer. Nothing is sent anywhere.

Question 1

What does NEES test that NIS does not?

Model answer

NEES compares the actual state error with the reported covariance, testing the estimate itself; NIS only tests the measurement residuals and needs no truth. A filter can pass NIS and still be inconsistent in unobservable states.

Question 2

Why average over Monte Carlo runs?

Model answer

One run is a single sample; averaging NN runs reduces the test variance so the chi-square band is tight enough to detect inconsistency.

Question 3

What does an average NEES well above nxn_{x} mean in practice?

Model answer

The filter is overconfident: real errors exceed its reported uncertainty, so anything using that covariance (gates, planners, monitors) will be too trusting.

References and further reading

Primary papers, official documentation and standards. Each claim above that is specific to a tool or version points here.

What’s next

Mark this lesson

Saved in this browser and synced to your account when you are signed in.

Next lesson

M3-T16: Sensor fault detection and isolation

Detect and isolate a faulty sensor with residual tests, cross-checks and voting, and measure detection delay.