ADR-0003: Ship with visual fusion disabled¶
- Status: accepted
- Date: 2026-09
Context¶
The 21-state anchor model (ADR-0001) is close to calibrated when GNSS is available: mean NEES 3.7 against 3 nominal, 2-sigma coverage 100%.
It is not well calibrated when GNSS is denied. From the generated benchmark,
15 s outage (python scripts/run_benchmark.py):
| Scenario | ATE RMSE | Claimed 1σ | Mean NEES | 2σ coverage |
|---|---|---|---|---|
| GNSS denied 5–20 s, vision off (control) | 3.782 m | 0.567 m | 4.1 | 100.0% |
| GNSS denied 5–20 s, vision on | 3.428 m | 0.154 m | 1051.3 | 16.0% |
Enabling the visual channel during a GNSS outage barely improves the trajectory and makes the estimator dramatically overconfident: the claimed 1-sigma is roughly a quarter of the control's, and the mean NEES is 1051 against a nominal 3, with one epoch in six inside the claimed 2-sigma ellipsoid. Note that the ATE improves slightly, which is exactly the trap -- a marginally better path with a covariance that is wrong by two orders of magnitude is worse for any downstream consumer that trusts the covariance.
Superseded in part by ADR-0005. The table and the ATE comparison above are the measurement as of this decision, and they are kept as the evidence for it. The filter has since gained a chi-square FDIR gate, which rejects the GNSS fixes that return after the outage and makes the vision-on row worse rather than marginally better: ATE 5.059 m, mean NEES 1996.5. The decision to ship visual disabled is unchanged and reinforced, but the current numbers are in
README.mdand generated byscripts/run_benchmark.py. See blocker B5 inCONSTRAINTS.mdfor why the gate behaves this way.
Why this is structural¶
The visual anchor c_t is a constant offset in the body frame. In reality the
required offset drifts as inertial error grows, and a single constant model
cannot represent drift it does not have a parameter for. The filter therefore
maintains a constraint whose error it cannot track, and the induced
cross-covariance with the navigation block is wrong in a way no scalar
tuning corrects.
Three responses were considered:
- Constrain the anchor to the estimated position (assume perfect correlation). Tested. Produces a degenerate, non-positive-semidefinite covariance. Rejected.
- Add process noise to the anchor
(
anchor_pos_drift_sigma_m_s). Implemented and configurable, but it blurs the symptom rather than fixing the correlation structure. Defaulted to0.0because a knob that does not fix the problem should not look like it does. - Pose graph over visual keyframes. The actual fix. Deferred to ROADMAP stage 3.
Decision¶
vision_enabled defaults to False.
Shipping a filter that is confidently wrong is worse than shipping no visual
fusion, because a downstream consumer has no way to detect the failure. The
limitation is documented in the README and pinned by
test_gnss_denial_still_over_trusts_vision_and_that_is_pinned, so it cannot
be quietly forgotten or accidentally "fixed" by a test tweak.
Consequences¶
- The default configuration is honest: it is well calibrated, and its stated uncertainty can be trusted.
- Visual fusion is opt-in and clearly labelled as unvalidated during GNSS denial.
- A regression test now asserts the bad behaviour. That test is expected to fail when the pose graph lands, at which point ADR-0003 is superseded.