Skip to content

Add fuse odometry localization for the hangar_sim mobile base - #790

Draft
bkanator wants to merge 30 commits into
v10.0from
feat/19667-fuse-odometry
Draft

Add fuse odometry localization for the hangar_sim mobile base#790
bkanator wants to merge 30 commits into
v10.0from
feat/19667-fuse-odometry

Conversation

@bkanator

@bkanator bkanator commented Jul 17, 2026

Copy link
Copy Markdown

[written by AI]

Closes #19667.

needs: moveit_pro/#21443

Sensor frames in hangar_sim are declared as MJCF sites, and MuJoCo parented every site
to the scene root. That routes a lookup of a camera frame through the localization chain
this PR makes live, so the wrist camera inherited the localization error while the robot's
own links stayed at their simulated pose — the cause of the two Plan Path Along Surface
failures. moveit_pro#21443 parents sites to the robot link that carries them.

Problem

hangar_sim's mobile base drove on raw MuJoCo odometry — too clean to exercise the real localization stack. To represent a real Ridgeback, the base needs fuse-fused odometry (wheel + IMU) with realistic drift, and beluga_amcl must localize reliably against it during Navigate to Clicked Point — including where the scene is degenerate for scan matching (the smooth fuselage, the unmapped picking boxes).

Approach

  • Fuse on by default (use_fuse=true): fuse fuses wheel odometry + IMU into /odom_filtered.
  • odomworld drift injection: with fuse on, odom_world_drift publishes a live odomworld transform from the fuse estimate, so AMCL sees real drift to correct while worldbase stays ground truth for whole-body planning. odom_topic stays on raw /odom — no manual swap.
  • Slip-aware wheel covariance (slip_aware_odom, new C++ node): republishes wheel odometry with a yaw covariance that grows during sustained in-place spin (mecanum rollers slip), so fuse defers to the IMU while spinning and trusts the wheels when driving straight.
  • AMCL tuning for the mecanum base: OmniMotionModel; alpha1 0.1→0.4 to stop yaw lock-loss during spins; update_min_a 0.1 with resample_interval 3 to correct often without particle depletion.
  • AMCL likelihood relaxation: sigma_hit 0.1→0.25, z_hit 0.9→0.65, z_rand 0.1→0.3 so the unmapped picking boxes read as outliers instead of yanking the whole estimate — the box-divergence axis the fuse fix alone didn't touch.
  • fuse smoother window (lag_duration 0.5→0.3): a shorter fixed-lag window cuts the estimator's output latency so the fused pose tracks the truth more tightly during motion — moving-yaw error 0.70°→0.465° — while staying long enough to smooth transient spikes (0.25 reached a marginally lower median but reintroduced a rare ~125° transient).
  • Degeneracy gate (amcl_odom_gate, new C++ node): where AMCL's scan-match goes degenerate — the base hugging the smooth fuselage (slide-along ambiguity) or crossing the transient unmapped boxes — it holds the last good mapodom and coasts on fuse's odometry through the zone, then blends back once AMCL is trustworthy again. A large correction is neither hard-accepted nor hard-rejected: it is accepted only if it persists over a sliding window (a real fix persists at one pose; an ambiguity teleport thrashes), gated on both position and yaw, with particle-spread hysteresis as a second, independent trigger — so a valid large correction such as recovering from a bad initial-pose seed still gets through. A confident-but-wrong lock is the hard case the sliding window alone cannot catch: sliding along the smooth fuselage, AMCL settles at a wrong pose that also persists, with a severely spread cloud. spread_accept_max closes that gap — a persisted correction is accepted only while the cloud is tight enough, so a real recovery (which converges) is still adopted while a scan-slide divergence (which stays spread) is coasted through instead. AMCL runs tf_broadcast:=false; the gate is the sole mapodom publisher. The pure decision logic (detail::updateGate) has no ROS/TF deps and is unit-tested for every case (transparent tracking, thrashing/yaw teleports held, persistent correction accepted, severe-spread wrong-lock rejected, spread hysteresis, SE(2) math).
  • Latency compensation (latency_compensation_sec on amcl_odom_gate): during motion the mapbase estimate trailed the truth by ~14 cm — not the gate's smoothing but AMCL's own scan-processing lag (~300 ms), which its message stamp under-reports. The gate now buffers odombase history and composes AMCL's correction with the odometry from one lag ago (referenced to the cloud's own stamp), forward-projecting it to the present using real buffered odometry (so it cannot overshoot on sharp turns). 0 reproduces the old compose-at-latest behaviour exactly; hangar_sim sets 0.30, tuned to the measured lag (re-measure per platform). A backward sim-clock jump flushes the buffer so a reset cannot poison it.
  • Removed the per-objective SetInitialPose reseed from the clicked-point Objectives: superseded by slip_aware_odom's cross-controller re-anchoring (which keeps odom continuous across the whole-body↔nav handoff it was added for), and the unconditional reseed could cement a drifting estimate.
  • Turn-rate cap: wz_max 0.6 — below the velocity_smoother's 1.0 cap so it actually binds — keeps commanded spins within AMCL's correction bandwidth so the map tracks instead of lagging.
  • CPU reductions: odom_rate 50 Hz, tf_publish_rate 30 Hz, and the broadcaster update_rates decoupled from the high-rate control loop, giving the localization stack headroom.

