前几天在B站看见某up主用java编写了一个病毒扩散仿真器,当时就在寻思用Python它不香吗?于是说干就干!
文件目录展示
本项目GUI部分是用PyQt5实现,并且使用了正态分布模拟群体分布以及群体运动轨迹。
在这里插入图片描述
演示成果
仿真器可以对多个数据进行模拟,包括健康者人数、潜伏期人数、发病者人数、已经隔离的人数、已经死亡的人数、空余床位、继续床位、病毒传播率、病毒潜伏期、医院收治响应时间、医院当前床位、安全距离、平均流动意向。
运行run.py,如果不进行设置,程序会利用初始值进行模拟,初始发病人数为50人,群体数为5000人。
在这里插入图片描述
中间区域的若干个点表示处于各种状态的群体,白色的表示健康、黄色表示潜伏期、红色表示发病、黑色表示死亡。右侧的竖条表示医院的床位,初始值是100。如果用参数值进行模拟,100张床位很快就会被填满,结果显示不到3个月病毒在人群中就会大爆发,很快红点就会遍布人群,如下图所示:
在这里插入图片描述
通过改变参数来模拟相应的措施,将床位数适当扩大,流动意向设置为负数
在这里插入图片描述
可以看到不到8个月的时间疫情彻底结束,当然最后得到的结果取决于设置的参数,千万不要觉得很诧异。
在这里插入图片描述
代码的实现
主要说下如何绘制市民的状态,绘制的工作通过drawing.py文件的Drawing类来完成。该类是QWidget的子类,这也就意味着Drawing类本身是PyQt5的一个组件。与按钮、标签类似。只是并不需要往Drawing上放置任何子组件,只要在Drawing上绘制各种图形即可。Drawing类中paintEvent方法的代码如下:
def paintEvent(self, event): qp = QPainter() qp.begin(self) # 绘制城市的各种状态的市民 self.drawing(qp) qp.end()
在绘制图像前,需要创建QPainter对象,然后调用QPainter对象的begin方法,结束绘制后,需要调用QPainter对象的end方法。代码中的drawing方法用于完成具体的绘制工作。仿真器可以模拟5000个市民的状态,所以需要用5000个小矩形来表示这5000个市民。也就是在drawing方法中需要绘制这5000个表示市民的小矩形。代码如下:
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) ... ...
在上面的代码中,通过Persons对象的persons属性获取表示市民的对象(Person对象)列表。并在循环中根据Person对象的状态设置小矩形的颜色,以及分别统计不同人群的数量,这些数量会显示在仿真器右侧的组件中。最后,使用drawRect方法绘制表示每一个市民的小矩形。这样就绘制了当前状态的5000个市民。当然,这些状态要不断更新。这里使用线程每100毫秒刷新一次,这些功能在refresh.py文件的Refresh类中,代码如下:
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
每次刷新Drawing,需要调用update方法,调用该方法后,Drawing就会调用自身的paintEvent方法重新绘制整个组件的内容。在paintEvent方法中,还调用了Person对象的update方法,用于不断更新每一个人的状态,这些状态会根据多个参数进行协调。该方法属于Person类,代码如下:
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 < Params.broad_rate and self.distance(person) < Params.safe_distance: self.be_infected() break
update方法主要就是根据在params.py中的各种参数变量,以及随机值,计算下一次状态中潜伏期人数、感染人数、被隔离人数等数据,并且在每次刷新页面时更新这些数据。
源码加群:850591259