Sensor Fusion: Classical to Modern · Lesson 14 of 22

INS/GNSS Coupling — Bounding Inertial Drift

An IMU runs fast, smooth, and indoors — but drifts without bound. GNSS is absolute and bounded — but slow, noisy, and dies in tunnels. Bolt them together with an error-state Kalman filter and you get the workhorse architecture of real navigation: GNSS leashes the drift, the IMU coasts through dropouts.

Prerequisites: the Kalman filter (Lesson 5) + strapdown inertial navigation (Lesson 13) + basic linear algebra.
10
Chapters
6
Simulations
2
Worked Examples

Chapter 0: The Aiding Idea — Leashing a Sensor That Drifts Forever

In the previous lesson you watched an inertial navigation system (INS) — an IMU plus the strapdown mechanization that integrates it — do something both miraculous and tragic. Miraculous: it told you where you were, hundreds of times a second, smoothly, in a tunnel, in a parking garage, on the dark side of the moon, with no help from the outside world. Tragic: left alone, its position estimate drifts. The error does not wander randomly back and forth around the truth; it grows, roughly as time-squared, and it never comes back. After a minute a good automotive IMU is off by tens of meters. After ten minutes it is hopelessly, confidently lost.

This lesson is the direct answer to that tragedy. The cure for a sensor whose error grows without bound is a second sensor whose error stays bounded — one that, every so often, walks up to the drifting estimate and says "no, you are actually here." That second sensor is GNSS — the Global Navigation Satellite System, the family that includes American GPS, European Galileo, Russian GLONASS, and Chinese BeiDou. A GNSS receiver gives you an absolute position, fixed to the Earth, that is wrong by a few meters whether you ask it now or an hour from now. Its error is bounded. It does not grow.

The aiding idea in one sentence. The INS is fast, smooth, and self-contained but drifts without bound; GNSS is absolute and bounded but slow, noisy, and dies indoors. Fuse them so the INS carries you smoothly between GNSS fixes and through GNSS dropouts, while every GNSS fix resets the accumulated drift to zero. Neither alone is a navigation system. Together they are the navigation system that flies your plane, drives your car through the tunnel, and surveys the bridge.

Notice this is exactly the complementary fusion relationship from Lesson 1: two sensors measuring the same quantity (position) in non-overlapping regimes. Where the IMU is strong (fast, indoors, short time-scale) GNSS is weak, and where GNSS is strong (absolute, bounded, long time-scale) the IMU is weak. Their error curves cross, and fusion lives exactly at the crossing. What's new in this lesson is the machinery that does the fusing optimally and at scale — the error-state Kalman filter — and it is the single most important architecture in all of applied navigation.

Feel the three trajectories — why neither alone suffices

Before any math, let's see the problem and the cure. A vehicle drives along the dashed truth line below. You can watch three estimates: the INS alone (smooth but sliding away from truth as the drift accumulates), GNSS alone (bounded but a scatter of jumpy, noisy dots), and the fused estimate (smooth and bounded). Then flip on a GNSS dropout — a tunnel — and watch the fused estimate coast on the INS and snap back when satellites return.

INS alone vs GNSS alone vs fused — with a GNSS dropout

The vehicle drives along the dashed truth line. INS-only is smooth but drifts; GNSS-only is a noisy scatter that stays near truth; fused is smooth AND bounded. Toggle the dropout to cut GNSS for an interval and watch the fused estimate coast then snap back.

ready

Watch the INS-only line: it starts glued to the truth, because over one second an IMU is gorgeous, and then it peels away, the gap widening faster and faster — that is the time-squared drift from Lesson 13, made visible. Watch the GNSS-only dots: they never peel away, but they never settle either, jittering a meter or two around the truth on every fix and going completely silent in the tunnel. Now watch the fused line. It is as smooth as the INS and as anchored as GNSS: it rides the INS between fixes, gets yanked back onto the truth at every GNSS update, and during the dropout it coasts on the INS — drifting a little — until the fix returns and snaps it home.

Common misconception. "GNSS just replaces the IMU's estimate every second, so why bother with the IMU at all?" Two reasons. First, GNSS is slow (typically 1–10 Hz) and noisy — a car needs position and attitude at 100–1000 Hz, smooth enough to steer by, and the IMU provides that between fixes. Second, and more deeply: GNSS dies — tunnels, parking garages, urban canyons, under bridges, under heavy foliage. In those gaps the IMU is the only thing keeping you alive, and the better you've calibrated it against past GNSS fixes, the longer it coasts before the drift matters. The IMU is not a backup. It is the spine; GNSS is the leash.

Why the IMU also de-noises GNSS — the underappreciated half

The headline benefit is obvious: GNSS bounds the IMU's drift. But the relationship runs both ways, and the reverse benefit is easy to miss. Raw GNSS position is noisy — consecutive fixes scatter by a meter or more even when the receiver is sitting still, because of atmospheric delays, multipath, and receiver noise. If you steered a car directly off raw GNSS, it would twitch. The IMU smooths this: because the filter knows the vehicle's actual dynamics (it cannot teleport sideways), it treats a wild GNSS jump with suspicion and blends it gently. The fused position is therefore smoother than raw GNSS and more bounded than raw INS — better than either input on its own axis. This is the inverse-variance magic from Lesson 1: information adds, and the fused estimate is sharper than the best single source.

So the partnership is genuinely symmetric. GNSS gives the INS what it lacks (an absolute reference that never grows stale-in-magnitude), and the INS gives GNSS what it lacks (high rate, smoothness, and a physical model that rejects implausible jumps). That symmetry — high-pass the inertial, low-pass the aiding — is the seed of the entire lesson, and in Chapter 2 we'll see it is exactly the complementary filter of Lesson 6, generalized to be statistically optimal.

Why can't we navigate a car through a long tunnel using GNSS alone, even with a top-of-the-line receiver?

Chapter 1: Coupling Architectures — Loose, Tight, and Deep

We've agreed to fuse the INS and GNSS. The next question is brutally practical: at what point in the GNSS receiver's processing chain do we tap in? A GNSS receiver is itself a little pipeline. Raw radio signals from the satellites become pseudoranges (the apparent distance to each satellite, computed from signal travel time) and Doppler measurements (range-rate, from frequency shift), and then a least-squares or filtering step turns four-or-more pseudoranges into a single position-velocity-time solution. We can fuse with the INS at either end of that chain — and the choice has enormous consequences. The three classic levels are loosely coupled, tightly coupled, and deeply (ultra-tightly) coupled.

Loosely coupled — fuse the GNSS solution

The simplest architecture taps in at the end of the GNSS chain. The receiver does its own thing, computes a position-velocity solution, and hands that solution to the navigation filter as a measurement. The filter's job is just "the INS says I'm here, GNSS says I'm there, reconcile them." This is loosely coupled integration: two black boxes (an INS filter and a GNSS receiver) connected by a thin wire carrying a position/velocity fix.

Why it's appealing. It is modular. The GNSS receiver is a sealed unit you bought from someone else; you don't need to know how it computes its solution. You can swap receivers, upgrade firmware, or run the INS filter on a tiny processor. The measurement model is trivial: predicted position equals state position; the innovation is just (GNSS position − INS position). It is the architecture you reach for first, and it is everywhere.

