0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自動運転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の予測に使い、走行ログから合わせた車の応答は両方の制御に使いました。

Pure Pursuitは先の1点から舵を計算し、MPCは複数の未来から1本を選ぶ

説明用アニメーションの1コマ。左がPure Pursuit、右がサンプリング型MPCです。

1本目では、Pure Pursuitを先生役にしてLiDARモデルを学習させたE2E部門の準優勝の記録を書きました。今回はSW部門で、その先生にも使った制御の中身を説明します。

動画で見たい方はこちらです。

この記事の舞台と、数字の読み方

自動運転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手順です。

  1. レースライン上で、車より少し先に注視点を取る。
  2. いまの位置を通り、いまの向きに接し、注視点も通る円を求める。
  3. その円に沿って走るための舵を計算する。

0.1秒後には新しい位置からやり直します。

Pure Pursuitが注視点・円・舵を計算し直してS字の線を追う

速さ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の技術報告も、注視点を通る円から操舵を求める方法を説明しています。

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で割る補償は重ねません。

舵の補償とレースラインの最適化で変わったPure Pursuitのタイムと横ずれ

版(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は前の車の位置を入力していない

これは後の比較で大事な条件です。Pure Pursuitに車間制御や回避計画を組み合わせた実装まで、同じ挙動になるという意味ではありません。

2. MPC:未来を試してから決める

私が作ったのは、有限個の候補を試すサンプリング型MPCです。公式サンプルのMPCとは別の実装です。

流れは次のようになります。

MPCが候補を試走してコストを計算し、最初の0.1秒だけ実行する

前に遅い車がいる直線道路の説明用の例。右下の棒グラフは、各候補のコストです。

何通りの未来を試すのか

設定 今回の値
制御周期 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 にある最大加速度・転がり抵抗・速度に比例する抵抗の値です。最後の項が「曲がると減速する」分です。

MPCの向き・位置・操舵の遅れ・速度のモデル

提出版の式を使った例示の図です。図の減速項の $\kappa$ は曲率の大きさを表します。本文では左右の符号との区別を明確にするため、$|\kappa|$ と書きました。

予測の後半にはPure Pursuitを使う

21通りの舵を、2秒間ずっと保つわけではありません。

予測の時間 舵の決め方
0〜0.1秒 前回の指令。むだ時間を表す
0.1〜0.6秒 候補の舵を0.5秒間保つ
0.6〜2.0秒 Pure Pursuitで参照経路に戻る

MPCの予測は、前半が候補の舵、後半が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$ 予測中に進む距離。長いほどコストを下げる

横に並んで接触することを嫌う項など、細かな補正は省略しています。

MPCのコスト関数と、線・壁・前の車・舵・進行距離の各項

図の $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. 実測で比べる:単独走行と、前に遅い車がいる場面

AWSIMの単独6周と、検証台の単独・他車あり1周の比較

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本目の記事にまとめています。

次に同じことを試すなら

私なら、次の順に進めます。

  1. Pure Pursuitで単独走行し、位置・向き・注視点・操舵指令をログに残す。
  2. 操舵指令と実舵角、向きの変化を並べ、ゲインと遅れを確認する。
  3. 注視距離の弧長と、操舵式の分母に使う直線距離を区別する。
  4. MPCの候補ごとに、横ずれ・壁・他車・進行距離のコストを出す。重みを振る前に、候補間の差を見る。
  5. 単独走行と他車ありを分け、参照経路や相手の条件を記録する。検証台で良くなった変更をAWSIMでも確認する。

まずPure Pursuitで走れる状態を作ると、車の応答を測る道具にもなります。その測定結果と操舵の式は、MPCを作るときにも使えます。

参考文献・出典

公開資料の最終確認:2026年10月2日。

AWSIMの周回タイムと応答の測定値は、筆者の走行記録・開発ログに基づきます。公開資料にあるルール、実走で測った値、自作の検証台で得た値を本文で区別しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?