Trying to tune robot_localization EKF for a Segway RMP (differential drive) with IMU + wheel odom + GPS outdoors.... currently getting catastrophic divergence on some runs, need tuning advice
Hey everyone, I've been banging my head against this for a while and could really use some experienced eyes on my setup.
The robot: Segway RMP platform, differential drive. It's a ground robot that does long outdoor runs, typically 30-60 minutes, covering several kilometers on a university campus. Mix of open areas, tree-lined paths, and some areas with buildings nearby causing GPS multipath.
Sensors:
- IMU: Microstrain 3DM-GX3-45 (6-axis, no magnetometer, so no absolute heading)
- Wheel encoders publishing as
nav_msgs/Odometryon/odom/wheels - GPS through
navsat_transform_nodeoutputting to/gps/odometry - All fused through
ekf_nodeat 150Hz
The problem: Most of the time the filter works okay-ish, but on some runs it completely falls apart. Like, the estimated position jumps to somewhere millions of meters away after what I think is a GPS spike getting accepted, and then the filter never recovers. It just keeps publishing nonsense from that point on. On other runs the path length ratio is completely off (filter thinks the robot traveled either way more or way less than it actually did).
Also running ukf_node in parallel to compare, and that one is just spitting out Critical Error, NaNs were detected in the output state of the filter almost constantly. So the UKF option seems totally broken for my setup.
Current config:
rl_ekf:
ros__parameters:
frequency: 150.0
sensor_timeout: 0.5
two_d_mode: true
transform_time_offset: 0.0
transform_timeout: 0.1
print_diagnostics: false
debug: false
map_frame: map
odom_frame: odom
base_link_frame: base_link
world_frame: odom
# Wheel odometry: fuse Vx and Vyaw only (differential drive, no lateral velocity)
odom0: /odom/wheels
odom0_config: [false, false, false,
false, false, false,
true, false, false,
false, false, true,
false, false, false]
odom0_differential: false
odom0_relative: false
odom0_queue_size: 10
odom0_twist_rejection_threshold: 4.03
# GPS via navsat_transform: fuse XY position
odom1: /gps/odometry
odom1_config: [true, true, false,
false, false, false,
false, false, false,
false, false, false,
false, false, false]
odom1_differential: false
odom1_relative: false
odom1_queue_size: 10
odom1_pose_rejection_threshold: 3.72
# IMU: roll/pitch only (no magnetometer, no absolute yaw), angular vel, accel
imu0: /imu/data
imu0_config: [false, false, false,
true, true, false,
false, false, false,
true, true, true,
true, true, true]
imu0_differential: false
imu0_relative: false
imu0_remove_gravitational_acceleration: true
imu0_queue_size: 50
process_noise_covariance: [0.05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.06, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.03, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.03, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.06, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.025, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.025, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.04, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.02, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.015]
Process noise covariance is diagonal, values roughly in the 0.01-0.06 range for position/orientation and 0.01-0.025 for velocity/acceleration. I basically eyeballed these from examples online.
Specific questions:
- The GPS divergence issue: my rejection threshold is
sqrt(chi2(2, 0.999)) = 3.72. Is this too loose? Too tight? When a GPS spike comes in and passes the gate, is there any way to recover without restarting the node? - No magnetometer means I have no absolute yaw reference at startup. I'm not fusing yaw from the IMU at all (the
falsein that position). The filter figures out heading from GPS travel. How long does this take to converge and is there a better way to handle it? odom0_differential: falsewith wheel odometry publishing absolute twist... is this correct or should it betrue? I've seen conflicting advice on this.- Process noise tuning: is there a systematic way to do this or is it always just trial and error? My position noise (0.05) vs velocity noise (0.025) ratio.... does that even make sense dimensionally?
- For long outdoor runs where GPS quality varies a lot (open field vs. near buildings), is there any way to get the filter to automatically trust GPS less when it's bad, without manually tuning per-environment?
Running ROS 2 Jazzy on Ubuntu 24.04.