はじめに
自動運転AIチャレンジ2026のSW部門(Sim to Real)に、個人チーム bonsai で参加しました。結果は SIM予選95位 / 132チーム(学生クラスとは別枠の一般クラスでは65位 / 87)で、決勝には進めませんでした。
SW部門の予選では、シミュレータAWSIM(Unity製)上のレーシングカートを、オープンソースの自動運転ソフトウェアAutowareをベースにしたコードで走らせます。
形式は、提出したコードが他チームと自動で繰り返し対戦する 常時稼働型のマッチメイキング です。
1レースは3台同時・6周です。3台のうち2台が参加チームの車で、残る1台は運営が走らせる NPC(Non-Player Character。どの参加チームも操作しない車のことで、ダッシュボード上の表記は「Computer」)です。
勝敗でレートが増減し、9/1時点のレートで順位が決まります。
この記事では、参加して気づいたことを 環境構築・戦略・苦労したこと の3つに分けて共有します。
一番大きな気づきは、このコンペはタイムアタックではなかった ことです。
私は20日間、本番のデータを見ずにローカルの数字(タイムや完走率)を改善し続け、それが順位にほとんど結びついていなかったことに後から気づきました。
この記事で書くこと・書かないこと
- 書くこと: このコンペで 何が順位を決めていたか を、本番レースの実データで解剖した結果と、そこに至るまでの失敗
- 書かないこと: 速く走る方法や追い越しの実装(私は追い越しを最後まで実装できませんでした)。MPCのチューニングやV2Xを使った追い越しは、予選を突破したtamasyさんの予選参戦レポートが詳しいです
数値はすべて2026年のルールでの実測です。今後も汎用的に使える部分は4章にまとめました。
予選期間の順位の動きは以下の通りです。以降の章は、この図のどこで何が起きていたかの説明になっています。

