Skip to content

v0.3.6

Choose a tag to compare

@manankharwar manankharwar released this 12 Aug 19:13
· 21 commits to main since this release

Four GNSS defects, all of them found by running FusionCore on real hardware rather than by the test suite. Three were reported or triggered by issue #73.

The common thread: a quality gate expressed as an absolute distance in metres, calibrated against clean or simulated GPS, silently discarding good measurements on a real receiver. A rejected fix costs accuracy but surfaces nowhere except a throttled log line, so the symptom looks like "the filter is bad" rather than "the gate is wrong".

The DOP gate compared metres against thresholds named as DOP

sensor_msgs/NavSatFix carries no DOP fields, so the node derives fix quality from position_covariance as sqrt(variance), which is metres. That was compared against gnss.max_hdop (4.0) and gnss.max_vdop (6.0), parameters every ROS user reads as the dimensionless geometry factor. What the defaults actually meant was "reject any fix worse than 4 m horizontal or 6 m vertical".

Measured on 500 fixes from a u-blox NEO-M9N outdoor run: 3.6 to 6.0 m horizontal, 14.4 to 24.0 m vertical. All 500 were rejected at shipped defaults, and the filter dead-reckoned the whole run.

GnssFix now carries explicit sigma_xy / sigma_z in metres, gated by the new gnss.max_sigma_xy (25.0) and gnss.max_sigma_z (50.0). max_hdop / max_vdop keep their original meaning and apply only when a fix carries no covariance, which in practice means gps_msgs/GPSFix reporting receiver-native DOP. Accepted fixes are fused exactly as before: replaying the same bag gives a bit-identical trajectory.

The jump gate rejected ordinary GPS noise

gnss.max_speed bounded a fix at max_speed * gap + margin, entirely absolute metres. At 1 Hz with max_speed: 2.0 and the default 5 m margin that is a 7 m bound, and a receiver whose own sigma is ~6 m trips it constantly. Measured: 157 of 500 good fixes rejected, loop closure 2.62 m to 7.27 m, on a rover whose real top speed is 0.6 m/s, so no honest motion was ever involved.

The bound now adds gnss.max_speed_sigma_k (default 5.0) multiples of the receiver's reported sigma. With the gate enabled at 2.0 the same bag gives 0 of 500 rejected and a 2.62 m closure, identical to disabling it, while a 700 m spike is still rejected.

The noise term scales with the receiver's sigma and deliberately not with the filter's covariance. Chi-squared is already the covariance-scaled test, and this gate exists to catch what a coast-inflated chi-squared admits, so scaling it by P would reopen the hole it was built to plug. A test pins that property.

GPS track heading overrode better heading sources

Track heading derives yaw from the GPS displacement bearing, which is course over ground. On a curved path that differs from body heading by a real bias, not just noise, so fusing it pulls the estimate wrong regardless of its covariance.

It ran even when a stronger absolute source was already active (dual antenna, magnetometer, or 9-axis IMU orientation), all of which the heading-source ladder already ranks above it. Separately, gps_track_heading_min_speed and gps_track_heading_max_yaw_rate were documented as guarding this fusion but only gated the heading_validated_ flag, so a slow turning robot fused its turn radius as a heading.

Reported by a user whose Nav2 path was straight without GPS and a zig-zag with it, on a robot with a stable magnetometer. Both guards now apply to the fusion. Robots with no absolute heading source are unaffected, pinned by a regression test.

GNSS rejections were reported as NOT_PROCESSED

The enum-to-string mapper feeding /fusion/debug/gnss_status had no case for IMPLAUSIBLE_JUMP, so it fell through to NOT_PROCESSED, the value meaning "update_gnss was never called". Fixes disappeared with no stated cause. The mapper is now exhaustive and covers the two new sigma reasons.

Also

  • Corrected the GPSFix driver list: nmea_navsat_driver publishes NavSatFix, not gps_msgs/GPSFix (verified against the driver source, reported on #73)
  • UKF::last_position_correction() exposes how far the last update moved position, for per-sensor attribution
  • The Wm[0] = -99 sigma-weight conditioning hazard at 23 states is documented in ukf.cpp

126 tests, 0 failures.

A note on the benchmarks

tools/benchmark_baseline.json is marked SUPERSEDED in this release. Every entry in it was measured with the jump gate in its old form, so those numbers are not a valid reference for current main. The published NCLT figures remain stale pending a controlled full-suite re-run, and nothing in this release updates them.