Pythonによる交通シミュレーション:Intelligent Driver Model (IDM) による挙動の物理モデル化

Python
この記事は約50分で読めます。

こんにちは、JS2IIUです。
これまでの連載では、基本的な交通シミュレーション環境の構築から始まり、マルチエージェント化、そしてFinite State Machine(FSM)を用いた状態遷移による行動制御までを解説してきました。

前回までの実装で、車は「前の車がいなければ進む」「近づいたら止まる」という基本的な判断ができるようになりました。しかし、シミュレーション画面を眺めていて、どこか「ロボット的」な不自然さを感じなかったでしょうか?

「前の車がいなくなった瞬間に最高速度で急発進する」
「前の車に追いついた瞬間にピタッと急停止する」

現実のドライバーはもっと滑らかに運転しますよね。今回は、この「カクカクした動き」を卒業し、人間のような滑らかな加減速を実現するための技術を紹介します。

テーマは、Intelligent Driver Model (IDM) の実装です。

単純なif文の条件分岐(ロジック)から、数式を用いた物理モデル(物理挙動)へとステップアップすることで、シミュレーションのリアリティは劇的に向上します。今回もよろしくお願いします。

1. はじめに:なぜ「物理モデル」が必要なのか

前回のFSM(有限オートマトン)のアプローチでは、以下のような離散的なロジックで速度を決めていました。

  • 状態が STOP なら速度 = 0
  • 状態が DRIVE なら速度 = Max

これはゲームのNPC(Non-Player Character)の簡易的な制御としては十分ですが、「渋滞が自然発生する様子」や「事故のリスク分析」といった高度な交通シミュレーションを行うには力不足です。現実の交通現象は、ドライバーが周囲の状況を感じ取り、アクセルとブレーキをアナログに調整することで生まれる非線形(Non-linear)な相互作用だからです。

そこで登場するのが 追従モデル(Car-Following Model) です。これは「前走車との関係性(距離や速度差)」を入力として、ドライバーが次にどうアクセル/ブレーキを踏むか(加速度)を出力する数理モデルの総称です。

今回はその中でも、交通工学の分野で標準的に使われ、かつ実装が比較的容易な IDM (Intelligent Driver Model) を採用します。

2. Intelligent Driver Model (IDM) の仕組み

IDMは一見難しそうな数式に見えますが、コンセプトは非常にシンプルです。ドライバーの頭の中にある「2つの相反する欲求」をバランスさせているだけだからです。

  1. フリー走行項(アクセル): 邪魔な車がいなければ、自分の希望速度(Desired Speed)まで加速したい。
  2. 相互作用項(ブレーキ): 前の車に近づきすぎたら、安全な距離を保つために減速したい。

IDMの基本式

IDMによって算出される加速度 $\dot{v}$ は以下の式で表されます。

$$ \dot{v} = a \left[ 1 – \left( \frac{v}{v_0} \right)^\delta – \left( \frac{s^*(v, \Delta v)}{s} \right)^2 \right] $$

少し複雑に見えますが、Pythonコードに直すときはパーツごとに分解すれば簡単です。まずはパラメータの意味を理解しましょう。

重要なパラメータ

シミュレーションの挙動を調整するために、以下の変数が重要になります。

  • \(v_0\) (希望速度): 道が空いている時にドライバーが出したい目標速度。
  • \(T\) (車頭時間 / Time Headway): 前の車と「何秒分の距離」を空けたいか。例えば $T=1.5$ 秒なら、時速100km走行時は約42mの車間を保とうとします。これが性格(慎重派かイケイケ派か)を決めます。
  • \(s_0\) (最小車間距離): 渋滞で完全に停止した時でも確保する最低限の隙間(2mなど)。
  • \(a\) (最大加速度): フルアクセル時の加速度。
  • \(b\) (快適減速度): 通常のブレーキ時の減速度。

このモデルの優れている点は、「遠くでは滑らかに加速し、前の車に近づくと徐々にブレーキを強め、最終的に安全な車間距離で速度を合わせる」という一連の動作を、たった一つの数式で表現できる点にあります。

3. 実装編:IDMをPythonコードに落とし込む

では、実際にPythonで実装してみましょう。
ここでは、科学計算ライブラリの numpy を使用しますが、基本的な演算のみなので標準ライブラリの math でも代用可能です。

IDM加速度計算関数

まず、現在の自分の状態と前の車の状態を受け取り、次の瞬間の「加速度」を返す関数を作成します。

Python
import numpy as np
import matplotlib.pyplot as plt

def calculate_idm_accel(v, v_lead, distance, params):
    """
    IDM (Intelligent Driver Model) に基づく加速度を計算する関数

    Args:
        v (float): 自分の現在の速度 (m/s)
        v_lead (float): 前走車の速度 (m/s)
        distance (float): 前走車との車間距離 (m) ※バンパー間の純粋な隙間
        params (dict): IDMのパラメータ辞書

    Returns:
        float: 計算された加速度 (m/s^2)
    """
    # パラメータの展開(可読性のため)
    v0 = params['v0']      # 希望速度
    T = params['T']        # 安全車頭時間
    s0 = params['s0']      # 停止時最小車間距離
    a = params['a']        # 最大加速度
    b = params['b']        # 快適減速度
    delta = params['delta'] # 加速指数(通常は4)

    # 1. フリー走行項: (v / v0)^delta
    # 速度が希望速度に近づくほど、加速力は弱まる
    free_road_term = (v / v0) ** delta

    # 2. 相互作用項(ブレーキ項)の計算

    # 相対速度 (自分の速度 - 相手の速度)
    # 正の値なら「近づいている」、負の値なら「離れている」
    delta_v = v - v_lead

    # "希望車間距離" (Desired Gap) s* の計算
    # s* = s0 + (v * T) + (v * delta_v) / (2 * sqrt(a * b))
    # 第3項が「相手より速い場合にブレーキを強める」ロジックの核心です
    s_star = s0 + (v * T) + (v * delta_v) / (2 * np.sqrt(a * b))

    # 実際の距離との比率の二乗
    interaction_term = (s_star / distance) ** 2

    # 最終的な加速度
    accel = a * (1.0 - free_road_term - interaction_term)

    return accel

数値積分(オイラー法)

加速度が求まったら、それを使って速度と位置を更新する必要があります。
物理シミュレーションでは、時間を短い刻み(\(dt\))に区切り、少しずつ状態を進める数値積分(Numerical Integration)を行います。今回は最も基本的で実装が簡単なオイラー法(Euler Method)を使います。

$$ v_{t+1} = v_{t} + a \times dt $$
$$ x_{t+1} = x_{t} + v_{t} \times dt $$

4. 実験と検証:グラフで見る「滑らかな追従」

作成した関数が正しく動作するか、シンプルなシナリオで実験します。
「遅いトラック(リーダー)に、速い乗用車(フォロワー)が追いつく」という状況をシミュレートし、Matplotlibで挙動をグラフ化してみましょう。

実験用コード

Python
# --- パラメータ設定 ---
# 単位はメートル(m)と秒(s)
params = {
    'v0': 30.0,    # 希望速度: 30m/s (約108km/h)
    'T': 1.5,      # 車頭時間: 1.5秒空けたい
    's0': 2.0,     # 最低車間距離: 2m
    'a': 1.0,      # 加速性能: 1.0 m/s^2
    'b': 1.5,      # ブレーキ性能: 1.5 m/s^2
    'delta': 4.0   # 加速指数
}

# --- シミュレーション初期設定 ---
dt = 0.1          # 時間刻み (0.1秒)
max_time = 60.0   # シミュレーション時間 (秒)
steps = int(max_time / dt)

# 車両1(リーダー):前を走る車
x1 = 200.0        # かなり前方に配置
v1 = 10.0         # 10m/s (36km/h) で等速走行する遅い車

# 車両2(フォロワー):後ろから追い上げる車
x2 = 0.0          # スタート地点
v2 = 25.0         # 25m/s (90km/h) で勢いよく走っている

# 履歴記録用リスト
time_history = []
v2_history = []
dist_history = []

# --- メインループ ---
current_time = 0.0

