【Python】数式とコードで再現する! 交差点の交通流シミュレーション自作入門

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

こんにちは、JS2IIUです。
通勤やドライブの最中、赤信号で止まっているときに「もっとうまく信号が変われば渋滞しないのに」と思ったことはありませんか? あるいは、自動運転車が普及したら交差点の景色はどう変わるのだろうと想像したことがあるかもしれません。

現代の都市工学において、こうした疑問を解決するための強力なツールが「交通流シミュレーション」です。

今回は、Pythonを使ってゼロから交差点の交通流シミュレーションを作成します。単に動かすだけでなく、物理法則に基づいた車両の挙動モデルを組み込み、信号機に従って車が停止・発進する様子を動画として出力するところまでを実践します。

コードを書きながら、複雑な交通現象をどのようにプログラムに落とし込むか、その数理的な背景と共に学んでいきましょう。今回もよろしくお願いします。

交通流シミュレーションの基礎理論

プログラムを書く前に、少しだけ理論の話をしましょう。交通流のモデルには大きく分けて3つのアプローチがあります。

  1. マクロモデル: 交通流を「流体(水やガス)」のように扱い、密度や流量の関係式で計算する手法。高速道路網全体の渋滞予測などに使われます。
  2. ミクロモデル: 個々の車両を1台ずつ計算する手法。交差点の設計や自動運転の解析に向いています。
  3. メソモデル: 上記の中間的な手法。

今回作成するのは、最も詳細な動きを再現できるミクロシミュレーションです。その中でも基本となる「追従モデル(Car-following Model)」を採用します。

追従モデルの数理

追従モデルとは、「前の車との関係性」によって自分の速度を決める考え方です。ドライバーは通常、以下の2つを意識して運転しています。

  1. 目標速度: 前に誰もいなければ、制限速度まで加速したい。
  2. 安全車間距離: 前に車がいれば、ぶつからないように減速したい。

これを簡易的な数式で表すと、次のようになります。

ある時刻 $t$ における車両の位置を \(x(t)\)、速度を \(v(t)\) とします。車両は通常、一定の加速度 \(a\) で加速しようとします。

$$ v(t+\Delta t) = v(t) + a \cdot \Delta t $$

しかし、前走車との距離 \(d\) が安全距離 \(d_{safe}\) を下回った場合、ブレーキ(減速度 \(b\))をかけます。

$$ v(t+\Delta t) = v(t) – b \cdot \Delta t $$

実際の高度なモデル(GippsモデルやIDM: Intelligent Driver Modelなど)では、これらを連続的な関数として扱いますが、今回は理解しやすさを優先し、条件分岐を用いたルールベースのモデルで実装します。

Python実装の設計図

今回は以下の構成でプログラムを作成します。

  • Car クラス: 1台の車を表します。位置、速度、進行方向を持ち、周囲の状況を見て「加速・減速」を判断します。
  • TrafficSimulation クラス: 交差点全体を管理します。信号機の制御、車両の発生(スポーン)、時間の進行を担当します。
  • 描画 (Matplotlib): 計算結果をアニメーションとして可視化し、動画ファイルとして保存します。

モンテカルロシミュレーションの要素

このシミュレーションには確率的な要素が含まれます。現実世界では、車は一定の間隔で規則正しくやってくるわけではありません。ランダムなタイミングで到着します。
本プログラムでは、各タイムステップごとに一定の確率(例:5%)で新しい車を生成します。これは確率分布に従って事象を発生させるモンテカルロ法的なアプローチであり、シミュレーションのリアリティを高める重要な要素です。

詳細コーディング解説

それでは、実際のコードを見ていきましょう。
※実行には numpymatplotlib が必要です。また、MP4出力には ffmpeg が必要ですが、ない場合はGIFとして出力されるように配慮しています。

Step 1: 環境設定とパラメータ

まず、シミュレーションの物理定数を定義します。これらの数値を変更することで、「慎重なドライバーが多い世界」や「スピード狂が多い世界」を作ることができます。

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

# ==========================================
# 設定パラメータ
# ==========================================
SIMULATION_STEPS = 500   # シミュレーションを行う総フレーム数
MAX_SPEED = 0.5          # 車の最高速度 (1フレームあたりの移動距離)
ACCELERATION = 0.1       # 加速度:1フレームごとの増速量
DECELERATION = 0.3       # 減速度:ブレーキ時の減速量
SAFE_DISTANCE = 4.0      # 安全車間距離:これより近づくと減速
STOP_LINE = 10.0         # 交差点中心から停止線までの距離
SPAWN_RATE = 0.08        # 車が発生する確率 (0.0~1.0)

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

