現(xiàn)離散事件仿真:從資源排隊(duì)到系統(tǒng)優(yōu)化)
簡(jiǎn)介一份面向Python開(kāi)發(fā)者與系統(tǒng)仿真初學(xué)者的完整技術(shù)文檔以SimPy離散事件建模為主線幫助讀者從零掌握復(fù)雜系統(tǒng)仿真的核心方法。文檔內(nèi)容系統(tǒng)完整先從離散事件系統(tǒng)的基本概念講起再深入講解SimPy的環(huán)境、進(jìn)程、事件、資源等核心組件并提供了醫(yī)院急診室、交通路口、供應(yīng)鏈系統(tǒng)三大真實(shí)場(chǎng)景的完整仿真案例實(shí)現(xiàn)每個(gè)案例都包含問(wèn)題描述、模型假設(shè)、代碼實(shí)現(xiàn)與結(jié)果分析。同時(shí)還涵蓋了仿真結(jié)果的數(shù)據(jù)收集與可視化分析、自定義事件與進(jìn)程、資源優(yōu)先級(jí)管理、多環(huán)境并行仿真等進(jìn)階應(yīng)用技巧。資源為1個(gè)PDF文件共32頁(yè)大小4.98MB支持目錄章節(jié)跳轉(zhuǎn)與閱讀器大綱定位排版清晰完整。已有207人學(xué)習(xí)使用適合需要快速上手SimPy進(jìn)行復(fù)雜系統(tǒng)建模與仿真的技術(shù)開(kāi)發(fā)人員和高校學(xué)生。1. 為什么拿 SimPy 做離散事件建模先解決「資源排隊(duì)」這類算不清的賬做排班優(yōu)化、產(chǎn)線擴(kuò)容或者急診室流程改造的時(shí)候最頭疼的問(wèn)題不是找不到方案而是沒(méi)人說(shuō)得清方案投下去到底有沒(méi)有用。拿醫(yī)院急診室舉例加一個(gè)醫(yī)生到底能讓平均等待時(shí)間降多少新增一臺(tái)設(shè)備值不值這類問(wèn)題靠 Excel 手工算算不動(dòng)靠寫(xiě)多線程模擬要處理大量并發(fā)和時(shí)間管理成本又太高。SimPy 正是為這種場(chǎng)景設(shè)計(jì)的它基于 Python 的生成器機(jī)制用事件驅(qū)動(dòng)的思路模擬「誰(shuí)在什么時(shí)刻做了什么、占用了什么資源、等了多少時(shí)間」。這篇筆記會(huì)從核心概念、建模范式、三個(gè)實(shí)戰(zhàn)案例到高頻踩坑逐個(gè)拆讓你拿到就能照著搭自己的仿真模型。2. 核心組件與基本建模范式環(huán)境、進(jìn)程、事件怎么配合2.1 環(huán)境與進(jìn)程仿真世界的「時(shí)鐘」和「演員」先建立兩個(gè)最基本的認(rèn)知環(huán)境Environment管時(shí)間進(jìn)程Process管行為。Environment 內(nèi)部維護(hù)一個(gè)事件隊(duì)列和當(dāng)前仿真時(shí)刻env.now所有的事件都按時(shí)間順序排列環(huán)境只負(fù)責(zé)按次序把事件拿出來(lái)執(zhí)行。進(jìn)程則是寫(xiě)在生成器函數(shù)里的行為邏輯用yield暫停和恢復(fù)執(zhí)行。這里的關(guān)鍵是理解yield的語(yǔ)義進(jìn)程遇到y(tǒng)ield env.timeout(n)時(shí)相當(dāng)于說(shuō)「我先睡 n 個(gè)時(shí)間單位到點(diǎn)再叫我」。環(huán)境把這條等待記錄掛進(jìn)事件隊(duì)列然后把執(zhí)行權(quán)交給其他進(jìn)程。SimPy 是協(xié)作式調(diào)度不是多線程搶占所有進(jìn)程都在同一個(gè) Python 線程里依次切換不存在共享內(nèi)存競(jìng)爭(zhēng)問(wèn)題。import simpy def car(env): while True: # 停車階段 print(f開(kāi)始停車當(dāng)前時(shí)間: {env.now}) yield env.timeout(5) # 行駛階段 print(f開(kāi)始行駛當(dāng)前時(shí)間: {env.now}) yield env.timeout(2) env simpy.Environment() env.process(car(env)) env.run(until15)邏輯說(shuō)明car是個(gè)無(wú)限循環(huán)生成器env.timeout(5)讓進(jìn)程暫停 5 個(gè)時(shí)間單位env.timeout(2)再暫停 2 個(gè)。env.run(until15)的意思是讓仿真時(shí)鐘一直推進(jìn)到 15 為止進(jìn)程會(huì)循環(huán)執(zhí)行停車和行駛的動(dòng)作。參數(shù)until可以設(shè)成整數(shù)也可以是datetime.timedelta對(duì)象后者常用于真實(shí)日期時(shí)間的仿真。跑這段代碼會(huì)看到時(shí)間點(diǎn) 0、5、7、12、14 交替出現(xiàn)這就是環(huán)境按事件時(shí)間戳調(diào)度進(jìn)程的直觀效果。一個(gè)重要認(rèn)知進(jìn)程的執(zhí)行順序不取決于env.process()的調(diào)用順序而取決于事件被喚醒的時(shí)間順序。哪怕你先注冊(cè)了后到的進(jìn)程只要它的喚醒時(shí)間靠后它就必須等前面的時(shí)間事件先處理完。理解這一點(diǎn)對(duì)調(diào)試多進(jìn)程交互非常重要。2.2 事件機(jī)制yield 暫停與恢復(fù)的底層邏輯事件Event是 SimPy 的基本調(diào)度單位所有能讓進(jìn)程暫停的結(jié)構(gòu)本質(zhì)上都是 Event。最常用的是env.timeout(delay)它在env.now delay時(shí)刻觸發(fā)。還有event.succeed()手動(dòng)觸發(fā)的同步事件以及simpy.Resource的資源請(qǐng)求事件。進(jìn)程通過(guò)yield event來(lái)「等待」事件一旦觸發(fā)進(jìn)程從yield處恢復(fù)繼續(xù)往下執(zhí)行。import simpy def event_example(env): print(f當(dāng)前時(shí)間: {env.now}) # 創(chuàng)建一個(gè)超時(shí)事件 event env.timeout(5) # 等待事件觸發(fā) yield event print(f超時(shí)事件觸發(fā)當(dāng)前時(shí)間: {env.now}) env simpy.Environment() env.process(event_example(env)) env.run()邏輯說(shuō)明env.timeout(5)拿到的是一個(gè)事件對(duì)象yield event把進(jìn)程掛起直到事件觸發(fā)。當(dāng)仿真環(huán)境處理到這個(gè)超時(shí)事件時(shí)進(jìn)程被喚醒并繼續(xù)執(zhí)行后續(xù)代碼。注意env.run()不帶參數(shù)時(shí)會(huì)一直運(yùn)行到所有活躍進(jìn)程全部終止對(duì)這個(gè)例子來(lái)說(shuō)事件處理完進(jìn)程即結(jié)束仿真自然停止。參數(shù)層面timeout的延遲單位完全由你的模型自定義。你可以約定 1 個(gè)單位 1 分鐘也可以 1 小時(shí)SimPy 本身不做單位換算。這點(diǎn)從建模第一天就必須明確不然后期分析和真實(shí)系統(tǒng)對(duì)比數(shù)據(jù)時(shí)容易出偏差。我一般會(huì)在腳本頭部寫(xiě)清楚時(shí)間單位的注釋并統(tǒng)一用同一個(gè)單位常量。3. Resource 與隨機(jī)建模排隊(duì)系統(tǒng)的核心套路3.1 Resource代表服務(wù)臺(tái)、醫(yī)生、工位這些有限容量資源離散事件仿真里最常遇到的場(chǎng)景就是排隊(duì)顧客到收銀臺(tái)結(jié)賬、患者到診室看病、工件到機(jī)床加工。這類系統(tǒng)的共同點(diǎn)是資源有限、需求隨機(jī)。SimPy 的 Resource 組件正是為此設(shè)計(jì)的它內(nèi)部維護(hù)一個(gè)容量計(jì)數(shù)和等待隊(duì)列capacity參數(shù)決定同時(shí)可服務(wù)的數(shù)量。import simpy import random # 平均顧客到達(dá)間隔時(shí)間 ARRIVAL_INTERVAL 5 # 平均服務(wù)時(shí)間 SERVICE_TIME 3 def customer(env, name, counter): print(f{name} 于 {env.now} 到達(dá)超市) with counter.request() as req: yield req print(f{name} 于 {env.now} 開(kāi)始接受服務(wù)) service_time random.expovariate(1 / SERVICE_TIME) yield env.timeout(service_time) print(f{name} 于 {env.now} 完成服務(wù)離開(kāi)超市) def customer_arrivals(env, counter): i 0 while True: yield env.timeout(random.expovariate(1 / ARRIVAL_INTERVAL)) i 1 env.process(customer(env, f顧客 {i}, counter)) env simpy.Environment() counter simpy.Resource(env, capacity1) env.process(customer_arrivals(env, counter)) env.run(until20)邏輯說(shuō)明customer進(jìn)程用with counter.request() as req申請(qǐng)收銀臺(tái)資源申請(qǐng)成功后才進(jìn)入服務(wù)狀態(tài)。with塊的好處是無(wú)論正常結(jié)束還是中途異常資源都會(huì)自動(dòng)釋放這是 SimPy 最推薦的寫(xiě)法。customer_arrivals進(jìn)程用指數(shù)分布生成隨機(jī)到達(dá)間隔每到一個(gè)顧客就調(diào)env.process()創(chuàng)建新進(jìn)程。capacity1表示只有一個(gè)收銀臺(tái)如果改成 3 就意味著三個(gè)收銀臺(tái)并行服務(wù)這是實(shí)驗(yàn)參數(shù)里最值得調(diào)優(yōu)的變量。這里有個(gè)容易被忽略的細(xì)節(jié)request()返回的請(qǐng)求對(duì)象在進(jìn)入with塊時(shí)可能不會(huì)立即滿足——如果當(dāng)前資源被占滿進(jìn)程在這個(gè)yield處掛起進(jìn)入資源的等待隊(duì)列。資源空閑時(shí)按隊(duì)列順序喚醒對(duì)應(yīng)的進(jìn)程。SimPy 默認(rèn)是 FIFO 隊(duì)列PriorityResource則支持按優(yōu)先級(jí)插隊(duì)這在后面的高級(jí)章節(jié)會(huì)展開(kāi)。3.2 隨機(jī)分布與仿真參數(shù)為什么用指數(shù)分布來(lái)模擬到達(dá)大多數(shù)排隊(duì)系統(tǒng)里顧客到達(dá)過(guò)程不是均勻間隔的。進(jìn)店的客人總是一陣一陣來(lái)有時(shí)連續(xù)兩三個(gè)人有時(shí)五分鐘沒(méi)人。指數(shù)分布模擬這種「無(wú)記憶性」的隨機(jī)到達(dá)最合適——它對(duì)應(yīng)泊松過(guò)程的到達(dá)間隔每個(gè)時(shí)間點(diǎn)到達(dá)概率恒定與已經(jīng)等了多少時(shí)間無(wú)關(guān)。代碼里random.expovariate(1 / ARRIVAL_INTERVAL)的lambd參數(shù)是到達(dá)率1 / 5表示平均每 5 分鐘來(lái)一個(gè)。import simpy import random import numpy as np import matplotlib.pyplot as plt ARRIVAL_INTERVAL 5 SERVICE_TIME 3 SIMULATION_TIME 100 waiting_times [] def customer(env, name, server): arrival_time env.now with server.request() as req: yield req waiting_time env.now - arrival_time waiting_times.append(waiting_time) service_duration random.expovariate(1 / SERVICE_TIME) yield env.timeout(service_duration) def customer_arrivals(env, server): i 0 while True: yield env.timeout(random.expovariate(1 / ARRIVAL_INTERVAL)) i 1 env.process(customer(env, f顧客 {i}, server)) env simpy.Environment() server simpy.Resource(env, capacity1) env.process(customer_arrivals(env, server)) env.run(untilSIMULATION_TIME) # 用 NumPy 計(jì)算平均等待時(shí)間 average_waiting_time np.mean(waiting_times) print(f平均等待時(shí)間: {average_waiting_time}) # 用 Matplotlib 繪制等待時(shí)間直方圖 plt.hist(waiting_times, bins20, edgecolork) plt.xlabel(等待時(shí)間) plt.ylabel(顧客數(shù)量) plt.title(顧客等待時(shí)間直方圖) plt.show()邏輯說(shuō)明這段代碼在上一例基礎(chǔ)上增加了數(shù)據(jù)收集——waiting_times列表記錄每個(gè)顧客從到達(dá)資源請(qǐng)求到真正獲得服務(wù)的等待時(shí)長(zhǎng)。仿真結(jié)束后用 NumPy 算平均值用 Matplotlib 畫(huà)直方圖看分布形態(tài)。參數(shù)until100把仿真時(shí)長(zhǎng)拉長(zhǎng)到 100 個(gè)時(shí)間單位為了讓采樣量足夠大統(tǒng)計(jì)結(jié)果才有代表性。參數(shù)選擇上要注意SERVICE_TIME 3是平均服務(wù)時(shí)長(zhǎng)指數(shù)分布的方差等于均值平方所以波動(dòng)很大。真實(shí)場(chǎng)景中如果想模擬更穩(wěn)定的服務(wù)時(shí)間可以用正態(tài)分布random.gauss或三角分布需要根據(jù)實(shí)測(cè)數(shù)據(jù)的形態(tài)來(lái)定。做敏感性分析時(shí)一般會(huì)把這幾個(gè)參數(shù)設(shè)為變量跑多組取對(duì)比。4. 三個(gè)復(fù)雜系統(tǒng)案例拆解從模型假設(shè)到可運(yùn)行代碼4.1 醫(yī)院急診室分診護(hù)士和醫(yī)生的兩級(jí)排隊(duì)急診室的典型場(chǎng)景是患者到達(dá)后先分診、再就診。分診護(hù)士是第一個(gè)瓶頸醫(yī)生資源是第二個(gè)瓶頸。建模的核心是先畫(huà)流程、再定資源容量和到達(dá)分布。模型假設(shè)患者平均每 6 分鐘到一位分診服務(wù)時(shí)間服從均值為 2 的指數(shù)分布醫(yī)生接診時(shí)間均值為 10 分鐘共有 2 名醫(yī)生。import simpy import random ARRIVAL_INTERVAL 6 TRIAGE_TIME 2 DOCTOR_TIME 10 NUM_DOCTORS 2 SIM_TIME 480 # 模擬一個(gè) 8 小時(shí)班次 def patient(env, name, triage_nurse, doctor): arrival_time env.now # 分診階段 with triage_nurse.request() as req: yield req yield env.timeout(random.expovariate(1 / TRIAGE_TIME)) # 候診階段等醫(yī)生 with doctor.request() as req: yield req yield env.timeout(random.expovariate(1 / DOCTOR_TIME)) total_time env.now - arrival_time print(f{name} 總耗時(shí) {total_time:.1f}) def patient_arrivals(env, triage_nurse, doctor): i 0 while True: yield env.timeout(random.expovariate(1 / ARRIVAL_INTERVAL)) i 1 env.process(patient(env, f患者 {i}, triage_nurse, doctor)) env simpy.Environment() triage_nurse simpy.Resource(env, capacity1) doctor simpy.Resource(env, capacityNUM_DOCTORS) env.process(patient_arrivals(env, triage_nurse, doctor)) env.run(untilSIM_TIME)邏輯說(shuō)明患者進(jìn)程先請(qǐng)求分診護(hù)士資源完成后立刻請(qǐng)求醫(yī)生資源。兩級(jí)資源串行構(gòu)成了兩級(jí)隊(duì)列。capacityNUM_DOCTORS的調(diào)參意義在于把NUM_DOCTORS從 2 改成 3 再跑一遍對(duì)比總耗時(shí)的變化就能判斷新增醫(yī)生的邊際收益。常見(jiàn)做法是記錄每個(gè)患者的total_time到列表里最后畫(huà)直方圖看長(zhǎng)尾在哪。這個(gè)模型里沒(méi)有區(qū)分患者危重程度實(shí)際項(xiàng)目建議把患者分類為輕癥和重癥用PriorityResource讓重癥優(yōu)先插隊(duì)。4.2 交通路口信號(hào)燈周期和車輛排隊(duì)等待交通路口仿真的核心是信號(hào)燈狀態(tài)切換對(duì)車輛到達(dá)/離開(kāi)節(jié)奏的影響。用一個(gè) Simulation 模擬主干道單路口車輛隨機(jī)到達(dá)停車線紅燈亮?xí)r排隊(duì)等待綠燈亮?xí)r依次通過(guò)。信號(hào)燈周期固定紅燈 30 秒、綠燈 40 秒循環(huán)切換。import simpy RED_TIME 30 GREEN_TIME 40 ARRIVE_INTERVAL 3 SIM_TIME 600 def traffic_light(env, green_event): while True: # 紅燈階段紅燈不單獨(dú)建模綠色事件未觸發(fā)即禁止通行 yield env.timeout(RED_TIME) # 綠燈亮觸發(fā)事件喚醒所有等待車輛 green_event.succeed() green_event env.event() yield env.timeout(GREEN_TIME) green_event env.event() def car(env, name, green_event): print(f{name} 在 {env.now} 到達(dá)路口) yield green_event print(f{name} 在 {env.now} 通過(guò)路口) def car_arrivals(env, green_event): i 0 while True: yield env.timeout(ARRIVE_INTERVAL) i 1 env.process(car(env, f車輛 {i}, green_event)) env simpy.Environment() green_event env.event() env.process(traffic_light(env, green_event)) env.process(car_arrivals(env, green_event)) env.run(untilSIM_TIME)邏輯說(shuō)明green_event是手動(dòng)管理的事件對(duì)象。紅燈期間到達(dá)的車輛執(zhí)行yield green_event掛起。綠燈亮?xí)rgreen_event.succeed()會(huì)喚醒所有等待車輛然后重建一個(gè)新事件對(duì)象供下一輪使用。注意succeed()只能觸發(fā)一次所以必須在每個(gè)周期都創(chuàng)建新的事件實(shí)例。參數(shù)ARRIVE_INTERVAL和RED_TIME、GREEN_TIME的比例直接決定擁堵程度可調(diào)的實(shí)驗(yàn)杠桿是綠燈時(shí)長(zhǎng)分配。這個(gè)模型做了簡(jiǎn)化所有車輛到達(dá)即排隊(duì)不考慮通過(guò)時(shí)間也不區(qū)分左轉(zhuǎn)直行。如果要模擬多車道和多相位建議把信號(hào)燈狀態(tài)機(jī)拆成獨(dú)立的 Process每個(gè)相位用env.timeout推進(jìn)相位切換時(shí)更新通行事件。邏輯雖然復(fù)雜但核心思路還是「掛起等待 事件喚醒」。4.3 供應(yīng)鏈系統(tǒng)庫(kù)存消耗、補(bǔ)貨觸發(fā)與安全庫(kù)存驗(yàn)證供應(yīng)鏈仿真的典型問(wèn)題是確定再訂貨點(diǎn)和訂貨量庫(kù)存太低容易缺貨太高占用資金。一個(gè)簡(jiǎn)化的倉(cāng)庫(kù)模型需求隨機(jī)到達(dá)每單消耗 1 件庫(kù)存庫(kù)存低于再訂貨點(diǎn)時(shí)觸發(fā)補(bǔ)貨補(bǔ)貨提前期固定 5 天記錄缺貨次數(shù)和平均庫(kù)存水位。import simpy import random INITIAL_STOCK 20 REORDER_POINT 10 ORDER_QTY 30 LEAD_TIME 5 DEMAND_INTERVAL 2 SIM_TIME 100 stock INITIAL_STOCK reorder_in_transit 0 stock_out_count 0 stock_levels [] def demand_process(env): global stock, reorder_in_transit, stock_out_count while True: yield env.timeout(random.expovariate(1 / DEMAND_INTERVAL)) stock_levels.append(stock) if stock 0: stock - 1 else: stock_out_count 1 # 達(dá)到再訂貨點(diǎn)且沒(méi)有在途訂單觸發(fā)補(bǔ)貨 if stock REORDER_POINT and reorder_in_transit 0: reorder_in_transit 1 env.process(replenishment(env)) def replenishment(env): global stock, reorder_in_transit yield env.timeout(LEAD_TIME) stock ORDER_QTY reorder_in_transit 0 env simpy.Environment() env.process(demand_process(env)) env.run(untilSIM_TIME) print(f缺貨次數(shù): {stock_out_count}) print(f平均庫(kù)存: {sum(stock_levels) / len(stock_levels):.1f})邏輯說(shuō)明用全局變量stock維護(hù)庫(kù)存水位。reorder_in_transit是個(gè)布爾標(biāo)志防止重復(fù)下單。stock REORDER_POINT觸發(fā)補(bǔ)貨邏輯LEAD_TIME是下單到入庫(kù)的延遲用來(lái)模擬真實(shí)供應(yīng)鏈的滯后。每次需求到達(dá)后記錄庫(kù)存快照仿真結(jié)束計(jì)算平均庫(kù)存。調(diào)整REORDER_POINT和ORDER_QTY這兩個(gè)參數(shù)觀察缺貨率變化就是這個(gè)模型的核心實(shí)驗(yàn)。這里有個(gè)容易被忽略的細(xì)節(jié)庫(kù)存統(tǒng)計(jì)的時(shí)間加權(quán)問(wèn)題。每步固定間隔采樣的stock_levels是離散快照如果需求不是均勻到達(dá)均值會(huì)和真實(shí)時(shí)間加權(quán)平均有一些偏差。更精確的做法是在每次庫(kù)存變化時(shí)記錄變化時(shí)刻和變化值用時(shí)間加權(quán)公式計(jì)算平均庫(kù)存。小規(guī)模模型用快照法問(wèn)題不大跑敏感性分析時(shí)建議換成事件驅(qū)動(dòng)的統(tǒng)計(jì)方式。5. 仿真常見(jiàn)問(wèn)題與避坑記錄五個(gè)翻車現(xiàn)場(chǎng)5.1 進(jìn)程死循環(huán)env.run() 永遠(yuǎn)跑不完現(xiàn)象設(shè)置了until100程序卻像卡死一樣一直不結(jié)束CPU 占用率拉滿。原因進(jìn)程內(nèi)的while True循環(huán)里有yield env.timeout(0)或直接yield一個(gè)已觸發(fā)的事件事件循環(huán)不停地把零時(shí)延事件塞回隊(duì)列仿真時(shí)間始終不前進(jìn)。這在場(chǎng)景里最常出現(xiàn)在那些忘了加超時(shí)時(shí)間的分支邏輯上。解決檢查所有yield語(yǔ)句確認(rèn)每次循環(huán)至少有一個(gè)正時(shí)長(zhǎng)的env.timeout()。排查時(shí)可以臨時(shí)在進(jìn)程開(kāi)頭加計(jì)數(shù)器超過(guò)某個(gè)閾值就拋異常退出。我一般會(huì)在每個(gè)while True循環(huán)的入口打一行日志跑到哪個(gè)分支立刻能看出來(lái)。5.2 隨機(jī)結(jié)果不可復(fù)現(xiàn)兩次跑同一模型結(jié)果差很大現(xiàn)象改了一個(gè)參數(shù)后重跑發(fā)現(xiàn)連改之前的結(jié)果都對(duì)不上了很難判斷效果到底是參數(shù)引起的還是隨機(jī)波動(dòng)引起的。原因沒(méi)有固定隨機(jī)種子。random.expovariate每次調(diào)用都用系統(tǒng)熵源生成結(jié)果兩次運(yùn)行從第一分鐘起就分道揚(yáng)鑣了。解決在腳本開(kāi)頭調(diào)用random.seed(42)。生產(chǎn)級(jí)實(shí)驗(yàn)應(yīng)該把種子作為命令行參數(shù)每輪實(shí)驗(yàn)記錄種子值以便回歸。這樣做敏感性分析時(shí)每個(gè)參數(shù)組合都用相同的隨機(jī)數(shù)序列對(duì)比才有意義。5.3 with 塊縮進(jìn)錯(cuò)誤統(tǒng)計(jì)數(shù)據(jù)被提前記錄了現(xiàn)象等待時(shí)間的列表里出現(xiàn)很多 0 值明顯不符合直覺(jué)。原因把統(tǒng)計(jì)等待時(shí)間的append語(yǔ)句寫(xiě)在了with server.request()之前進(jìn)程還在排隊(duì)呢就已經(jīng)把等待時(shí)間記錄成 0 了。解決記錄邏輯必須放在yield req之后——只有yield返回了資源才算真正拿到此刻env.now - arrival_time才是真實(shí)等待時(shí)長(zhǎng)。拿不準(zhǔn)的時(shí)候把打印語(yǔ)句放在不同位置跑一次短仿真日志順序一目了然。5.4 事件對(duì)象復(fù)用導(dǎo)致?tīng)顟B(tài)丟失現(xiàn)象手動(dòng)管理的事件在第二個(gè)周期失效進(jìn)程在應(yīng)該被喚醒時(shí)繼續(xù)掛起。原因event.succeed()調(diào)用后事件就進(jìn)入觸發(fā)狀態(tài)不能用于下一輪。沒(méi)有為每個(gè)周期創(chuàng)建新的事件實(shí)例導(dǎo)致后續(xù)車輛永遠(yuǎn)等一個(gè)已經(jīng)觸發(fā)過(guò)的事件。解決在觸發(fā)后立刻重新賦值一個(gè)新事件對(duì)象像交通燈示例里那樣green_event env.event()。凡是手動(dòng)管理的周期事件都要養(yǎng)成「觸發(fā)即重建」的習(xí)慣。5.5 時(shí)間單位不統(tǒng)一參數(shù)看起來(lái)對(duì)但結(jié)果離譜現(xiàn)象改成真實(shí)數(shù)據(jù)后仿真結(jié)果比實(shí)際情況大幾百倍或者小幾百倍。原因有人把到達(dá)間隔寫(xiě)成「5 分鐘」服務(wù)時(shí)間寫(xiě)成「3 秒」直接混用沒(méi)換算仿真輸出完全不可解釋。解決在腳本頭部定義明確的單位常量并寫(xiě)注釋例如TIME_UNIT 1 # 1 單位 1 分鐘所有參數(shù)都基于這個(gè)單位換算。如果項(xiàng)目需要跨系統(tǒng)對(duì)比建議用datetime.timedelta作為底層表示SimPy 官方支持只是速度慢一些。6. 最后一塊拼圖事件組合、優(yōu)先級(jí)資源與可視化驗(yàn)證6.1 事件組合與條件等待從「等一個(gè)」到「等一組」SimPy 支持用和|組合事件。event1 event2表示兩個(gè)事件都觸發(fā)后才會(huì)喚醒等待進(jìn)程event1 | event2表示任一事件觸發(fā)即可喚醒。這在建?!傅任锪系轿煌瑫r(shí)等設(shè)備空閑」這類并聯(lián)條件時(shí)非常實(shí)用。import simpy def machine_repair(env): # 物料到達(dá)和維修工空閑兩個(gè)條件同時(shí)滿足才開(kāi)工 parts_ready env.timeout(4) worker_free env.timeout(6) yield parts_ready worker_free print(f維修開(kāi)始當(dāng)前時(shí)間: {env.now}) env simpy.Environment() env.process(machine_repair(env)) env.run()邏輯說(shuō)明parts_ready在時(shí)刻 4 觸發(fā)worker_free在時(shí)刻 6 觸發(fā)組合事件要等兩者都完成所以env.now輸出是 6。如果用|就會(huì)變成 4——誰(shuí)先到都算滿足。注意組合事件的觸發(fā)時(shí)間只取決于較快或較慢的那個(gè)邊界不能用來(lái)表達(dá)「還需要再等一段固定時(shí)間」的語(yǔ)義那種情況還是得用env.timeout。6.2 優(yōu)先級(jí)資源與可視化驗(yàn)證PriorityResource是Resource的子類request(priority1)可以指定優(yōu)先級(jí)。數(shù)字越小優(yōu)先級(jí)越高高優(yōu)先級(jí)請(qǐng)求會(huì)排在低優(yōu)先級(jí)前面搶占資源。醫(yī)院分診里給危重患者高優(yōu)先級(jí)、供應(yīng)鏈里給關(guān)鍵客戶訂單提前插隊(duì)都是典型用法。from simpy.resources.resource import PriorityResource resource PriorityResource(env, capacity1) # 低優(yōu)先級(jí)請(qǐng)求先到高優(yōu)先級(jí)后到但高優(yōu)先級(jí)先獲得資源 env.process(low_priority_process(env, resource)) env.process(high_priority_process(env, resource))可視化方面記住一個(gè)原則先看分布再看均值。plt.hist(waiting_times, bins20)看長(zhǎng)尾plt.boxplot(waiting_times)看離群點(diǎn)plt.plot(env.now_list, stock_levels)看隨時(shí)間的變化趨勢(shì)。多組參數(shù)對(duì)比時(shí)把多條折線畫(huà)在同一張圖里一眼就能看出瓶頸在哪組參數(shù)下消失。從那以后我每次跑仿真都強(qiáng)制走一遍固定種子、時(shí)間單位注釋、事件實(shí)例重建檢查這三步再做任何參數(shù)實(shí)驗(yàn)心里都有底。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取