for step in range(steps):
    # 1. 現時点での車間距離を計算 (リーダー位置 - フォロワー位置 - 車両長)
    # ここでは車両長さを5mと仮定
    car_length = 5.0
    distance = x1 - x2 - car_length

    # 安全のため、距離がゼロ以下の場合は計算しない(衝突処理は今回は割愛)
    if distance <= 0.1:
        distance = 0.1 

    # 2. IDMでフォロワーの加速度を計算
    acc = calculate_idm_accel(v2, v1, distance, params)

    # 3. オイラー法で状態更新
    # 速度の更新
    v2 += acc * dt
    if v2 < 0: v2 = 0  # バックはしない

    # 位置の更新
    x2 += v2 * dt

    # リーダーは等速直線運動(加速度0と仮定)
    x1 += v1 * dt

    # 4. 履歴の保存
    time_history.append(current_time)
    v2_history.append(v2)
    dist_history.append(distance)
    current_time += dt

# --- グラフ描画 ---
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

# 速度の変化
ax1.plot(time_history, v2_history, label='Follower Speed', color='blue', linewidth=2)
ax1.axhline(y=v1, color='red', linestyle='--', label='Leader Speed (Target)')
ax1.set_title('Speed Profile: Smooth Deceleration', fontsize=14)
ax1.set_ylabel('Speed (m/s)', fontsize=12)
ax1.legend(fontsize=12)
ax1.grid(True, alpha=0.5)

# 車間距離の変化
ax2.plot(time_history, dist_history, label='Gap to Leader', color='green', linewidth=2)
# 理論上の安全車間距離 (均衡状態) = s0 + v1 * T
safe_gap = params['s0'] + v1 * params['T']
ax2.axhline(y=safe_gap, color='orange', linestyle='--', label=f'Desired Gap ({safe_gap}m)')

ax2.set_title('Gap Profile: Converging to Safe Distance', fontsize=14)
ax2.set_xlabel('Time (s)', fontsize=12)
ax2.set_ylabel('Gap (m)', fontsize=12)
ax2.legend(fontsize=12)
ax2.grid(True, alpha=0.5)

plt.tight_layout()
plt.show()

結果の分析

このコードを実行すると、2つのグラフが表示されます。

  1. Speed Profile (速度変化):
    青い線(フォロワー)は、最初は25m/sで走っていますが、赤い破線(リーダーの速度10m/s)に近づくにつれて緩やかに速度を落としていきます。FSMのような「急ブレーキ」ではなく、曲線を描いて収束していく様子が見て取れます。これがIDMの非線形な特性です。
  2. Gap Profile (車間距離):
    緑の線(車間距離)は、最初は急速に縮まりますが、ある一定の距離でピタッと安定します。この安定した距離は、パラメータで設定した T(車頭時間)に基づいた「ドライバーが快適と感じる距離」です。

このように、IDMを使うと「衝突せず、かつ離れすぎず、適切な距離を保って追従する」という高度な制御が、if文を一切書かずに数式だけで実現できるのです。

5. 既存システムへの統合方法

前回の記事で作成した Agent クラスにこの機能を組み込むのは簡単です。
update メソッドの中で、固定値で加減速させていた部分を calculate_idm_accel 関数の呼び出しに置き換えるだけです。

Python
class Agent:
    def __init__(self, x, v, params):
        self.x = x
        self.v = v
        self.params = params  # IDM用パラメータを持たせる

    def update(self, dt, leader_agent=None):
        if leader_agent:
            # 前に車がいる場合:車間距離を計算してIDM
            distance = leader_agent.x - self.x - 5.0 # 車両長
            acc = calculate_idm_accel(self.v, leader_agent.v, distance, self.params)
        else:
            # 前に車がいない場合:仮想的に遠くの車がいるか、
            # もしくはフリー走行項のみを使って計算
            # (distanceを非常に大きくすればフリー走行と同じになります)
            acc = calculate_idm_accel(self.v, 0, 10000.0, self.params)

        # 状態更新
        self.v += acc * dt
        if self.v < 0: self.v = 0
        self.x += self.v * dt

これにより、すべてのエージェントがIDMに従って自律的に加減速するようになり、列を作って走行する「自然な渋滞」シミュレーションが可能になります。