Step 2 & 3: Carクラスの実装

ここがプログラムの心臓部です。特に重要なのが update メソッド内の「前走車の認識ロジック」です。

単純に「一番近い車」を探すと、対向車線の車や、交差する道路の車に反応して急ブレーキをかけてしまいます。そこで、「自分と同じ進行方向」かつ「自分の前方にある」車だけを対象にするフィルタリングを行います。

Python
class Car:
    def __init__(self, x, y, dx, dy, direction):
        self.x = x
        self.y = y
        self.dx = dx  # 進行方向ベクトル x成分 (-1, 0, 1)
        self.dy = dy  # 進行方向ベクトル y成分 (-1, 0, 1)
        self.speed = MAX_SPEED
        self.direction = direction  # 'H'(横移動) または 'V'(縦移動)

    def update(self, cars, traffic_light_state):
        # 1. 前走車との距離を計測
        dist_to_next = 1000  # 初期値として十分大きな数を設定

        for other in cars:
            if other is self: continue

            # 【重要】同じ方向軸('H'同士など)の車のみを考慮
            if self.direction == other.direction:

                # 【重要】進行方向が逆(対向車)なら無視する
                # ベクトルの積が負なら逆方向とみなせる
                if self.dx * other.dx < 0 or self.dy * other.dy < 0:
                    continue

                # --- 同じ向きの車に対する距離計算 ---
                # 横方向 (East-West) の場合
                if self.direction == 'H':
                    # 左から右へ (dx > 0) : 相手が自分の右側(xが大きい)にいるか
                    if self.dx > 0 and other.x > self.x:
                        dist = other.x - self.x - 2 # 車体サイズ分を引く
                        if dist < dist_to_next: dist_to_next = dist
                    # 右から左へ (dx < 0) : 相手が自分の左側(xが小さい)にいるか
                    elif self.dx < 0 and other.x < self.x:
                        dist = self.x - other.x - 2
                        if dist < dist_to_next: dist_to_next = dist

                # 縦方向 (North-South) の場合
                elif self.direction == 'V':
                    # 下から上へ (dy > 0)
                    if self.dy > 0 and other.y > self.y:
                        dist = other.y - self.y - 2
                        if dist < dist_to_next: dist_to_next = dist
                    # 上から下へ (dy < 0)
                    elif self.dy < 0 and other.y < self.y:
                        dist = self.y - other.y - 2
                        if dist < dist_to_next: dist_to_next = dist

        # 2. 信号機による停止判定
        # 自分の進行方向の信号が赤(または黄)で、かつ停止線を超えていない場合、
        # 「停止線」を障害物とみなして距離を計算します。
        dist_to_stop_line = 1000

        # 横方向の車:縦信号が青/黄のとき(つまり横信号は赤)は止まる
        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
            elif self.dx < 0 and self.x > STOP_LINE: # 右から停止線へ
                dist_to_stop_line = self.x - STOP_LINE

        # 縦方向の車:横信号が青/黄のときは止まる
        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
            elif self.dy < 0 and self.y > STOP_LINE:
                dist_to_stop_line = self.y - STOP_LINE

        # 3. 速度の決定(物理モデルの適用)
        # 前の車、または停止線のうち、近い方をターゲットにします
        target_dist = min(dist_to_next, dist_to_stop_line)

        # 距離に応じた加減速ロジック
        if target_dist < SAFE_DISTANCE:
            # 近すぎる場合は急減速(停止)
            if target_dist <= 1.0:
                self.speed = 0
            else:
                self.speed -= DECELERATION
        elif target_dist < SAFE_DISTANCE * 2.5:
            # 少し近づいたら緩やかに減速(コースティング)
            self.speed *= 0.92
        else:
            # 十分遠ければ加速
            if self.speed < MAX_SPEED:
                self.speed += ACCELERATION

        # 速度が負にならないよう、かつ最高速度を超えないように制限
        if self.speed < 0: self.speed = 0
        if self.speed > MAX_SPEED: self.speed = MAX_SPEED

        # 4. 位置の更新
        self.x += self.dx * self.speed
        self.y += self.dy * self.speed

