Mímisbrunnr知恵の泉

← シミュレーション 一覧

🎓 レベル:基礎 | 重要度:A(必須)

📎 前提:モデル化の流れ(系・状態・入力・出力) | 関連:待ち行列のシミュレーション

要点(BLUF)

1. なぜイベント駆動か

待ち行列・在庫・通信網のような系は、状態(行列の人数、在庫量)が特定の瞬間にしか変わりません。客が「到着」した瞬間、サービスが「完了」した瞬間——その間の時間、状態は一定です。

この性質を使うのが DES です。時間を細かい Δt\Delta t で刻んで毎ステップ確認する固定時間刻みは、何も起きない時間も計算するので無駄が多い。DES は「次に状態が変わるのはいつか」を事象リストで管理し、クロックをそこへジャンプさせる。イベント数だけ計算すれば済むので、まばらに事象が起きる系で圧倒的に効率的です。

方式時間の進め方向く系
固定時間刻みΔt\Delta t ごとに更新連続的に変化(ODE・物理)
離散事象(DES)次の事象時刻へジャンプ飛び飛びに変化(待ち行列・在庫)

2. 中核の部品

flowchart TD
    A["FELから最早事象を取り出す"] --> B["クロックをその時刻へ進める"]
    B --> C["状態を更新する"]
    C --> D["新しい事象をFELに登録する"]
    D --> E{"終了条件?"}
    E -->|"いいえ"| A
    E -->|"はい"| F["統計を集計"]

3. 実装:次イベント方式の単一窓口

到着事象とサービス完了事象だけを管理する最小の DES です。ヒープ(heapq)が FEL の役割を果たします。

import numpy as np
import heapq

def next_event_mm1(lam, mu, max_customers, seed):
    rng = np.random.default_rng(seed)
    clock = 0.0
    n_in_system = 0
    events = []                                   # 未来事象リスト(時刻, 種別)
    heapq.heappush(events, (rng.exponential(1/lam), 'arrival'))
    queue = []                                    # 待ち客の到着時刻
    waits = []
    served = 0
    while served < max_customers:
        clock, kind = heapq.heappop(events)       # 次の事象へクロックを飛ばす
        if kind == 'arrival':
            queue.append(clock)
            heapq.heappush(events, (clock + rng.exponential(1/lam), 'arrival'))
            if n_in_system == 0:                  # サーバ空き→即サービス
                arr = queue.pop(0)
                waits.append(clock - arr)         # 待ち0
                heapq.heappush(events, (clock + rng.exponential(1/mu), 'departure'))
            n_in_system += 1
        else:                                     # サービス完了(退出)
            n_in_system -= 1
            served += 1
            if queue and n_in_system >= 1:        # 次の客をサービス開始
                arr = queue.pop(0)
                waits.append(clock - arr)
                heapq.heappush(events, (clock + rng.exponential(1/mu), 'departure'))
    return np.array(waits)

lam, mu = 0.8, 1.0
w = next_event_mm1(lam, mu, 200_000, seed=5)
print(f"処理客数={len(w)}, 平均待ち時間={w.mean():.3f}, 理論Wq={(lam/mu)/(mu-lam):.3f}")

出力:

処理客数=200001, 平均待ち時間=3.986, 理論Wq=4.000

出力の意味:20万人を処理して平均待ち時間 3.986。M/M/1 の理論値 Wq=ρ/(μλ)=4.0W_q = \rho/(\mu-\lambda) = 4.0 と一致します。注目すべきは、ループが回るのは事象の数だけで、客がいない時間は1ステップも計算していない点。これが DES の効率です。実務では heapq を直書きせず、次トピックのように SimPy のような専用ライブラリで書きます。

4. SimPy という選択肢

FEL やクロックを毎回手書きするのは大変なので、SimPy(プロセスベースの DES ライブラリ)が便利です。「客」をジェネレータ関数として書き、yield env.timeout(...) で時間経過、Resource で窓口の取り合いを表現します。中身は同じイベント駆動ですが、可読性が大きく上がります(待ち行列のシミュレーションで使用)。

数式の直観的意味

DES の効率は「情報がゼロの区間を積分しない」ことに尽きます。固定刻みは [0,T][0,T]T/ΔtT/\Delta t 個のステップに分け、ほとんどのステップで「何も変わらない」を確認するだけ。DES は状態関数が**区分定数(階段関数)**であることを利用し、段差(事象)の位置だけを追う。計算量は O(T/Δt)O(T/\Delta t) から O(事象数)O(\text{事象数}) へ落ちる。理論値と一致するのは、到着間隔・サービス時間を正しい指数分布(代表的分布の生成(正規・指数・ポアソン))で生成し、状態遷移ロジックが M/M/1 の仮定を満たすからです。

⚠️ よくある誤解・落とし穴

対応シミュレーション参照

本文の次イベント方式 M/M/1(default_rng(5)、平均待ち 3.986 ≈ 理論 4.0)。

関連ノート