Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro kilted showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro lyrical showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro rolling showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro ardent showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro bouncy showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro crystal showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro eloquent showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro dashing showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro galactic showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro foxy showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro iron showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro lunar showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro jade showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro indigo showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro hydro showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro kinetic showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro melodic showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.
No version for distro noetic showing humble. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/manankharwar/fusioncore.git
VCS Type git
VCS Version main
Last Updated 2026-05-26
Dev Status MAINTAINED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

README

FusionCore

CI arXiv DOI Docs Newsletter

ROS 2 UKF sensor fusion for robots that run in the real world. IMU + wheel encoders + GPS at 100 Hz. Handles bad calibration, timestamp jitter, delayed GPS, wheel slip, and ARM hardware out of the box. Apache 2.0.

FusionCore running on a real robot


Install

Option A: From source (ROS 2 Jazzy on Ubuntu 24.04 or Humble on Ubuntu 22.04):

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash  # or /opt/ros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Verify it works (single command, replaces the 4-terminal manual test):

bash tools/quick_test.sh

Starts FusionCore with fake sensors and checks all outputs in about 15 seconds. Prints [PASS] / [FAIL] for each check.

Option B: Docker (no ROS install required)

docker pull ghcr.io/manankharwar/fusioncore:latest
docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash

Inside the container, verify everything works:

bash tools/quick_test.sh


Works on the hardware you actually have

Most sensor fusion tutorials assume clean data. Real robots don’t have clean data. FusionCore was built around the problems you actually run into.

The problem How FusionCore handles it
IMU calibration is approximate Gyro and accel bias are filter states, estimated continuously. init.stationary_window: 2.0 estimates startup bias before motion begins, dropping startup drift from ~10 cm to under 1 cm.
Extrinsic calibration is never exact Reads frame_id from every IMU message and looks up the TF rotation to base_link automatically. Set imu.frame_id to override broken frame names from drivers (e.g. Gazebo TurtleBot3). No manual rotation matrices.
Timestamp jitter and zero-stamped drivers dt is clamped to prevent divergence from missed timer ticks. Wall clock fallback for drivers that publish stamp={sec=0}.
GPS arrives late (50–200 ms) IMU ring buffer replays 1 second of buffered updates when a delayed fix arrives. The state at the GPS timestamp is reconstructed exactly, not approximated.
Wheel odometry is noisy or slipping Adaptive noise covariance updates from the innovation sequence. GPS velocity fusion (optional) compares GPS-reported speed against wheel speed every cycle: the innovation reveals slip and the Kalman gain down-weights the slipping wheel automatically.
Noise parameters require days of tuning Two numbers from your IMU datasheet: imu.gyro_noise (ARW) and imu.accel_noise (VRW). Everything else adapts within the first minute of operation.
Robot runs on Raspberry Pi or Jetson Under 0.2 ms per cycle on i7. Under 1 ms on Raspberry Pi 4. Same binary on ARM (NEON auto-detected) and x86 (AVX auto-detected) via Eigen. No recompilation, no parameter changes.
Two IMUs on the platform Set imu2.topic to fuse a second IMU as an independent measurement. No pre-merging with imu_filter_madgwick needed.
GPS drops out in tunnels or canopy Inertial coast mode maintains position integrity during sustained GPS dropout. Outlier gate relaxes automatically to reacquire when GPS returns.
Robot sits still for minutes ZUPT (zero velocity update) fuses a zero-velocity pseudo-measurement when encoder speed and angular rate are both below threshold. Prevents IMU noise from integrating into position drift during idle periods.

Benchmark

FusionCore vs robot_localization on the NCLT dataset: same IMU + wheel odometry + GPS, no manual tuning. Twelve full-length sequences across all seasons. RL-EKF run with chi-squared-equivalent thresholds at 99.9% confidence.

Sequence Season Duration FC ATE RMSE RL-EKF ATE RMSE Winner
2012-01-08 Winter 92 min 18.6 m 41.2 m FC +55%
2012-02-04 Winter 77 min 49.7 m 265.5 m FC +81%
2012-03-31 Spring 87 min 22.0 m 156.5 m FC +86%
2012-05-11 Spring 84 min 9.7 m 11.5 m FC +16%
2012-06-15 Summer 55 min 49.2 m 18.2 m RL +63%
2012-08-20 Summer 83 min 98.3 m 10.6 m RL +89%
2012-09-28 Fall 77 min 10.8 m 55.7 m FC +81%
2012-10-28 Fall 85 min 29.9 m 60.0 m FC +50%
2012-11-04 Fall 79 min 60.1 m 122.0 m FC +51%
2012-12-01 Winter 75 min 21.0 m 90.7 m FC +77%
2013-02-23 Winter 78 min 59.4 m 82.2 m FC +28%
2013-04-05 Spring 68 min 12.1 m 268.9 m FC +96%

RL-UKF diverges with NaN on all twelve sequences. FusionCore wins 10/12 sequences. RL-EKF’s losses trace to a single root cause: the GPS driver reports 3m sigma, but measured against RTK ground truth, actual p95 noise is 9.7-53.1m depending on the day. RL’s gate is calibrated to the stated 3m and rejects valid fixes on bad-GPS days. FusionCore’s adaptive noise estimation (adaptive.gnss: true) keeps chi2 statistics calibrated in real time.

