🎓 レベル:基礎 | 重要度:A(必須)
📎 前提:モデル化の流れ(系・状態・入力・出力) | 関連:待ち行列のシミュレーション
要点(BLUF)
- 離散事象シミュレーション(DES):状態が飛び飛びのイベント(到着・退出など)でのみ変化する系を、イベントごとに時間を進めて再現します。
- 中核は未来事象リスト(FEL)とクロック。「次に起きる事象」へクロックを一気に飛ばし、何も起きない時間は計算しません。
- 単一窓口を次イベント方式で実装し、平均待ち時間 3.986 が M/M/1 理論値 4.000 と一致することを確かめます。
1. なぜイベント駆動か
待ち行列・在庫・通信網のような系は、状態(行列の人数、在庫量)が特定の瞬間にしか変わりません。客が「到着」した瞬間、サービスが「完了」した瞬間——その間の時間、状態は一定です。
この性質を使うのが DES です。時間を細かい で刻んで毎ステップ確認する固定時間刻みは、何も起きない時間も計算するので無駄が多い。DES は「次に状態が変わるのはいつか」を事象リストで管理し、クロックをそこへジャンプさせる。イベント数だけ計算すれば済むので、まばらに事象が起きる系で圧倒的に効率的です。
| 方式 | 時間の進め方 | 向く系 |
|---|---|---|
| 固定時間刻み | ごとに更新 | 連続的に変化(ODE・物理) |
| 離散事象(DES) | 次の事象時刻へジャンプ | 飛び飛びに変化(待ち行列・在庫) |
2. 中核の部品
- シミュレーションクロック:現在時刻。連続的にではなく、事象から事象へ飛ぶ。
- 未来事象リスト(Future Event List, FEL):これから起きる事象を「時刻つき」で保持し、最も早い事象を取り出せる優先度付きキュー(ヒープ)。
- 状態変数:系の現在(在庫量、行列の人数、サーバの空き)。
- 事象処理ルーチン:各事象が起きたとき、状態を更新し、次の事象を FEL に登録する。
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 の理論値 と一致します。注目すべきは、ループが回るのは事象の数だけで、客がいない時間は1ステップも計算していない点。これが DES の効率です。実務では heapq を直書きせず、次トピックのように SimPy のような専用ライブラリで書きます。
4. SimPy という選択肢
FEL やクロックを毎回手書きするのは大変なので、SimPy(プロセスベースの DES ライブラリ)が便利です。「客」をジェネレータ関数として書き、yield env.timeout(...) で時間経過、Resource で窓口の取り合いを表現します。中身は同じイベント駆動ですが、可読性が大きく上がります(待ち行列のシミュレーションで使用)。
数式の直観的意味
DES の効率は「情報がゼロの区間を積分しない」ことに尽きます。固定刻みは を 個のステップに分け、ほとんどのステップで「何も変わらない」を確認するだけ。DES は状態関数が**区分定数(階段関数)**であることを利用し、段差(事象)の位置だけを追う。計算量は から へ落ちる。理論値と一致するのは、到着間隔・サービス時間を正しい指数分布(代表的分布の生成(正規・指数・ポアソン))で生成し、状態遷移ロジックが M/M/1 の仮定を満たすからです。
⚠️ よくある誤解・落とし穴
- 「固定時間刻みの方が正確」ではない:DES は事象時刻を厳密に扱うので、 の離散化誤差がなく、むしろ正確。
- 「FEL は配列でいい」ではない:毎回最小時刻を探すと 。ヒープ(優先度付きキュー)で にします。
- 「同時刻の事象は順不同でいい」ではない:同時刻イベントの処理順序が結果を変えることがあります(タイブレークルールを決める)。
- 「状態を毎ステップ記録すればいい」ではない:時間平均(行列長など)は区間×値で積分。事象間の一定値を時間重みで足します(出力解析(過渡・定常・複製))。
- 「連続変化する系にも使える」ではない:状態が連続的に変わる系(化学反応の濃度など)は固定刻みや ODE 解法向き。DES は飛び飛びの変化専用。
対応シミュレーション参照
本文の次イベント方式 M/M/1(default_rng(5)、平均待ち 3.986 ≈ 理論 4.0)。
関連ノート
- モデル化の流れ(系・状態・入力・出力)(前提・系と状態の切り分け)
- 待ち行列のシミュレーション(次のトピック・SimPy で M/M/1)
- 出力解析(過渡・定常・複製)(時間平均と過渡の扱い)
- 代表的分布の生成(正規・指数・ポアソン)(到着・サービス時間の生成)
- 第5章 離散事象シミュレーション 目次
- シミュレーション・モンテカルロ法 全体目次