摘要:本文首先会解释一下到底什么是"冠状病毒",以及杀死"冠状病毒"的方法。然后会利用Python实现一个"冠状病毒"传播仿真器,来演示一下为何“不出门“ +“疯狂建医院”会间接杀死病毒(动态模拟了从发生疫情,到疫情结束的整个过程)。以及如果控制不好,会有什么后果(一定是很严重的)。


????相信大家现在一定和我一样,无比郁闷。好不容易盼到过年了,买了一大堆好吃的,结果哪也去不了,只能在家消耗这些美食了。而且很多人宅在家里的每一天都会做同一个伟大的计划!!

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对象作为标准的组件放在了主界面的左侧。设计界面如下图所示:

????在设置程序中,通过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[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()


源代码下载(github)


个人简介

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