変更後のサンプルコード全体

Python
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import random
import math

# ==========================================
# 設定パラメータ
# ==========================================
SIMULATION_STEPS = 500   # シミュレーションの総フレーム数
DT = 1.0                 # 時間刻み幅 (1ステップを1単位時間とする)

# IDM (Intelligent Driver Model) パラメータ
IDM_PARAMS = {
    'v0': 0.6,    # 希望速度 (Max Speed)
    'T': 5.0,     # 安全車間時間 (Time Headway)
    'a': 0.05,    # 最大加速度
    'b': 0.1,     # 快適減速度
    's0': 2.0,    # 最小車間距離 (Jam distance)
    'delta': 4.0  # 加速度指数の定数 (通常は4)
}

STOP_LINE = 10.0         # 交差点の中心から停止線までの距離
SPAWN_RATE = 0.08        # 車が発生する確率
CAR_LENGTH = 2.0         # 車両の長さ

# 信号機の設定 (サイクル数)
GREEN_DURATION = 80
YELLOW_DURATION = 20

# ==========================================
# 物理モデル関数 (IDM)
# ==========================================

def calculate_idm_accel(v, v_leader, gap, params):
    """
    Intelligent Driver Model (IDM) に基づいて加速度を計算する
    v: 自分の速度
    v_leader: 前走車の速度
    gap: 前走車との車間距離 (純粋な隙間)
    params: IDMパラメータ辞書
    """
    v0 = params['v0']
    T = params['T']
    a = params['a']
    b = params['b']
    s0 = params['s0']
    delta = params['delta']

    # 接近率 (相対速度: 正なら近づいている)
    delta_v = v - v_leader

    # IDMの核となる「希望車間距離」の計算
    # s* = s0 + v*T + (v * delta_v) / (2 * sqrt(a * b))
    s_star = s0 + v * T + (v * delta_v) / (2 * math.sqrt(a * b))

    # 加速度の計算
    # acc = a * [1 - (v/v0)^delta - (s*/gap)^2]
    
    # gapが非常に小さい(衝突している)場合のゼロ除算防止
    if gap <= 0.1:
        gap = 0.1
    
    acc = a * (1 - (v / v0)**delta - (s_star / gap)**2)
    return acc

# ==========================================
# クラス定義
# ==========================================

