A physics-free ROS 2 Humble testbed for Nav2, running on a real occupancy grid produced by LiDAR SLAM on a Raspberry Pi robot.
There is no Gazebo, no Webots, no physics engine. A fake_odometry node integrates
/cmd_vel into /odom plus TF at 50 Hz, which is enough to exercise the full Nav2 stack —
global planning, local control, costmaps, recovery — on a laptop, headless, in seconds per
trial. That makes it cheap to run the same scenario five times and report a spread instead
of a single anecdote.
Everything below was measured this way: 7 scenarios × 5 trials, headless, on a real SLAM map.
| Node | Purpose |
|---|---|
fake_odometry |
Integrates /cmd_vel → /odom + TF at 50 Hz. Optional Gaussian velocity noise. |
dynamic_obstacle |
Moves an obstacle through the costmap. Modes: ahead, crossing. |
trajectory_recorder |
Records executed poses and /plan for post-hoc analysis. |
navigation_statistics |
Computes cross-track error, path length, replan counts. |
sim_control |
Drives a trial: sends the goal, detects arrival, tears down. |
Plus a_star.py — a from-scratch A* reference implementation, written to understand the
algorithm rather than to be used by Nav2 (Nav2 runs its own global planner).
Nav2 is configured with an A* global planner and the DWB local controller.
src/my_navigation_sim/
launch/full_sim.launch.py single argument-driven entry point
config/nav2_params.yaml Nav2 planner/controller/costmap params
maps/ the SLAM-produced occupancy grid
urdf/{car,drone}/ two robot descriptions
scripts/collect_report_data.{py,sh} the experiment harness
results/ 7 scenarios x 5 trials of raw JSON + summary
docs/experiments.md data collection notes
Requires ROS 2 Humble and Nav2.
mkdir -p ~/nav_ws/src && cd ~/nav_ws
git clone https://github.com/Lochan25526/ros2-nav2-navigation-sim.git src/nav_sim
rosdep install --from-paths src -y --ignore-src
colcon build --symlink-install
source install/setup.bash
ros2 launch my_navigation_sim full_sim.launch.pyOne launch file drives every scenario:
| Argument | Values | Effect |
|---|---|---|
robot |
car | drone |
which URDF to load |
map |
path | occupancy grid to navigate |
enable_noise |
bool | inject Gaussian noise into commanded velocity |
noise_sigma |
float | noise std-dev (overrides the built-in 0.05) |
enable_dynamic_obstacles |
bool | spawn the moving obstacle |
obstacle_mode |
ahead | crossing |
how it moves |
record_bag |
bool | record a rosbag |
rviz |
bool | start RViz |
Example — noisy odometry with a crossing obstacle, headless:
ros2 launch my_navigation_sim full_sim.launch.py \
enable_noise:=true noise_sigma:=0.05 \
enable_dynamic_obstacles:=true obstacle_mode:=crossing \
rviz:=falseReproduce the full experiment suite:
./scripts/collect_report_data.sh # collect + analyse
./scripts/collect_report_data.sh --analyze-only # rebuild tables onlyThree goals picked from free space by farthest-point sampling with ≥0.6 m clearance, all
reachable. n = 5 per scenario, ROS_DOMAIN_ID=42, headless. Success = final pose within
0.2 m of goal before timeout.
Path-tracking accuracy (cross-track error vs the first global plan):
| Goal | Distance | Mean XTE | RMS XTE | Max XTE |
|---|---|---|---|---|
| G1 | ~7.1 m | 0.059 m | 0.078 m | 0.199 m |
| G2 | ~15.2 m | 0.039 m | 0.052 m | 0.143 m |
| G3 | ~8.8 m | 0.031 m | 0.048 m | 0.163 m |
Dynamic obstacle avoidance (goal G1):
| Scenario | Executed | Time | Replans | Success |
|---|---|---|---|---|
| Static baseline | 7.00 m | 27.8 s | 0.0 | 5/5 |
| Dynamic, ahead | 14.93 m | 72.9 s | 6.8 | 5/5 |
| Dynamic, crossing | 13.72 m | 129.2 s | 14.2 | 4/5 |
Timing: global planning 1.20 ms mean / 3.27 ms max; controller loop 20.0 Hz; goal → first plan 9.5 ms.
Two results worth reading carefully rather than skimming:
dyn_crossing_G1reached 4/5, not 5/5. One trial hit the 240 s timeout while the crossing obstacle repeatedly blocked the corridor. That is a real outcome and is reported as such. The dynamic-obstacle scenarios are also far less repeatable than the nominal ones (72.9 ± 38.2 s vs 27.8 ± 0.1 s) — the mean alone would hide that.- Odometry noise barely moved the numbers (RMS XTE 0.078 → 0.079 → 0.071 across σ = 0, 0.02, 0.05). That is not evidence of robustness — see below.
Full data, metric definitions and caveats: results/README.md. Raw
per-trial JSON in results/raw/.
- AMCL is not correcting drift.
nav2_params.yamlsets AMCLtf_broadcast: Falseand a static identitymap→odomtransform owns that link. The map-frame pose therefore equals the raw integrated odometry. This is the main caveat on every number above and the reason the noise results look flat: noise is injected on commanded velocity, and the controller closes the loop on that same odometry, so the error never enters the map frame. A genuine localisation test needs AMCL broadcasting. - No physics.
fake_odometryintegrates/cmd_veldirectly — no mass, no inertia, no wheel slip, no actuator limits beyond what the controller itself enforces. Results are an upper bound on real-world tracking. - Final-pose error (~0.24 m) exceeds the 0.2 m arrival radius because the robot coasts
after arrival is detected. Consistent with Nav2's own
xy_goal_toleranceof 0.25 m. - Paths inside
results/raw/*.jsonstill reference the original~/nav_ws/layout. Cosmetic — it is recorded data, not code.
Three parts of one internship project on the same Raspberry Pi robot:
| Repository | What it does |
|---|---|
| ros2-lidar-inertial-slam | LiDAR + IMU SLAM — produced the map this repo navigates. |
| ros2-orbslam3-vio | Camera-based SLAM with ORB-SLAM3, and the attempt at visual-inertial odometry. |
| ros2-nav2-navigation-sim | (this repo) Nav2 autonomous navigation. |
Apache-2.0 — see LICENSE.
Built during a research internship at IIT Kharagpur, 2026. This is the navigation half of the project; the SLAM that produced the map is in the repositories above.