Results

On a fresh sim with adequate CPU, a 40-goal aggressive-turning route reaches 40/40 goals at ~0.12 m ATE with no divergence, reproducibly across runs. The map still lags slightly during fast turns (inherent AMCL correction latency), but Navigate to Clicked Point reaches every goal.

The degeneracy gate targets the two failure modes that remained: on a stress test that drives a goal 0.3 m from the smooth fuselage (deep in costmap inflation), bare likelihood_field diverges and never recovers on ~75% of attempts; with the gate that drops to ~10% transient failures that self-recover within 1–2 attempts. The box cluster is contained with no strand. The gate assumes odometry is trustworthy for the (transient) duration of a degenerate zone — a bound documented in the node.

Latency compensation cut the residual in-motion error further: on the fuselage route, moving-pose error dropped 13.8→8.2 cm and moving-yaw 1.2→0.9° (3-run medians, tuning latency_compensation_sec), with at-rest error and objective success unchanged and the wrong-lock protection intact. A long soak (252 goals across the hangar) ran with zero failures.

Localization quality is CPU-bound: a co-scheduled second sim can starve AMCL into divergence even with this config, so run one sim per host when benchmarking.

Docs

Paired documentation PR: PickNikRobotics/moveit_pro#20578 (Localization Tuning + Whole-Body Mobile Architecture guides).

Manual verification

ros2 launch hangar_sim and run Navigate to Clicked Point to a goal that requires a large heading change; the robot reaches the clicked point. use_fuse:=false falls back to raw odometry.

Release notes

Enhancement: hangar_sim now localizes its mobile base with fuse odometry (wheel + IMU) and tuned beluga_amcl, with a degeneracy-aware gate that keeps localization stable where the scan match is ambiguous (the smooth fuselage and unmapped boxes) and compensates the estimator's motion latency so the map tracks the base more tightly while driving, so Navigate to Clicked Point reaches goals reliably during simulated navigation.

@bkanator bkanator added this to the 10.0.0 milestone Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added slip-aware odometry for smoother, continuous navigation pose updates.
    • Added localization gating to reject unreliable AMCL corrections and recover gradually.
    • Added simulation odometry drift tracking using ground-truth pose data.
  • Improvements

    • Enabled sensor fusion by default and refined localization and SLAM launch behavior.
    • Tuned AMCL, navigation, controller, and sensor update rates for improved stability.
    • Updated navigation behaviors to avoid resetting the initial pose between objectives.
  • Testing

    • Added automated coverage for localization gating and odometry drift handling.

Walkthrough

Changes

The PR adds slip-aware odometry, AMCL transform gating, and dynamic odom -> world drift publishing. It connects these nodes to simulation launch, fuse, Nav2, MuJoCo, package dependencies, build targets, and unit tests.

Fuse localization integration