class Car:
    def __init__(self, x, y, dx, dy, direction, params):
        self.x = x
        self.y = y
        self.dx = dx  # 進行方向ベクトル x
        self.dy = dy  # 進行方向ベクトル y
        self.speed = params['v0'] * 0.8 # 初期速度は少し遅めに
        self.direction = direction  # 'H' (横) or 'V' (縦)
        self.params = params

    def update(self, cars, traffic_light_state, dt):
        # 1. 前方の状況(車および停止線)を確認
        min_gap = 10000.0   # 最も近い障害物までの距離
        leader_speed = 0.0  # その障害物の速度(停止線なら0)
        found_obstacle = False

        # --- A. 前走車の探索 ---
        for other in cars:
            if other is self: continue

            # 同じ方向軸かつ同じ向き(対向車でない)を確認
            if self.direction == other.direction:
                if self.dx * other.dx < 0 or self.dy * other.dy < 0:
                    continue

                dist = 10000.0
                # 横方向 (East-West)
                if self.direction == 'H':
                    # 左から右 (dx > 0) かつ 前方にいる
                    if self.dx > 0 and other.x > self.x:
                        dist = other.x - self.x - CAR_LENGTH
                    # 右から左 (dx < 0) かつ 前方にいる
                    elif self.dx < 0 and other.x < self.x:
                        dist = self.x - other.x - CAR_LENGTH
                
                # 縦方向 (North-South)
                elif self.direction == 'V':
                    # 下から上 (dy > 0)
                    if self.dy > 0 and other.y > self.y:
                        dist = other.y - self.y - CAR_LENGTH
                    # 上から下 (dy < 0)
                    elif self.dy < 0 and other.y < self.y:
                        dist = self.y - other.y - CAR_LENGTH
                
                # 最も近い車を記録
                if dist < min_gap:
                    min_gap = dist
                    leader_speed = other.speed
                    found_obstacle = True

        # --- B. 信号機(停止線)の探索 ---
        dist_to_stop_line = 10000.0
        must_stop = False

        # 横方向の信号判定 (赤か黄なら停止)
        if self.direction == 'H' and traffic_light_state in ['V_GREEN', 'V_YELLOW']:
            if self.dx > 0 and self.x < -STOP_LINE: # 左から接近
                dist_to_stop_line = -STOP_LINE - self.x
                must_stop = True
            elif self.dx < 0 and self.x > STOP_LINE: # 右から接近
                dist_to_stop_line = self.x - STOP_LINE
                must_stop = True

        # 縦方向の信号判定
        if self.direction == 'V' and traffic_light_state in ['H_GREEN', 'H_YELLOW']:
            if self.dy > 0 and self.y < -STOP_LINE: # 下から接近
                dist_to_stop_line = -STOP_LINE - self.y
                must_stop = True
            elif self.dy < 0 and self.y > STOP_LINE: # 上から接近
                dist_to_stop_line = self.y - STOP_LINE
                must_stop = True

        # 信号で止まるべき状況で、停止線の方が前走車より近い場合
        # 「速度0の車が停止線にいる」とみなしてIDM計算に使用する
        if must_stop and dist_to_stop_line < min_gap:
            min_gap = dist_to_stop_line
            leader_speed = 0.0 # 停止線は動かない
            found_obstacle = True
        
        # 2. 加速度の計算 (IDM)
        if found_obstacle:
            # 前に車か停止線がある場合
            acc = calculate_idm_accel(self.speed, leader_speed, min_gap, self.params)
        else:
            # フリー走行 (前走車なし = gap無限大、leader_speed=0 として計算)
            acc = calculate_idm_accel(self.speed, 0, 10000.0, self.params)

        # 3. 速度更新
        self.speed += acc * dt
        if self.speed < 0: self.speed = 0 # バックはしない
        
        # 4. 位置更新
        self.x += self.dx * self.speed * dt
        self.y += self.dy * self.speed * dt


class TrafficSimulation:
    def __init__(self):
        self.cars = []
        self.time = 0
        self.light_state = 'H_GREEN'
        self.light_timer = 0

    def step(self):
        self.time += 1
        self.update_traffic_lights()
        self.spawn_cars()

        for car in self.cars:
            car.update(self.cars, self.light_state, DT)

        # 画面外に出た車を削除
        self.cars = [c for c in self.cars if -60 < c.x < 60 and -60 < c.y < 60]

    def update_traffic_lights(self):
        self.light_timer += 1
        if self.light_state == 'H_GREEN' and self.light_timer > GREEN_DURATION:
            self.light_state = 'H_YELLOW'
            self.light_timer = 0
        elif self.light_state == 'H_YELLOW' and self.light_timer > YELLOW_DURATION:
            self.light_state = 'V_GREEN'
            self.light_timer = 0
        elif self.light_state == 'V_GREEN' and self.light_timer > GREEN_DURATION:
            self.light_state = 'V_YELLOW'
            self.light_timer = 0
        elif self.light_state == 'V_YELLOW' and self.light_timer > YELLOW_DURATION:
            self.light_state = 'H_GREEN'
            self.light_timer = 0

    def spawn_cars(self):
        if random.random() < SPAWN_RATE:
            route = random.choice(['W-E', 'E-W', 'S-N', 'N-S'])

            # スポーン位置とパラメータ
            if route == 'W-E':
                spawn_car = Car(-55, -2, 1, 0, 'H', IDM_PARAMS)
            elif route == 'E-W':
                spawn_car = Car(55, 2, -1, 0, 'H', IDM_PARAMS)
            elif route == 'S-N':
                spawn_car = Car(-2, -55, 0, 1, 'V', IDM_PARAMS)
            elif route == 'N-S':
                spawn_car = Car(2, 55, 0, -1, 'V', IDM_PARAMS)

            # 出現位置の安全確認
            is_safe = True
            for c in self.cars:
                dist_sq = (c.x - spawn_car.x)**2 + (c.y - spawn_car.y)**2
                # IDMパラメータのs0(最小車間)などを考慮して余裕を持たせる
                if dist_sq < (IDM_PARAMS['s0'] * 3)**2:
                    is_safe = False
                    break

            if is_safe:
                self.cars.append(spawn_car)