Step 4: シミュレーション管理

ここでは信号機の色の切り替えと、ランダムな車両生成を行います。車両生成部分に random.random() を使うことで、現実のような不規則な交通流を作り出します。

Python
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)

        # 画面外(遠く)に行った車をリストから削除してメモリを節約
        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')
            elif route == 'E-W': 
                spawn_car = Car(55, 2, -1, 0, 'H')
            elif route == 'S-N': 
                spawn_car = Car(-2, -55, 0, 1, 'V')
            elif route == 'N-S': 
                spawn_car = Car(2, 55, 0, -1, 'V')

            # 出現位置に既に車がいる場合は生成しない(重なり防止)
            is_safe = True
            for c in self.cars:
                dist_sq = (c.x - spawn_car.x)**2 + (c.y - spawn_car.y)**2
                if dist_sq < (SAFE_DISTANCE * 1.5)**2:
                    is_safe = False
                    break

            if is_safe:
                self.cars.append(spawn_car)

Step 5: 可視化と実行

最後に matplotlib.animation を使って、計算結果を動画にします。以下のコードで、シミュレーションを実行し、結果を保存します。

Python
# ==========================================
# 描画とアニメーション実行
# ==========================================

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()

    # 時間を1ステップ進める
    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')
    ax.text(30, 42, f"南北: ●", color=light_color_v, fontsize=16, fontweight='bold')

    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("動画生成中...")
# ffmpegがあればmp4、なければgifとして保存
try:
    ani.save('traffic_simulation_fixed.mp4', writer='ffmpeg', fps=20)
    print("保存完了: traffic_simulation_fixed.mp4")
except:
    ani.save('traffic_simulation_fixed.gif', writer='pillow', fps=20)
    print("保存完了: traffic_simulation_fixed.gif")

plt.close()

シミュレーション結果と考察

このコードを実行すると、traffic_simulation_fixed.mp4(または.gif)というファイルが生成されます。

動画を開くと、次のような様子が観察できるはずです。

  1. 信号待ちの行列: 赤信号のとき、車が停止線で止まり、後続車がその後ろに等間隔で並びます(待ち行列の形成)。
  2. 発進波: 信号が青になると、先頭の車から順に加速を始めます。すべての車が一斉に動き出すのではなく、反応の連鎖が後ろへ伝わっていく様子がわかります。
  3. 黄信号のジレンマ: 黄色信号の瞬間に交差点直前にいた車は、停止が間に合わずそのまま通過し、少し離れていた車は減速して止まります。

パラメータ SPAWN_RATE(車の発生率)を 0.08 から 0.15 くらいに上げて実行し直してみてください。すると、一度の青信号で車をさばききれなくなり、次の赤信号になっても行列が解消されない「飽和交通流(渋滞)」の状態が観察できるでしょう。

このように、単純な物理モデルの積み重ねだけで、現実と同じ渋滞現象が再現できるのがシミュレーションの醍醐味です。

発展:よりリアルな挙動を目指して

今回のモデルは基礎的なものでしたが、ここからさらに発展させるアイデアは無限にあります。

  • IDM (Intelligent Driver Model): 今回はif文で速度を決めましたが、IDMという微分方程式モデルを使うと、より人間らしく滑らかな加減速が再現できます。
  • 車線変更: 複数車線を導入し、前の車が遅いときに追い越しをする「車線変更モデル(MOBILなど)」を組み合わせる。
  • AI信号機: このシミュレーション環境を強化学習(Reinforcement Learning)の環境として使い、「渋滞を最小にするように信号時間を自分で学習するAI」を作ることも可能です。

まとめ

今回はPythonを使って、交差点の交通流シミュレーションをゼロから構築しました。

  • ミクロシミュレーションは個々の車の相互作用を計算する。
  • 追従モデルを使えば、前走車との距離に応じた自律的な速度制御が可能になる。
  • モンテカルロ的なランダム生成により、動的な交通状況を作り出せる。
  • 「対向車を前走車と誤認しない」といった、幾何学的な判定ロジックが実装の肝である。

このコードはわずか200行足らずですが、交通工学のエッセンスが詰まっています。ぜひパラメータをいじったり、道路を増やしたりして、自分だけの街をシミュレーションしてみてください。

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

コメント

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