GPS validation
Algorithm validate-gps-1.0.0 · parameters from v1-default · source engine/src/dtta/engine/validate/gps.py.
Validation is a pure function of the GPS table and the parameter set. It never deletes rows: it sets a
bit mask quality_flags on every sample and derives is_valid. Downstream stages use only valid
samples and carry the flags through to the profiles.
Flags
| bit | name | rule (parameter) | invalidates |
|---|---|---|---|
| 1 | NO_FIX |
fix quality < gps_fix_min (3, i.e. no 3-D fix), or latitude and longitude both exactly 0 |
yes |
| 2 | HIGH_DOP |
dilution of precision > gps_dop_max (3.0); confidence is halved above gps_dop_confidence_taper (2.0) |
yes |
| 4 | DUPLICATE_STATIONARY |
identical position to the previous sample while speed < gps_stationary_speed_ms (0.5 m/s): the receiver is holding, treated as stationary |
no |
| 8 | POSITION_JUMP |
identical position while moving (≥ gps_moving_speed_ms, 3 m/s), or a step from the last good sample larger than (gps_jump_speed_factor, gps_jump_noise_m, gps_jump_speed_floor_ms) |
yes |
| 16 | SPEED_SPIKE |
reported speed > gps_speed_max_ms (80 m/s); or a speed step whose acceleration exceeds gps_spike_accel_ms2 (15 m/s²) and reverses within gps_spike_recovery_samples (3); or a Hampel outlier (window gps_hampel_window 7, gps_hampel_nsigma 3 × 1.4826 MAD, and at least gps_hampel_min_ms 2 m/s away) |
yes |
| 32 | ACCEL_IMPLAUSIBLE |
sustained |dv/dt| > gps_implausible_accel_ms2 (12 m/s²) for longer than gps_implausible_accel_s (0.5 s): confidence lowered to 0.3 |
no |
| 64 | TIME_NONMONOTONIC |
sample time not increasing | yes |
| 128 | INTERPOLATED |
speed filled by linear interpolation inside a gap ≤ speed_gap_fill_s (0.5 s) |
no |
| 256 | PRELOCK |
every sample before the first run of gps_prelock_valid_run_s (2 s) of fixed, low-DOP samples |
yes |
| 512 | EDGE |
first and last 0.5 s of each contiguous run (filter warm-up); excluded from peak statistics | no |
| 1024 | PROJ_LOST |
reserved for samples whose track projection was lost; the projection stage reports this per lap (proj_lost_count) rather than in the GPS flags |
no |
is_valid is true when none of the invalidating bits is set.
Session-level checks
- Valid fraction = valid samples / all samples. Below
gps_valid_fraction_min(0.8) the session is blocked ("not enough valid GPS"). - Longest valid run must be at least
gps_min_valid_run_s(60 s) or nothing can be analysed. - Gaps: a gap is any interval longer than
gps_gap_factor(3) sample periods; gaps >gps_gap_s(1.0 s) split the data into separate runs for lap detection. - Effective rate = (valid samples − 1) / (time span of valid samples), reported in Hz.
- Speed consistency (independent witness): over
gps_speed_consistency_window_s(2 s) windows the position-derived speed and the receiver's reported speed must agree withingps_speed_consistency_max_ms(1.5 m/s) in at leastgps_speed_consistency_fraction(95 %) of moving windows; otherwise a warning is raised and confidence is downgraded. Calibrated in Phase 4 on the 18 Hz HERO9 file (docs/proofs/phase-04/qc_report.md).
Projection to metres
Positions are converted to a local east-north plane about the track origin:
, with
m (earth_radius_m). Track extents are under 1 km, so the flat-earth error is below
1 mm.
What was verified on real data
- HERO9 (18 Hz, GPS5): 7 speed spikes with |dv/dt| > 15 m/s² in one heat, all caught; 958 duplicate positions, all while stationary.
- HERO9 chaptered file: the first 18 samples have fix = 0 and DOP 99.99 (
PRELOCK). - Valid fraction on the fixture sessions 99.8–100 %; effective rates 10.0 Hz (HERO11) and 17.7 Hz (HERO9).
Tests: engine/tests/unit/test_validate_gps.py (every rule triggered and not triggered),
engine/tests/synthetic (injected spike, pre-lock zeros, duplicates, jump, gap), regression goldens for
flag counts on both fixtures.