Layer / File(s) Summary
Ground-truth drift publisher
src/hangar_sim/description/ur5e_ridgeback.xml, src/hangar_sim/include/hangar_sim/odom_world_drift_logic.hpp, src/hangar_sim/include/hangar_sim/se2.hpp, src/hangar_sim/src/odom_world_drift.cpp, src/hangar_sim/test/test_odom_world_drift.cpp
Adds the base_gt site, rail-joint index resolution, shared SE(2) operations, dynamic odom -> world publication, and rail-joint tests.
Slip-aware odometry relay
src/hangar_sim/src/slip_aware_odom.cpp, src/hangar_sim/config/fuse/fuse.yaml
Adds continuous odometry relaying, source handoff handling, slip tracking, stale-source bridging, and the /odom_slip_aware fuse input.
AMCL odometry gate and tests
src/hangar_sim/include/hangar_sim/amcl_odom_gate_logic.hpp, src/hangar_sim/src/amcl_odom_gate_logic.cpp, src/hangar_sim/src/amcl_odom_gate.cpp, src/hangar_sim/test/test_amcl_odom_gate.cpp
Adds buffered odometry interpolation, gated AMCL correction logic, transform broadcasting, and unit coverage for persistence, hysteresis, and pose handling.
Fuse launch and localization configuration
src/hangar_sim/CMakeLists.txt, src/hangar_sim/package.xml, src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py, src/hangar_sim/params/nav2_params.yaml, src/hangar_sim/config/control/picknik_ur.ros2_control.yaml, src/hangar_sim/config/fuse/fuse.yaml, src/hangar_sim/description/picknik_ur_mujoco_ros2_control.xacro, src/hangar_sim/objectives/*.xml
Builds and installs the new nodes, enables fuse by default, selects conditional TF publishers, retunes localization and controller rates, and removes navigation-objective initial-pose actions.

Possibly related PRs

Suggested reviewers: griswaldbrooks


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore (reviewers only)

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Human Review Check ❌ Error The PR adds about 2,056 lines across launch orchestration, odometry, AMCL localization, TF, Nav2/configuration, build metadata, objectives, and tests, so it is a large cross-cutting change. This PR requires review by a requested human reviewer. After review, a non-author requested reviewer should override this pre-merge check.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description clearly explains the fuse-based localization, odometry, AMCL gating, tuning, testing, and related changes in the pull request.

Comment @coderabbitai help to get the list of available commands.

@bkanator
bkanator force-pushed the feat/19667-fuse-odometry branch 3 times, most recently from ecb0e20 to 98ef35f Compare July 19, 2026 15:34
@bkanator
bkanator marked this pull request as ready for review July 21, 2026 11:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py`:
- Around line 319-335: Update the odom_world_drift and slip_aware_odom Node
definitions to pass the use_sim_time LaunchConfiguration as their ROS parameter,
ensuring both nodes use simulation time consistently when stamping outputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6834a0a6-300e-4af5-bd44-d25a11fda468

📥 Commits

Reviewing files that changed from the base of the PR and between 6cfc8c0 and 98ef35f.

📒 Files selected for processing (10)
  • src/hangar_sim/CMakeLists.txt
  • src/hangar_sim/config/control/picknik_ur.ros2_control.yaml
  • src/hangar_sim/config/fuse/fuse.yaml
  • src/hangar_sim/description/picknik_ur_mujoco_ros2_control.xacro
  • src/hangar_sim/description/ur5e_ridgeback.xml
  • src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py
  • src/hangar_sim/package.xml
  • src/hangar_sim/params/nav2_params.yaml
  • src/hangar_sim/script/odom_world_drift.py
  • src/hangar_sim/src/slip_aware_odom.cpp

Comment thread src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py Outdated
@bkanator
bkanator force-pushed the feat/19667-fuse-odometry branch from 98ef35f to b3952a6 Compare July 21, 2026 12:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py`:
- Around line 190-193: Update the state estimator launch configuration
associated with the use_fuse argument to pass the use_sim_time launch value as
the node parameter {"use_sim_time": use_sim_time} alongside the existing
fuse.yaml configuration, ensuring Fuse uses the simulation clock.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 137c1df9-a38e-4fc2-bd73-ece87806e72f

📥 Commits

Reviewing files that changed from the base of the PR and between 153262c and b3952a6.

📒 Files selected for processing (10)
  • src/hangar_sim/CMakeLists.txt
  • src/hangar_sim/config/control/picknik_ur.ros2_control.yaml
  • src/hangar_sim/config/fuse/fuse.yaml
  • src/hangar_sim/description/picknik_ur_mujoco_ros2_control.xacro
  • src/hangar_sim/description/ur5e_ridgeback.xml
  • src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py
  • src/hangar_sim/package.xml
  • src/hangar_sim/params/nav2_params.yaml
  • src/hangar_sim/script/odom_world_drift.py
  • src/hangar_sim/src/slip_aware_odom.cpp

Comment thread src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py
@bkanator
bkanator force-pushed the feat/19667-fuse-odometry branch from 995a373 to e9fa7da Compare July 23, 2026 23:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py (1)

401-411: 🗄️ Data Integrity & Integration | 🟠 Major

fuse_state_estimator still doesn't pass use_sim_time — this was already flagged in a prior review round and appears unaddressed here.

fuse_state_estimator only loads fuse.yaml; with use_fuse now defaulting to true, Fuse stays on the wall clock while the rest of the sim stack (including the new odom_world_drift, slip_aware_odom, and amcl_odom_gate nodes just above, which now all correctly receive {"use_sim_time": use_sim_time}) runs on sim time. This is the same gap previously raised on state_estimator.

Suggested fix
     fuse_state_estimator = Node(
         package="fuse_optimizers",
         executable="fixed_lag_smoother_node",
         name="state_estimator",
         parameters=[
-            PathJoinSubstitution([hangar_sim_pkg, "config", "fuse", "fuse.yaml"])
+            PathJoinSubstitution([hangar_sim_pkg, "config", "fuse", "fuse.yaml"]),
+            {"use_sim_time": use_sim_time},
         ],
         output="screen",
         condition=IfCondition(LaunchConfiguration("use_fuse")),
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py` around
lines 401 - 411, Update the fuse_state_estimator Node parameters to include the
launch use_sim_time value alongside fuse.yaml, matching the {"use_sim_time":
use_sim_time} configuration used by the surrounding simulation nodes. Preserve
the existing package, executable, condition, and output settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py`:
- Around line 340-349: The launch configuration must provide a map->odom
publisher when localization is enabled and use_fuse is false. Update the
amcl_odom_gate/AMCL launch conditions or add a fallback publisher so the
use_fuse=false localization path receives map->odom, while preserving the
existing static_tf_map_to_odom behavior for localization=false.

In `@src/hangar_sim/package.xml`:
- Around line 44-48: Add the missing test dependency declaration for
ament_cmake_gmock in src/hangar_sim/package.xml, alongside the existing
dependency entries, so the test_amcl_odom_gate.cpp gmock target configured by
ament_add_gmock has its required package dependency.

---

Outside diff comments:
In `@src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py`:
- Around line 401-411: Update the fuse_state_estimator Node parameters to
include the launch use_sim_time value alongside fuse.yaml, matching the
{"use_sim_time": use_sim_time} configuration used by the surrounding simulation
nodes. Preserve the existing package, executable, condition, and output
settings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b7ab5b22-211b-48ce-90f4-5c1623ea3a3c

📥 Commits

Reviewing files that changed from the base of the PR and between f0634e4 and 995a373.

📒 Files selected for processing (11)
  • src/hangar_sim/CMakeLists.txt
  • src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py
  • src/hangar_sim/objectives/navigate_to_clicked_point.xml
  • src/hangar_sim/objectives/navigate_to_clicked_point_with_replanning.xml
  • src/hangar_sim/package.xml
  • src/hangar_sim/params/nav2_params.yaml
  • src/hangar_sim/script/odom_world_drift.py
  • src/hangar_sim/src/amcl_odom_gate.cpp
  • src/hangar_sim/src/amcl_odom_gate_logic.hpp
  • src/hangar_sim/src/slip_aware_odom.cpp
  • src/hangar_sim/test/test_amcl_odom_gate.cpp
💤 Files with no reviewable changes (2)
  • src/hangar_sim/objectives/navigate_to_clicked_point.xml
  • src/hangar_sim/objectives/navigate_to_clicked_point_with_replanning.xml
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/hangar_sim/params/nav2_params.yaml
  • src/hangar_sim/script/odom_world_drift.py
  • src/hangar_sim/src/slip_aware_odom.cpp

Comment thread src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py
Comment thread src/hangar_sim/package.xml Outdated
@bkanator
bkanator force-pushed the feat/19667-fuse-odometry branch 5 times, most recently from c57b768 to 72b4afe Compare July 28, 2026 23:18
@bkanator

Copy link
Copy Markdown
Author

[written by AI]

Addressed the CodeRabbit review comments (force-pushed; the branch was also squashed to a single commit and rebased onto latest main):

  • use_sim_time on state_estimator — added {"use_sim_time": use_sim_time} to the fuse_state_estimator node so Fuse stays on the sim clock. (Real gap — fixed.)
  • use_sim_time on odom_world_drift / slip_aware_odom — both already pass {"use_sim_time": use_sim_time} in the current code; no change needed.
  • use_fuse:=false still needs a map->odom sourceamcl.tf_broadcast is now rewritten via param_substitutions to true when use_fuse:=false (no amcl_odom_gate) and false when use_fuse:=true (gate is sole publisher), so a use_fuse:=false + localization:=true run has AMCL broadcast map->odom itself instead of nothing. (Real gap — fixed.)
  • Missing ament_cmake_gmock test_depend — added to package.xml. (Fixed.)

@coderabbitai full review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/hangar_sim/src/amcl_odom_gate.cpp (1)

115-115: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Wall timer paired with sim-time stamps.

create_wall_timer is unaffected by /clock, but publish() stamps with now() (ROS time under use_sim_time). If the sim runs slower than realtime, consecutive broadcasts can carry identical or barely-advancing stamps; faster-than-realtime under-publishes. rclcpp::create_timer(this, get_clock(), rclcpp::Duration::from_seconds(kPubPeriod), cb) ties the period to the same clock the stamps come from.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hangar_sim/src/amcl_odom_gate.cpp` at line 115, Replace the wall-clock
timer assigned to timer_ in the constructor with an rclcpp timer created using
get_clock() and an rclcpp::Duration derived from kPubPeriod, while preserving
the existing publish() callback.
src/hangar_sim/CMakeLists.txt (1)

11-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Set cxx_std_17 on slip_aware_odom too.

src/slip_aware_odom.cpp uses std::clamp (C++17), but unlike amcl_odom_gate this target doesn't declare the standard and relies on whatever the toolchain/dependency defaults provide.

♻️ Proposed change
 add_executable(slip_aware_odom src/slip_aware_odom.cpp)
+target_compile_features(slip_aware_odom PRIVATE cxx_std_17)
 ament_target_dependencies(slip_aware_odom rclcpp nav_msgs)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hangar_sim/CMakeLists.txt` around lines 11 - 12, Update the
slip_aware_odom target declaration to require C++17, matching the existing
amcl_odom_gate target configuration, so src/slip_aware_odom.cpp can use
std::clamp consistently across toolchains.
src/hangar_sim/src/slip_aware_odom.cpp (1)

66-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider exposing kHoldGap/kHoldPeriod as node parameters.

The hold threshold (15 ms) is sized against the controller's measured ~500 Hz rate while its config declares publish_rate: 50.0. If the fork is ever fixed to honour that param, every inter-message interval (20 ms) exceeds kHoldGap and the node starts injecting fabricated zero-velocity samples into fuse during normal driving. Making these declarable parameters (or deriving the gap from the observed inter-message interval) removes the hidden coupling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hangar_sim/src/slip_aware_odom.cpp` around lines 66 - 69, Expose kHoldGap
and kHoldPeriod as configurable node parameters, and use the resolved values in
the silent-source hold logic instead of fixed constants. Preserve the current
defaults (0.015 and 0.02 seconds) so existing behavior remains unchanged unless
parameters are explicitly overridden.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py`:
- Around line 94-105: Update the localization include in the launch flow, rather
than only the nav2_container parameters, so localization_launch.py receives
params with the tf_broadcast rewrite from param_substitutions. Ensure
use_fuse:=false produces AMCL tf_broadcast=true and use_fuse:=true preserves
false, either by passing the rewritten YAML or applying the equivalent
RewrittenYaml transformation inside localization_launch.py.

In `@src/hangar_sim/script/odom_world_drift.py`:
- Around line 97-110: Update _joints so the incoming name list is cached even
when resolving RAIL_JOINTS fails, and guard the position lookup against self.idx
being None. Preserve the early return for messages missing rail joints while
ensuring repeated messages with the same names cannot iterate over a cleared
index cache.

In `@src/hangar_sim/test/test_amcl_odom_gate.cpp`:
- Around line 165-176: Update InterpolateOdomDuplicateStampsNoNan so it no
longer claims to exercise the unreachable zero-span interpolation guard: either
revise the test comment and assertions to verify the target-at-back clamp
behavior, or remove the test while retaining the span guard as defensive code.

---

Nitpick comments:
In `@src/hangar_sim/CMakeLists.txt`:
- Around line 11-12: Update the slip_aware_odom target declaration to require
C++17, matching the existing amcl_odom_gate target configuration, so
src/slip_aware_odom.cpp can use std::clamp consistently across toolchains.

In `@src/hangar_sim/src/amcl_odom_gate.cpp`:
- Line 115: Replace the wall-clock timer assigned to timer_ in the constructor
with an rclcpp timer created using get_clock() and an rclcpp::Duration derived
from kPubPeriod, while preserving the existing publish() callback.

In `@src/hangar_sim/src/slip_aware_odom.cpp`:
- Around line 66-69: Expose kHoldGap and kHoldPeriod as configurable node
parameters, and use the resolved values in the silent-source hold logic instead
of fixed constants. Preserve the current defaults (0.015 and 0.02 seconds) so
existing behavior remains unchanged unless parameters are explicitly overridden.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d46e613a-c8d7-412f-b899-1395492dfccf

📥 Commits

Reviewing files that changed from the base of the PR and between 14ef4a1 and 72b4afe.

📒 Files selected for processing (16)
  • src/hangar_sim/CMakeLists.txt
  • src/hangar_sim/config/control/picknik_ur.ros2_control.yaml
  • src/hangar_sim/config/fuse/fuse.yaml
  • src/hangar_sim/description/picknik_ur_mujoco_ros2_control.xacro
  • src/hangar_sim/description/ur5e_ridgeback.xml
  • src/hangar_sim/include/hangar_sim/amcl_odom_gate_logic.hpp
  • src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py
  • src/hangar_sim/objectives/navigate_to_clicked_point.xml
  • src/hangar_sim/objectives/navigate_to_clicked_point_with_replanning.xml
  • src/hangar_sim/package.xml
  • src/hangar_sim/params/nav2_params.yaml
  • src/hangar_sim/script/odom_world_drift.py
  • src/hangar_sim/src/amcl_odom_gate.cpp
  • src/hangar_sim/src/amcl_odom_gate_logic.cpp
  • src/hangar_sim/src/slip_aware_odom.cpp
  • src/hangar_sim/test/test_amcl_odom_gate.cpp
💤 Files with no reviewable changes (2)
  • src/hangar_sim/objectives/navigate_to_clicked_point_with_replanning.xml
  • src/hangar_sim/objectives/navigate_to_clicked_point.xml

Comment thread src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py Outdated
Comment thread src/hangar_sim/script/odom_world_drift.py Outdated
Comment thread src/hangar_sim/test/test_amcl_odom_gate.cpp Outdated
@bkanator
bkanator force-pushed the feat/19667-fuse-odometry branch 2 times, most recently from 3dd5f34 to 6a0255e Compare July 29, 2026 14:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hangar_sim/description/ur5e_ridgeback.xml`:
- Around line 218-221: Update the base_gt site’s rgba value to use a visible
alpha while preserving its red color and existing position, size, and TF
behavior.

In `@src/hangar_sim/params/nav2_params.yaml`:
- Around line 56-58: Update the AMCL likelihood-field weights in the parameter
block so the active values z_hit and z_rand sum to 1.0, increasing one by 0.05;
keep z_short and z_max unchanged because they are inactive for this laser model.

In `@src/hangar_sim/src/slip_aware_odom.cpp`:
- Around line 91-108: Replace the wall-clock timer creation in SlipAwareOdom’s
constructor with a node-clock/ROS-time timer so the hold-gap logic follows
simulated time. Apply the same timer change in amcl_odom_gate.cpp for its 30 Hz
publishing and stale-check timers; update both affected sites, preserving their
existing periods and callbacks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c9118a06-f1cf-43b6-ad24-2a778c5bae8e

📥 Commits

Reviewing files that changed from the base of the PR and between 2e74b69 and 6a0255e.

📒 Files selected for processing (16)
  • src/hangar_sim/CMakeLists.txt
  • src/hangar_sim/config/control/picknik_ur.ros2_control.yaml
  • src/hangar_sim/config/fuse/fuse.yaml
  • src/hangar_sim/description/picknik_ur_mujoco_ros2_control.xacro
  • src/hangar_sim/description/ur5e_ridgeback.xml
  • src/hangar_sim/include/hangar_sim/amcl_odom_gate_logic.hpp
  • src/hangar_sim/launch/sim/robot_drivers_to_persist_sim.launch.py
  • src/hangar_sim/objectives/navigate_to_clicked_point.xml
  • src/hangar_sim/objectives/navigate_to_clicked_point_with_replanning.xml
  • src/hangar_sim/package.xml
  • src/hangar_sim/params/nav2_params.yaml
  • src/hangar_sim/script/odom_world_drift.py
  • src/hangar_sim/src/amcl_odom_gate.cpp
  • src/hangar_sim/src/amcl_odom_gate_logic.cpp
  • src/hangar_sim/src/slip_aware_odom.cpp
  • src/hangar_sim/test/test_amcl_odom_gate.cpp
💤 Files with no reviewable changes (2)
  • src/hangar_sim/objectives/navigate_to_clicked_point.xml
  • src/hangar_sim/objectives/navigate_to_clicked_point_with_replanning.xml

Comment thread src/hangar_sim/description/ur5e_ridgeback.xml Outdated
Comment thread src/hangar_sim/params/nav2_params.yaml
Comment thread src/hangar_sim/src/slip_aware_odom.cpp
@bkanator
bkanator force-pushed the feat/19667-fuse-odometry branch 4 times, most recently from c055aa7 to 1ea32dc Compare July 31, 2026 15:56
@PickNikRobotics PickNikRobotics deleted a comment from github-actions Bot Jul 31, 2026
@PickNikRobotics PickNikRobotics deleted a comment from github-actions Bot Jul 31, 2026
@PickNikRobotics PickNikRobotics deleted a comment from github-actions Bot Jul 31, 2026
@PickNikRobotics PickNikRobotics deleted a comment from github-actions Bot Jul 31, 2026
@PickNikRobotics PickNikRobotics deleted a comment from github-actions Bot Jul 31, 2026
actions-user and others added 21 commits August 20, 2026 05:09
…ring

Each wheel approximated its rollers with 8 spheres on a 59.4 mm ring, so
the effective rolling radius swung 4.52 mm (6%) eight times per
revolution. That polygon drove the whole base: front_rocker swung 1.9
deg peak-to-peak against its own 0.11 deg range, and the camera — rigid
on a chassis with no roll or pitch DOF — was shaken in-plane instead.

Going to 16 rollers quarters the polygon error. Total roller mass per
wheel is unchanged. Contact solref goes 0.002 -> 0.016 s, which is the
2*timestep MuJoCo wants at the 0.008 s step this scene runs.

front_rocker's range/stiffness/damping clamp is reverted: it was added
in 98a1f5f to suppress this rocking and is unnecessary once the wheels
are round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16 rollers removed the judder but doubled the ground-contact count
(ncon 64 -> 135), and the extra contact-solve time starves controller
mode-switching: 6 surface-following objectives died on "Current goal
cancelled during deactivate transition". 12 rollers with condim=3 costs
less (ncon 115) and keeps most of the benefit, but still fails 5 of
those objectives against a clean 98/98 baseline.

Recorded so the trade is visible; this is not yet shippable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The earlier 12- and 16-roller attempts kept the original 16.5 mm sphere
on the original 59.4 mm ring, so the rollers overlapped and every one
collided with its neighbours: 48 spurious roller-roller contacts per
robot, contact count 64 -> 115, and the extra solve time starved
controller mode-switching (5-6 objectives failed).

Sizing the sphere to sit tangent to its neighbours instead
(r = a*sin(pi/N), outer radius held at 75.9 mm) removes that entirely.
20 rollers now costs 9 extra contacts, not 51:

                  contacts   rocker p2p   base yaw std   camera jitter
  8 (main)             64      1.729 deg    1.490 deg/s      17.60 mm
  20 tangent           73      0.042 deg    0.084 deg/s       5.46 mm

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 3% non-overlap clearance was applied to the sphere radius after the
ring radius had been solved, so a + r came out at 75.5918 mm — 0.31 mm
(0.41%) under the original wheel, an unintended change to ride height and
rolling radius. Folding the clearance into the tangency condition instead
(a = OUT / (1 + 0.97*sin(pi/N))) holds a + r at 75.9000 mm exactly and
keeps the same 3% margin.

Behaviour is unchanged: yaw std 0.071 deg/s and camera jitter 6.02 mm,
against 1.490 deg/s and 17.60 mm on main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment said only that overlapping rollers self-collide, which read as
if the shipped 20-roller layout overlapped. Tangency is the boundary
between two failure modes, so name both: bigger spheres self-collide,
smaller ones leave gaps that vary the rolling radius.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…submodule

Update Phoebe dependency after licensing remediation
…subtree

Running "ML Move Boxes to Loading Zone" emitted two deprecation warnings on
every cycle, both from the subtree it calls.

Swap AddPoseStampedToVector for PushBackVector, remapping `input` to `element`
and splitting `vector` into `input_vector`/`output_vector` on the same
blackboard key so the ForEach still accumulates. ResetPoseStampedVector already
clears the vector ahead of the loop.

On PlanCartesianPath, fold the deprecated velocity_scale_factor,
acceleration_scale_factor, and trajectory_sampling_rate ports into the
trajectory_timing port. All three values carry over unchanged, so the planned
lift is timed exactly as before.

Fixes PickNikRobotics/BCR_platform_mirror#27

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-judder

fix(hangar_sim): round out the mecanum wheels so the base stops juddering
Drops ~520 MB from a `--recurse-submodules` clone. Nothing in this
workspace consumed the package: no `exec_depend`, no
`model_package="moveit_pro_clipseg"` port. Text-prompt segmentation moved
to SAM3 (`GetMasks2DFromExemplar`), and the `GetMasks2DFromTextQuery`
Behavior that used the CLIPSeg models no longer exists in MoveIt Pro.

Also corrects two stale CLIPSeg references left behind by the SAM3 move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Give hangar_sim's mobile base a realistic localization stack so beluga_amcl is
exercised against fuse-fused odometry (wheel + IMU) with real drift, including
where the scene is degenerate for scan matching (the smooth fuselage, the
unmapped picking boxes).

- Fuse on by default (use_fuse=true) -> /odom_filtered; odom_world_drift injects
  a live odom->world from the fuse estimate so AMCL sees drift to correct while
  world->base stays ground truth for whole-body planning.
- slip_aware_odom (new C++ node): grows wheel-yaw covariance during sustained
  in-place spin (mecanum roller slip) so fuse defers to the IMU while spinning
  and trusts the wheels driving straight; also re-anchors odom across the
  whole-body<->nav controller handoff.
- AMCL tuning: OmniMotionModel; alpha1 0.1->0.4; update_min_a 0.1 with
  resample_interval 3; likelihood relaxed (sigma_hit 0.1->0.25, z_hit 0.9->0.65,
  z_rand 0.1->0.3) so unmapped boxes read as outliers instead of yanking the pose.
- fuse lag_duration 0.5->0.3: shorter smoother window cuts output latency
  (moving-yaw 0.70->0.465deg) while still smoothing transient spikes.
- amcl_odom_gate (new C++ node): sole map->odom publisher (AMCL tf_broadcast
  false). Holds the last good map->odom and coasts on fuse odom where AMCL is
  degenerate, blending back when trustworthy. A large correction is accepted
  only if it persists over a sliding window (gated on position AND yaw) with
  particle-spread hysteresis; spread_accept_max additionally rejects a
  confident-but-WRONG lock (which persists yet stays spread) so a scan-slide
  divergence is coasted through while a real recovery (which converges) is still
  adopted. latency_compensation_sec composes the correction with odom->base from
  one estimator-lag ago (referenced to the cloud stamp), forward-projecting it
  with real buffered odometry -> moving-pose error 13.8->8.2cm, moving-yaw
  1.2->0.9deg, no overshoot on turns; 0 disables, hangar_sim uses 0.30. The pure
  decision logic (detail::updateGate, interpolateOdom, appendOdomSample) has no
  ROS/TF deps and is fully unit-tested.
- Removed the per-objective SetInitialPose reseed from the clicked-point
  Objectives (superseded by slip_aware_odom re-anchoring; the unconditional
  reseed could cement a drifting estimate).
- wz_max 0.6 (below the velocity_smoother cap so it binds) keeps spins within
  AMCL's correction bandwidth; odom_rate 50Hz / tf_publish_rate 30Hz and the
  broadcaster rates decoupled from the control loop give the stack CPU headroom.

Closes #19667.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eness guards

Extracts slip_aware_odom's re-anchor/covariance-ramp logic into a pure,
unit-tested header (mirroring amcl_odom_gate's existing pattern), and adds
staleness guards flagged by review: odom_world_drift now withholds
odom->world when /odom_filtered goes stale instead of broadcasting a frozen
estimate with a fresh timestamp; amcl_odom_gate now treats a stale odom
buffer as unavailable (with a dedicated warning) instead of silently
clamping to an old sample; and slip_aware_odom's hold() now stops (and
warns) after a bounded gap instead of fabricating "parked" odometry forever
if a controller dies. Routes the three new nodes' logs to console
(output=both) so their watchdog warnings are actually visible, and drops
internal issue-number references from code comments per
.claude/rules/cpp-style.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… site

The base_gt site reported a true world pose only because MuJoCo parented sites
to the scene root. Sensor frames now descend from the robot's own links, so the
site reads as identity against the base and no longer carries ground truth.

world -> ridgeback_base_link already is ground truth: robot_state_publisher
builds it from the MJCF's own slide and hinge joints, in absolute simulation
coordinates and independent of fuse/AMCL.
odom_qos_relay.py published /odom on /odom_reliable with RELIABLE QoS so
fuse could consume it. Nothing in the tree subscribes to /odom_reliable
(verified by a repo-wide grep: the publisher on line 74 was the only
reference), and since moveit_pro#21948 the upstream /odom publisher is
RELIABLE anyway, so the QoS bridge it provided is redundant as well as
unused.

Removes only the odometry half. The IMU and cmd_vel relays in the same
node are still live and are left untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019N5Dz4fsgVHcrHHXrovMfz
kinematics.wheels_radius was 0.0666 in both the whole-body and the body-frame
mecanum controllers, ~11.9% below the wheels' true rolling radius.

The tread is a ring of 20 spheres of r=0.010 centred at a=0.0659002081. Rolling
on that ring traces the convex hull of the spheres — 20 tangent segments of
2*a*sin(pi/20) plus arcs summing to one circle of radius r — giving a rolling
radius of (20*2*a*sin(pi/20) + 2*pi*r) / (2*pi) = 0.07563. This is deliberately
not the 0.0759 outer radius a+r, which is the static ride height PR #882 uses.

The controller divides commanded body velocity by this constant, so the base
drove ~13.6% faster than commanded; wheel odometry multiplies by it, so it
under-reported distance by ~11.9% — about 0.72 m over a 6 m leg, the dominant
source of the localization drift this stack was built to absorb.

Valid only against main's rebuilt 20-roller wheels; the older 8-roller geometry
had a different answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124EJ6ebbmvMJMjyMYPnWHD
…ift values

The alphas were inflated ~2x in this PR to spread the particle cloud wide enough
to cover odometry that under-reported distance by ~11.9%, with alpha1 pushed to
0.4 specifically to hold yaw lock through spins. Both were compensation for the
wheel rolling radius being 11.9% low, which the previous commit corrects.

With the cause fixed the compensation is no longer earned, so alpha1..alpha5 go
back to main's 0.05/0.05/0.1/0.05/0.05. They should be re-checked once the base
has actually been driven at its corrected speed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124EJ6ebbmvMJMjyMYPnWHD
@bkanator
bkanator force-pushed the feat/19667-fuse-odometry branch from f2bea8b to 0662c5b Compare August 26, 2026 00:38
The hangar_sim mecanum wheels are sphere rings, so ride height and rolling radius
are different numbers derived from the same geometry, and using one where the
other belongs shows up as localization drift rather than as an obvious error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124EJ6ebbmvMJMjyMYPnWHD
@github-actions

Copy link
Copy Markdown

MoveIt Pro Example WS - Objectives Integration Test Report

@github-actions

Copy link
Copy Markdown

MoveIt Pro Example WS - Objectives Integration Test Report

@github-actions

Copy link
Copy Markdown

MoveIt Pro Example WS - Objectives Integration Test Report

@bkanator
bkanator marked this pull request as draft September 4, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants