TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Robot Navigation Simulation with ROS 2 and Gazebo: Not Trusting the "Perfection" Claimed by Wheels

Status: Draft (2026-10-18), Measured. Measurements were taken on 2026-10-17 in a single temporary pod on a home lab Kubernetes cluster. The node is a mini PC (Intel Core Ultra series with 22 threads, 31 GiB memory), and the pod limits were set to 3 CPU cores and 4 GiB memory, without GPU or /dev/dri (no privileged mode, no extra capabilities). The image is ros:jazzy(ros-base, Ubuntu 24.04.5) with ros-jazzy-ros-gz·ros-jazzy-turtlebot3-gazebo added via apt. The versions are Gazebo Sim 8.15.0 (Harmonic), ros_gz 1.0.24, and turtlebot3_gazebo 2.3.7. The tests were repeated 2 times for B, 3 times for C, and 1 time for the rest; the values in the table are single measurements per run (not medians). Raw data (rosbag2 mcap, summary JSON, 1-second cgroup samples) are stored in the ROS measurement folder in the working directory, and the article only shows values with node names and addresses masked out.

Table of Contents

1. Intuition: A Robot Counting Steps with Eyes Closed

It is easy to believe that if you close your eyes and walk "ten steps forward, 90 degrees left" four times, you will return to your starting point, because you counted the steps accurately. However, if the floor is slippery, you take a shorter step, and if your feet slip while turning, you rotate 80 degrees instead of 90. Someone who only counts steps will not know the difference.

Wheel robot odometry (odom) is precisely this step-counting process. It integrates wheel revolutions to declare, "I am here now." The simulator provides one more thing on top of this: the true position (ground truth pose) known to the physics engine. This post demonstrates through actual measurements what happens when both values are recorded side by side.

2. Setup: Headless Simulation Inside a Single Pod

파드 하나 안에서 도는 헤드리스 시뮬레이션 쿠버네티스 임시 파드: CPU 상한 3코어, 메모리 상한 4 GiB, GPU 없음 gz sim -s (화면 없음)물리 1 ms, 라이다·IMU ros_gz_bridgegz 토픽 ↔ ROS 2 토픽 주행 노드 (50 Hz)/odom 보고 /cmd_vel 냄 rosbag2 기록기mcap 파일 한 개 /odom /cmd_vel 기록하는 토픽: /odom /cmd_vel /imu /gt_poses(정답 자세) /scan 정답 자세는 gz 의 dynamic_pose 를 브리지로 꺼내 odom 과 나란히 기록합니다. 실행 A: 원본 월드(렌더링 라이다) B: 물리만 C: B + 실시간 제한 해제
Figure: Headless simulation running inside a single pod: gz sim, bridge, navigation node, recorder

Image costs were also measured. ros:jazzy is 283 MiB compressed and took 18.3 s to download. Adding the simulation packages via apt took 272 s, downloading 768 packages and 554 MB, which increased the installed size by 2,637 MB to a total of 2,637 MB. For reference, judged solely by registry manifests, osrf/ros:jazzy-desktop is 1,203.9 MiB compressed, and jazzy-simulation is 698.3 MiB (not downloaded). If you find the installation time wasteful on every run, baking this step into an image is your next task.

3. Four Worlds

Name What's Different
A Original TurtleBot3 empty world. Because it includes the Sensors system (ogre2) and requires rendering gpu_lidar , it was launched with --headless-rendering .
A2 Two static boxes were added to A. In an empty world, all LiDAR values are infinite, making it impossible to verify if rendering works properly.
B The Sensors system was removed. The floor is an inline plane, and friction matches the original floor with mu 100 and mu2 50. Neither rendering nor network is used.
C In B, real_time_factor is changed to 0 to run at maximum speed without real-time constraints.

4. Navigation Node: Following Odom, Not Time

The navigation node is not an open-loop controller like "go straight for 5 seconds." It is a closed-loop controller that visits the corners (1,0), (1,1), (0,1), and (0,0) in sequence by checking /odom , operating at a maximum of 0.2 m/s, 0.6 rad/s, and 50 Hz based on simulation time. This must match real robot waypoint navigation so that the "error against target" can be interpreted as the combined effect of the controller, odometry, and physics. The core lies in a few lines of the straight-line phase.

if self.phase == 'drive':
    tx, ty = CORNERS[self.leg]
    dx, dy = tx - x, ty - y
    # 진행 방향 위로 남은 거리. 지나치면 음수가 된다.
    seg_heading = HEADINGS[self.leg - 1] if self.leg else 0.0
    along = dx * math.cos(seg_heading) + dy * math.sin(seg_heading)
    if along <= POS_TOL:                      # 5 mm 안이면 멈추고 돌기 시작
        self.send(0.0, 0.0)
        self.mark('corner')
        self.phase = 'turn'
        return
    bearing = math.atan2(dy, dx) if math.hypot(dx, dy) > 0.05 else seg_heading
    w = max(-W_MAX, min(W_MAX, 2.0 * wrap(bearing - th)))
    v = max(0.02, min(V_MAX, 0.8 * along))
    self.send(v, w)

(Excerpted from TT Lab experimental code square_driver.py , with comments shortened or expanded to fit the article. Rotation targets were given as absolute orientations of 0, π/2, π, and −π/2 at each corner to prevent accumulated rotational errors.)

5. Results: The Cost of Maintaining Real-Time and Corner Errors

Run RTF Wall Clock per Lap Actual Corner Error mm (1→4) Actual Final Position Error Max odom↔Actual Avg CPU Max anon gz RSS
A Render LiDAR 1.000 47.5 s 11, 33, 40, 25 16 mm 41 mm 0.91 core 467 MiB 515 MiB
A2 A + Boxes 1.000 47.5 s 11, 47, 66, 43 50 mm 74 mm 0.92 core 466 MiB 509 MiB
B Physics Only 1.000 47.6 s 17, 23, 14, 5 16 mm 26 mm 0.73 cores 196 MiB 156 MiB
B iteration 1.000 47.5 s 15, 33, 28, 13 16 mm 32 mm 0.74 cores 196 MiB Not measured
C Max speed 11.11 4.3 s 19, 27, 20, 7 16 mm 39 mm 2.88 cores 196 MiB 156 MiB
C iteration 2 11.76 4.1 s 20, 28, 20, 8 17 mm 39 mm 2.54 cores 196 MiB Not measured
C iteration 3 10.88 4.4 s 19, 28, 20, 7 16 mm 39 mm 2.86 cores 196 MiB Not measured

RTF (Real-Time Factor) is /odom the increase in simulation time in the header divided by the increase in wall-clock time received by the recorder. CPU averaged the driving section by reading the pod cgroup's cpu.stat every second, and memory is the anon max of memory.stat . memory.current shows up as 3.0–3.6 GiB, but since most of it is page cache created during apt installation, the actually held memory is considered to be anon.

렌더링을 빼고 실시간 제한을 풀면 한 바퀴 벽시계 시간(s) A 원본(렌더링 라이다) 47.5 A2 A + 상자 두 개 47.5 B 물리만 47.6 C 물리만 + 최대 속도 4.3 CPU 평균(코어, 상한 3) A 원본(렌더링 라이다) 0.91 A2 A + 상자 두 개 0.92 B 물리만 0.73 C 물리만 + 최대 속도 2.88 anon 메모리 최대(MiB) A 원본(렌더링 라이다) 467 A2 A + 상자 두 개 466 B 물리만 196 C 물리만 + 최대 속도 196 C 의 RTF(실시간 대비 속도)는 11.11. 같은 51.7 s 시뮬레이션이 4.3 s 에 끝납니다.
Figure: Wall-clock time, CPU, and memory according to rendering and real-time limits

6. When a floor friction line is omitted

When initially creating the world without rendering (B), the floor model of the original was replaced with an inline plane, and the friction setting was omitted. As a result, the physics engine's (ODE) default value mu 1 is used. The floor after the fix has the following single line.