# ==========================================
# 描画
# ==========================================

sim = TrafficSimulation()
fig, ax = plt.subplots(figsize=(6, 6))

def draw_background():
    ax.set_xlim(-50, 50)
    ax.set_ylim(-50, 50)
    ax.set_facecolor('#333333')

    # 道路
    ax.add_patch(plt.Rectangle((-50, -4), 100, 8, color='#555555'))
    ax.add_patch(plt.Rectangle((-4, -50), 8, 100, color='#555555'))

    # 白線
    ax.plot([-50, 50], [0, 0], color='white', linestyle='--', linewidth=1)
    ax.plot([0, 0], [-50, 50], color='white', linestyle='--', linewidth=1)

    # 停止線
    ax.plot([-4, 4], [-STOP_LINE, -STOP_LINE], color='white', linewidth=2)
    ax.plot([-4, 4], [STOP_LINE, STOP_LINE], color='white', linewidth=2)
    ax.plot([-STOP_LINE, -STOP_LINE], [-4, 4], color='white', linewidth=2)
    ax.plot([STOP_LINE, STOP_LINE], [-4, 4], color='white', linewidth=2)

def init():
    return []

def animate(i):
    ax.clear()
    draw_background()

    sim.step()

    # 車を描画
    for c in sim.cars:
        ax.plot(c.x, c.y, 's', color='cyan', markersize=8, markeredgecolor='black')

    # 信号表示
    light_color_h = 'grey'
    light_color_v = 'grey'

    if sim.light_state == 'H_GREEN': light_color_h = '#00FF00'; light_color_v = '#FF0000'
    elif sim.light_state == 'H_YELLOW': light_color_h = '#FFFF00'; light_color_v = '#FF0000'
    elif sim.light_state == 'V_GREEN': light_color_h = '#FF0000'; light_color_v = '#00FF00'
    elif sim.light_state == 'V_YELLOW': light_color_h = '#FF0000'; light_color_v = '#FFFF00'

    # 信号機インジケータ
    ax.text(-45, 42, f"東西: ●", color=light_color_h, fontsize=16, fontweight='bold', path_effects=[])
    ax.text(30, 42, f"南北: ●", color=light_color_v, fontsize=16, fontweight='bold', path_effects=[])

    ax.set_title(f"Time: {i}")
    ax.set_xticks([])
    ax.set_yticks([])
    return []

ani = animation.FuncAnimation(fig, animate, frames=SIMULATION_STEPS, init_func=init, interval=50)

print("動画生成中...")
try:
    ani.save('traffic_simulation_idm.mp4', writer='ffmpeg', fps=20)
    print("保存完了: traffic_simulation_idm.mp4")
except:
    ani.save('traffic_simulation_idm.gif', writer='pillow', fps=20)
    print("保存完了: traffic_simulation_idm.gif")

plt.close()

6. まとめ

今回は、交通シミュレーションに「物理的なリアリティ」を与えるための 追従モデル (IDM) の実装について解説しました。

  • FSMの限界: 単純なif文制御では、挙動がロボット的で不自然になる。
  • IDMの利点: 「希望速度で走りたい」「ぶつかりたくない」という2つの要素を数式化することで、滑らかな追従走行を実現できる。
  • 実装のポイント: 加速度を計算し、オイラー法などの数値積分で位置と速度を更新する。

これで、前後の動き(縦方向の制御)に関してはかなり賢いエージェントになりました。
しかし、実際の道路には「隣の車線」があります。前の車が遅ければ、追い越したくなりますよね?

次回は、このIDMに加えて「車線変更モデル(MOBILなど)」を導入し、複数車線での追い越しや割り込みを含めた、さらに複雑な交通流の再現に挑戦します。

最後まで読んでいただきありがとうございました。

コメント

タイトルとURLをコピーしました