Skip to content

wls: add RAIM fault detection & exclusion to reject outlier SVs#1

Closed
mx4 wants to merge 1 commit into
mainfrom
raim-outlier-rejection
Closed

wls: add RAIM fault detection & exclusion to reject outlier SVs#1
mx4 wants to merge 1 commit into
mainfrom
raim-outlier-rejection

Conversation

@mx4

@mx4 mx4 commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Problem

The ION HackRF recording was producing a fix with an improbable 37,530 m altitude (52.734, 4.543), while HDOP/VDOP read a healthy 1.0/1.7 and 11 SV.

Root cause, traced via the built-in GNSS_TRUTH_ECEF residual probe: at the true receiver location (52.1773°N, 4.4900°E, ~85 m), 10 of 11 satellites' residuals collapse to a common 168.0 ± 0.3 km (the receiver-clock term). One satellite, G05, sat at 388 km — its pseudorange ~220 km (≈0.73 ms) too long, a classic cross-correlation / wrong-code-phase false lock.

The WLS solver (wls.rs) self-calibrates a per-constellation σ but had no per-SV outlier rejection (RAIM). So G05's gross error was least-squares-smeared across the solution: position slid ~63 km horizontally and ~37.5 km up, and the nuisance states blew up (σ_GPS→68 km, ISB→−102 µs, clock jumped +3.7 ms). HDOP/VDOP are pure geometry and never reflected the blunder. The only guard — the |pos| ∈ [6.25e6, 6.5e6] terrestrial gate — tolerates ~±125 km of altitude, so it passed.

Fix

Add a RAIM detect–identify–exclude loop around the core solve:

  1. Solve over the full pool.
  2. While the pool still has redundancy (more measurements than states), find the SV whose residual is the largest multiple of a fixed a-priori σ and, if it exceeds a 6σ gate, drop it and re-solve.

The integrity test deliberately normalizes against a nominal σ (10 m), not the self-calibrated weighting σ — a gross outlier inflates the latter (G05 drove σ_GPS to ~68 km), which would mask the very fault we're testing for. A 6σ gate gives a per-SV false-alarm rate ~2e-9, so healthy SVs are essentially never dropped, while a km-scale blunder trips it by orders of magnitude.

Excluded SVs are reported in WlsSolution::excluded, logged as raim-excl=…, and removed from the published SV count.

Result

On the ION HackRF set, the final fix is now:

before after
position 52.734, 4.543 52.1773, 4.4900
altitude 37,530 m 84.8 m
σ_GPS 68 km 3.9 m
ISB −101,918 ns +2.3 ns
log raim-excl=G05

Tests

  • cargo test --release — 74 pass, 0 fail (incl. the wls_solve cross-check)
  • cargo fmt --check and cargo clippy --release clean

Notes / out of scope

  • The displayed date reading 2037 is a separate known bug: the fixed +2048 GPS week reconstruction at gps_lnav.rs:600 is only valid for 2019-04…2038-11, and this NL recording predates that window (off by one 1024-week rollover). Independent of the altitude; not touched here.
  • The |pos| terrestrial gate is left in place as a backstop.

🤖 Generated with Claude Code

The WLS solver had no per-SV outlier rejection, so a single bad
pseudorange was least-squares-smeared across the whole fix. On the ION
HackRF set a cross-correlation false lock on G05 (~220 km / 0.73 ms too
long) pushed the published fix 63 km horizontally and 37.5 km up
(altitude 37,530 m), while HDOP/VDOP stayed 1.0/1.7 — geometry factors
are blind to a measurement blunder. The only guard, the
|pos| in [6.25e6, 6.5e6] terrestrial gate, tolerates ~±125 km of
altitude and let it through.

Add a RAIM detect-identify-exclude loop around the core solve: while the
pool still has redundancy, drop the SV whose residual is the largest
multiple of a fixed a-priori sigma (10 m) when it exceeds a 6-sigma gate,
then re-solve. The test deliberately normalizes against a nominal sigma,
not the self-calibrated weighting sigma — a gross outlier inflates the
latter (G05 drove sigma_GPS to ~68 km), which would mask the fault.

Excluded SVs are reported in WlsSolution::excluded, logged as
`raim-excl=...`, and dropped from the published SV count.

On ION HackRF the final fix is now 52.1773,4.4900 h=84.8 m with
raim-excl=G05, sigma_GPS 3.9 m, ISB +2.3 ns (was -102 us).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mx4 mx4 closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant