Skip to content

The VIO Regression Suite

This is the checklist a VIO pipeline is certified against. Every row exists because its failure happened here; the storyline tells which. A row’s pass criterion is independent of cortex, so it carries over to another pipeline. The test here column names the cortex test case that implements it (Catch2 case names, under tests/).

Status:

  • locked: asserted in CI.
  • characterization: a test pins a known defect, so a fix flips it.
  • partial / weak: a test exists but doesn’t yet assert the full criterion.
  • gated: skipped in CI, which has no EuRoC dataset; run locally.
  • warn: runs and reports, doesn’t assert.
  • missing: no test yet; the issue beside it tracks one.
#RequirementPass criterionTest hereStatus
A1The world’s IMU integrates to its ground truthDead reckoning with the true biases and no noise stays within 5 cm over 12 s; v and a are continuous at the onsetsynthetic_world.cpp “A1: …” cases (dead reckoning at onsets 0.25, 1 and 2 s; 200 vs 800 Hz onset continuity; a #475 step fixture fails)locked
A2Gravity-sign contract across modulesRecovered gravity is +g in the preintegrator’s convention; the seed tilt is a few degrees, never about 180°; a sign-flipped fixture failsstage_bench_s0_s1.cpp S1 invariants; sensor_model_probe.cpp gravity signlocked
A3Camera modelsRound trip and Jacobians match finite differencesthe camera-model testslocked
A4The synthetic camera matches the real one it imitatesIntrinsics and distortion equal EuRoC cam0’ssynthetic_world.cpp “A4: the synthetic world’s camera is EuRoC cam0” (every parameter, exactly)locked
A5Input characterizationStatic Allan variance matches the configured Q; timestamps are monotonicallan_variance.cpppartial #268
#RequirementPass criterionTest hereStatus
B1S1: the scale-observability cliffDeclines below 5 cm of resolved motion; scale error below 1% on well-excited windows”S1 scale-observability gate declines at low excitation (the cliff)“locked
B2S1: init consistency on noisy SfMAn accepted scale is within 5%; otherwise the window is declined”S1 SfM bench: the 0.1 px window builds … but its scale is off”characterization #501
B3S1: SfM under realistic noiseThe window builds at 1–2 px and rejects about 0.05 px of pseudo-parallax”S1 SfM sweep: … 2 px of noise never builds”characterization #500
B4S1: known-answer scale with a lever armExact data recovers the scale to rounding (now 0.78% off)init-window known-answer fixture, 2% toleranceweak #499
B5S1: bootstrap pair selectionThe window builds at 20 Hz where adjacent pairs fail”S1 SfM bench: the alternatives on the same windows”locked
B6S2: propagation preserves the gaugeΦ·N = N each step, in both error states; the invariant Φ doesn’t depend on the estimate; the body-frame Φ is the exact Jacobian of the mean it propagates (finite differences, 1e-8)“R-IEKF Phase A: the invariant propagation Phi is state-independent”; “S2 bench sweep: the mean’s own Jacobian leaks no yaw, a truncated Phi does”; “S2 interval: Phi is the finite-difference Jacobian of the RK4 mean”locked (body-frame since #616 )
B6aS2: the mean closes on a continuous truthIntegrating a fast spline’s own noise-free IMU (1.3 rad/s roll and pitch) over a 0.55 s clone window stays within what the camera resolves (a tenth of 0.5 px: 1.09e-4 rad, 0.22 mm at 2 m) and converges at second order; holding one sample over the interval misses it 28× at first ordergt_spline.cpp: “S2 closure, the shipped S2 within what the camera resolves (#616)” and its companionslocked #616
B7S2: process noisePredicted covariance matches a Monte-Carlo within 1% per block, 20 m from the origin; cross-correlations within 0.06”… predicted covariance matches a Monte-Carlo far from the origin”locked
B8S2: propagation-only NEES at the shipped QAbout dof (6.4 against 6), monotone in the Q scalethe S2 probelocked
B9S3/S9: augmentation and marginalizationExact block copy and principal submatrix; residual about 0the S3 and S9 bencheslocked
B10S5: triangulation at low parallaxRefinement reduces the error (median 2.76 → 0.76 m at about 1°), never raises the cost, keeps exact features to 1e-9, rejects points behind a camerainvariant_update.cpp “invariant triangulation: …“locked
B11S6a: JacobiansMatch finite differences to 1e-6, calibration blocks includedcalib_jacobians.cpplocked
B12S6b: gauge annihilation‖H·N‖ ≤ 1e-15 at a consistent point; yaw leak against σ reported; invariant H·N below 1e-14 under perturbation; translation leak 0observability_probe.cpp; observability_inspect.cpp; ”… annihilates the 4-DoF gauge”locked
B13S6: no information along NThe component of Hᵀ S⁻¹ r along N is at roundoff”S6 update: no information along the unobservable directions, true or estimated”locked
B14S6: update algebra at matched (P, R)NIS/dof in [0.98, 1.02] over 4000 updates; Nᵀ H_f = 0 to 1e-16; P stays PSDupdate_probe.cpplocked
B15S6d: gate calibrationInlier acceptance about 1 − α at every dof”S6d sweep: the shipped gate admits 1 - alpha of inliers at every dof” (0.95 ± 0.05 at dof 1, 3, 9, 21); math/stats.cpp χ² quantiles against the tablelocked ( #481 )
B16S10: calibration recoveryA wrong extrinsic is recovered with a consistent covariance (NEES 0.49 against a bound of 22.5)calib_recovery.cpplocked
B17Iterative kernels convergeEigen solvers in fewer than 20 sweeps, relative residual below 1e-14two_view_sfm.cpp Jacobi cases (the sweep-count pin was lost in the MTL5 port)partial
B18Front end: track survivalDetections lie outside the KLT border band; at most 5% of new tracks are lost on their first frameklt_tracker.cpp “klt_trackable marks exactly the points the tracker does not reject at the border”; S4 bench trackable_detect variant (upper bound 5%, 0 on every fixture). The shipped default still detects in the band (over 50% on the bench, 81% on V1_01), because the margin costs about 10% EuRoC ATElocked with the margin on; the default is a characterization ( #474 , #574 )
#RequirementPass criterionTest hereStatus
C1Joint step against finite differencesOver several propagate → update → retract cyclesinvariant_joint_step_fd.cpplocked
C2Square-root and full covariance agreeδx and P agree to 1e-12 on a rectangular factor after augmentation”the square-root update takes the rectangular factor …“locked
C3Stage-boundary fixtures replay bit for bitCaptured fixtures reproduce; the first violation points at a real faultstage_capture.cpplocked (the oracle has a false positive, #487 )
C4Where the loop gains unobservable informationThe unobservable basis carried through the loop by the filter’s own Jacobians closes against N(x̂) + Σ per-stage defects to roundoff; S3, S9 and untapped changes add no defect; no information along global translation; the yaw shares add up to the totalobservability_ledger.cpp (5 cases); on EuRoC, vio_pipeline --observability-ledgerlocked (instrument); body-frame leak characterized: S6’s mean shift carries 95–99%, #480

D. The system, on synthetic data with matched noise

Section titled “D. The system, on synthetic data with matched noise”
#RequirementPass criterionTest hereStatus
D1Monte-Carlo consistency21 seeds or more. Every seed: RMS below 0.15 m, normalized full NEES in (0.25, 3), roll/pitch below 6, yaw below 3, position below 4. Across the seeds: median RMS below 0.06 mCI: “tracks and stays consistent on every seed” (4 seeds). Manual: the hidden “21-seed sweep” ([.sweep])partial: 4 seeds locked (R-IEKF), 21 manual
D2Slow-start robustnessThe D1 bands at onsets of 1 s and 2 sCI: “a slow 2 s start stays consistent” (2 seeds at 2 s). Manual: the hidden “21-seed sweep at slow onsets” (1 s and 2 s)partial: 2 s on 2 seeds locked (R-IEKF); 1 s and 21 seeds manual
D3A consistency control under noise, never noise-freePose NEES about dof under 1 px plus the nominal IMU noise, for every backendR-IEKF: D1. Body-frame: “S10 matched-noise control: the body-frame MSCKF leaks information into yaw” pins the leak (median pose NEES above 9, yaw σ falling)locked (R-IEKF); characterization (body-frame) #551
D4Every arithmetic type end to endfloat, posit32, long double: P PSD every frame; positions within 1000·max(ε_type, ε_double) metres of the double result”the estimator runs end to end in every arithmetic type”locked
D5Genericity auditA strict type with explicit-only conversions matches double bit for bitstrict_double.cpplocked
D6Per-stage precisionOne stage in float within 1000 float ulps; PSD checked at the narrowest stage’s εstage_types.cpplocked
#RequirementPass criterionTest hereStatus
E1Accuracy with the published calibrationV1_01 ATE below 0.50 m; MH_05 and V2_03 below 1.5 m (now 0.305, 0.785, 0.293 m); ATE scored from init onward with Horn alignmentvio_euroc.cpp replay casesgated
E2Divergence guardA finite trajectory; fail fast at the divergent frame; a forced bad path stays bounded (below 100 m)“MH_05_difficult forced dynamic VI-init fires + stays bounded”gated
E3Each init path forcedStatic, gravity-align and dynamic each forced, the selected InitMethod asserted; dynamic within the 1.5 m gate (measured 19.9 m)none: the E2 case prefers dynamic init but allows a fallback and asserts only the bounded trajectorymissing #254
E4Consistency on real dataGauge-anchored per-block NEES and NIS beside ATE on V1_01, MH_05, V2_03; NEES about dofvio_euroc.cpp prints NEES and NISwarn #212 , #365
E5RPEBelow 5% of distance travelledreported, not assertedwarn #212
E6Mixed-precision presets on real dataNo divergence for any shipped stage-type preset; ATE and NEES per presetvio_pipeline EuRoC runsmanual
E7Sanitizers on a real replayASan and UBSan clean end to endrun by handmissing in CI #561
E8LatencyReal-time factor 1.0× or better (now 0.10×, p50 506 ms); synthetic budget 75 ms double, 50 ms floatvio_latency.cppgated, informational #213
E9Consistency gate: told the true noise, the filter improvesOne truth (a B-spline through the ground truth) feeds the synthesized IMU, the ideal tracks (1 and 0.5 px, noise in normalized coordinates) and the score; seeds 1–3, all 11 sequences, the same shipped S6d gate for every backend: no run with R matched worse than 10× OpenVINS mono; matched R beats the default R on the median; median (smallest world-yaw σ ÷ yaw σ at the gauge anchor) at least 0.5scripts/euroc-consistency.sh --backend msckf or riekf (results in scripts/euroc-consistency.json); gt_spline.cpp locks the truth (S2 closure, noise densities); pose_consistency.cpp the NEES splitfailing, manual. NIS 1.00 on every sequence for both since #616 . MSCKF: fails 1 (2 of 66 diverge) and 3 (yaw σ ratio 0.179, the S6 yaw leak), passes 2; R-IEKF: fails 1 only (the same 2 runs), passes 2 and 3 (0.984). Both divergences start from the shared initializer’s seed ( #609 ). Conditions 1 and 2 still move with the noise draw at three seeds #613 ( #605 , #608 )
#RequirementPass criterionStatus
F1Two or more compilers in CI, whole suiteGCC and Clang per-frame traces bit-identical; MSVC also runslocked
F2Portable test noiseGaussian draws built from the raw engine output, so every platform sees the same realization for a seedlocked (the S10 controls)
F3Refactors preserve bitsbench_snapshot --compare --rel 0 over every output, worst difference 0; the EuRoC run.jsonl byte-identical, or a justified exemption of 1e-12 or lesslocked
F4Tools fail loudlyA missing output directory is an error, not a silent skiplocked