ステータス:ドラフト(2026-10-18)、実測。 計測は2026-10-17にホームラボのKubernetesの単一一時Pod上で行いました。ノードはミニPC 1台(Intel Core Ultra系 22スレッド、メモリ 31 GiB)であり、Podの上限はCPU 3コア、メモリ 4 GiBであり、GPUは
/dev/dri割り当てませんでした(特権なし、追加のcapabilityなし)。イメージはros:jazzy(ros-base、Ubuntu 24.04.5)にaptでros-jazzy-ros-gz·ros-jazzy-turtlebot3-gazeboを追加しました。バージョンはGazebo Sim 8.15.0(Harmonic)、ros_gz 1.0.24、turtlebot3_gazebo 2.3.7です。繰り返し回数はBが2回、Cが3回、その他は1回であり、表の値は実行ごとに1回測定した値です(中央値ではありません)。生データ(rosbag2 mcap、サマリーJSON、1秒間隔のcgroupサンプル)は作業ディレクトリのROS測定フォルダにあり、記事にはノード名とアドレスをマスクした値のみを記載しています。
目次
- 1. 直観:目を閉じて歩数を数えるロボット
- 2. 構成:1つのPod内でのヘッドレスシミュレーション
- 3. 4つのワールド
- 4. 走行ノード:時間ではなくodomを見て移動する
- 5. 結果:リアルタイム性を維持するコストと角の誤差
- 6. 床の摩擦の1行を書き忘れた場合
- 7. GPUなしで動作するLiDAR
- 8. CIで使用可能な最高速モード
- 9. 遭遇した問題
- 10. 制限事項
- 11. 次回予告
- 参考
1. 直観:目を閉じて歩数を数えるロボット
目を閉じて「前に10歩、左に90度」を4回繰り返せば、元の場所に戻ると簡単に信じ込んでしまいます。歩数は正確に数えたからです。しかし、床が滑りやすいと1歩分が足りなくなり、曲がる時に足が空転すると90度ではなく80度しか曲がりません。歩数しか数えない人はその違いに気づきません。
車輪ロボットの オドメトリ(odom) こそがまさに歩数カウントです。車輪が何回転したかを積分し、「今ここにいる」と言います。シミュレータはこれに1つの要素を追加します。物理エンジンが把握している 実際の位置(正解の姿勢) です。この記事は、2つの値を並べて記録すると何が見えてくるのかを実測値で示します。
2. 構成:1つのPod内でのヘッドレスシミュレーション
gz sim -s -rでサーバーのみを起動しました。画面(GUI)がないため、XサーバーやVNCは不要です。ros_gz_bridgeがGazeboのトピックとROS 2のトピックを接続します。TurtleBot3がデフォルトで使用するブリッジ設定に1行を追加し、Gazeboの/world/default/dynamic_pose/infoを/gt_posesという名前で取り出しました。これが正解の姿勢です。ros2 bag record -s mcapが/odom,/cmd_vel,/gt_poses,/imu(LiDARのあるワールドは/scanまで)を1つのファイルにまとめます。- DDSのディスカバリーがPodのネットワーク外に漏れないように
ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOSTで制限しました。
イメージのコストも測定しました。 ros:jazzy は圧縮時283 MiBであり、ダウンロードに18.3秒かかりました。ここにシミュレーションパッケージをaptで追加すると、768個のパッケージ、554 MBをダウンロードしてインストール後に2,637 MB増加し、272秒かかりました。参考として、レジストリのマニフェストでのみ確認した osrf/ros:jazzy-desktop は圧縮時1,203.9 MiB、 jazzy-simulation は698.3 MiBです(ダウンロードしていません)。毎回インストールする時間が惜しいのであれば、このステップをイメージとして固めることが次の作業になります。
3. 4つのワールド
| 名前 | 違い |
|---|---|
| A | TurtleBot3の空のワールドのオリジナルです。Sensorsシステム(ogre2)とレンダリングが必要な gpu_lidar があるため、 --headless-rendering で起動しました。 |
| A2 | Aに静的な箱を2つ配置しました。空のワールドではLiDARの値がすべて無限大になり、レンダリングが正しく行われているか確認できないためです。 |
| B | Sensorsシステムを除外しました。床はインラインの平面であり、摩擦はオリジナルの床と同じくmu 100、mu2 50です。レンダリングもネットワークも使用しません。 |
| C | Bから real_time_factor を0に変更し、リアルタイム性に縛られず最大速度で動作させます。 |
4. 走行ノード:時間ではなくodomを見て移動する
走行ノードは「5秒間直進」のようなオープンループではありません。 /odom を確認しながら角 (1,0)、(1,1)、(0,1)、(0,0) を順番に訪れるクローズドループであり、最大0.2 m/s、0.6 rad/s、シミュレーション時間基準で50 Hzです。実機のウェイポイント走行と同じ方式でなければ、「目標に対する誤差」がコントローラ、オドメトリ、物理演算の合算として読み取れないためです。核心は直進段階の数行です。
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)
(TT Labの実験コード square_driver.py から抜粋し、コメントは記事に合わせて短縮または追加しました。回転目標は角ごとに絶対方位 0、π/2、π、−π/2 として与え、回転誤差が蓄積しないようにしました。)
5. 結果:リアルタイム性を維持するコストと角の誤差
| 実行 | RTF | 1周のウォールクロック | 角の実際の誤差 mm (1→4) | 終点位置の実際の誤差 | odom↔実際の最大 | CPU平均 | anon最大 | gz RSS |
|---|---|---|---|---|---|---|---|---|
| A レンダリング LiDAR | 1.000 | 47.5 s | 11, 33, 40, 25 | 16 mm | 41 mm | 0.91コア | 467 MiB | 515 MiB |
| A2 A + 箱 | 1.000 | 47.5 s | 11, 47, 66, 43 | 50 mm | 74 mm | 0.92コア | 466 MiB | 509 MiB |
| B 物理のみ | 1.000 | 47.6 s | 17, 23, 14, 5 | 16 mm | 26 mm | 0.73コア | 196 MiB | 156 MiB |
| B 繰り返し | 1.000 | 47.5 s | 15, 33, 28, 13 | 16 mm | 32 mm | 0.74コア | 196 MiB | 未測定 |
| C 最高速 | 11.11 | 4.3 s | 19, 27, 20, 7 | 16 mm | 39 mm | 2.88コア | 196 MiB | 156 MiB |
| C 繰り返し 2 | 11.76 | 4.1 s | 20, 28, 20, 8 | 17 mm | 39 mm | 2.54コア | 196 MiB | 未測定 |
| C 繰り返し 3 | 10.88 | 4.4 s | 19, 28, 20, 7 | 16 mm | 39 mm | 2.86コア | 196 MiB | 未測定 |
RTF(リアルタイム係数)は、 /odom 冒頭のシミュレーション時間の増加量を、レコーダーが受信したウォールクロック時間の増加量で割った値です。CPUはPodのcgroupの cpu.stat を1秒ごとに読み取って走行区間を平均し、メモリは memory.stat のanonの最大値です。
memory.current は3.0–3.6 GiBと表示されますが、大部分はaptのインストール時に生成されたページキャッシュであるため、実際に占有しているメモリはanonとして確認します。
- odom基準ではすべての実行がほぼ完璧でした。 角の誤差は4.6–5.0 mm(停止許容値 5 mm)、回転は89.74–89.78°(許容値 0.005 rad)でした。表の実際の誤差はほぼすべて、odomと実際の値が乖離した分です。
- その場で90°回転するたびに実際の位置が12–13 mm移動しました(Bの実行、odomは0.3 mm以下)。キャスターの引きずりが原因と推測されますが、確認はしていません(推定)。
- 起動は
gz simの実行から最初の/odomまで3.6–3.7秒であり、Aの初回実行のみオリジナルの床モデルのダウンロードに5.4秒かかりました。
6. 床の摩擦の1行を書き忘れた場合
レンダリングのないワールド(B)を最初に作成する際、オリジナルの床モデルをインライン平面に変更したものの、摩擦の設定を記述しませんでした。その結果、物理エンジン(ODE)のデフォルト値であるmu 1が適用されます。修正後の床には次の1行が含まれます。
FRIC = '<surface><friction><ode><mu>100</mu><mu2>50</mu2></ode></friction></surface>' # 원본 바닥과 같게
下の図は、同じ走行ノードが2つのワールドで走行した記録です。図をクリックすると、rosbagに残った実際の軌跡を4倍速で再生します。
| 実行 | 回転あたりの実際の角度 | 角の実際の誤差 mm (1→4) | 終点位置の実際の誤差 / 向き | odomが認識した終点位置の誤差 |
|---|---|---|---|---|
| B(摩擦あり) | 89.2, 89.7, 89.7, 89.7° | 17, 23, 14, 5 | 16 mm / −0.98° | 5 mm |
| B 摩擦なし | 80.3, 79.7, 80.3, 80.9° | 10, 138, 344, 487 | 503 mm / −37.99° | 5 mm |
| C 摩擦なし | 78.0, 79.2, 80.5, 80.9° | 19, 164, 399, 533 | 549 mm / −40.24° | 4 mm |
その場での回転時に車輪がスリップし、90°ではなく約80°しか回転せず、そのずれが角ごとに蓄積しました。しかし、odomは車輪の関節角度を積分するため、スリップを検知できません。そのため、odomは最後まで「5 mm以内に戻ってきた」と報告しました。もしodomのみで結果を判定していた場合、このワールドはテストをパスしていたことになります。
正解の姿勢が正しいかどうかも個別に確認しました。実行終了後に gz model -m burger -p で問い合わせた位置を /gt_poses の最後の値と比較しました。Cの摩擦なしの実行において、 gz model は(−0.2505, 0.4883) m、向き−40.0°を返し、 /gt_poses は(−0.2505, 0.4884) m、−40.2°でした。摩擦のあるBでは、2つの値は(−0.0130, 0.0099) mで一致していました。
教訓。 シミュレーションでロボットのパフォーマンスを判定する際は、ロボットが自己推定した位置とシミュレータの正解の位置を 一緒に 記録し、判定は正解側を基準に行います。推定位置だけを見ると、物理設定の設定ミスがロボットの成功に見えてしまいます。逆に正解だけを見ると、推定器がどれほどずれているのかが分かりません。両者の差(図の赤い線)がオドメトリのドリフトであり、これが実際のロボットで位置推定器を別途用意する理由です。
7. GPUなしで動作するLiDAR
AとA2はレンダリングによって距離を測定する gpu_lidar(360ビーム、5 Hz)を使用します。PodにはGPUも /dev/dri もありませんでしたが、 --headless-rendering(EGL)で起動し、RTF 1.000を維持しました。
/scan周期は5.00 Hzであり、モデルに記述されていたupdate_rate 5のままでした。- A2において、最後の5回のスキャン1,800ビームのうち185ビームが有限の距離を示しました。これは箱を実際に認識したことを意味します。空のワールドAは0/1,800(すべて無限大)で正常です。
- レンダリングのコスト(AとBの差)は、CPU +0.18コア、anonメモリ +270 MiB、gzプロセスのRSS 156 → 515 MiBでした。
- ログにはogre2レンダリングプラグインをロードしたという行のみがあり、レンダラー名は記載されていません。GPUがないためMesaのソフトウェアレンダラーで描画されたと推測されますが、確認はできていません(未確認)。
- カメラセンサー(解像度と周期がはるかに大きい)は測定していません(未実測)。1台のLiDARが動作するからといって、カメラも動作するとは限りません。
8. CIで使用可能な最高速モード
Cは real_time_factor を0に設定したBです。1 mの正方形1周(シミュレーション時間 47.7–51.7 s)がウォールクロック時間 4.1–4.4 sで終了し、RTFは10.88–11.76でした。
- この速度は、CPU上限3コアでの計測値です(平均2.54~2.88コア)。物理1スレッドとブリッジ、ロガー、走行ノードで上限を分け合いました。コア数を変更した場合のRTFの変化は計測していません(未計測)。
- 再現性がむしろ向上しました。 Cを3回実行した際のコーナー誤差は1mm以内に収まりました(19~20、27~28、20、7~8 mm)。リアルタイムBを2回実行した際は、最後のコーナーが5mmと13mmで異なりました。 リアルタイムモードでは、ウォールクロック(壁時計時刻)に合わせる処理が実行ごとに微妙に干渉しているものと考えられます(推測)。
- 1点注意すべき点があります。Cでは
dynamic_poseがウォールクロック基準で出力されるため、正解姿勢がシミュレーション時刻ベースで約12 Hzと粗くなります。正解とodomを時刻ベースで同期させるには補間が必要です。
PRごとに「ロボットが1mの正方形を2cmの誤差以内で周回する」テストを4秒間で実行できることを意味します。ただし、その判定はセクション6のように正解姿勢を用いて行う必要があります。
9. 発生した問題
set -uを有効にしたシェルスクリプトで/opt/ros/jazzy/setup.bashを読み込むと、未定義の変数が原因で停止します。- ジョブ制御のないスクリプトにおいて
&で起動したros2 bag recordはSIGINTを無視します。SIGTERMに変更したところ、rosbag2はSIGTERMでもファイルを正常に閉じました。 ros2 runラッパーを終了してもparameter_bridgeの子プロセスが残るため、バイナリを直接実行しました。Pose_Vをtf2_msgs/TFMessageでブリッジすると、frame_id、child_frame_id、タイムスタンプが空の状態で届きます。最初の項目がロボットモデルであることはgz model -pと突合して確認し、タイムスタンプはロガーが受信した時刻に合わせました。- odomの周期はシミュレーション時刻基準で50.0 Hzとして計測しました。モデルSDFの
odom_publisher_frequencyは30ですが、乖離の原因については検証していません(未確認)。
10. 制限事項
- ノード1台、実行あたり1~2回です。誤差の分布を語るには試行回数が不足しています。
- 1種類の車輪型ロボット(burger)と平坦な床面のみを対象としています。傾斜、段差、異なる床素材は対象外です。
- キャスターの引きずり仮説(1回転あたり12~13 mm移動)はキャスターの摩擦を変更する実験で切り分け可能ですが、まだ実施していません。
- GPUノードでの比較およびカメラセンサーは未計測です。
11. 次回予告
次回は卓球ロボットに進みます。車輪の代わりにボール1個をMuJoCo上に配置し、反発と回転がシミュレーターの設定1行でどれほど影響を受けるかを計測します。 ロボットアーム自体の基礎(座標系、逆運動学)については、連載中の「卓球ロボットアームの基礎」シリーズ(目次)で取り上げています。
ティーティーの一言。 歩数だけ数えていれば、僕はいつも元の場所に戻ってきているんだ。だけど床が滑りやすいと、実際には半mほど横に立っていたりする。だからシミュレーターでは、必ず「本当の自分の位置」も一緒に記録しておくんだよ!
参考文献
すべてのリンクは2026年10月18日にアクセスし、閲覧できることを確認しました。バージョンは計測に使用したものです。
- ROS 2 Jazzy ドキュメント: https://docs.ros.org/en/jazzy/
- Gazebo Harmonic ドキュメント: https://gazebosim.org/docs/harmonic/
- ros_gz(ブリッジ、1.0.24): https://github.com/gazebosim/ros_gz
- TurtleBot3 シミュレーション(turtlebot3_gazebo 2.3.7): https://github.com/ROBOTIS-GIT/turtlebot3_simulations
- rosbag2: https://github.com/ros2/rosbag2、MCAP形式: https://mcap.dev/spec
- 公式イメージ
ros:jazzy: https://hub.docker.com/_/ros - 引用コードはすべてTT Labの実験コードです。