前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >冠状病毒传播仿真器(Python版本)【附源代码】

冠状病毒传播仿真器(Python版本)【附源代码】

作者头像
蒙娜丽宁
发布2020-04-13 12:35:03
1.2K0
发布2020-04-13 12:35:03
举报
文章被收录于专栏:极客起源极客起源极客起源

本文首先会解释一下到底什么是"冠状病毒",以及杀死"冠状病毒"的方法。然后会利用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()

源代码下载

https://github.com/geekori/virus_covid19

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-02-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 极客起源 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档