図1: 予選期間の順位の推移。背景の色は期間を表し、青は前の車に近づくと減速する機構(ガバナ)を入れる前、オレンジはガバナだけの期間、緑はガバナに通過モードを足した後です(詳しくは2.4)。右は最後の2日間を拡大したものです。
1. 環境構築で気づいたこと
1.1 Apple SiliconのMacではAWSIMを実行できない
手元のMac(Apple Silicon)でAWSIMを起動すると、起動直後にクラッシュします(tramp-amd64.c のassertでSIGABRT)。AWSIMはLinux x86-64向けのビルドで、Intel向けのプログラムを変換して動かすRosettaが、AWSIM内部のMonoのJIT実行に対応していないためです。GPUの有無とは関係がなく、回避できません。
そこでLinuxのVPSを借り、ビルド・走行・評価をすべてそちらで行いました。Macはコードを読むことと、結果ファイルを見ることに使いました。以下、この自前の環境でシミュレーションを動かすことを「ローカル」、大会サーバ上の対戦を「本番」と呼びます。
1.2 GPUなしでも1台の評価は回せるが、3台同時の走行には8コアでは足りなかった
| 項目 | 内容 |
|---|---|
| VPS | ConoHa・Ubuntu 22.04・8コア・メモリ24GB |
| 料金 | 月8,576円 |
| GPU | なし |
AWSIMを -headless(画面を描画しないモード)で起動すれば、GPUなしでも1台の6周評価は普通に回ります。代わりにrviz2の3Dビューは描画できないので、走りの確認は評価後に生成される motion_analytics.html(コース俯瞰図を速度で色分けしたもの)で行いました。
誤算は、8コアでは3台同時走行をローカルで安定して再現できなかった ことです。本番は3台なので、一番知りたい「他車がいるときの挙動」を最後まで手元で測れませんでした。次回借りるなら12〜16コア以上にします。
1.3 ローカルのどの評価モードも本番と一致しない
公式が用意しているローカルの評価手段は、どれも本番と条件が違いました。make eval は、1台で6周走らせてタイムを出す標準の評価コマンドです。
make eval |
3台モード | 本番 | |
|---|---|---|---|
| 車両数 | 1 | 3 | 3 |
| NPC | なし | なし | 毎レース1台同走 |
| 車同士の衝突 | 対象外 | off | on |
特に影響が大きかったのが、本番のNPCをローカルで再現できない ことです。本番のNPCは運営が用意したAutowareの車で、参加チームの車と同じように他車の位置情報(V2X)に載ります。
ローカルのAWSIMにもNPCを出すオプション(--npcs)はありますが、出てくるのはAWSIMに組み込まれた試験的な車で、名前は同じでも本番のNPCとは別物 です。
こちらはV2Xに載りません(参加者用の公式Slackで運営が回答していました)。
そのため、V2Xで前方車を見て減速する機能は、ローカルのNPC相手には一度も働きませんでした(試せたのは、止まっている車を1台置く試験だけでした)。この差が後で大きな問題になります(2.4)。
結局、make eval は「壊れていないか」と「ペースの上限」の確認だけに使い、採否は本番のデータで判断するのが正解でした。
2. 戦略面で気づいた、このコンペの順位の決まり方
予選終了の5日前の8/27になって、大会サイトのレース詳細ページに 3台全員のラップタイムが入ったJSONが埋め込まれている ことに気づき、それまでの39レースを全部集計しました(取り方は4.1)。分かったのは以下のことです。
2.1 NPCの後ろに付くと、1周80秒失う
NPCのペースは 約146〜152秒/周 でほぼ一定で、かなりゆっくり走ります。自車は単独なら1周67〜68秒ですが、NPCの後ろに付くとNPCのペースに固定されます。
| 状態 | 自車のラップ |
|---|---|
| NPCの後ろにいるとき | 約145〜164秒 |
| NPCの前に出たとき | 67.3〜68.8秒 |
ガバナ(2.4)を入れた8/11以降、本番のベストラップはほぼずっと146〜149秒でした。「本番環境は処理が重くて全車2倍くらい遅いのだろう」と思っていましたが、単にNPCの後ろに行列を作って並んでいただけ でした。同じレースで相手が1周50秒台で走っている例を見て、ようやく気づきました。
予選全体の自車のラップを並べると、はっきり分かります。

