這幾年台北大巨蛋炒得沸沸騰騰,容留人數上限由原來規劃的13萬人,經過都發局防災避難系統『模擬』,最多只能以59,833人為上限,透過模擬我們可以評估整個系統的承載容量。也可以透過資源的配置,改善系統的服務水準(Service Level),其他應用還包括客服中心人力的規劃、生產資源的配置、火車/客機/客運售票窗口規劃、郵局/便利商店的服務櫃台設置等等,應用範疇很廣,值得我們好好的研究一番,進而建構相關的系統。
簡單的說,模擬(Simulation)是希望建構電腦模型以模擬真實世界的運作狀況,並進一步分析系統的承載容量(Capacity)或找出瓶頸,進而針對瓶頸增加資源(Resource)或變更流程,從而改善系統運作的順暢度。
要建構一套模擬系統,我們必須先描繪出真實世界的流程及各項作業,以戲院入場流程為例:
圖一. 戲院入場流程
接著,要描述各個作業的到站人數與作業時間,例如:
之後就可以配置資源,進行系統模擬:
依上述說明,定義模擬的一些專業術語:
模擬的專業術語與『強化學習』(Reinforcement Learning)相似,但強化學習目標是優化Agent的行動策略,而模擬是評估或優化整個系統流程。
筆者在校時,曾學習相關軟體GPSS,它是安裝在計算機中心的大型主機上,離開校園就無法使用了,非常可惜,還好,有善心人士以Python開發許多套件,雖然功能沒有那麼強,但比較容易自行修改擴充。以下就以SimPy套件進行實作。
安裝:
pip install simpy
範例 1. 簡單範例,說明程式基本架構。
import simpy
def clock(env):
while True:
print("開始:", env.now) # env.now獲取當前時間
yield env.timeout(1) # 等待1秒
print("結束:", env.now) # env.now獲取當前時間
# 建立環境
env = simpy.Environment()
# 啟動處理作業
env.process(clock(env))
# 運行模擬環境5秒
env.run(until=5)
範例 2. 建立兩個平行處理作業,一個每隔0.5秒觸發clock函數,另一個每隔1秒觸發clock函數。
import simpy
def clock(env, name, tick):
while True:
print(name, env.now)
yield env.timeout(tick)
env = simpy.Environment()
env.process(clock(env, 'fast', 0.5))
env.process(clock(env, 'slow', 1))
env.run(until=2)
範例 3. 實作戲院入場流程,程式來自『SimPy: Simulating Real-World Processes With Python』。
import simpy
import random
import statistics
wait_times = [] # 記錄觀眾等待時間
# 戲院類別
class Theater(object):
def __init__(self, env, num_cashiers, num_servers, num_ushers):
self.env = env
self.cashier = simpy.Resource(env, num_cashiers) # 售票口
self.server = simpy.Resource(env, num_servers) # 驗票口
self.usher = simpy.Resource(env, num_ushers) # 小吃部銷售員
# 購票時間 1~3分鐘
def purchase_ticket(self, moviegoer):
yield self.env.timeout(random.randint(1, 3))
# 驗票時間 3秒鐘
def check_ticket(self, moviegoer):
yield self.env.timeout(3 / 60)
# 購物時間 1~5分鐘
def sell_food(self, moviegoer):
yield self.env.timeout(random.randint(1, 5))
# 觀眾進場
def go_to_movies(env, moviegoer, theater):
# 到場
arrival_time = env.now
# 購票
with theater.cashier.request() as request:
yield request
yield env.process(theater.purchase_ticket(moviegoer))
# 驗票
with theater.usher.request() as request:
yield request
yield env.process(theater.check_ticket(moviegoer))
# 購物:1/2機率會購物
if random.choice([True, False]):
with theater.server.request() as request:
yield request
yield env.process(theater.sell_food(moviegoer))
# 入座
wait_times.append(env.now - arrival_time)
# 模擬函數
def run_theater(env, num_cashiers, num_servers, num_ushers):
# 建立環境
theater = Theater(env, num_cashiers, num_servers, num_ushers)
# 初始人數:3人
for moviegoer in range(3):
env.process(go_to_movies(env, moviegoer, theater))
# 模擬
while True:
yield env.timeout(0.20) # 每隔0.2分鐘有一觀眾到場
moviegoer += 1
env.process(go_to_movies(env, moviegoer, theater))
def get_average_wait_time(wait_times):
average_wait = statistics.mean(wait_times)
# 計算分、秒
minutes, frac_minutes = divmod(average_wait, 1)
seconds = frac_minutes * 60
return round(minutes), round(seconds)
# 設定資源個數
def get_user_input():
# 輸入資源個數
num_cashiers = input("售票口個數: ")
num_servers = input("驗票口個數: ")
num_ushers = input("小吃部銷售員人數: ")
params = [num_cashiers, num_servers, num_ushers]
if all(str(i).isdigit() for i in params): # Check input is valid
params = [int(x) for x in params]
else:
print(
"Could not parse input. Simulation will use default values:",
"\n1 cashier, 1 server, 1 usher.",
)
params = [1, 1, 1]
return params
def main():
# Setup
random.seed(42)
num_cashiers, num_servers, num_ushers = get_user_input()
# 啟動模擬
env = simpy.Environment()
env.process(run_theater(env, num_cashiers, num_servers, num_ushers))
env.run(until=90) # 模擬90分鐘
# 統計等待時間
mins, secs = get_average_wait_time(wait_times)
print(
"Running simulation...",
f"\n觀眾平均進場時間:{mins} 分 {secs} 秒.",
)
if __name__ == "__main__":
main()
測試:輸出結果如下,每隔0.2分鐘有一觀眾到場,售票需要3分鐘,故會大排長龍,觀眾平均進場時間高達23分30 秒。
售票口個數: 5
驗票口個數: 5
小吃部銷售員人數: 5
Running simulation...
觀眾平均進場時間:23 分 30 秒.
測試2:輸出結果如下,安排大量人力資源,觀眾幾乎不必等待。
售票口個數: 50
驗票口個數: 50
小吃部銷售員人數: 50
Running simulation...
觀眾平均進場時間:3 分 29 秒.
戲院入場流程只是一個很簡單的場景,實際上的應用可能複雜許多,因此,SimPy、GPSS等套件都支援更多的功能,另外,如何將模擬視覺化,讓使用者可以觀察到模擬的景象,進而察覺瓶頸,也是一個很重要的課題,大巨蛋緊急疏散模擬動畫,就讓我們看到光復南路/忠孝東路交界處是一個瓶頸,一堆人堵塞在那裏。
後續文章希望能作更深入的探討,本篇的程式碼放在GitHub,讀者可自行下載。