前几天在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中的各种参数变量,以及随机值,计算下一次状态中潜伏期人数、感染人数、被隔离人数等数据,并且在每次刷新页面时更新这些数据。
閱讀更多 有趣的程序媛 的文章