Why it bites. It needs a complete GNSS solution, which requires at least four satellites in view (three for position, one for the receiver clock). Drop below four — partway into a canyon, half-blocked by a building — and the receiver outputs nothing, even though it can still see two or three satellites that carry real information. You've thrown that information away. Worse, the receiver's internal filter makes the solution's errors time-correlated (today's error looks like yesterday's), which violates the Kalman filter's white-noise assumption and makes the fused result over-confident.

Tightly coupled — fuse the raw pseudoranges

The tightly coupled architecture taps in earlier, before the receiver collapses everything into a solution. Instead of fusing a position fix, you fuse the raw pseudoranges and Doppler for each individual satellite. The navigation filter holds the satellites' known positions (from the broadcast ephemeris) and, for each satellite, predicts what the pseudorange should be given the INS's current position estimate. The measurement model maps state → predicted pseudorange:

ρ̂i = ||psat,i − pINS|| + c · δtclk

where ρ̂i is the predicted pseudorange to satellite i, psat,i is that satellite's known position, pINS is the INS position estimate, c is the speed of light, and δtclk is the receiver clock bias (now carried as an extra state). The innovation is (measured pseudorange − predicted pseudorange), per satellite.

The decisive advantage of tight coupling. Because each satellite contributes its own scalar measurement, the filter can use fewer than four satellites. In an urban canyon where only two satellites peek between the towers, a loosely coupled receiver outputs nothing — but a tightly coupled filter still folds in those two pseudoranges, each constraining the drift along one direction. Partial information is still information. This is why tightly coupled INS/GNSS dominates demanding applications: it degrades gracefully as satellites disappear, instead of falling off a cliff at four.

The cost of tight coupling is complexity and intimacy. You need access to the receiver's raw measurements (not all consumer receivers expose them), you must model each satellite's clock and atmospheric delays, and you must carry the receiver clock bias and drift as filter states. It is more code, more states, and more knowledge of GNSS internals — but it buys robustness exactly where loose coupling fails.

Deeply / ultra-tightly coupled — the INS aids the receiver

The deepest architecture reverses part of the flow. In deeply coupled (also ultra-tightly coupled) integration, the INS doesn't just receive from the GNSS chain — it feeds back into the receiver's signal-tracking loops. A GNSS receiver tracks each satellite's carrier and code with phase-locked and delay-locked loops; under heavy vibration or in a deep fade those loops can lose lock. If the INS tells the tracking loops "the platform just accelerated this way, so expect the Doppler to shift by this much," the loops can stay locked through dynamics and jamming that would otherwise break them. Conceptually: the INS stabilizes the receiver's front end, and the receiver stabilizes the INS's drift — a mutual aiding loop. Deep coupling lives in high-end military and aerospace systems; for this lesson it's enough to know it exists and points the opposite direction (nav → receiver).

Below, watch loose vs tight under a shrinking satellite count. As you drag the satellite slider down, the loosely coupled solution holds firm until it hits four, then collapses to "no fix." The tightly coupled solution keeps constraining the estimate all the way down to one satellite — weaker, but never nothing.

Loose vs tight under a shrinking constellation

Drag the slider to set how many satellites are visible. Loosely coupled needs ≥4 to output anything — below that it goes dark. Tightly coupled still folds in every visible pseudorange, so its position constraint degrades smoothly instead of vanishing.

Satellites visible 6
drag the satellite slider
ArchitectureFusesMin satsProsCons
Loosely coupledGNSS position/velocity solution4simple, modular, swap receivers freelycliff at <4 sats; time-correlated errors
Tightly coupledraw pseudoranges + Doppler1graceful degradation, uses partial constellationsneeds raw data + GNSS modeling; more states
Deeply coupledraw signals; INS aids tracking loops<1 (tracks through fades)survives jamming & heavy dynamicsmost complex; requires receiver internals
How to choose. Start loose — it is simple and works in open sky. Go tight the moment your environment routinely drops below four satellites (cities, forests, canyons) or you need the robustness. Go deep only for jamming, extreme vibration, or high-dynamics platforms where the tracking loops themselves are at risk. Most ground-vehicle and consumer systems are loose or tight; most survey-grade and aerospace systems are tight or deep.

An autonomous car routinely drives through downtown where only 2–3 satellites are visible between skyscrapers. Why is tightly coupled integration the right call over loosely coupled?

Chapter 2: The Error-State Kalman Filter — Filter the Mistake, Not the Position

We know what to fuse and where to tap in. Now the central machine: the filter that actually does the fusing. The naive idea — run a Kalman filter directly on the full navigation state (position, velocity, attitude) — turns out to be a bad idea, and the reason it is bad reveals the most elegant trick in all of navigation: the error-state Kalman filter (ESKF), also called the indirect Kalman filter.

Why filtering the full state directly is painful

Imagine a Kalman filter whose state is the full navigation state — call it the total state x: position (meters, spanning the whole Earth), velocity (up to hundreds of m/s), attitude (a full 3-D orientation). Three problems pile up:

So the full state is large, nonlinear, fast, and partly non-Euclidean. A Kalman filter wants the opposite: small, linear, slow, Euclidean. The error-state trick gives the filter exactly the state it wants.

The decomposition — nominal ⊕ error

Here is the move. Split the true state into two pieces: a nominal state and a small error state. The nominal state is what the high-rate strapdown mechanization computes by integrating the IMU (Lesson 13) — a smooth, full-dynamic-range trajectory that is almost right but slowly drifting. The error state, written δx, is the small difference between that nominal trajectory and the truth:

xtrue = xnominal ⊕ δx

Read it as: truth = (what the strapdown thinks) plus (a small correction). The symbol ⊕ is "plus" for ordinary states like position and velocity, and a slightly fancier composition for attitude (Chapter 4) — but the spirit is just addition. The Kalman filter's entire job is now reframed: don't estimate the position — estimate the position error. Don't filter the full nonlinear state; filter the tiny correction to it.

The whole trick in one image. A delivery driver follows a slightly wrong map (the nominal state from the IMU). Rather than re-survey the entire city every second, the driver tracks just one small quantity: "how far off is my current guess from where I really am?" That quantity — the error — is small, changes slowly, and the math relating it to GNSS fixes is linear. Estimate the small error, then nudge the map by it. That nudge-the-map step is the famous injection, and the small-and-linear error is why an ordinary Kalman filter is exactly the right tool.

Why the error dynamics ARE linear — the justification

Why does filtering the error work when filtering the full state was so painful? Because the error is small, and small quantities propagate linearly. Take any nonlinear dynamics and look at how a tiny perturbation evolves: to first order, the perturbation obeys a linear differential equation — the Jacobian of the dynamics evaluated along the nominal trajectory. This is just calculus: a smooth function looks linear if you zoom in close enough, and the error is a close zoom-in on the truth.

Concretely, the error-state dynamics come out as a linear system:

δẋ = F · δx + G · w

