太阳不下山 发表于 2021-6-23 21:32:21

冠状病毒传播仿真器的原理和实现(Python版)

李宁老师免费视频已开通,扫码看免费视频:或直接访问:免费视频
摘要:本文首先会解释一下到底什么是"冠状病毒",以及杀死"冠状病毒"的方法。然后会利用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)  # 幸运数字,随机数
            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类以及相关的代码如下:以上就是这个病毒传播仿真器的基本实现方法,其中涉及到了大量PyQt5的知识,如果大家想详细了解PyQt5技术,可以参考我的《PyQt5(Python)开发与实战视频课程》课程。另外,《冠状病毒传播仿真器的原理和实现(Python版)》视频课程即将推出,欢迎关注。
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
        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()
源代码下载(github)
【项目实战】冠状病毒传播仿真器(Python版本)视频课程



个人简介李宁,沈阳欧瑞科技创始人,宁哥教育创始人,畅销书作家,51CTO金牌讲师,企业内训讲师,拥有超过20年软件开发和培训经验。曾经出版超过40本IT畅销书,代表作有《Python爬虫技术:深入理解原理、技术与开发》、《Python从菜鸟到高手》,制作视频课程超过1000小时,拥有学员和读者数百万。 主要使用的技术包括Python、Go、JavaScript、Java、Flutter等。目前正在带领团队开发超平台开发工具UnityMarvey,使用自研发的Ori语言,可以同时跨操作系统、数据库、云平台等多个平台,通过超布局技术实现可视化UI设计,支持客户端服务端一体化技术。
《Python爬虫技术:深入理解原理、技术与开发》已经出版


页: [1]
查看完整版本: 冠状病毒传播仿真器的原理和实现(Python版)