図2: 本番レースでの自車のラップタイム(ラップが記録に残っている全レース)。色は期間を表し、青は前の車に近づくと減速する機構(ガバナ)を入れる前、オレンジは入れていた期間、緑は通過モードを足した後です(詳しくは2.4)。オレンジの期間は、ほぼすべてのラップがNPCのペース(約149秒)に張り付いています。
2.2 レースは「6周」ではなく、時間で打ち切られる
完走したラップの合計は、どのレースでも約475秒を超えませんでした(行列に並んだまま3周したレースは、450秒前後に集中)。行列ペースの約150秒/周だと、ちょうど3周で埋まる時間です。6周を走り切ったことは一度もありません。 目的は「6周を速く走る」ではなく「決まった時間の中で、相手より前にいる」ことでした。
2.3 私の勝利の3分の2は、相手が途中で走れなくなったレースだった
- 勝敗は マッチした相手チームより前でゴールしたか だけで決まります。NPCが何位かは関係ありません
- 予選を通した32勝のうち 約21勝は、相手が途中で止まったり大きく失速したりしたレース で、速く走って勝ったのは5勝だけでした
- 一方、私自身も、8/27までの39戦のうち 8戦(21%)で走行不能 になっていました(0〜2周で止まって、そのまま走れなくなる)
つまり優先順位はこうでした。
1. 走行不能にならない ← 途中で止まると勝ち筋がほぼなくなる
2. NPCの行列から抜ける ← 1周+80秒
3. 対戦回数を積む
4. ラップタイムを縮める ← 1〜3が片付くまで順位にほとんど結びつかない
私は本番データを見るまでの20日間、2番の存在に気づかず、主に4番を磨いていました。
2.4 減速するだけの安全機構は、「一番遅い車に合わせる装置」になる
行列から抜けられなかった原因は、自分で作っていました。追突ペナルティを避けるため、前の車に近づいたら減速する機構(以下「ガバナ」)を早い段階で入れていたのです。
ガバナは減速するだけで、追い越すための横方向の経路を作りません。
NPCに追いついた瞬間から、ずっとNPCのペースで走ることになります。
図2の青い点の通り、ガバナを入れる前は(おそらく接触しながら)NPCを抜いていて、68〜120秒台のラップが出ていました。
追突ペナルティは「10秒間、5km/hに制限」ですが、行列は「1周+80秒」です。小さいペナルティを避けて、桁違いに大きい損を取っていました。
しかも公式の仕様では、追突ペナルティは追突した後ろの車にだけ課されます(運営が共有したAWSIMのソースにも「追突した側だけを罰する」とあります)。抜こうとして接触したときのリスクは抜く側が負いますが、それでも行列に並び続ける損よりはずっと小さいものでした。
なお、1位を走る車には最高速度が25km/h付近に抑えられる仕様(ハンディキャップ)もあり、先頭に出れば楽に逃げ切れるわけでもありません。
1.3の通り、ローカルのNPC相手ではガバナが働かないので、手元では気づけませんでした。
対策として、前方の車がNPCくらい遅いとき(2.6 m/s未満)はガバナを止めて自分の計画どおり走る「通過モード」を入れました。
スタート直後の密集で前の車に突っ込まないよう、レース開始から一定時間は通過モードを封印するガードも付けました。
ここで面白いことが起きました。ガードを200秒にしていたときの本番ラップは、1〜2周目が152 / 150秒(行列)、3〜4周目が89 / 83秒で、ガードの解除点がラップタイムにそのまま表れていました。
ガードを 200秒 → 90秒に縮める設定1行の変更 で、3周目にはローカル単独と同じ約68秒が出るようになり(図3)、本番8戦6勝、98位 → 73位 と、予選で最も大きく順位を上げた変更になりました。
速度系のパラメータは一切触っていません。
ただし、6勝の内、速さで相手を上回って勝ったのは2戦で、残りは相手が0〜1周で止まったレースです。