where δẋ is the rate of change of the error, F is the (time-varying) error-dynamics matrix built from the nominal trajectory, w is the IMU's white noise (driving the error), and G maps that noise into the error. Crucially, F is the Jacobian of the nav equations about the nominal — and because the error is small, that first-order linear model is an excellent approximation. The error state is also Gaussian to first order: it is driven by Gaussian IMU noise through a linear system, and a linear function of a Gaussian is Gaussian. Linear dynamics + Gaussian noise = the exact home turf of the Kalman filter. That is why the ESKF is valid where a direct filter struggles: we moved the nonlinearity out of the filter (into the high-rate strapdown integrator) and left the filter a clean linear-Gaussian problem.

Predict, update, inject, reset — the four-beat cycle

The ESKF runs a four-beat loop. Two beats happen at IMU rate (fast), two at GNSS rate (slow):

1 · Mechanize (IMU rate)
Integrate the IMU through the strapdown equations to advance the nominal state. The error state δx stays at zero in its mean — we just grow its covariance.
2 · Predict covariance (IMU rate)
Propagate the error covariance P forward with the linear F: P ← ΦPΦᵀ + Q. This is where the drift & bias uncertainty grows.
↓ (GNSS fix arrives)
3 · Update (GNSS rate)
Form the innovation y = z − h(xnominal), compute Kalman gain K, and estimate the error: δx̂ = K · y. Now δx̂ is a real, nonzero correction.
4 · Inject & reset (GNSS rate)
Fold the estimated error into the nominal: xnominal ← xnominal ⊕ δx̂. Then reset δx̂ to zero — the correction has been applied, so the error is once again "the unknown small mistake," starting fresh.
↻ repeat

That reset in beat 4 is the signature of the error-state filter and trips up everyone the first time. After you inject the estimated error into the nominal state, the nominal state is the corrected estimate — so the remaining error is, by construction, back to (unknown, small, mean-zero). You zero the error-state mean and keep its covariance. The error then re-grows from zero as the IMU drifts, until the next GNSS fix lets you estimate and inject it again. This produces a characteristic sawtooth: the error climbs as the IMU coasts, then drops to zero at each fix. We'll animate exactly that sawtooth below.

Closed-loop (feedback) vs feedforward

When you inject the correction back into the strapdown integrator so the nominal state itself is corrected (beat 4 above), that's closed-loop or feedback mode. The alternative, feedforward, leaves the strapdown running free and only adds the estimated error at the output. Feedback is almost always preferred because it keeps the nominal trajectory close to truth, which keeps the small-error linearization valid — if you let the strapdown drift far without correcting it, the error stops being "small" and the linear F stops being accurate. Feedback continuously re-centers the linearization point. We'll return to this in the Practical chapter.

The error sawtooth — grow on coast, reset on fix

The nominal trajectory (smooth, from the IMU) drifts; the error grows. At each GNSS fix the filter estimates the error and injects it, resetting the error to zero. Drag the fix interval to see how spacing the fixes lets the error climb higher before each reset. Press Run.

GNSS fix interval 30 ready
The signature of the error-state filter. The nonlinear, fast, full-range strapdown integration lives outside the Kalman filter. Inside the filter is only a small, slow, linear-Gaussian error state. The filter estimates that error from each aiding measurement, injects it as a correction, and resets it to zero. The result is a numerically clean, optimal complementary fuser — the workhorse of real navigation.
Why does the error-state Kalman filter estimate the error δx rather than the full navigation state directly?

Chapter 3: The 15 States — What the Filter Actually Tracks

We said the error state δx is "small." Now let's open it up and see what's inside. The standard INS/GNSS error state has fifteen elements, in five blocks of three. Memorize these five blocks — they are the canonical state vector you will see in every navigation paper, in PX4's EKF2, in NovAtel's SPAN, in textbook after textbook:

δx = [ δp(3),   δv(3),   δθ(3),   ba (3),   bg (3) ]
BlockSymbolWhat it isWhy it's here
Position errorδp (3)how far the nominal position is from truth, in N/E/D metersthis is the drift GNSS directly bounds — the headline error
Velocity errorδv (3)error in the nominal velocity, m/s in each axisvelocity error feeds position error (integrate it) — you must track it to predict where position drift is going
Attitude errorδθ (3)a small rotation (3 angles) between nominal and true orientationa tilted IMU mis-projects gravity into horizontal accel → the #1 driver of velocity & position drift; only 3 numbers (Chapter 4)
Accel biasba (3)slowly-varying offset in each accelerometer axisa constant accel bias integrates to t² position drift; estimating it lets the IMU coast far longer through dropouts
Gyro biasbg (3)slowly-varying offset in each gyroscope axisa gyro bias makes the attitude itself drift, which then corrupts everything downstream — the most insidious error

Three plus three plus three plus three plus three: fifteen states. (Tightly coupled filters add two more for the receiver clock bias and drift, making seventeen; some systems add scale-factor errors or a lever-arm estimate.) Let's walk each block and feel why it earns its place.

Position and velocity error — the cascade

Position error δp is the obvious one: it is the gap between where the strapdown thinks the vehicle is and where it actually is — the very drift we're fighting. GNSS measures position, so a GNSS fix speaks directly to δp. But position error doesn't appear from nowhere; it accumulates from velocity error. If your velocity estimate is off by 0.1 m/s, then after 10 seconds your position is off by 1 m, purely from integration. So δv is in the state because it is the source of position drift. The filter watches velocity error so it can predict where position error is heading — and a GNSS velocity (Doppler) measurement speaks directly to δv.

The error cascade — why all five blocks couple. Gyro bias → attitude drifts → gravity is mis-projected as horizontal acceleration → velocity error grows → integrated into position error. Accel bias enters the same chain one step later (directly into velocity). This cascade is exactly the structure of the F matrix from Chapter 2: each block feeds the next. It's also why estimating the biases matters so much — kill the bias and you cut the cascade off at its root, so the IMU drifts far slower during the next GNSS dropout.

The bias states — the secret to long dropouts

Here is the deepest reason the 15-state filter beats a naive position-only filter: the accelerometer bias ba and gyro bias bg. These are slowly-varying offsets baked into every IMU — the accelerometer reads, say, 0.02 m/s² too high even when perfectly still; the gyro reads 0.01°/s of rotation when motionless. A position-only filter ignores them, so during a GNSS dropout the uncorrected biases integrate straight into runaway drift. The 15-state filter estimates the biases from the GNSS fixes (the geometry of how the error grows reveals the bias driving it) and subtracts them from the IMU. With the bias removed, the IMU coasts dramatically longer before drift matters.

This is the difference between a tunnel you survive and one you don't. A 15-state ESKF that has been observing GNSS for a few minutes has learned its IMU's biases tightly; when GNSS drops, it removes those biases and the residual drift is far smaller. The bias states are not bookkeeping — they are the mechanism that turns a short coast into a long one.

Common misconception. "Biases are constant, so just calibrate them once at the factory and bake them in." No — biases drift with temperature, age, and even from power-cycle to power-cycle (the famous turn-on bias). A factory number is stale within minutes of warming up. That's why the biases are states, continuously re-estimated in flight, with a small process noise that lets them slowly wander. Model them as constant and your filter will fight a phantom; model them as states with random-walk noise and the filter tracks them as they breathe.

Below, toggle the bias states on and off, then run a GNSS dropout. With bias estimation on, the coast through the dropout is tight; with it off, the same dropout sends the position error sprinting away — the visible payoff of those six extra states.

