Library

fusioncore v0.4.0

Versionv0.4.0
Stars★ 360
Released2026-09-15

ROS 2 localization for outdoor robots: IMU, wheel encoders and GPS fused in a 23-state UKF at 100 Hz. When the estimate goes wrong it reports which sensor and why, instead of drifting silently. Apache 2.0.

Release notes

First release with breaking changes. Two public `FusionCoreConfig` fields were removed, so code built directly against `fusioncore_core` will need a change, and four defaults now alter behaviour for anyone upgrading without touching their config. Nothing errors and every YAML still loads.

## Breaking

| setting | was | now | effect |
|---|---|---|---|
| `gnss.recovery_rejection_n` | 0, off | 15 | post-blackout P inflation fires |
| `gnss.continuity_auto` | new | `true` | fix-to-fix gate arms itself after 100 fixes |
| `zupt.accel_std_threshold` | new | 0.5 | the IMU can veto a ZUPT |
| `gnss.gps_track_heading_cross_check_deg` | new | 15.0 | a disagreeing GPS track heading is refused |

Removed from `FusionCoreConfig`: `gnss_recovery_timeout_s` (declared, documented, set in a shipped config, never read by anything) and `encoder_nhc_vy_sigma` (written by the node into a field nothing consumed, while the working assignment reached `encoder.vel_noise_y`). The ROS parameter `gnss.recovery_timeout_s` is still declared so old configs load, and the node now warns once if it is set.

Rejection reason codes are unchanged. `NOT_FINITE` and `QUALITY_OTHER` were appended at 12 and 13, and `MIN_SATS` kept its 5.

## A filter that dead-reckoned through a GNSS outage now comes back

This is the reason for the version bump. It took six separate defects.

```
NCLT 2012-06-15, error 300 s after fixes returned     277 m  ->  13 m
a log with 275/129/65/55 s gaps, 300 s after the 129s 746 m  ->  78 m
NCLT 2013-04-05 ATE                                 277.6 m  -> 189.7 m
```

The inflation had no constant that could work, so it is now sized from the rejected innovation itself. Only the chi2 path could arm it, so cascades starting at any other gate never did. A 3-DOF gate cannot be opened by inflating two of its axes. The gap test was in absolute seconds, which is meaningless at 1 Hz where the healthy spacing between fixes *is* the threshold, so it is now relative to the receiver's own cadence.

The fifth hid the other four: the continuity history was allowed to span the gap. That history is also where fix cadence is derived, so a buffer spanning an outage reported a mean spacing of 115 s instead of 0.2, and "have we just had an outage" silently became "was the gap longer than 231 seconds". The gate was healthy by every measure you could take from outside. What was broken was a statistic it exported to something else.

The sixth: the code clearing the post-outage latch sat inside `reset()` and never ran on an accepted fix, so **any blackout disabled spike rejection for the rest of the run**. Measured with a sustained 300 m offset after a clean recovery: 586 of 601 spike fixes accepted and 299.97 m of error before, 0 of 601 and 25.39 m after.

## Also new

A fix-to-fix continuity gate that measures its own threshold from the receiver over the first 100 fixes, because chi2 judges a fix against the filter and cannot see a metre-scale spike. An IMU check before ZUPT, since wheels reporting zero is not the same as a stationary robot and a dead encoder previously convinced the filter it was parked while driving. A GPS track heading cross-check. `gnss.min_satellites` can no longer silently reject every `NavSatFix`. Encoder rejections now say why. Radar and GNSS velocity inputs substitute a predicted yaw rate instead of a fabricated zero.

CI now fails on a ROS parameter that never reaches the filter, contributed by @Rayan-and-beyond.

## Benchmarks

FusionCore beats robot_localization on 10 of 12 NCLT sequences and on spike rejection. The two losses are both multi-minute blackouts where robot_localization's 2D model structurally cannot leak roll and pitch into yaw. That remains open and the honest path through it is an absolute heading source, not tuning.

Note for anyone reproducing: 2012-06-15 repeated to 27.6% between two runs of identical code in this release's validation, so a single number from that sequence is not usable in either direction. `tools…

Share this resource


Discovered 2026-09-15 Source GitHub Archive 2026-09 →