冠状病毒传播仿真器的原理和实现(Python版)【附源代码】
银河使者 人气:0本文摘要:
本文首先会解释一下到底什么是"冠状病毒",以及杀死"冠状病毒"的方法。然后会利用Python实现一个"冠状病毒"传播仿真器,来演示一下为何“不出门“ +“疯狂建医院”会间接杀死病毒(动态模拟了从发生疫情,到疫情结束的整个过程)。以及如果控制不好,会有什么后果(一定是很严重的)。
相信大家现在一定和我一样,无比郁闷。好不容易盼到过年了,买了一大堆好吃的,结果哪也去不了,只能在家消耗这些美食了。而且很多人宅在家里的每一天都会做同一个伟大的计划!!
那么我们为什么不能出门呢?答案大家都已经知道了,这就是一种被称为“冠状病毒”的东西在捣乱。据钟南山院士和其他一些专家推测,这种“冠状病毒”的潜伏期是14天左右,所以需要尽可能避免在14天内接触更多的人。而宅在家里是最好的选择。不过这是头一次放这么长的假,而且还必须呆在家里,也是相当郁闷,恍惚间竟想起了猴哥,500年你是怎么熬过来的呢!
下面就先来了解下到底什么是冠状病毒!
1. 什么是冠状病毒
如果详细解释什么是冠状病毒,先要从病毒讲起,估计要写一本书了,不过完全没有必要。大家只要知道一点就好。病毒不是生物(类生物,有生物的部分特征),不能独立生存,需要依赖于宿主,也就是人体的细胞。换句话说,病毒需要攻击人体的细胞才能生存。病毒可以利用细胞中的成分复制自身,从而达到繁殖的目的。如果受到感染的细胞过多,人体就会生病。
那么"冠状病毒"呢?当然也属于病毒的一种,不过它不是普通的病毒。首先看看"冠状病毒"的样子,病毒表面有很多凸起的冠(看起来还挺漂亮的),所以称为"冠状病毒"。
"冠状病毒"除了外形上与普通病毒有差异,在本质上与普通病毒也是不同的。普通病毒通常都是DNA病毒,也就是双螺旋结构。而"冠状病毒"属于RNA病毒,也就是单螺旋结构。双螺旋结构的DNA病毒更稳定,不容易变异。但RNA病毒只有一个链,非常不稳定,容易变异。这才是"冠状病毒"的杀手锏,通过变异抵抗人类的反击。目前"冠状病毒"是否变异,还很难说。
2. 如何杀死"冠状病毒"
现在的问题是,如何杀死"冠状病毒"。当然,最直接的方法是将"冠状病毒"赶出细胞,或直接从细胞中清除,不过目前人类的技术还做不到这一点。这应该属于比细胞医学更高端的医学:分子医学或原子医学。目前人类的科技水平甚至还没完全达到细胞医学的高度(最多0.7个细胞医学),因为癌症等细胞疾病还无法完全治愈。
既然病毒目前还无法直接杀死,那么这么多痊愈的病人是如何做到的呢?其实可以采用如下两种方法间接杀死"冠状病毒",也包括其他大多数病毒。
1. 利用人体内的抗体细胞(如T细胞),将被感染的细胞连同"冠状病毒"一起消灭
2. 干扰"冠状病毒"在细胞内的复制过程(例如,RNA干扰),让"冠状病毒"无法正常复制自身,也就是让"冠状病毒"没有后代,这样"冠状病毒"就会由于自身的生命终结而死亡。
目前绝大多数痊愈的病人属于第一种情况,也就是通过自身的抗体细胞(如T细胞)检测人体内被感染的细胞,然后通知这些被感染的细胞启动自毁程序,当这些细胞被销毁后,那么病毒也就被消灭了。
不过可能有的同学会问,既然病毒可以被抗体细胞消灭,那么这么多医护人员和医疗设备岂不是多余了。其实并不是多于的,而是非常必要。这是因为病毒在攻击正常细胞的同时,也让人体的抵抗力开始下降,人体内的抗体细胞是有限的,这些抗体细胞会到处救火,顾不上对付"冠状病毒"了。正是由于这些医疗器械(如呼吸机),尽可能让人体机能恢复到接近正常人的水平,这样各种抗体细胞就可以集中力量对付"冠状病毒"了。所以说,这些痊愈的病人其实是通过自身的抗体细胞消灭了"冠状病毒",而医疗器械、各种药物,医务人员的护理,其实是抗体细胞的援军。另外,由于不同人的体质不同。极个别的个体,抗体细胞非常强大,造成了"冠状病毒"无法快速复制(但仍然在复制,只是增速变慢),这也是为什么有的个体的潜伏期会超过14天的原因,但这毕竟是极少数。潜伏期是被病毒感染的细胞数量达到足以致病的时间(人体内如果只有少量的被病毒感染的细胞,是不会表现出任何症状的)。
还有就是,为什么"冠状病毒"感染者在发病时大多会有肺炎的症状,而且伴随着发烧呢?其实这就是抗体细胞在和"冠状病毒"进行较量呢!进医院隔离,是为了增强抗体细胞对抗"冠状病毒"的筹码。不过由于一些人的抗体细胞的战斗力实在太差(可能有部分人是因为年龄太大的原因),所以就算抗体细胞等来了援军,也于事无补,这些就是已经死亡的被感染者(大多是60岁以上的老年人)。所以等疫情结束后,好好锻炼身体吧,拥有强壮的身体,不能保证你不得病,但至少可以增加得病后活下来的几率。
对于目前正在研发的抗"冠状病毒"的药物主要是通过第2种方式消灭病毒的。也就是干扰病毒的复制过程,不过很遗憾,到现在为止,还没有被证明非常有效的药物可以做到这一点。
3. 对付"冠状病毒"的手段
从生物学角度,我们已经了解了"冠状病毒"的发病原理,但在现实中,如何操作呢?
其实对付"冠状病毒"以及其他大多数病毒,基本上就是基于12个字: 有症状赶快治,没症状要隔离。这也是国际上通用的原则。
前6个字容易理解,有症状了,就直接进医院了。如果没症状呢?没症状有两种情况:疑似和正常人。疑似主要是指与被感染者近距离接触,或从外地来本地的人员(输入者),由于"冠状病毒"的潜伏期是14天左右,所以这些疑似者至少需要被隔离14天才会确定是否真的被感染。而正常人只要没和被感染者近距离接触,就不会被感染。这些人之所以也需要隔离,是因为怕被别人感染。不过这里的隔离通常是在自己的家中,不出门。通过隔离,可以大幅度减少病毒感染新的人群,也就是让存量不再增加或少增加。而还有很多被感染者,这些人就需要在医院里接受治疗了。不过由于被感染者太多,所以武汉等地区快速建起了很多临时医院。 这是用来减少存量的。 当存量不但不会增加、而且在不断减少,直到被感染者为0,疑似者为0时,疫情才会彻底结束,这也是本文要介绍的病毒扩散仿真器的基本原理。
4. 用病毒扩散仿真器来演示病毒扩散和疫情结束的全过程
在实现这个仿真器之前,先来演示下这个仿真器。
仿真器可以对多个数据进行模拟,包括健康者人数、潜伏期人数、发病者人数、已经隔离的人数、已经死亡的人数、空余床位、继续床位、病毒传播率、病毒潜伏期、医院收治响应时间、医院当前床位、安全距离、平均流动意向。
启动程序,会利用初始值进行模拟,初始发病人数为50人,市民总数为5000人。如下图所示。
中间区域的若干个点表示各种状态的市民。白色的表示健康市民、黄色表示潜伏期市民、红色表示发病市民、黑色表示死亡的市民。右侧的竖条表示医院的床位,初始值是100。如果用参数值进行模拟,100张床位很快就会被填满,然后病毒在人群中就会大爆发,很快红点就会遍布人群,如下图所示
当前天数已经显示过了31天(耽误了一个月),感染者已经接近1000了,这时政府开始采取紧急措施。主要有两个:封城和关闭娱乐场所、增加医院的床位。前者是为了避免感染更多的人,后者是为了消耗被感染者的存量。所以通过下面的设置来调整参数。例如,将流动意向调整为-1.71。并且增加床位323个。
这时总床位数变成了423。这里的流动意向在-3和3之间,如果是3,表示市民的活动意愿非常强烈,例如,正好是春节时期,市民逛商场,聚会非常频繁。流动意愿越小,流动意愿就越低。这里调成-1.76,表示市民不能参加聚会、不能出城、出门需要戴口罩,但市民仍然可以在市内流动。 流动意愿远低于春节正常的值。不过尽管政府采取了一定的措施,但由于是在疫情开始后一个月才采取了紧急措施,所以病毒已经扩散了,因此,疫情并没有得到非常明显的缓解。如下图所示。 主要表现为市民仍然可以自由活动(尽管不能参加聚会),仍然存在一定的感染风险。 而且医院床位明显不足。
为了更进一步控制疫情,政府开始封闭小区,更进一步限制人员的活动,以及军方开始干预,派出了大量的医护人员以及各种医疗设备,并且建立的多个方舱医院。医院床位得到了很大的缓解。这里将参数设置成最大值来模拟这一过程,增加床位1200个,流动意向设置为-3.0,也就是说基本上市民不流动了。如下图所示。
这时看到床位已经增加到了1623,比急需的床位多了不少,而且人员趋于不流动,发病人数不断减少(都被送进了医院),而且潜伏期人数逐渐转换为发病人数,也被送进了医院,最终,潜伏期人数和发病者人数都是0,疫情结束,如下图所示。共耗费了60天。当然,实际情况没这么顺利。仿真器可以立刻增加医院床位数,可以立刻隔离人员,但在实际操作中,建立医院需要时间,隔离也需要协调,尤其是上千万人的大城市。
不过只要能做到隔离和及时就医,冠状病毒疫情结束也只是时间问题。当然,这要在这两点做的比较好的情况下,如果处理失当,那么仿真器就会呈现下图的状态,完全失控,人类将面临一场浩劫。
5. 病毒传播仿真器的实现
现在来谈谈仿真器实现的原理。仿真器使用Python和PyQt5实现。PyQt5是封装了Qt library的跨平台GUI开发库,基于Python语言。
这里主要涉及到仿真器效果绘制,以及如何模拟多个参数。先来说一下绘制市民的状态。绘制的工作通过drawing.py文件的Drawing类来完成。该类是QWidget的子类,这也就意味着Drawing类本身是PyQt5的一个组件。与按钮、标签类似。只是并不需要往Drawing上放置任何子组件。只要在Drawing上绘制各种图形即可。
在PyQt5中,任何一个QWidget的子类,都可以实现一个paintEvent方法,当组件每次刷新时,就会调用paintEvent方法重新绘制组件的内容。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中的各种参数变量,以及随机值,计算下一次状态中潜伏期人数、感染人数、被隔离人数等数据,并且在每次刷新页面时更新这些数据。
以上的描述就是如何绘制表示5000个市民的状态。右侧各种数据并不是绘制在页面上的,而是通过QtDesigner设计的右侧的界面,然后将Drawing对象作为标准的组件放在了主界面的左侧。设计界面如下图所示:
然后通过pyuic将.ui文件生成.py文件,在程序中调用即可。这些组件的更新同样是在前面给出的drawing方法中。
另外,这个仿真器还提供了动态设置参数的功能。这是通过另外一个程序实现的,两个程序通过socket通讯。这个设置程序同样是通过QtDesigner设计的,设计界面如下图所示。
在设置程序中,通过Transmission类的send_command方法向仿真器发布命令,例如,更新床位数的代码如下:
from PyQt5.QtWidgets import * from socket import * class Transmission: def __init__(self,ui): self.ui = ui self.host = 'localhost' self.port = 5678 self.addr = (self.host, self.port) # 向仿真器发布命令 def send_command(self, command, value = None): tcp_client_socket = socket(AF_INET, SOCK_STREAM) tcp_client_socket.connect(self.addr) if value == None: value = 0 data = command + ':' + str(value) tcp_client_socket.send(('%s\r\n' % data).encode(encoding='utf-8')) data = tcp_client_socket.recv(1024) result = data.decode('utf-8').strip() tcp_client_socket.close() return result # 更新床位数 def update_bed_count(self): print(self.ui.horizontalSliderBedCount.value()) result = self.send_command('add_bed_count',self.ui.horizontalSliderBedCount.value()) if result == 'ok': QMessageBox.information(self.ui.centralwidget, '消息', f'成功添加了{self.ui.horizontalSliderBedCount.value()}张床位', QMessageBox.Ok) 在仿真器端,通过Receiver以及TCPServer来接收设置程序发过来的命令,如果成功设置,返回ok。Receiver类以及相关的代码如下: from socketserver import (TCPServer as TCP,StreamRequestHandler as SRH) from common import * from params import * from hospital import * from PyQt5.QtCore import * import sys # 响应客户端请求事件的类 class MyRequestHandler(SRH): def handle(self): # 读取客户端发送的数据 data = str(self.rfile.readline(),'utf-8') index = data.find(':') command = data[:index] value = data[index + 1:] value = int(value) # 执行具体的命令 if command == 'add_bed_count': Params.hospital_bed_count += value Hospital().free_bed_count = Hospital().free_bed_count + value Hospital().compute(value) elif command == 'set_flow_intention': Params.average_flow_intention = value / 100 elif command == 'set_broad_rate': Params.broad_rate = value / 100 elif command == 'set_latency': Params.virus_latency = value * 10 elif command == 'close': Params.app.quit() self.wfile.write(b'ok\r\n') # 在线程中监听客户端的请求 class Receiver(QThread): tcp_server = None def __init__(self): super(Receiver,self).__init__() self.host = '' self.port = 5678 self.addr = (self.host,self.port) Receiver.tcp_server = TCP(self.addr, MyRequestHandler) def run(self): Receiver.tcp_server.serve_forever()
以上就是这个病毒传播仿真器的基本实现方法,其中涉及到了大量PyQt5的知识,如果大家想详细了解PyQt5技术,可以参考我的《PyQt5(Python)开发与实战视频课程》课程。另外,《冠状病毒传播仿真器的原理和实现(Python版)》视频课程即将推出,欢迎关注。
源代码下载(github)
技术支持请关注“极客起源”公众号。
《Python爬虫技术:深入理解原理、技术与开发》已经出版,敬请关注!
《冠状病毒传播仿真器(Python版)》(扫描进入课程页面)
https://ke.qq.com/course/1045964?tuin=a22a65ce
《PyQt5实战视频课程》(扫描进入课程页面)
https://ke.qq.com/course/374285?tuin=a22a65ce
加载全部内容