Bias states earn their keep during a dropout

Run a GNSS dropout with bias estimation ON vs OFF. With biases estimated, the IMU coasts tightly (small error); with biases ignored, the uncorrected bias integrates into runaway drift during the same dropout.

ready

Worked example — one error-state injection step, by hand

Let's do an actual injection with real numbers, the way the filter does it at a GNSS fix. We'll work the east position channel to keep it scalar and clear. Setup at the moment a GNSS fix arrives:

Step 1 — innovation. The measurement model predicts the GNSS reading from the nominal state. With H = 1, the predicted east position is just xnom, so the innovation (measured − predicted) is:

y = z − xnom = 409.80 − 412.30 = −2.50 m

GNSS says we are 2.5 m west of where the strapdown thinks — that's the drift, made visible.

Step 2 — innovation covariance and Kalman gain. The innovation covariance is S = H P Hᵀ + R = P + R = 9 + 4 = 13. The Kalman gain is the trust slider from Lesson 5:

K = P · Hᵀ / S = 9 / 13 = 0.692

Because the prior (P = 9) is larger than the measurement noise (R = 4), the filter trusts GNSS more than the strapdown — K is above 0.5, leaning toward the measurement, exactly as inverse-variance weighting demands.

Step 3 — estimate the error. The error state starts at zero (we reset it last time). The update sets:

δx̂ = K · y = 0.692 × (−2.50) = −1.73 m

The filter's best estimate of the east position error is −1.73 m — the nominal is about 1.73 m too far east.

Step 4 — inject. Fold the error into the nominal state:

xnom ← xnom + δx̂ = 412.30 + (−1.73) = 410.57 m

The corrected east position is 410.57 m — pulled most of the way from the strapdown's 412.30 toward GNSS's 409.80, but not all the way, because GNSS is noisy too. The fused answer sits between them, weighted by trust.