図3: ガード90秒では、1周目は行列、2周目で抜け始め、3周目にはローカル単独と同じ約68秒が4レースで再現しました。ガード200秒では、抜けるのが1周遅れます。4周目に再び遅くなったのは、おそらくまた前の車に追いついたためです。
2.5 提出は多ければいいわけではない
常時稼働型なので「たくさん出して対戦を積めばいい」と思いがちですが、実測ではそうなりませんでした。
| 日 | 提出 | 戦績 | 順位 |
|---|---|---|---|
| 8/31 | 4回 | 8勝3敗 | 110 → 73 |
| 9/1 | 10回(上限) | 3勝8敗 | 73 → 95 |
ただし、比べているのは2日分だけで、走らせたコードも時間帯も違うので、「提出が多いと負ける」とまでは言えません。言えるのは以下の2つです。
- 勝率が約42%を下回るコードは、提出するほど順位が下がる。 最終2日の実測では、1勝で平均 +9.4位、1敗で平均 −6.7位で、ここから逆算した損益分岐の勝率が約42%です。何もしないときの下落は1日あたり約3位なので、勝率が分岐点を大きく下回るコードを出し続けると、何もしないより速く順位が落ちます
- 短時間にコードを次々差し替えると、何が悪かったのか分からなくなる。 最終日の深夜は、0周対策の修正を次々提出しながら0勝5敗でした。どの修正に効果があったのか、負けのうちどこまでが時間帯の影響だったのかを、後から区別できませんでした。1つの時間帯には1つのコードだけを走らせるべきでした
3. 苦労したこと
3.1 ローカルで25秒速くしたコードが本番で大敗した
2章に気づく前、加速度の上限を上げてローカルの6周合計を 409.6秒 → 384.6秒(−25秒) に縮めたコードを提出しました。
結果は3台中3位で、レートは −20でした。横方向の加速度の上限も上げたさらに攻めた設定は、ローカルで17本すべて完走していましたが、本番では走行0周でした。
当時は、「本番はCPU負荷が高いから攻めた設定が破綻するのでは」と考えていましたが、見当外れでした。
本番のラップは行列ペースに固定されていて、自分の速さが勝敗に入る経路がそもそもなかったのです。
本番の一次データ(3台分のラップ)を見ずにローカルの数字を最適化していた のが根本の失敗です。レースJSONを1回取れば、2週間抱えていた仮説はすぐ否定できました。
3.2 サンプルMPCの土台にバグがあった
ローカル評価で、2〜3本に1本が同じS字で壁に当たって止まっていました。
調べると、原因は公式サンプルのMPC(モデル予測制御。少し先までの走りを毎回最適化して、ハンドルと速度を決める手法)の3か所にありました。
平たく言うと、以下の3つになります。
- ハンドルを切る速さの制限が、意図の約8倍厳しかった ので、S字で曲がりきれず毎周外に膨らんでいた
- 最適化の計算を毎回ゼロからやり直していた ので、反復回数の上限までに解き終わらないことがあった
- 解き終わっていない途中の答えで、そのままハンドルを切っていた ので、上限を大きく超える舵角を含む答えが使われていた
MPCを触っている方向けに、詳細を表にまとめます。
| 問題 | 内容 | 対策 |
|---|---|---|
| ステアリングレート制約が約8倍きつい | 予測ステップは空間刻み(約1m)なのに、1ステップの時間を制御周期0.025秒で計算していた。実際は1m ÷ 速度 ≈ 0.2秒。S字が要求する曲率変化の1/5しか計画できず、毎周3.9m外に膨らんでいた | ステップごとの実経過時間で制約を計算 |
| OSQPが毎回ゼロから解いている | 毎サイクル osqp.OSQP() を作り直していて、平均696反復、1.0%が反復上限に到達 |
前回解でウォームスタート |
| 解けていない解を舵に使っている | 反復上限で打ち切られた解をそのまま使い、上限32°の舵角に対して最大81.6°の解が出ていた | ステータスがsolved / solved inaccurate以外なら前回の指令を保持 |
詳しく:バグを突き止めた経緯とコード(MPCを触っている方向け)
解けていない解が原因だと分かった経緯
評価1本(約17,600回の求解)でソルバの状態を数えると、平均反復回数は695.9回で、反復上限(4000回)に達したのは173回(1.0%)でした。
一方、舵角の上限(32°)を破った解は168回で、反復上限に達した回数とほぼ一致しました。
「上限で打ち切られた解が、そのまま舵角の計算に使われている」と考えると、両方の数字が説明できます。
ウォームスタート
サンプルは毎サイクル osqp.OSQP() を作り直していたので、前回の解(primalの x とdualの y)を保存し、setup() の直後に渡すようにしました。
制約もコスト関数も変えていません。
self.optimizer = osqp.OSQP()
self.optimizer.setup(P=P, q=q, A=A_full, l=l, u=u, verbose=False)
# 追加: 問題の形が前回と同じなら、前回の解から解き始める
if (self._prev_x is not None and self._prev_x.shape[0] == P.shape[0]
and self._prev_y is not None and self._prev_y.shape[0] == A_full.shape[0]):
self.optimizer.warm_start(x=self._prev_x, y=self._prev_y)
# 解いた後: 次のサイクルのために保存する
self._prev_x = np.array(dec.x, copy=True)
self._prev_y = np.array(dec.y, copy=True)
平均反復回数は695.9回から365.9回に減りました。
なお、大会公式リポジトリの高速化PR(#252)では、OSQPを作り直さずに使い回す方式で前サイクルの解からウォームスタートすると、約200サイクルで発散することが報告されています。
私の方式(毎サイクル作り直してから前回の解を渡す)では、評価を10本以上走らせた範囲で問題は見えていませんが、取り入れる場合はソルバの状態の監視とセットにするのが安全です。
解けていない解を使わない
ウォームスタート後も約1%のサイクルで反復上限に達し、最大81.6°の舵角を含む解が出ていたので、ステータスを見て捨てるようにしました。
status = str(dec.info.status)
if status not in ("solved", "solved inaccurate"):
return self._last_u # 前のサイクルで採用した指令を保持する(25ms据え置き)
solved inaccurate は採用しています。舵角の上限違反の回数と反復上限に達した回数がほぼ一致していたので、inaccurateの解は実質的に上限を破っていないと判断しました。
反復上限を上げる対処は取りませんでした。1回の求解は平均3.35ms(setup 1.16ms + solve 2.19ms)で、1反復あたり約6µsです。4000反復に達したサイクルはそれだけで約24msかかり、制御周期の25msをほぼ使い切ってしまうためです。
ステアリングレート制約
このMPCは、予測ステップを時間ではなく距離(隣り合うwaypointの間隔、約1m)で刻んでいます。
ところが舵角の変化量の制約は、1ステップの時間を制御周期(1/40秒)として計算していました。
# 修正前: どのステップも 0.025 秒として扱う
per_step = max_steering_rate * Ts
# 修正後: ステップごとの実経過時間(距離 / 目標速度)を使う
dt = np.clip(delta_s / max(v_ref, 0.5), Ts, 0.25)
per_step = max_steering_rate * dt
時速18km(約5m/s)なら1ステップは約0.2秒なので、制約は約8倍きつくなっていました。
ホライズン全体(19区間)で計画できる曲率の変化は0.101 1/mしかなく、コースで一番きついS字が要求する0.539 1/mに対して5倍足りませんでした。
なお、アクチュエータが実際に出せる舵角速度は、制御出力側の別のクリップで守られているので、計画側の制約を緩めても物理的な限界を超える指令は出ません。
このうちステアリングレート制約の問題は、大会公式リポジトリにissue #367として報告しました(残りの2件は、既存のPR #325・#252で議論されています)。
直す前はローカル評価の3本中2本が壁で止まっていましたが、3か所を直した後は14本すべて壁に当たらず完走し、本番でもこの時期に予選の最高順位(56位)を記録しました。
ここでの学びは、土台にバグがある間のチューニング結果は信用できない ことです。
実際、加速度上限を上げる変更は、土台を直す前は「0/3完走」で棄却していましたが、直した後に同じ変更を試すと −25秒の改善になりました。
3.3 答えは参加者用の公式Slackに先に書かれていた
この大会には、参加者全員が入れる公式のSlackワークスペースがあり、運営からの連絡や、質問チャンネルでの質疑応答が行われていました。
後から振り返ると、私が苦労して突き止めたことの答えが、既に書かれていました。
時系列で並べると以下の通りです。
| 日付 | 公式Slack | 私 |
|---|---|---|
| 8/5 | 他チームの方が、ギアをRに入れて後退させる手順を投稿 | (まだ読んでいない) |
| 8/17 | 本番のautoware.logを提出の詳細から取得できる、という投稿 | (まだ読んでいない) |
| 8/20 | スタック復帰を試作するが、車が後退しない。「後退指令を送る経路がない」と判断して諦める | |
| 8/26 | 過去の投稿をまとめて読み、8/5の手順に気づく。8/17の投稿は見落とす | |
| 8/27 | 8/5の手順どおりにスタック復帰を作り直す | |
| 9/1 02:45 | 深夜に0周が続き、ようやくautoware.logを取得する |
順を追って説明します。
① スタック復帰を諦めた(8/20)
壁や他車に引っかかって止まったときに、ギアをRに入れて少し下がり、再発進する「スタック復帰」を作ろうとしました。ところが、後退させようとしても車は1ミリも動きませんでした。そこでコードを読み、「後退指令を送る経路がない」と判断して諦めました。
② Slackに手順が書かれていた(8/26〜8/27)
6日後、公式Slackの過去の投稿をまとめて読んだところ、私が諦める15日前(8/5)に、他チームの方が正しい手順を共有してくださっていました。要点は 「ギアの切り替え中は加速度指令を厳密に0.0にする」 ことで、0以外の指令が混ざるとAWSIMがギア変更を受け付けません。翌日この手順で作り直すと、本番でのギアの切り替えは、記録に残る21回すべて成功しました。
③ それでも0周が続いた(9/1深夜)
ただし、下がる距離が足りずに抜け出せないケースが残っていて、最終日の深夜には0周が続きました。原因を調べるには本番のautoware.log(自車の内部ログ)が必要で、ここで初めて取得しました。締切の約9時間前です。実はその取り方も8/17の公式Slackに書かれていて、8/26に読んだ投稿の中に含まれていましたが、見落としていました。
④ ログで原因を切り分けた(9/1未明)
ログを見ると、0周には2種類あると分かりました。1つは、他車と押し合っていて後退する距離が足りなかったケース。もう1つは、周りに車がいないのに壁に斜めに刺さっていたケースです。前者には後退する時間と距離を延ばし(2.0秒→4.5秒、1.0m→2.5m)、後者には、下がっても抜け出せなかったときに、次の後退で舵を左右交互に振るようにして対処しました。
コードだけを読んで「できない」と結論する前に、Slackを検索するべきでした。 一度読んだだけでは見落とすので、困ったときにキーワードで検索し直すことも大事でした。
詳しく:スタック復帰の作り方(AWSIMでギアを切り替えて後退する)
状態遷移
止まってから走り出すまでを、以下の状態遷移で制御しました。ギアを切り替えている間は、加速度の指令を0に固定します。
AWSIMのギア切り替えの癖
- ギアは
/control/command/gear_cmd(GearCommand)に送り、/vehicle/status/gear_status(GearReport)で切り替わったことを確認します。値はDRIVE=2、NEUTRAL=1、REVERSE=20、PARK=22です - ギアの切り替えが確定するまで、加速度の指令を厳密に0.0にする必要があります。 0以外の指令が混ざると、AWSIMは要求を拒否して1秒以内にPARKへ戻し、その後10秒間はRに入らなくなります(他チームの方が公式Slackで共有してくださった観察です)
- 加速度にローパスフィルタをかけている場合は、フィルタに残った値も含めて0にします
- 共有された手順に合わせて、NとRの間に「Nで1秒待つ」を入れています
後退中は加速度を直接与える
通常の制御では、目標速度と今の速度の差に比例して加速度を出しています(acc = KP * (u - v))。
Rに入れた後に目標速度を負にしても、この値は負になり、ブレーキとして扱われて車はほとんど動きませんでした(1回の後退で1〜2cm)。
そこで後退中だけは、加速度を0.7m/s²と直接与えています。
発動条件と空振り対策
- 速度0.3m/s未満が3秒続き、かつ走行ラインから1.2m以上外れているときだけ発動します。ライン上で止まっている場合(スタート待ちや減速中)は発動しません
- 一度2m/sを超えて走り出すまでは発動しません。スタート前のグリッドで誤って作動しないようにするためです
- 後退しても抜け出せずにまた止まった場合(ラインからのずれが0.3m以上改善していない場合)は、次に発動するまでの待ち時間を5秒から倍々に延ばし(上限60秒)、次の後退では舵を左右交互に0.35rad振って車体の向きを変えます
- Rのまま通常の制御に戻すのが一番危ないので、Dに入ったことを確認できるまでN→Dを繰り返します
動作確認のしかた
本番と同じスタックをローカルで再現するのは難しいので、「走行開始から指定した秒数で一度だけ強制的に発動する」設定を用意しました。1台だけの走行で、すべての状態遷移を確認できます。本番では必ず無効に戻します。
4. 今後も汎用的に使えること
NPCのペースや時間の上限は、次回は変わるかもしれません。
ここでは、そうしたルールの変化に左右されないことをまとめます。
4.1 本番の一次データを初日から集める
この記事の発見は、すべて本番のデータから出ています。
大会ダッシュボードの提出詳細ページ(各レースのページ)には、以下の3つのダウンロードボタンがあります(2026年10月に確認)。
| ボタン | 取れるもの |
|---|---|
| Download Result Summary | 3台分の順位・周回数・全ラップ(result-summary.json) |
| Download Autoware Log | 提出したチームの車のAutowareの実行ログ(autoware.log) |
| Download Rosbag | 提出したチームの車の走行記録(.mcap) |
他チームから挑まれたレースのページは相手の提出ページなので、ログと走行記録は相手の車のものになります。
3台分のラップを一覧にするコード
提出詳細ページで開発者ツールのコンソールを開き、以下を貼り付けて実行すると、3台分のラップが表で出ます。
const link = [...document.querySelectorAll('a')].find(a => a.href.endsWith('/result-summary.json'));
const summary = await (await fetch(link.href)).json();
console.table(summary.vehicles.map(v => ({
車両: v.vehicle_name, // GoKart1 = 対戦を挑んだ側、GoKart2 = 挑まれた側、GoKart3 = NPC
順位: v.final_position,
周回: v.lap_count,
ラップ: v.laps.map(t => t.toFixed(1)).join(' / '),
})));
予選の最終戦で実行した結果は以下の通りです(この戦は私が挑んだ側なので、自車はGoKart1)。
| 車両 | 順位 | 周回 | ラップ(秒) |
|---|---|---|---|
| GoKart3(NPC) | 1 | 3 | 149.4 / 147.1 / 149.0 |
| GoKart1(自車) | 2 | 3 | 158.7 / 233.6 / 67.3 |
| GoKart2(相手) | 3 | 3 | 158.6 / 251.4 / 57.4 |
NPCがほぼ一定の149秒前後で走り、自車と相手は2周目にそろって大きく遅れた後(おそらく接触)、3周目に抜け出しているのが読み取れます。
1レース分でも、ダッシュボードの「順位」と「ベストラップ」だけでは見えないことが分かります。
罠: トップレベルの laps や min_time はGoKart1のコピーです。他チームから挑まれたレースでは 相手のデータ になるので、3台分は必ず vehicles[] から読んでください(GoKartの対応は、予選のレース記録と突き合わせて確認)。
autoware.logで、本番で走ったコードを照合できる
ログの冒頭にサンプルMPCが、config.yaml の全文を出力するので、「本当にそのコードが本番で走ったか」をログだけで照合できます。提出物の作り直し忘れや取り違えもここで分かります。
4.2 改善が本番の順位に届く経路を、先に確かめる
3.1の失敗は、次の線が途中で切れていたのに気づかなかったことです。
何かを改善する前に、この線を1本書いて、各矢印が本番データで確かめられるか を見ておけば、20日を失わずに済みました。あわせて、以下の2つも有効だと考えています。
- 仮説には「これが偽なら何が観測されるか」を書き添える。 「本番は全車2倍遅い」なら「同じレースで相手と自分のラップ比が大きくずれることはない」はずで、レース結果を1回見れば否定できました
- 棄却した理由が「実測」か「コードを読んだだけ」か「推測」かを記録する。 3.3の「後退指令を送る経路がない」は、コードを読んだだけの判断でした。読解や推測で棄却したものは、後で見直す対象として残しておきます
4.3 計測の小技と落とし穴
開発中に役立った小技と、はまった落とし穴を4つ紹介します。
NPCのラップタイムを「環境が正常か」の物差しにする
NPCは毎レース同じコードで走るので、正常なレースでは1周146〜152秒でほぼ一定です。
ところが、NPC自身が1周250秒を超えたレースが3回あり(285秒、301.6秒、309.9秒)、そのレースでは自車も相手もまともに走れていませんでした。
NPCのラップを見れば、負けたのが自分のコードのせいか、レース環境の異常のせいかをある程度切り分けられます。
ただし、環境のせいにして調べるのをやめてはいけません。最終日の深夜に0周で終わった3レースのうち、環境の異常だったのは1レースだけで、残りの2レースは自分のコードの問題でした。
原因の切り分けの決め手は、ログの1行だった
最終日の0周には2種類ありました(3.3)。
ラインからのずれやスタックした時刻も2つのグループに分かれていましたが、原因を直接示していたのは「前方に車を検出した」というログが、止まる直前まで出ていたかどうかでした。
1つ目は止まる4.6秒前まで前方の車を検出していて、他車と押し合っていたと分かりました。2つ目は最後の検出が106秒前で、周りに車がいないのに壁に刺さっていたと分かりました。
復旧処理のログには、「そのとき周りに何がいたか」を1行入れておくと、後から原因を追えます。
grepの角括弧で、数え間違えた
ログの行数を数えるのに grep -c "[governor]" と書いたところ、952行中951行にマッチしました。
角括弧が正規表現の文字クラス(g, o, v, e, r, nのどれか1文字)として解釈されたためです。
grep -cF "[governor]"(固定文字列として検索)か、grep -c "\[governor\]" と書く必要がありました。
数えた結果を判断の根拠にするときは、使ったパターンも一緒に記録しておくと安心です。
「遅く走れば安全」とは限らず、判定には20本必要だった
他車がいる条件(ローカルでNPCを2台出すモード)で、設定ごとに途中で止まらずに完走できた割合を測りました。
| 設定 | 加速度の上限 | 横加速度の上限 | 本数 | 完走 |
|---|---|---|---|---|
| 基準 | 0.70m/s² | 6.5m/s² | 16 | 4本(25%) |
| 攻め | 0.85m/s² | 7.0m/s² | 5 | 1本(20%) |
| 守り | 0.60m/s² | 6.0m/s² | 10 | 1本(10%) |
遅くした「守り」の設定がむしろ一番悪く、「遅くすれば安全」という直感は支持されませんでした(ただし、統計的に有意な差ではありません)。
同じ設定でも、最初の6本では平均4.0周、次の10本では平均2.1周と、走れる周回数が大きくばらつきました。
結局、この条件で優劣を判定するには設定ごとに20本程度が必要でした。1本約11分なので、1つの設定に約4時間かかります。
5. 次回参加時にやること
| やること | 今回やった日 |
|---|---|
| Linux x86-64、12〜16コア以上の環境を用意する | エントリー直後(8コアで不足) |
| 公式Slackを全部読む | 8/26 |
| 本番のレース結果(3台分の全ラップ)を集める仕組みを作る | 8/27(予選終了の5日前) |
| 本番のautoware.logを取得して読む | 9/1 02:45(締切の9時間前) |
| サンプルMPCのソルバ周りを直す | 8/12〜13 |
環境の用意以外は、どれもやろうと決めた日に半日で終わったものです。遅れただけでした。
技術的には、今回唯一作れなかった 「NPCを横から抜く追い越し」 から始めます。減速だけでは行列から抜けられないことが、今回一番よく分かったことだからです。今の時点での案は以下の通りです。
- サンプルMPCは、走行可能な幅(回廊)の中央を横位置の目標にしています。通過モードのときだけ、この目標を左右どちらかに 0.3〜0.6mずらす(回廊からはみ出さないようにクリップする)
- この目標は最適化の「目的」(コスト関数)にしか入らず「守るべき条件」(制約)には触れないので、ずらしても最適化が解けなくなる心配はありません
- 最初にやるのは実装ではなく 試験環境づくり です。ローカルのNPCはV2Xに載らないので、走行ライン上をゆっくり走る車をV2Xに載る形で出せるモードを先に作ります
おわりに
決勝には届きませんでしたが、「レースに勝つとは何か」を実データで理解できたのは大きな収穫でした。
運営の皆さま、公式Slackで知見を共有してくださった参加者の皆さま、ありがとうございました。