The two FC losses are driven by a GPS data quality issue on 2012-08-20 (105 corrupt mode-3 fixes in a 24-second window at a blackout boundary) and accumulated heading error during a 462-second GPS blackout on 2012-06-15. See benchmarks/README.md for full per-sequence analysis including root causes and path-to-fix.

Trajectory overlay: all 9 sequences, SE3-aligned to RTK GPS ground truth


File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to FusionCore

Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help.

The fastest way to contribute

The most impactful contributions right now are hardware configs. If you have FusionCore running on a robot, platform, or IMU that isn’t in the repo yet, open a PR adding a YAML under fusioncore_ros/config/. See the hardware config section below.

Before you start

  • Check open issues: the bug may already be reported
  • Check Discussions: the question may already be answered
  • For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code

Development setup

# Clone and build
git clone https://github.com/manankharwar/fusioncore.git
cd fusioncore

source /opt/ros/jazzy/setup.sh  # replace jazzy with humble on Ubuntu 22.04
rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y  # replace jazzy with humble on Ubuntu 22.04
colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON

# Run all tests before and after your change
colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros
colcon test-result --verbose

All 45 tests must pass. CI will catch it if they don’t.

Hardware configs

A hardware config is a YAML file under fusioncore_ros/config/ named after the platform (e.g. clearpath_husky.yaml, ublox_f9p.yaml).

Copy fusioncore_ros/config/fusioncore.yaml as the starting point and adjust:

  • imu.gyro_noise / imu.accel_noise: pull from your IMU’s datasheet
  • gnss.base_noise_xy: your GPS receiver’s CEP spec
  • Any topic remaps specific to your platform

Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster.

Pull request checklist

  • All 49 tests pass (colcon test-result --verbose shows 0 failures)
  • For new features: tests added in fusioncore_core/tests/
  • For hardware configs: YAML includes a comment with platform + sensor details
  • Commit message describes why, not just what

Code style

C++17. Follow the style of the surrounding code: no reformatting unrelated lines. clang-format is not enforced but is appreciated.

Reporting bugs

Use the Bug Report issue template. Include the output of colcon test-result --verbose if tests are involved.

Questions

Open a Discussion rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas.

Response time: typically within 24 hours.

# Contributing to FusionCore Thanks for your interest. Contributions are welcome: hardware configs, bug fixes, tests, and documentation all help. ## The fastest way to contribute The most impactful contributions right now are **hardware configs**. If you have FusionCore running on a robot, platform, or IMU that isn't in the repo yet, open a PR adding a YAML under `fusioncore_ros/config/`. See the [hardware config section](#hardware-configs) below. ## Before you start - Check [open issues](https://github.com/manankharwar/fusioncore/issues): the bug may already be reported - Check [Discussions](https://github.com/manankharwar/fusioncore/discussions): the question may already be answered - For anything bigger than a typo fix, open an issue or Discussion first so we can align before you write code ## Development setup ```bash # Clone and build git clone https://github.com/manankharwar/fusioncore.git cd fusioncore source /opt/ros/jazzy/setup.sh # replace jazzy with humble on Ubuntu 22.04 rosdep install -r --from-paths . --ignore-src --rosdistro jazzy -y # replace jazzy with humble on Ubuntu 22.04 colcon build --packages-up-to compass_msgs fusioncore_core fusioncore_ros --cmake-args -DBUILD_TESTING=ON # Run all tests before and after your change colcon test --packages-select compass_msgs fusioncore_core fusioncore_ros colcon test-result --verbose ``` All 45 tests must pass. CI will catch it if they don't. ## Hardware configs A hardware config is a YAML file under `fusioncore_ros/config/` named after the platform (e.g. `clearpath_husky.yaml`, `ublox_f9p.yaml`). Copy `fusioncore_ros/config/fusioncore.yaml` as the starting point and adjust: - `imu.gyro_noise` / `imu.accel_noise`: pull from your IMU's datasheet - `gnss.base_noise_xy`: your GPS receiver's CEP spec - Any topic remaps specific to your platform Add a comment at the top with: platform name, IMU model, GPS receiver model, and whether it was field-tested or tuned from datasheet only. Field-tested configs get merged faster. ## Pull request checklist - [ ] All 49 tests pass (`colcon test-result --verbose` shows 0 failures) - [ ] For new features: tests added in `fusioncore_core/tests/` - [ ] For hardware configs: YAML includes a comment with platform + sensor details - [ ] Commit message describes *why*, not just *what* ## Code style C++17. Follow the style of the surrounding code: no reformatting unrelated lines. `clang-format` is not enforced but is appreciated. ## Reporting bugs Use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) issue template. Include the output of `colcon test-result --verbose` if tests are involved. ## Questions Open a [Discussion](https://github.com/manankharwar/fusioncore/discussions) rather than an issue. Issues are for bugs and tracked work; Discussions are for questions, configs, and ideas. Response time: typically within 24 hours.