Step 5 — update covariance and reset. The posterior covariance shrinks: P ← (1 − K H) P = (1 − 0.692) × 9 = 0.308 × 9 = 2.77 m² (so σ dropped from 3 m to about 1.66 m — the fix sharpened us, and 2.77 is smaller than both the prior 9 and the measurement's 4, the inverse-variance bonus). Finally, reset the error-state mean to zero: the −1.73 m has been applied, so the remaining error is once again unknown-and-small. The covariance P = 2.77 carries forward; the mean restarts at zero and re-grows as the IMU drifts until the next fix.

That five-step cycle IS the filter. Innovation → gain → estimate error → inject → reset. Everything else — the 15 states, the cascade, the bias estimation — is the same five steps applied to a bigger vector with cross-coupling in P. Get this scalar example in your bones and the matrix version is just bookkeeping.
Why are the accelerometer-bias and gyro-bias states (6 of the 15) so important for surviving a long GNSS dropout?

Chapter 4: Attitude Error & the Multiplicative EKF

Four of the five error blocks — position, velocity, accel bias, gyro bias — live happily in flat R³, where "plus" means ordinary vector addition. The fifth block, attitude error δθ, does not. Orientation is the one part of the navigation state that lives on a curved manifold, and naively adding a correction to it breaks things. The fix — representing attitude error multiplicatively — is the multiplicative extended Kalman filter (MEKF), and it is one of the most beautiful ideas in estimation.

Why attitude resists ordinary addition

The cleanest way to store an orientation is a quaternion q — four numbers with a hard constraint: they must have unit length, ||q|| = 1. (Lesson 13 and the dedicated quaternion lessons cover why; here we just need the constraint.) Now try to run a Kalman filter that carries the quaternion in its state and corrects it by adding a small vector, the way it corrects position. Two things break:

The multiplicative fix — error as a small rotation

The MEKF sidesteps both problems with one decision: keep the full attitude as a unit quaternion in the nominal state, but represent the error as a small rotation multiplied onto the nominal, not added. The true orientation is the nominal orientation composed with a tiny error rotation:

qtrue = δq ⊗ qnominal

where ⊗ is quaternion multiplication and δq is a small error quaternion. Because the error rotation is tiny, its quaternion is, to first order:

δq ≈ [ 1,   ½δθx,   ½δθy,   ½δθz ]

The scalar part is essentially 1 (a tiny rotation barely rotates), and the vector part is half the small-angle error vector δθ — three numbers. That three-number vector δθ is what the Kalman filter carries in its state — not the four-number quaternion. The filter's attitude error is a clean 3-vector in R³, with a well-behaved 3×3 covariance and no constraint to violate. The full unit quaternion lives safely in the nominal state, untouched by the filter's linear algebra.

The MEKF in one image. Keep the full, exact orientation as a quaternion (nominal). Track only the tiny tilt between that quaternion and the truth as three little angles (the error). When GNSS aids the filter, it estimates those three angles, then rotates the nominal quaternion by them (multiplicative injection) and resets the three angles to zero. Multiplication respects the manifold; addition would tear it. Three numbers for three degrees of freedom — no waste, no constraint, no singularity.

Inject and reset, the attitude way

The injection step for attitude is the only place where ⊕ is fancier than plain addition. After the filter estimates the error angles δθ̂, it builds the small error quaternion from them and multiplies it onto the nominal:

qnominal ← δq̂ ⊗ qnominal,    then   δθ̂ ← 0

The estimated tilt is rotated into the nominal orientation, and the error angles reset to zero — the same inject-and-reset rhythm as position, but with rotation composition instead of addition. This keeps the quaternion exactly unit-length at all times (because we only ever multiply unit quaternions), and keeps the filter's covariance honest (it only ever describes a 3-D tilt). Position, velocity, and biases inject additively; attitude injects multiplicatively. That single distinction is the whole content of "multiplicative" EKF.

Why the MEKF is the standard. Spacecraft attitude determination (Markley & Crassidis), drone flight controllers (PX4, ArduPilot), and Solà's reference ESKF all use the multiplicative attitude-error representation for the same reason: it gives the filter a minimal 3-parameter, singularity-free, constraint-free attitude error while keeping the full quaternion globally valid. It is the correct marriage of "Kalman filters want flat vector spaces" and "orientation is a curved manifold."

Why attitude error is the most dangerous of all

It's worth dwelling on why attitude gets this special treatment beyond mathematical tidiness: a tiny attitude error does outsized damage. The accelerometer measures specific force, which includes gravity (about 9.81 m/s²). If your attitude estimate is tilted by even a small angle θ, the strapdown mis-projects gravity, leaking a horizontal acceleration of roughly g·sinθ into your velocity. A 0.1° tilt (0.00175 rad) leaks 9.81 × 0.00175 ≈ 0.017 m/s² of phantom horizontal acceleration — which integrates to velocity and then position drift. Attitude error is the dominant upstream driver of the whole error cascade, which is exactly why getting its representation right (and estimating gyro bias to keep it small) matters so much.

Below, watch a small attitude tilt leak gravity into horizontal acceleration. Drag the tilt and see the phantom acceleration — and the position drift it produces — grow.

A small tilt leaks gravity into phantom acceleration

Drag the attitude tilt. The accelerometer feels gravity (down). A tilt of θ mis-projects a horizontal component g·sinθ that the strapdown mistakes for real motion — the phantom acceleration that drives drift.

Attitude tilt θ (deg) 2.0
drag the tilt slider
Why does the MEKF represent attitude error as a small rotation multiplied onto the reference quaternion, rather than a 4-vector added to it?

Chapter 5: The Lever Arm — The Antenna Isn't Where the IMU Is

Here is an error that is pure geometry, costs nothing to fix, and silently poisons countless first-attempt navigation filters: the lever arm. The IMU sits somewhere on the vehicle — bolted near the center of mass, say. The GNSS antenna sits somewhere else — up on the roof, out on a wing, on a mast. They are not at the same point. The filter estimates the position of the IMU, but GNSS measures the position of the antenna. If you fuse the antenna's position as if it were the IMU's, the filter sees a constant discrepancy and spends its life fighting a phantom error that isn't an error at all.

The fix — predict the antenna, not the IMU

The cure is to make the measurement model honest. The antenna's position is the IMU's position plus the lever arm r (the fixed offset vector from IMU to antenna), rotated into the navigation frame by the current attitude. So the predicted GNSS position is:

antenna = pIMU + Rnav · r

where pIMU is the IMU position (the filter's state), r is the lever arm measured in the body frame, and Rnav is the rotation matrix from body to navigation frame (from the attitude). The filter compares GNSS to this prediction, so the constant offset is accounted for and the innovation reflects real drift, not the mounting geometry.

For velocity there's a second, subtler term. When the vehicle rotates, a point offset from the rotation center moves even if the center is still — think of a kid on the end of a spinning merry-go-round. So the antenna's velocity is the IMU's velocity plus a rotational term ω × r (angular velocity crossed with the lever arm), rotated into the nav frame:

antenna = vIMU + Rnav · (ω × r)

where ω is the body's angular rate (straight from the gyros). Skip this ω×r term and your filter will see a phantom velocity error every time the vehicle turns — a heading-dependent error that's maddening to diagnose because it only appears in turns.

The lever arm is the #1 silent killer. An uncompensated lever arm doesn't crash the filter — it quietly biases it. Position shows a constant offset; velocity shows errors that appear only in turns. The filter, trying to explain a discrepancy that geometry already explains, mis-estimates the biases and attitude to compensate, corrupting everything. And the fix is free: measure the offset once with a tape measure and put it in the measurement model. The hard part is remembering it exists.

Worked example — the lever-arm correction, by hand

Let's compute an actual lever-arm correction with real numbers. A survey van has its IMU at the floor near the rear axle and its GNSS antenna on a mast. The lever arm from IMU to antenna, measured in the body frame (x = forward, y = right, z = down), is:

r = [ 1.5,   0.0,   −2.0 ] m

That is: the antenna is 1.5 m forward of the IMU and 2.0 m up (z is down, so −2.0 means up). Take the simple case where the van faces due north and is level, so the body frame aligns with the nav frame (Rnav = identity).

Position correction. With Rnav = identity, the predicted antenna position is just the IMU position plus r. Say the filter's IMU position is pIMU = [North 200.0, East 50.0, Down 0.0] m. Then:

antenna = [200.0, 50.0, 0.0] + [1.5, 0.0, −2.0] = [201.5, 50.0, −2.0] m

GNSS will read the antenna at roughly [201.5, 50.0, −2.0] (2 m up, 1.5 m north of the IMU). If we'd predicted the bare IMU position [200.0, 50.0, 0.0], the innovation would show a phantom 1.5 m north error and a 2.0 m altitude error forever — pure mounting geometry the filter would waste effort chasing.

Now rotate the van. Turn it to face east (90° clockwise from north). The "forward" 1.5 m of lever arm now points east, not north. The rotation matrix swaps the contribution: the predicted antenna offset becomes [North 0.0, East 1.5, Down −2.0]. So now:

antenna = [200.0, 50.0, 0.0] + [0.0, 1.5, −2.0] = [200.0, 51.5, −2.0] m

The same physical lever arm produces a 1.5 m offset to the east now instead of north — the correction is heading-dependent, which is exactly why a filter that ignores it can't compensate with a single constant: the phantom error rotates with the vehicle. Only the full Rnav·r term tracks it.

Velocity correction in a turn. Suppose the van is turning at ω = [0, 0, 0.2] rad/s (yawing, z-axis down, about 11°/s). The rotational velocity of the antenna is ω × r:

ω × r = [0,0,0.2] × [1.5, 0, −2.0] = [ 0·(−2.0) − 0.2·0,   0.2·1.5 − 0·(−2.0),   0·0 − 0·1.5 ] = [0, 0.3, 0] m/s

So during this turn the antenna is moving 0.3 m/s to the right (east, in body frame) relative to the IMU, purely from the rotation. GNSS Doppler will see that 0.3 m/s; if the filter doesn't add the ω×r term, it mistakes it for a real 0.3 m/s velocity error — but only while turning. Add the term and the phantom vanishes.

The lever arm, summarized. Predicted antenna position = IMU position + Rnav·r. Predicted antenna velocity = IMU velocity + Rnav·(ω×r). Measure r once with a tape measure, put both terms in the measurement model, and the phantom offsets (constant in position, turn-only in velocity) disappear. Forget them and you'll chase heading-dependent ghosts forever.

Below, rotate the body and watch the antenna swing around the IMU on its lever arm. The phantom error shows what GNSS would appear to report if you forgot the Rnav·r correction — notice it rotates with the body, so no constant offset can cancel it.

The lever arm rotates with the body

Rotate the body. The antenna is offset from the IMU by the lever arm r. If you forget the R·r correction, GNSS appears to report the antenna where you expect the IMU — a phantom error that swings with heading and no constant can cancel.

Body heading (deg) 0
rotate the body
A drone's GNSS antenna is mounted 0.4 m forward of its IMU. The filter ignores the lever arm. What's the symptom?

Chapter 6: Showcase — A Vehicle, a Tunnel, and the Fused Estimate

Everything comes together here. A vehicle drives a 2-D loop. You can watch three trajectories at once: the INS-only estimate sliding ever further from truth, the GNSS fixes scattering near truth, and the fused ESKF estimate — smooth, bounded, and glued to the dashed truth. Then trigger a GNSS dropout (a tunnel) and watch the fused estimate coast on the INS, drift a little, and snap back the instant satellites return. The shaded ellipse is the filter's position-error covariance — it swells during the dropout (uncertainty grows when coasting) and shrinks on reacquisition (a fix sharpens it).

The full INS/GNSS fuser with a tunnel dropout

Press Run. The vehicle laps the dashed truth loop. INS-only drifts away; GNSS fixes dot near truth; the fused estimate stays glued. Hit Tunnel to cut GNSS — watch the fused estimate coast and the covariance ellipse swell, then collapse when the fix returns. Drag the IMU-quality slider to make the coast better or worse.

IMU quality 60
press Run

Play with it until the architecture feels inevitable. Run with the tunnel off and notice the fused line is smoother than the GNSS dots and far more bounded than the INS line — it is better than either input. Hit the tunnel and watch the fused estimate calmly coast where GNSS-only would simply stop and INS-only would sprint away; the covariance ellipse grows to honestly report rising uncertainty. Drop the IMU quality to the floor and the coast gets ugly fast (a cheap IMU drifts quickly through the dropout); raise it and the vehicle can cross a long tunnel barely noticing. That single trade — IMU quality buys dropout endurance — is the entire economics of navigation-grade vs consumer-grade inertial sensors.

What you're watching is the world's navigation systems. This exact picture — an error-state Kalman filter fusing a high-rate IMU with bounded GNSS fixes, coasting through dropouts on estimated biases, reporting honest covariance — runs in your phone, your car, every airliner, every survey drone, every autonomous vehicle. There is no more important architecture in applied estimation. You now understand it from the inside.

Chapter 7: Use Cases & Real Products

INS/GNSS coupling via an error-state filter is not an academic curiosity — it is the silent backbone of nearly everything that knows where it is. Here is where it actually runs, and the names of the stacks that run it.

Automotive dead-reckoning — through the tunnel

Every modern car navigation system fuses a low-cost MEMS IMU (and often wheel-speed and steering-angle sensors) with GNSS in a loosely or tightly coupled filter. When you drive into a tunnel or a multi-level parking garage and your phone's blue dot keeps moving instead of freezing, that's INS/GNSS dead-reckoning coasting on the IMU. Automotive suppliers (Bosch, Continental, u-blox with its on-chip dead-reckoning) ship exactly this. Add wheel odometry and the coast through a long tunnel becomes remarkably tight — the wheels measure distance traveled, which directly bounds along-track drift.

Aircraft & UAV navigation

Commercial and military aircraft carry navigation-grade INS fused with GNSS as the primary position-velocity-attitude source. On the drone side, the open-source autopilots PX4 and ArduPilot both run an error-state navigation filter — PX4's is literally called EKF2, an error-state EKF fusing IMU, GNSS, magnetometer, barometer, and optical flow. It is the single most-deployed ESKF in the world by unit count, flying millions of drones. The 15-state structure from Chapter 3, the multiplicative attitude from Chapter 4, the lever arm from Chapter 5 — all of it is in EKF2.

Surveying & mapping — RTK + INS

High-accuracy mobile mapping (the vans that build street-view and HD maps, airborne LiDAR survey, agricultural field mapping) demands centimeter-level position and precise attitude to georeference every sensor sample. These systems fuse RTK GNSS (real-time kinematic, carrier-phase GNSS good to centimeters) with a high-grade IMU in a tightly coupled ESKF. The commercial leaders are NovAtel SPAN (their GNSS+INS product line) and Applanix POS (the standard for airborne and marine mapping). Tight coupling matters here because survey routes run under bridges and tree canopy where satellites drop below four.

Smartphone fused location

Your phone's "fused location provider" blends GNSS, the MEMS IMU, Wi-Fi, and cell-tower positioning. The inertial part does pedestrian dead-reckoning (step detection plus heading) fused with GNSS so the blue dot stays smooth and survives brief urban-canyon dropouts. It's a lighter-weight cousin of the full ESKF, but the same complementary logic: high-rate inertial for smoothness, bounded GNSS to kill drift.

Agriculture & marine

Precision-agriculture auto-steer (tractors driving centimeter-accurate rows) uses RTK-GNSS+INS so the tractor holds its line even as terrain pitches and rolls the antenna — the INS provides the attitude that georeferences the RTK fix and bridges momentary GNSS gaps behind a tree line. Marine survey and dynamic-positioning systems on ships fuse GNSS with INS (and sometimes Doppler velocity logs) to hold station and georeference sonar, riding out the slow heave and roll that would smear an unfused GNSS track.

DomainGNSS sourceIMU gradeCouplingNamed stack
Automotivestandard GNSS + wheel odoMEMS (cheap)loose / tightu-blox DR, Bosch, Continental
UAV / dronestandard / RTK GNSSMEMSerror-state EKFPX4 EKF2, ArduPilot EKF3
Survey / mappingRTK / PPPtactical / nav-gradetightNovAtel SPAN, Applanix POS
Smartphonestandard GNSS + Wi-FiMEMSloose (pedestrian DR)fused location provider
Agriculture / marineRTK GNSStacticaltightauto-steer, marine DP/survey
One architecture, every scale. From a $5 phone chip doing pedestrian dead-reckoning to a $100k airborne survey rig hitting centimeters, the skeleton is identical: an error-state filter fusing a high-rate IMU with bounded GNSS. What changes is the IMU grade, the GNSS quality (standard vs RTK vs PPP), and the coupling depth. The math you learned this lesson is the math in all of them.
PX4's EKF2 and ArduPilot's EKF3 are described as "error-state" navigation filters. What does that tell you about how they handle attitude and the full state?

Chapter 8: Practical Application — Building One That Works

You understand the architecture. Now the engineering decisions that separate a filter that works in the lab from one that survives the road. These are the dials and choices you'll actually turn.

Choosing loose vs tight

Decide by your environment and your access. Go loose if you mostly operate in open sky, you bought a sealed receiver that only outputs a solution, and you value modularity (swap receivers freely). Go tight if you routinely drop below four satellites (cities, forests, canyons), you can get raw pseudoranges from your receiver, and you need graceful degradation. The single most common mistake is staying loose in a satellite-starved environment and watching the filter go blind every time the count dips — when two visible satellites could have kept it constrained.

Tuning process noise for the bias states

The bias states need a small process noise (the Q matrix entries) that models them as a slow random walk. Too small and the filter believes the bias is frozen — it stops tracking the real drift, and the estimate ages out. Too large and the filter lets the bias wander to absorb every wiggle, including ones that are really measurement noise or unmodeled dynamics, making it twitchy and stealing observability from the true states. The right value comes from the IMU's datasheet (bias instability, often quoted as an Allan-variance number) tuned up a bit on real data. Rule of thumb: start from the datasheet bias-instability, then increase Q until the bias estimate tracks slow temperature drift without chasing noise.

Feedback vs feedforward, revisited. Prefer closed-loop feedback: inject the estimated error back into the strapdown so the nominal trajectory stays near truth. This keeps the small-angle / small-error linearization valid, which keeps F accurate. Feedforward (let the strapdown run free, add the error only at the output) is simpler but lets the nominal drift far between corrections, eventually breaking the linearization. Use feedforward only for short missions or when you can't modify the strapdown integrator.

ZUPT and NHC — free aiding for ground vehicles

Ground vehicles get two powerful, sensor-free aiding measurements that dramatically extend dropout endurance:

RTK and PPP — upgrading the GNSS source

Standard GNSS gives meter-level position. Two upgrades give centimeters, and they slot straight into the same ESKF as a better (lower-R) position measurement:

Both are just lower-noise position (and sometimes attitude, via dual-antenna) measurements to the filter; the architecture doesn't change, only R shrinks and the fused estimate sharpens.

Initialization & alignment

The ESKF assumes the error is small — which means you must start close to truth, or the linearization is invalid from tick one. Position and velocity init from the first GNSS fixes. Attitude is harder: at rest, the accelerometers sense gravity, which fixes roll and pitch (you can see "down"), and a magnetometer or dual-antenna GNSS or a brief drive (course-over-ground from GNSS velocity) fixes heading. This alignment phase — coarse then fine — must finish before you trust the filter. A bad initial heading is the classic "filter slowly diverges for the first minute" bug.

The practical hierarchy of free wins. Before spending money on a better IMU: add wheel odometry, add ZUPT, add NHC, and get the lever arm and alignment exactly right. These cost nothing and often buy more dropout endurance than a 10×-pricier inertial sensor. Hardware is the last resort, not the first.
A ground robot drives through a 30-second tunnel. Which free, sensor-light aiding measurements most extend how well it coasts?

Chapter 9: Debugging & Failure Modes

INS/GNSS filters fail in a small set of characteristic, recognizable ways. Learn the signature of each and you'll diagnose in minutes what otherwise costs days. Here is the field guide.

1. Uncompensated lever arm

Symptom: a constant position offset that rotates with heading (north when facing north, east when facing east), plus velocity errors that appear only in turns. Detect: log the GNSS innovation and correlate it with heading — if the position innovation's direction tracks the vehicle's heading, it's the lever arm. Fix: measure r and add Rnav·r (and ω×r for velocity) to the measurement model. The most common silent bug, from Chapter 5.

2. GNSS multipath & spoofing — the poisoned fix

In urban canyons, GNSS signals bounce off buildings before reaching the antenna, arriving late and producing a multipath error — a confident, plausible, wrong fix, sometimes tens of meters off. Spoofing is the malicious version: a transmitter feeds the receiver fake satellite signals. A naive filter ingests the bad fix and corrupts its whole state — one poisoned measurement can ruin minutes of careful estimation. Detect & defend: innovation gating (a.k.a. the chi-squared / NIS test). Before applying any measurement, check whether its innovation is consistent with the filter's predicted innovation covariance S. If the innovation is implausibly large (say, more than a few sigma), reject the measurement — the filter trusts its own physics over a wild outlier. This is the competitive-fusion idea from Lesson 1 (reject the liar) applied to GNSS.

Innovation gating is your immune system. Without it, a single multipath or spoofed fix poisons everything downstream — the filter dutifully injects a huge phantom error and corrupts position, velocity, attitude, and biases at once. With gating, a fix whose innovation exceeds the gate is simply dropped, and the filter coasts on the IMU as if GNSS had briefly disappeared. But beware over-tight gating: if you set the gate too small, you'll reject good fixes after a real maneuver, and the filter slowly diverges with no aiding. Gating is a balance, tuned on real data.

3. Time-sync & latency between IMU and GNSS

Symptom: velocity-dependent errors — small when slow, large when fast — that worsen with speed. Cause: the IMU and GNSS timestamps are misaligned, so a GNSS fix from time t gets applied to the INS state at time t+δ. At 30 m/s, a 20 ms timing error is 0.6 m of position error. Detect: errors that scale with velocity and vanish at rest point straight at time-sync. Fix: hardware-timestamp both sensors against a common clock (PPS from the GNSS receiver is the gold standard), and handle GNSS latency explicitly (apply the fix to the past state it corresponds to, then re-propagate — an out-of-sequence-measurement problem).

4. Filter overconfidence after a long outage

Symptom: after a long GNSS dropout, the filter rejects the first good fix on reacquisition (gates it out) or jumps violently. Cause: during a long coast the true error grew large, but if the covariance didn't grow enough (under-tuned process noise) the filter thinks it's still confident — so the now-distant-but-correct GNSS fix looks like an outlier and gets gated. Fix: make sure the predicted covariance grows realistically during coasting (tune Q so P swells honestly), and consider relaxing the gate right after a long outage so the filter can recapture. The covariance ellipse from the showcase swelling during the tunnel is the healthy behavior; a filter whose ellipse stays small while it coasts is lying to itself.

5. Attitude unobservable when stationary

Symptom: heading drifts or won't converge while the vehicle sits still or drives in a straight line, then suddenly snaps right after the first turn. Cause: some states are unobservable without excitation. Heading (yaw) is not observable from GNSS position/velocity when you're moving in a straight line — you have to turn (or have a second antenna, or a magnetometer) for GNSS to constrain heading. Likewise, biases are weakly observable without dynamics. Detect: heading variance stays large and only collapses after maneuvers. Fix: excite the system (turns, accelerations) during alignment, add a heading source (dual-antenna, magnetometer), and don't expect full observability at rest. This is why aircraft do S-turns during alignment.

6. Consistency checks — NEES & NIS

How do you know your filter is even honest — that its reported covariance matches its real errors? Two standard tests:

A filter that passes NIS in the field and NEES on the bench is consistent; one that fails them is lying about its uncertainty, and every downstream consumer (the gate, the path planner) inherits the lie.

The debugging reflex. Position offset that rotates with heading → lever arm. Wild jump after a fix → multipath/spoofing, add innovation gating. Errors that scale with speed → time-sync. Good fix rejected after a long outage → under-grown covariance. Heading won't converge at rest → unobservability, go turn. Filter "feels" wrong → run NIS/NEES to see if it's over- or under-confident. Six signatures, six fixes.
After exiting a 40-second tunnel, your filter rejects the first perfectly-good GNSS fix and keeps drifting. What's the most likely cause?

Chapter 10: Connections, References & Cheat-Sheet

You've built, from the inside, the workhorse architecture of real navigation: an error-state Kalman filter fusing a high-rate IMU with bounded GNSS, with multiplicative attitude, bias estimation, and lever-arm compensation. Let's connect it to the rest of the series and leave you a one-page reference.

The complementary-filter connection — ESKF is the optimal generalization

Cast your mind back to Lesson 6, the complementary filter. There, you fused a drifting-but-smooth inertial source with a bounded-but-noisy reference by high-passing the inertial and low-passing the reference, with a fixed crossover frequency you tuned by hand. The error-state Kalman filter is exactly that idea, made statistically optimal. It high-passes the IMU (trusts it on short time-scales, where its drift hasn't accumulated) and low-passes GNSS (trusts it on long time-scales, where its bounded error wins) — but instead of a hand-tuned crossover, the Kalman gain sets the crossover automatically and optimally, tick by tick, from each source's tracked variance. The complementary filter is the ESKF with a frozen, hand-picked gain; the ESKF is the complementary filter that computes its own gain. Same physics, optimal weighting.

The throughline of this whole series. Lesson 1 named complementary fusion and weighted by inverse variance. Lesson 5 made that optimal and recursive (the Kalman filter). Lesson 6 built the cheap fixed-gain version (complementary filter). Lesson 7 handled nonlinear sensors (the EKF). This lesson assembles all of them into the canonical real-world system: complementary GPS+IMU fusion, done with an error-state EKF, with the attitude manifold handled correctly. Every piece you learned earlier is load-bearing here.

The whole ESKF update in code

Here is the verbose, commented numpy ESKF measurement update — predict the error covariance, fold in a GNSS position fix, then inject and reset. This is the matrix version of the scalar worked example from Chapter 3:

python
import numpy as np

# --- state layout (15): [dp(3) dv(3) dth(3) ba(3) bg(3)] ---
# nominal state lives OUTSIDE the filter (strapdown). The filter only
# ever carries the ERROR covariance P and a (usually-zero) error mean.

def predict_error_cov(P, Phi, Q):
    # Phi = discrete error-state transition (I + F*dt), built from the
    # nominal trajectory; Q = process noise (IMU noise + bias random walk).
    return Phi @ P @ Phi.T + Q          # the drift/uncertainty GROWS here

def gnss_position_update(nominal, P, z_gps, R, lever_r):
    # --- 1. predicted measurement: IMU position + rotated lever arm ---
    R_nav = nominal['R_nav']                 # body->nav rotation (from attitude)
    p_imu = nominal['p']
    p_pred = p_imu + R_nav @ lever_r        # predict the ANTENNA, not the IMU

    # --- 2. innovation: how far GNSS disagrees with our prediction ---
    y = z_gps - p_pred                      # (3,) residual

    # --- 3. measurement Jacobian H: dz/d(error). Position error maps
    #       straight through (I on the dp block); zeros elsewhere. ---
    H = np.zeros((3, 15))
    H[:, 0:3] = np.eye(3)            # dp block

    # --- 4. innovation covariance + chi-squared GATE (reject bad fixes) ---
    S = H @ P @ H.T + R
    nis = y @ np.linalg.solve(S, y)         # normalized innovation squared
    if nis > 16.0:                          # ~3-sigma gate for 3 dof
        return nominal, P, False            # multipath/spoof? coast on IMU

    # --- 5. Kalman gain + error estimate ---
    K  = P @ H.T @ np.linalg.inv(S)         # (15,3) trust weights
    dx = K @ y                              # (15,) ESTIMATED error state

    # --- 6. covariance update (Joseph form is more stable in practice) ---
    I = np.eye(15)
    P = (I - K @ H) @ P @ (I - K @ H).T + K @ R @ K.T

    # --- 7. INJECT the error into the nominal, then RESET it to zero ---
    nominal = inject(nominal, dx)         # dp,dv,ba,bg add; dth multiplies (MEKF)
    # error mean is now zero by construction; only P carries forward.
    return nominal, P, True

def inject(nominal, dx):
    nominal['p'] += dx[0:3]               # position: additive
    nominal['v'] += dx[3:6]               # velocity: additive
    nominal['q'] = quat_mul(small_quat(dx[6:9]), nominal['q'])  # attitude: MULTIPLICATIVE
    nominal['ba'] += dx[9:12]             # accel bias: additive
    nominal['bg'] += dx[12:15]            # gyro bias: additive
    return nominal

And the same idea, compressed — the four conceptual beats, stripped to their essence:

python
# compact ESKF: nominal lives outside; filter handles only the error
def eskf_step(nominal, P, Phi, Q, z, R, H, lever_r):
    P = Phi @ P @ Phi.T + Q                                  # 1. predict (error grows)
    y = z - (nominal['p'] + nominal['R_nav'] @ lever_r)      # 2. innovation (lever-arm aware)
    S = H @ P @ H.T + R
    if y @ np.linalg.solve(S, y) > 16.0: return nominal, P    # gate: drop outliers
    K = P @ H.T @ np.linalg.inv(S)
    nominal = inject(nominal, K @ y)                          # 3+4. inject & (mean) reset
    P = (np.eye(15) - K @ H) @ P
    return nominal, P

Where this sits, and what's next

This lesson fused inertial with a radio absolute reference (GNSS). The very next conceptual step is to replace — or augment — the radio reference with a visual one. When GNSS is unavailable entirely (indoors, underground, on Mars, in a GPS-denied warzone), a camera can play GNSS's role: it provides bounded, drift-correcting observations of the world that leash the IMU's drift. That is visual-inertial odometry (VIO) — the same error-state, IMU-as-spine philosophy, with image features instead of satellites as the aiding source. Lesson 16: Visual-Inertial Odometry is the vision-aided analog of everything you just learned: the IMU still drifts, something bounded still corrects it, and an error-state filter (or its optimization cousin) still does the fusing. If you understood this lesson, VIO will feel like coming home with a new aiding sensor.

This lesson (INS/GNSS)Lesson 16 (VIO)
IMU is the high-rate spineIMU is the high-rate spine (same)
GNSS provides bounded absolute fixescamera provides bounded drift-correcting features
error-state KF, multiplicative attitudeerror-state KF / optimization, multiplicative attitude (same)
dies when GNSS dropout is too longdies when the scene is textureless or dark
lever arm: IMU-to-antennaextrinsics: IMU-to-camera (same idea)

Cheat-sheet

ConceptOne-line takeaway
Aiding ideaGNSS bounds the IMU's unbounded drift; the IMU coasts through GNSS dropouts and de-noises GNSS
Loose couplingfuse the GNSS position/velocity solution; simple & modular; needs ≥4 sats; time-correlated errors
Tight couplingfuse raw pseudoranges/Doppler; works with <4 sats; graceful degradation; needs GNSS modeling
Deep couplingINS aids the receiver's tracking loops; survives jamming & heavy dynamics
Error-state KFfilter the small linear error δx, not the full nonlinear state; nominal lives in the strapdown
Inject & resetfold δx̂ into the nominal, then zero the error mean (keep P); produces the sawtooth
15 statesδp, δv, δθ, accel-bias, gyro-bias (3 each); biases are the key to long dropouts
MEKFattitude error = small rotation MULTIPLIED onto the nominal quaternion; 3 dof, no norm constraint
Lever armpredict the antenna: p + R·r (position), v + R·(ω×r) (velocity); else heading-dependent ghosts
Feedbackinject into the strapdown to keep the linearization valid (vs feedforward output-only)
ZUPT / NHCfree aiding: velocity=0 at rest; zero lateral/vertical body velocity for wheeled vehicles
Innovation gatingchi-squared/NIS test rejects multipath & spoofed fixes before they poison the filter
NEES / NISconsistency checks: is the filter's reported covariance honest? over- or under-confident?

Related lessons on Engineermaxxing

Lesson 13, IMU & Inertial Navigation, is the strapdown mechanization this lesson sits on top of — the source of the nominal state and the drift we spend this whole lesson bounding. Lesson 16, Visual-Inertial Odometry, is the vision-aided sequel.

The motto. What I cannot create, I cannot understand; what I cannot understand, I cannot teach. You just built the world's navigation filter from its error-state foundations — nominal plus error, inject and reset, multiplicative attitude, lever-arm geometry, innovation gating. You can now create it; you understand it; you can teach it.

References

  1. Groves, P. D. Principles of GNSS, Inertial, and Multisensor Integrated Navigation Systems, 2nd ed. Artech House, 2013. The definitive engineering reference for loose/tight/deep coupling, the error-state filter, and lever-arm modeling.
  2. Solà, J. "Quaternion kinematics for the error-state Kalman filter." 2017. The clearest modern derivation of the ESKF with multiplicative attitude error. arXiv:1711.02508
  3. Markley, F. L. and Crassidis, J. L. Fundamentals of Spacecraft Attitude Determination and Control. Springer, 2014. The canonical treatment of the multiplicative EKF (MEKF) and small-angle attitude error.
  4. Farrell, J. A. Aided Navigation: GPS with High Rate Sensors. McGraw-Hill, 2008. A rigorous, implementation-oriented account of INS aiding, observability, and consistency.
A colleague says "the error-state Kalman filter is just a totally different beast from the complementary filter we used for AHRS." What's the precise correction?