前幾天在B站看見某up主用java編寫了一個病毒擴散仿真器,當時就在尋思用Python它不香嗎?於是說幹就幹!
文件目錄展示
本項目GUI部分是用PyQt5實現,並且使用了正態分佈模擬群體分佈以及群體運動軌跡。
在這裡插入圖片描述
演示成果
仿真器可以對多個數據進行模擬,包括健康者人數、潛伏期人數、發病者人數、已經隔離的人數、已經死亡的人數、空餘床位、繼續床位、病毒傳播率、病毒潛伏期、醫院收治響應時間、醫院當前床位、安全距離、平均流動意向。
運行run.py,如果不進行設置,程序會利用初始值進行模擬,初始發病人數為50人,群體數為5000人。
在這裡插入圖片描述
中間區域的若干個點表示處於各種狀態的群體,白色的表示健康、黃色表示潛伏期、紅色表示發病、黑色表示死亡。右側的豎條表示醫院的床位,初始值是100。如果用參數值進行模擬,100張床位很快就會被填滿,結果顯示不到3個月病毒在人群中就會大爆發,很快紅點就會遍佈人群,如下圖所示:
在這裡插入圖片描述
通過改變參數來模擬相應的措施,將床位數適當擴大,流動意向設置為負數
在這裡插入圖片描述
可以看到不到8個月的時間疫情徹底結束,當然最後得到的結果取決於設置的參數,千萬不要覺得很詫異。
在這裡插入圖片描述
代碼的實現
主要說下如何繪製市民的狀態,繪製的工作通過drawing.py文件的Drawing類來完成。該類是QWidget的子類,這也就意味著Drawing類本身是PyQt5的一個組件。與按鈕、標籤類似。只是並不需要往Drawing上放置任何子組件,只要在Drawing上繪製各種圖形即可。Drawing類中paintEvent方法的代碼如下:
<code>def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
# 繪製城市的各種狀態的市民
self.drawing(qp)
qp.end()/<code>
在繪製圖像前,需要創建QPainter對象,然後調用QPainter對象的begin方法,結束繪製後,需要調用QPainter對象的end方法。代碼中的drawing方法用於完成具體的繪製工作。仿真器可以模擬5000個市民的狀態,所以需要用5000個小矩形來表示這5000個市民。也就是在drawing方法中需要繪製這5000個表示市民的小矩形。代碼如下:
<code>def drawing(self, event):
... ...
# 繪製代表市民的小矩形
persons = Persons().persons
if persons == None:
return
normal_person_count = 0
latency_person_count = 0
confirmed_person_count = 0
freeze_person_count = 0
death_person_count = 0
# 掃描內一個人的狀態
for person in persons:
if person.state == NORMAL:
# 健康人
qp.setPen(Qt.white)
normal_person_count += 1
elif person.state == LATENCY:
# 潛伏期感染者
qp.setPen(QColor(255,238,0))
latency_person_count += 1
elif person.state == CONFIRMED:
# 確診患者
qp.setPen(Qt.red)
confirmed_person_count += 1
elif person.state == FREEZE:
# 已隔離者
qp.setPen(QColor(72, 255, 252))
freeze_person_count += 1
elif person.state == DEATH:
# 死亡患者
qp.setPen(Qt.black)
death_person_count += 1
person.update() # 更新每一個人的狀態
bed_half_size = Hospital().bed_size // 2
rect = QRect(person.x - bed_half_size, person.y - bed_half_size,Hospital().bed_size//2, Hospital().bed_size//2)
brush = QBrush(Qt.SolidPattern)
brush.setColor(qp.pen().color())
qp.setBrush(brush)
qp.drawRect(rect)
... .../<code>
在上面的代碼中,通過Persons對象的persons屬性獲取表示市民的對象(Person對象)列表。並在循環中根據Person對象的狀態設置小矩形的顏色,以及分別統計不同人群的數量,這些數量會顯示在仿真器右側的組件中。最後,使用drawRect方法繪製表示每一個市民的小矩形。這樣就繪製了當前狀態的5000個市民。當然,這些狀態要不斷更新。這裡使用線程每100毫秒刷新一次,這些功能在refresh.py文件的Refresh類中,代碼如下:
<code>from PyQt5.QtCore import *
from params import *
class Refresh(QThread):
def __init__(self, drawing):
super(Refresh, self).__init__()
self.drawing = drawing
def run(self):
while not Params.success:
try:
QThread.msleep(100)
# 刷新Drawing
self.drawing.update()
Params.current_time += 1
except:
pass/<code>
每次刷新Drawing,需要調用update方法,調用該方法後,Drawing就會調用自身的paintEvent方法重新繪製整個組件的內容。在paintEvent方法中,還調用了Person對象的update方法,用於不斷更新每一個人的狀態,這些狀態會根據多個參數進行協調。該方法屬於Person類,代碼如下:
<code> def update(self):
# 如果已經隔離或者死亡了,就不需要處理了
if self.state == FREEZE or self.state == DEATH:
return
# 處理已經確診的感染者(即患者)
if self.state == CONFIRMED and self.dead_time == 0:
destiny = random.randrange(1,10001) # 幸運數字,[1,10000]隨機數
if destiny >= 1 and destiny <= int(Params.fatality_rate * 10000):
# 幸運數字落在死亡區間
dt = int(sp.random.normal(Params.dead_time,Params.dead_variance))
self.dead_time = self.confirmed_time + self.dead_time
else:
self.dead_time = -1 # 逃過了死神的魔爪
if self.state == CONFIRMED and Params.current_time - self.confirmed_time >= Params.hospital_receive_time:
# 如果患者已經確診,且(世界時刻-確診時刻)大於醫院響應時間,即醫院準備好病床了,可以抬走了
bed = Hospital().pick_bed() # 查找空床位
if bed == None:
# 沒有空床位,報告需求床位數
if not self.need_bed:
Hospital().need_bed_count += 1
self.need_bed = True
else:
# 安置病人
self.used_bed = bed
self.state = FREEZE
self.x = bed.x + Hospital().bed_size // 2
self.y = bed.y + Hospital().bed_size // 2
if self.need_bed and Hospital().need_bed_count > 0:
Hospital().need_bed_count -= 1
bed.is_empty = False
# 處理病死者
if (self.state == CONFIRMED or self.state == FREEZE) and Params.current_time >= self.dead_time and self.dead_time > 0:
self.state = DEATH # 患者死亡
personpool.Persons().latency_persons.remove(self) # 已經死亡,無法傳染別人,需要從確診者中刪除
Hospital().empty_bed(self.used_bed) # 騰出床位
if Hospital().need_bed_count > 0:
Hospital().need_bed_count -= 1
# 增加一個正態分佈用於潛伏期內隨機發病時間
latency_symptom_time = sp.random.normal(Params.virus_latency / 2,25)
# 處理發病的潛伏期感染者
if Params.current_time - self.infected_time > latency_symptom_time and self.state == LATENCY:
self.state = CONFIRMED # 潛伏者發病
self.confirmed_time = Params.current_time # 刷新確診時間
# 處理未隔離者的移動問題
self.action()
# 處理健康人被感染的問題
persons = personpool.Persons().persons
# 不是健康人,返回
if self.state >= LATENCY:
return
# 通過一個隨機幸運值和安全距離決定感染其他人
latency_persons = personpool.Persons().latency_persons.copy()
for person in latency_persons:
random_value = random.random()
if random_value self.be_infected()
break/<code>
update方法主要就是根據在params.py中的各種參數變量,以及隨機值,計算下一次狀態中潛伏期人數、感染人數、被隔離人數等數據,並且在每次刷新頁面時更新這些數據。
閱讀更多 有趣的程序媛 的文章