Skip to content

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.

  • Mean — integrate each IMU interval from both of its samples: debias, interpolate the input linearly between the samples at t₍ₖ₋₁₎ and tₖ, and integrate R, v, p by RK4 (#616). An image between two samples is reached by holding the last sample; the next sample integrates the rest of its interval from the rate interpolated at the image time.
  • 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 noise Q_d should map measurement-space noise through the noise Jacobian G, producing θ–v–p cross-correlations and the higher-order Δt²/Δt³ scalings — not a plain diagonal.

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.

Signaturepropagate(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 / invariantsR ∈ 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 closes on a continuous truth over a clone window within what the camera resolves (1.09e-4 rad, 0.22 mm at 2 m), at second order
cortexmsckf/stages/s2_propagation.hpp (the convention), msckf/propagator.hpp (ImuInterval, rk4_strapdown, Φ, Q)

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”
checkresult
Φ reconstruction (real == reference)1.5e-16 — pass
R orthonormality ‖RᵀR − I‖ @0.5 s2.2e-16 — pass
P min-eigenvalue @0.5 s> 0 — PSD
global-position nullspace leak0 — preserved
GT-injection integrator orderO(Δt) (error halves with Δt: 59→30→15→7 mm @ 50→800 Hz) — this was #616’s defect, see below

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:

quantitycortex vs canonical
Q θ-σ, Q v-σmatch
Q position term0 vs 3.06e-6 m — dropped
Q v–p cross term0 vs 1.25e-9 — dropped
Q relative-Frobenius gap0.35% of Q dropped
position-σ under-report @0.05 s (inter-frame)7.4%
position-σ under-report @0.5 s0.7%

Diagonal-Q position-σ deficit vs canonical Per-step propagation error vs ground truth

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 scalepose NEES (target 6)
0.2523.5
0.512.2
1 (shipped)6.4
22.97
41.52

Propagation-only NEES vs Q-scale

At the default Q, NEES ≈ 6.4 ≈ the 6-DoF target — the propagated covariance is essentially honest on its own.

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.

Terminal window
s2_inspect --dataset /path/to/V1_01_easy/mav0 --out build/s2 --window-s 0.5
node docs-site/scripts/gen-propagation-figures.mjs build/s2 # → cov_growth.svg, pose_drift.svg

It 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-Q simplification, 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.

The mean: integrating from both samples (#616)

Section titled “The mean: integrating from both samples (#616)”

The probe above cleared S2’s covariance. Its mean carried a defect the probe measured and nobody bounded. Both backends held one IMU sample over each 5 ms interval, and they held different ones: MsckfBackend the sample that ends the interval, InvariantVioBackend the one that starts it. Either is a half-sample (2.5 ms) skew between IMU and camera. On the EuRoC consistency gate, with exact R and Q from one spline truth, NIS was 1.00 on the slow sequences and 1.1–1.9 on the fast ones (V1_03, V2_02, V2_03).

The closure test (tests/tools/gt_spline.cpp) integrates a spline’s own noise-free IMU over a 0.55 s clone window on a flight that rolls and pitches at about 1.3 rad/s, against what the camera resolves (a tenth of 0.5 px: 1.09e-4 rad, 0.22 mm at 2 m):

SchemeRotation ÷ boundPosition ÷ boundOrder
End sample held, Euler (MSCKF before #616)28.641first
Start sample held, Euler (R-IEKF before #616)28.06.9first
The two samples’ average, Euler0.2824first in position
The line between the samples, RK4 (shipped)0.270.10second

Centring the input removes the rotation error. The position error is the integrator’s: an Euler step rotates the specific force with the attitude at the start of the interval. RK4 rotates it along the interval.

Φ followed the mean. #471 found that a higher-order Φ leaks more yaw than the first-order I + A·Δt. That was true against the one-sample Euler mean, which the first-order Φ approximately linearizes. Against the RK4 mean the first-order Φ leaks 7× more than it used to (4.4e-6 per step at 2.5 ms, from 6.0e-7), so the lesson was about the agreement between Φ and the mean, not about truncation. The RK4 mean has the preintegration form R′ = R·ΔR, v′ = v + gΔt + R·Δv, p′ = p + vΔt + ½gΔt² + R·Δp, which a rotation about gravity commutes with. Φ is now that mean’s exact Jacobian, and it preserves the whole unobservable subspace, yaw included, to roundoff (tests/sdk/imu_interval.cpp checks it against finite differences).