自動運転AIチャレンジ2026のSW部門で、同じレーシングカートを走らせる制御を、Pure PursuitとMPC(モデル予測制御)の両方で作りました。
1台だけで6周走った記録は、Pure Pursuitが246.62秒、MPCが244.21秒。差は約1%で、どちらもペナルティ0でした。一方、自作の検証台で前に遅い車を置くと、走り方が大きく変わりました。
この記事では、先の1点から舵を決めるPure Pursuitと、複数の未来を試して舵を選ぶMPCを、実際に使った式・コード・動く図で説明します。最後に、比較の条件と、そこから言えることを整理します。
先に結論です。
- 1台だけで走るなら、今回のPure PursuitでもMPCに近いタイムが出ました。2.41秒の差だけで、MPCが速いとは判断できません。
- 前に遅い車を置いた試験では、Pure Pursuitが114.7秒、MPCが53.5秒でした。ただし、Pure Pursuitは他車の位置を入力していない版です。これは今回の実装どうしの比較です。
- 自作MPCの予測の後半にもPure Pursuitを使っています。Pure Pursuitの式をMPCの予測に使い、走行ログから合わせた車の応答は両方の制御に使いました。
説明用アニメーションの1コマ。左がPure Pursuit、右がサンプリング型MPCです。
1本目では、Pure Pursuitを先生役にしてLiDARモデルを学習させたE2E部門の準優勝の記録を書きました。今回はSW部門で、その先生にも使った制御の中身を説明します。
動画で見たい方はこちらです。
- SW部門の解説「MPC vs Pure Pursuit」:約14分。Pure Pursuitの式は3:06〜、MPCの式は5:17〜、比較は7:25〜。
- 86秒のリール
この記事の舞台と、数字の読み方
自動運転AIチャレンジのSW部門は、AWSIMとAutowareを使ってレーシングカートを走らせる競技です。SIM予選は3台、SIM決勝は4台が同時に走ります。公式のSW部門説明
私は高専生1人のチーム「KSK」として出場しました。ここで紹介する制御は、0.1秒ごとに自分の位置・向き・速さを受け取り、舵と加速度を指令します。
本文では、2つの走行環境が出てきます。
| 環境 | 何を測ったか | 読むときの注意 |
|---|---|---|
| 公式シミュレータAWSIM | 実際にノードを動かした6周のタイムなど | 周回タイムは予選前半の配布版での値。SIM決勝で使われた版とは異なる |
| 自作の検証台 | 単独走行や、前に遅い車を置いた1周の比較 | 走行ログから合わせた車両モデルで動く周回シミュレータ。AWSIMでの実測とは分けて扱う |
また、MPCの式・定数・コードは主にSIM決勝に提出した版を説明します。タイム比較に使ったMPCは、それより前の版です。係数や候補の除外処理が一部違うので、掲載コードそのままのタイムとして比較値を読まないでください。
先に、使う言葉をそろえます。
| 用語 | 意味 |
|---|---|
| レースライン | 事前に用意した、走りたい線。コース1周ぶんの点の列 |
| 注視点 | レースラインの上で、車より少し先に取る目標点 |
| 曲率 $\kappa$ | 曲がり具合。半径 $R$ の円なら大きさは $1/R$ |
| 自転車モデル | 車を前輪1つ・後輪1つに見立てた動きの式 |
| コスト | 候補の悪さを数えた値。小さい候補を選ぶ |
| ホライズン | 予測する未来の長さ。今回のMPCでは2秒 |
1. Pure Pursuit:先の1点を追いかける
Pure Pursuitの操舵計算は、次の3手順です。
- レースライン上で、車より少し先に注視点を取る。
- いまの位置を通り、いまの向きに接し、注視点も通る円を求める。
- その円に沿って走るための舵を計算する。
0.1秒後には新しい位置からやり直します。
速さ6 m/sでS字の線を追う例。右に、各時点の注視点の座標、曲率、舵角を表示しています。
曲率の式は、円を1つ描けば出てくる
車から見て前を $x$、左を $y$ とし、注視点を $P=(x,y)$ とします。
原点でいまの向きに接する円の中心は、$y$ 軸上の $(0,R)$ にあります。点 $P$ が円の上にあるので、
x^2 + (y-R)^2 = R^2
\quad\Longrightarrow\quad
x^2+y^2 = 2yR
注視点までのまっすぐな距離を $d$ とすると、$d^2=x^2+y^2$。したがって、曲率は
\kappa = \frac{1}{R} = \frac{2y}{d^2}
となります。$y>0$ なら左、$y<0$ なら右に曲がり、$y=0$ なら直進です。CoulterのPure Pursuitの技術報告も、注視点を通る円から操舵を求める方法を説明しています。
左が曲率の導出。右は、分母に弧長を使った場合の計算上のずれです。
ここで私は一度間違えました。注視点を選ぶために線に沿って測った長さ $L_d$ を、そのまま分母に使っていたのです。
分母に入るのは、注視点までの直線距離です。 線に沿った長さと、直線距離は一致しません。
たとえば、半径4 mの円を、線に沿って7.25 m先の点で追うとします。分母を $L_d^2$ にすると、必要な曲率の約0.75倍になります。これは実測ではなく、$\theta=L_d/R$ とおいた $2(1-\cos\theta)/\theta^2$ から計算した値です。弦の長さ $d$ を使えば、この円の上では正しい曲率になります。
曲率から舵角へ
自転車モデルでは、ホイールベースを $L$、舵角を $\delta$ とすると、旋回半径は $R=L/\tan\delta$ です。これを曲率で書き直すと、
\delta = \arctan(\kappa L)
となります。操舵の核になる式は、この式と $\kappa=2y/d^2$ の2つです。
注視点は、次の距離を線に沿って進んだ位置に取りました。$v$ の単位はm/sです。
L_d = \mathrm{clip}(3.0+0.5v,\ 3,\ 10)\quad[\mathrm{m}]
6 m/sなら6 m先、9 m/sなら7.5 m先です。速いときほど遠くを見るようにしています。この係数は私の実装の設定値です。
式が合っていても、車がその通り曲がるとは限らない
最初の版は、カーブで外に膨らみました。舵の指令と実際の舵角を並べて調べると、実際の舵が指令の約0.699倍しか動いていませんでした。
比較したのは、指令の /control/command/control_cmd と、実際の舵角の /vehicle/status/steering_status です。走行3本の定常区間を合わせると0.673〜0.688、遅れ0.144秒ぶんをずらして合わせると0.699でした。
まず指令を0.699で割って補償しました。その後、MPCの予測モデルを調整する中で、指令から向きの変化までを表す式を走行ログに合わせ、Pure Pursuitにも取り込みました。
\dot\theta = \frac{v\tan\delta_{\mathrm{cmd}}}{1.80+0.004v^2}
つまり、操舵計算では
L(v)=1.80+0.004v^2\quad[\mathrm{m}]
を使います。制御方式も日も異なる4走行で合わせたところ、定数は1.67〜1.89、速度の係数は0.002〜0.006でした。
ここでの $L$ は車の物理的な寸法ではなく、指令から旋回までの応答を合わせた実効値です。指令の効き方も含んでいるので、この式で計算した舵をさらに0.699で割る補償は重ねません。
| 版(AWSIM・1台) | 結果 |
|---|---|
| 最初の版 | 5周まで570.8秒。6周は完走できず |
| 舵の補償あり | 6周278.19秒 |
| レースラインも最適化 | 6周246.62秒・ペナルティ0 |
図の横ずれは各構成で測った値です。最終構成の横ずれ0.74 m・復帰0回は、同じ構成の走行の値であり、246.62秒の走行と同一の記録かは確認できていません。
レースラインの最適化では、コース幅の中で線を横にずらし、全開で走ったときのラップタイムが短くなる線を探しました。曲がれる上限や、壁からの余裕も制約に入れています。
この過程では、分母を弦の長さに直すほか、Pure Pursuitでも壁からの復帰処理を呼ぶようにし、線の点の間隔をそろえて曲率の偽のとがりを減らしました。タイムの表は、これらを直してきた過程の記録です。
実装では何を計算しているか
次は、提出コードの操舵部分を記事用に整理したものです。注視点が前方にある通常走行の計算を示し、復帰時などの分岐は省略しています。注視点を取る _pt_at などは元のノードの処理を使います。$L$ の値を走行ログから合わせた構成です。
# speed: m/s、yaw: rad、px, py: 車の位置(m)
ld = float(np.clip(3.0 + 0.5 * max(speed, 0.0), 3.0, 10.0))
tx, ty = self.obs._pt_at(s_ref + ld)
# 注視点を車から見た座標に変換する
cos_y, sin_y = math.cos(yaw), math.sin(yaw)
dx, dy = tx - px, ty - py
x_local = cos_y * dx + sin_y * dy
y_local = -sin_y * dx + cos_y * dy
# 注視点までの直線距離の2乗を使う
# L_effには、指令から旋回までの応答を含めている
L_eff = 1.80 + 0.004 * speed * speed
d2 = x_local * x_local + y_local * y_local
steer_cmd = math.atan2(2.0 * L_eff * y_local, max(d2, 1e-3))
$d^2>0$ なので、atan2(2L y, d²) は $\arctan(2Ly/d^2)$ と同じ値です。出力する際は、この後で舵角の上限などを適用します。
今回のPure Pursuitは、他の車を入力していない
この版の入力は、自分の位置・向き・速さと、レースラインです。他車の位置が届くV2Xトピックは購読していません。前に車がいても、線の上の注視点を追い続けます。
これは後の比較で大事な条件です。Pure Pursuitに車間制御や回避計画を組み合わせた実装まで、同じ挙動になるという意味ではありません。
2. MPC:未来を試してから決める
私が作ったのは、有限個の候補を試すサンプリング型MPCです。公式サンプルのMPCとは別の実装です。
流れは次のようになります。
前に遅い車がいる直線道路の説明用の例。右下の棒グラフは、各候補のコストです。
何通りの未来を試すのか
| 設定 | 今回の値 |
|---|---|
| 制御周期 | 0.1秒 |
| 予測時間 | 2秒。0.1秒刻みで20ステップ |
| 舵の候補 | −30°〜+30°を3°刻み、21通り |
| 加速度の候補 | 1通り。通常は全開の1.37 m/s² |
| 横オフセット | 他車がいる場合は5通り |
| 候補数 | 21通り、他車がいると最大105通り |
このカートでは、私が試した単独走行の構成は実質いつも全開で、コーナーでは曲がる抵抗で減速していました。SIM決勝に提出した版では、前の車に付いていく場面では加速度を抑えますが、その場面でも加速度候補は1通りです。
車をどう予測するか
未来の状態を計算するには、指令した舵や加速度で車がどう動くかを表すモデルが必要です。向きと位置には、自転車モデルを使いました。
\begin{aligned}
L(v)&=1.80+0.004v^2 \\
\dot\theta&=\frac{v\tan\delta}{L(v)} \\
x_{k+1}&=x_k+v\cos\theta\,\Delta t \\
y_{k+1}&=y_k+v\sin\theta\,\Delta t
\end{aligned}
ここでの $\delta$ は、操舵指令を遅れモデルに通した状態です。$L(v)$ に指令の効き方を含めているため、センサーから返るタイヤ角そのものとは区別します。
指令から舵が動くまでのむだ時間は、予測の最初の1ステップを前回の指令にすることで扱います。その後、次の一次遅れで指令に近づけます。
\delta_{k+1}=\delta_k+
(\delta_{\mathrm{cmd}}-\delta_k)
\left(1-e^{-\Delta t/\tau}\right)
$\Delta t=0.1$ 秒、$\tau=0.11$ 秒なら、1ステップで差の約60%だけ追いつきます。周りに車がいないときは $\tau=0.24$ 秒を使います。実装には舵を切る速さの上限もあります。
速さは、全開時を例にすると次の式です。
\dot v=1.37-0.37-0.03v-0.068v^2|\kappa|
\qquad
|\kappa|=\frac{|\tan\delta|}{L(v)}
1.37・0.37・0.03は、配布環境の vehicle.yaml にある最大加速度・転がり抵抗・速度に比例する抵抗の値です。最後の項が「曲がると減速する」分です。
提出版の式を使った例示の図です。図の減速項の $\kappa$ は曲率の大きさを表します。本文では左右の符号との区別を明確にするため、$|\kappa|$ と書きました。
予測の後半にはPure Pursuitを使う
21通りの舵を、2秒間ずっと保つわけではありません。
| 予測の時間 | 舵の決め方 |
|---|---|
| 0〜0.1秒 | 前回の指令。むだ時間を表す |
| 0.1〜0.6秒 | 候補の舵を0.5秒間保つ |
| 0.6〜2.0秒 | Pure Pursuitで参照経路に戻る |
最初は予測全体で同じ舵を保っていました。しかし、予測時間を長くすると遠い将来の誤差が強く効き、私の試験では20ステップ以上で完走できなくなりました。
後半をPure Pursuitにすると、「この一手を打った後、線に戻って走れるか」を比べられます。予測した全経路をそのまま実行するのでなく、最初の一手を選ぶために、その後も予測するという使い方です。
コストで何を比べるか
候補ごとに、横ずれ、壁、前の車、舵の切り替えを減点し、進む距離を加点します。SIM決勝に提出した版の主な項をまとめると、次の式です。
\begin{aligned}
J={}&\sum_{k=1}^{20}\left[
e_k^2
+w_{\mathrm{wall}}\max(0,2.2-d_k)^2
+\min(350,60q_k s_k^2)
\right] \\
&+30(\Delta\delta)^2+200(\Delta h)^2-30\ell
\end{aligned}
| 記号・項 | 意味 |
|---|---|
| $e_k$ | 参照経路からの横ずれ。線に直交する成分で測る |
| $d_k$ | コースの壁の地図から引いた壁までの距離 |
| $w_{\mathrm{wall}}$ | 壁の重み。通常200、追い越し中300 |
| $q_k$ | 前の車との横方向の重なり。0〜1 |
| $s_k$ | 足りない車間。実装では3 mを上限にする |
| $\Delta\delta$ | 前回の指令からの舵の変化 |
| $\Delta h$ | 横オフセットの変化。他車がいるときに使う |
| $\ell$ | 予測中に進む距離。長いほどコストを下げる |
横に並んで接触することを嫌う項など、細かな補正は省略しています。
図の $o_k$ が本文の $q_k$ に相当します。図では前の車の項の上限と、横オフセットの変化の項を省略しています。
前の車との距離だけを使うと、横に並んで抜く場面まで強く減点してしまいます。そこで、前後の車間と横の重なりを分けました。「横に重なっていて、車間も足りない」未来を嫌うようにしています。
また、前の車の減点を青天井にすると、壁に近づいてでも車から離れる候補を選びやすくなり、実際に壁への接触が増えました。このため、前の車の項には1ステップあたり350の上限を設けています。
SIM決勝版では、追突する予測に入った候補を、当たらない候補が1本でもある限り選ばない処理も追加しました。後の比較に使った前の版には、この除外処理は入っていません。
計算するのは、未来の位置の列
提出版の予測部分から抜粋します。候補の舵を保つ部分と、後半にPure Pursuitへ切り替える部分、細かな補正などは省略しています。
for k in range(H): # H=20、dt=0.1
# targetを、前回の指令・候補の舵・後半のPure Pursuitから決める
# ...(省略)...
d = (target - st) * (1.0 - math.exp(-dt / _tau_eff))
# ...(舵を切る速さの上限でdを制限)...
st = st + d
kap = abs(math.tan(st)) / (
self._mpc_wheel_base + self._mpc_wb_k * v * v
)
a = (
a_cmd - self._mpc_roll * (1.0 if v > 0.05 else 0.0)
- self._mpc_drag * v - self._mpc_scrub * v * v * kap
)
# ...(補正の項を1つ省略)...
v = max(v + a * dt, 0.0)
dist += v * dt
th = th + v * math.tan(st) / (
self._mpc_wheel_base + self._mpc_wb_k * v * v
) * dt
x = x + v * math.cos(th) * dt
y = y + v * math.sin(th) * dt
# ...(この位置からコストを加算)...
実走では、同じ式をCで書いた拡張を使います。Python版と結果が一致することをテストで確認しました。手元のAWSIM実走ログでの平均計算時間は1.4〜3.6 msで、おおむね約2〜3 msです。0.1秒の制御周期に収まっていました。
3. 重みを30,000にしても、走りが変わらなかった
MPCで一番勉強になったのは、コストの重みより、予測モデルが何を区別できるかでした。
最初の予測には、「曲がると減速する」項がありませんでした。その状態で、進んだ距離の重みを変えてみました。自作の検証台の、他車がいる同じ場面での結果です。
| 進む距離の重み | タイム | 追突 |
|---|---|---|
| 0 | 45.50秒 | 0回 |
| 30 | 45.50秒 | 0回 |
| 3,000 | 45.50秒 | 0回 |
| 30,000 | 45.50秒 | 0回 |
タイムだけでなく、軌跡も完全に一致しました。
この構成では加速度候補が1通りで、予測の速度も舵に依存しません。どの候補も同じ距離を進むので、コストの $-w\ell$ は全候補から同じ値を引いているだけでした。
\underset{i}{\operatorname{argmin}}\,(J_i-C)
=
\underset{i}{\operatorname{argmin}}\,J_i
\qquad\text{(全候補で同じ }C\text{)}
そこで、予測に $-0.068v^2|\kappa|$ を入れました。大きく曲がる候補ほど減速し、進める距離にも差が生まれます。
AWSIMの7走行、全開だった21,213フレームから減速を合わせた係数は0.0670でした。検証台の車両モデルが使っていた0.068と近い値です。
予測に0.068を入れると、検証台の6種類のテスト集合のうち5つで速くなりました。たとえば実戦形の192場面では、1本あたり約1.28秒の短縮でした。
ただし、AWSIMで3台レースをA/B各5本ずつ走らせた試験では、差を測りきれませんでした。1台の並列走行を各2本ずつ行った試験では6周で約1.8秒速くなりましたが、他車がいるときの改善をAWSIMで確認できたわけではありません。
ここで学んだのは、重みを調整する前に、候補ごとの各コストを出してみることです。重みを極端に変えても選択が変わらないなら、その項が候補間に差を生んでいるかを確認します。
4. 実測で比べる:単独走行と、前に遅い車がいる場面
3つの条件の時間を示しています。AWSIMは6周、検証台は1周です。環境と周回数が違う段どうしは、直接比較しません。
AWSIMで1台だけ走ると、差は約1%
同じ日の同じAWSIMで、1台だけで6周を走らせた記録です。
| 指標 | Pure Pursuit | MPC |
|---|---|---|
| 6周の合計 | 246.62秒 | 244.21秒 |
| ペナルティ | 0回 | 0回 |
差は2.41秒、約0.98%です。
同じ構成のPure Pursuitの有効な3走行は、246.62〜264.69秒でした。1本にはペナルティが1回あり、2.41秒はこのばらつきより小さい差です。統計的に同等と確かめた比較ではありませんが、この2本だけでMPCが速いとは言えません。
さらに、自作の検証台で1台だけ1周させると、Pure Pursuitは50.8秒、MPCは53.1秒で、順番が逆になりました。1台で線を追う場面なら、今回のPure Pursuitでも十分に速く走れました。
前に遅い車を置くと、挙動が変わった
次は、自作の検証台で他車を置いた1周の試験です。条件は次のとおりです。
- 自車は停止から発進する。
- 自車の20 m先を、他車1台が3.0 m/s(時速10.8 km)で走る。
- 他車は線の上を等速で走り、回避しない。
- 追突すると、検証台では時速5 kmで10秒間の速度制限を受ける。
公式のSIMルールにも、CRASHの速度制限は時速5 km・10秒とあります。ただし、ここでの判定と結果は自作の検証台のものです。公式の速度・ペナルティの説明
| 指標 | Pure Pursuit | MPC |
|---|---|---|
| 1周のタイム | 114.7秒 | 53.5秒 |
| 追突 | 5回 | 0回 |
| 追い越し | 0回 | 1回 |
| 車の中心間の最小距離 | 1.2 m | 2.33 m |
最小距離は車体どうしのすき間ではなく、中心間の距離です。
上が自車の速さ、下がコース沿いの車間。図は発進後4.6秒からのデータで、65秒までを表示しています。
Pure Pursuitは前の車の位置を入力していないので、追いついては当たり、速度制限を受ける動きを1周で5回繰り返しました。MPCは前の車との重なりと車間をコストに入れ、14秒過ぎに横から抜きました。
この比較には、次の条件差があります。
| 比較上の条件 | 今回の内容 |
|---|---|
| 他車の入力 | Pure Pursuitは入力しない。MPCは入力する |
| 参照経路 | Pure Pursuitは最適化したレースライン、MPCは別のラインを使い、予測用には曲率を均した経路を使う |
| 他車の走る線と初期位置 | 各ノードのレースラインに基づくため、両試験で約0.3 mのずれがある |
| MPCの版 | SIM決勝版より前。予測のSCRUB係数は0で、追突候補の除外処理もまだ入っていない |
したがって、「MPCという方式はPure Pursuitより必ず2倍速い」といった結論には使えません。今回確かめたのは、他車を見ずに線を追う実装と、他車を含む未来を評価する実装で、走りがどう違ったかです。
Pure Pursuitに車間制御や回避計画を足した構成も、比較対象になりえます。一方、横に出る複数の候補を、壁や相手との距離を含めて同じコストで比べられることは、自作MPCを使う上で便利でした。
検証台の数値をAWSIMへそのまま移すこともできません。比較の頃のMPCでは、横ずれの目安が検証台で0.2 m、AWSIMで0.9 mでした。検証台は候補を絞るために使い、採否はAWSIMで確認する必要があります。
5. 私はどう使い分けたか
| 観点 | Pure Pursuit | 今回のサンプリング型MPC |
|---|---|---|
| 操舵の決め方 | 注視点を通る円を求める | 複数の未来を予測し、コストで選ぶ |
| 主な調整箇所 | 注視距離、車の応答、レースライン | 車の予測モデル、コスト、候補の作り方 |
| 他車の扱い | 今回の版にはなし。別途組み合わせる必要がある | 前後の車間と横の重なりを評価する |
| 計算 | 操舵の核は数行 | 21〜105候補を20ステップ予測する |
| 実装での役割 | 単独追従、MPCの予測の後半、E2E学習の先生 | 他車がいるレースでの操舵と横オフセットの選択 |
本番のSIM決勝にはMPCで出ました。ただし、スタート直後に止まり、約2分後に動き出して4位で敗退しました。これは配信と私の記憶によるもので、停止の原因は未確定です。この記事の周回比較と、本番の敗退原因は分けて考えています。
Pure Pursuitから始めた利点は、式が短く、指令と実際の車の応答の違いを調べやすかったことでした。MPCの調整で得た応答モデルをPure Pursuitへ戻し、Pure Pursuitの式をMPCの予測の後半へ入れました。2つの間で部品を共有できました。
E2E部門では、GPUシミュレータ上のPure Pursuitを先生にして、LiDARから舵を決めるモデルを学習させました。その結果と学習の条件は1本目の記事にまとめています。
次に同じことを試すなら
私なら、次の順に進めます。
- Pure Pursuitで単独走行し、位置・向き・注視点・操舵指令をログに残す。
- 操舵指令と実舵角、向きの変化を並べ、ゲインと遅れを確認する。
- 注視距離の弧長と、操舵式の分母に使う直線距離を区別する。
- MPCの候補ごとに、横ずれ・壁・他車・進行距離のコストを出す。重みを振る前に、候補間の差を見る。
- 単独走行と他車ありを分け、参照経路や相手の条件を記録する。検証台で良くなった変更をAWSIMでも確認する。
まずPure Pursuitで走れる状態を作ると、車の応答を測る道具にもなります。その測定結果と操舵の式は、MPCを作るときにも使えます。
参考文献・出典
公開資料の最終確認:2026年10月2日。
- R. Craig Coulter, Implementation of the Pure Pursuit Path Tracking Algorithm, Technical Report CMU-RI-TR-92-01, Robotics Institute, Carnegie Mellon University, January 1992:https://publications.ri.cmu.edu/implementation-of-the-pure-pursuit-path-tracking-algorithm
- 自動運転AIチャレンジ RacingKart公式ドキュメント・SW部門説明:https://github.com/AutomotiveAIChallenge/aichallenge-documentation-racingkart/blob/main/docs/competition/sw-class.ja.md
- SW部門の解説動画、86秒のリール
- E2E部門の準優勝の記録
AWSIMの周回タイムと応答の測定値は、筆者の走行記録・開発ログに基づきます。公開資料にあるルール、実走で測った値、自作の検証台で得た値を本文で区別しています。