FRIC = '<surface><friction><ode><mu>100</mu><mu2>50</mu2></ode></friction></surface>'  # 원본 바닥과 같게

The figure below shows records of the same driving node running in two worlds. Clicking the figure replays the actual trajectory left in the rosbag at 4x speed.

바퀴로 센 위치(odom)와 실제 위치 같은 주행 노드, 같은 1 m 정사각형. 바닥 마찰 한 줄만 다릅니다. ▶ 누르면 재생(4배속) 바닥 마찰을 적은 월드 odom 이 본 끝 위치 오차: 5 mm 실제 끝 위치 오차: 16 mm, 방향 -1.0° 바닥 마찰을 빠뜨린 월드 odom 이 본 끝 위치 오차: 5 mm 실제 끝 위치 오차: 0.50 m, 방향 -38° 실제(시뮬레이터 정답) odom(바퀴 회전으로 적분) 목표 정사각형 두 위치 사이 거리
Figure: Odom and actual trajectory left by the same driving node in two worlds with different friction
Run Actual angle per rotation Corner actual error mm (1→4) End position actual error / orientation End position error seen by odom
B (with friction) 89.2, 89.7, 89.7, 89.7° 17, 23, 14, 5 16 mm / −0.98° 5 mm
B missing friction 80.3, 79.7, 80.3, 80.9° 10, 138, 344, 487 503 mm / −37.99° 5 mm
C missing friction 78.0, 79.2, 80.5, 80.9° 19, 164, 399, 533 549 mm / −40.24° 4 mm

During in-place rotation, the wheels slipped and turned only about 80° instead of 90°, and that discrepancy accumulated at every corner. However, since odom integrates wheel joint angles, it cannot detect the slip. Therefore, odom reported until the very end that it "returned within 5 mm". If the results were judged solely by odom, this world would have passed the test.

We also separately verified whether the ground-truth pose was correct. After the run ended, the position queried via gz model -m burger -p was compared with the last value of /gt_poses . In the C missing friction run, gz model returned (−0.2505, 0.4883) m and an orientation of −40.0°, while /gt_poses was (−0.2505, 0.4884) m and −40.2°. In B with friction, the two values were identical at (−0.0130, 0.0099) m.

Lesson. When evaluating robot performance in simulation, record both the robot's self-estimated position and the simulator's ground-truth position together , and make the judgment based on the ground truth. Looking only at the estimated position makes physics configuration mistakes look like robot success. Conversely, looking only at the ground truth makes it impossible to know how wrong the estimator is. The difference between the two (the red line in the figure) is odometry drift, which is why actual robots maintain a separate position estimator.

7. LiDAR running without a GPU

A and A2 use gpu_lidar(360 beams, 5 Hz), which measures distances via rendering. The pod had neither a GPU nor /dev/dri , but it launched with --headless-rendering(EGL) and maintained an RTF of 1.000.

8. Max speed mode usable for CI

C is B with real_time_factor set to 0. One lap of a 1 m square (simulation 47.7–51.7 s) finished in 4.1–4.4 s of wall-clock time, and the RTF was 10.88–11.76.

This means that per PR, you can run a 4-second test where "the robot drives a 1 m square within 2 cm." However, that judgment must be made using the ground truth pose as in section 6.

9. Troubleshooting

The odom frequency was measured at 50.0 Hz based on simulation time. The

The caster drag hypothesis (12–13 mm movement per revolution) can be tested by varying caster friction, but we have not done so yet.

Comparisons on GPU nodes and camera sensors are unmeasured.

11. Next Up The next part moves on to table tennis robots. Instead of wheels, a single ball is placed in MuJoCo to measure how much bounce and spin are affected by a single line of simulator settings. The basics of the robot arm itself (coordinate systems, kinematics) are covered in the "Table Tennis Robot Arm Basics" series (table of contents) currently being published together.

A word from TT.

If you only count steps, I always return to the same spot. But if the floor is slippery, I am actually standing half a meter to the side. That's why in the simulator, I always record my "real position" as well!

Sign in to like

Comments

No comments yet.

Sign in to leave a comment