S2 — IMU Propagation
Between camera frames the IMU runs at 200 Hz–1 kHz, and S2 integrates it: it
strapdown-integrates the mean (attitude, velocity, position) and propagates the
covariance P forward, so the filter arrives at the next image with both a
predicted pose and an honest uncertainty about it. This stage was a prime suspect
for the #212 over-confidence — and the probe
cleared it. This page is what a measured negative result looks like.
What it does
Section titled “What it does”- Mean — debias the IMU sample, then strapdown-integrate
R, v, p(zeroth-order hold or RK4). - Covariance —
P⁻_{k+1} = Φ P⁺_k Φᵀ + Q_d. The transition matrixΦcarries the off-diagonal skew blocks coupling orientation error into velocity and position error — gravity leakage living inside the covariance. The discrete noiseQ_dshould map measurement-space noise through the noise JacobianG, producingθ–v–pcross-correlations and the higher-orderΔt²/Δt³scalings — not a plain diagonal.
Why it was a suspect
Section titled “Why it was a suspect”The cortex Q_d is assembled diagonally — it drops the canonical position-noise
block and the velocity–position cross term. A diagonal Q_d understates
position/velocity process noise and omits correlations the update relies on, which
is a textbook over-confidence mechanism. The question is not “is it diagonal” (it
is) but how much does that actually cost — and the only honest way to answer is
to measure it against a canonical reference.
The contract
Section titled “The contract”| Signature | propagate(x⁻, P⁻, imu[t_k, t_{k+1}]) → (x⁻_{k+1}, P⁻_{k+1}) |
| Pre | Δt > 0, monotonic timestamps; measurements debiased; P⁺_k symmetric PSD |
| Post / invariants | R ∈ SO(3) (orthonormal, det 1) · P⁻ symmetric PSD · Q_d PSD with θ–v–p cross-correlations · the 4-D unobservable subspace (global position + yaw) stays in the propagation null space · GT-injection: zero-noise propagation reproduces the GT trajectory within integration tolerance |
| cortex | msckf/propagator.hpp (Φ assembly, Q, FEJ) |
Metrics
Section titled “Metrics”The S2 probe reconstructs the canonical Φ/Q_d
and compares them to the shipped Propagator, then drives the real propagator
on the synthetic world.
Φ reconstruction & the structural invariants — all pass
Section titled “Φ reconstruction & the structural invariants — all pass”| check | result |
|---|---|
| Φ reconstruction (real == reference) | 1.5e-16 — pass |
R orthonormality ‖RᵀR − I‖ @0.5 s | 2.2e-16 — pass |
P min-eigenvalue @0.5 s | > 0 — PSD |
| global-position nullspace leak | 0 — preserved |
| GT-injection integrator order | O(Δt) (error halves with Δt: 59→30→15→7 mm @ 50→800 Hz) |
The diagonal-Q deficit — real, but small
Section titled “The diagonal-Q deficit — real, but small”The cortex Q_d does drop terms — but measured against the canonical G Q_meas Gᵀ,
the cost is minor:
| quantity | cortex vs canonical |
|---|---|
| Q θ-σ, Q v-σ | match |
| Q position term | 0 vs 3.06e-6 m — dropped |
| Q v–p cross term | 0 vs 1.25e-9 — dropped |
| Q relative-Frobenius gap | 0.35% of Q dropped |
| position-σ under-report @0.05 s (inter-frame) | 7.4% |
| position-σ under-report @0.5 s | 0.7% |
Propagation-only NEES — consistent at the default Q
Section titled “Propagation-only NEES — consistent at the default Q”Driving the real propagator and scaling Q, the propagation-only 6-DoF pose NEES
sits right at its target with the shipped noise:
| Q scale | pose NEES (target 6) |
|---|---|
| 0.25 | 23.5 |
| 0.5 | 12.2 |
| 1 (shipped) | 6.4 |
| 2 | 2.97 |
| 4 | 1.52 |
At the default Q, NEES ≈ 6.4 ≈ the 6-DoF target — the propagated covariance is
essentially honest on its own.
Real-data inspector (s2_inspect)
Section titled “Real-data inspector (s2_inspect)”The measurements above come from the synthetic probe — known IMU, known Q, run
in CI. Its real-data companion, s2_inspect, runs the real propagator
(msckf::Propagator) over a real EuRoC IMU window. It seeds the prior pose+velocity
from ground truth at the window start (plus the filter’s initial-P seed), then
propagates step by step — no SDK change, since the propagator is already standalone.
s2_inspect --dataset /path/to/V1_01_easy/mav0 --out build/s2 --window-s 0.5node docs-site/scripts/gen-propagation-figures.mjs build/s2 # → cov_growth.svg, pose_drift.svgIt writes propagation.json and renders two figures:
- the covariance-growth heatmap — each of the 15 IMU error states’ σ over the
window (row-normalized), with the θ/p/v/b_g/b_a blocks marked. The position block
growing only via the velocity coupling (no direct position-Q term) is the
diagonal-
Qsimplification, now visible on real data. - drift vs the ±3σ envelope — the propagated pose’s dead-reckoning drift from
ground truth, plotted against the filter’s own growing 3σ band. Drift that
stays inside the band means the propagated covariance honestly covers the error;
drift that escapes it (red) is the local, real-data image of an under-noised
Q.
This is the real-data check on the synthetic finding below: on EuRoC the envelope covers the inter-frame drift, confirming propagation is essentially honest on its own — the #212 over-confidence enters elsewhere.