病毒扩散仿真程序火了,其实模型很简单!
前几天,天气回暖,阳光和煦,虽然疫情依然严重,但是已经震慑不住某些人蠢蠢欲动的内心了,这不 B 站上的一大佬就用技术来告诉人们 “为什么现在还没到出门的时候?”,然后这位大佬就火了。
事情是这样的,B 站 UP 主 @ele实验室,用了一夜的时间,写了一个简单的疫情传播仿真程序,告诉大家在家待着的重要性,视频如下:
https://github.com/KikiLetGo/VirusBroadcast
源码结构
源码结构比较简单,我们来一起看一下:
模型讲解
模型前提设置
S.NORMAL=0:正常。
S.SUSPECTED=1:疑似。
S.SHADOW=2:病毒携带潜伏者。
S.CONFIRMED=3:确诊。
S.FREEZE=4:隔离。
S.CURED=5:治愈。
int ORIGINAL_COUNT=50:初始感染数量。
float BROAD_RATE=0.8f:传播率。
float SHADOW_TIME=140:潜伏时间。
int HOSPITAL_RECEIVE_TIME=10:医院收治响应时间。
int BED_COUNT=1000:医院床位。
float u=0.99f:流动意向平均值。
模型启动初始化
模型启动时,我们在以 C 为中心 L 为半径的圆内随机产生 5000 个 P:
/**
* 以(400,400)为城市中心,在方圆100单位长度以内,
* 伪随机(近似正态分布)出5000人;
* 如果person的x轴坐标超过了700,则就按700算(为了限制到一定范围内)
*/
private PersonPool() {
City city = new City(400,400);
for (int i = 0; i < 5000; i++) {
/**
* random.nextGaussian()
* 返回均值0.0和标准差1的伪随机(近似)正态分布的double。
*/
Random random = new Random();
int x = (int) (100 * random.nextGaussian() + city.getCenterX());
int y = (int) (100 * random.nextGaussian() + city.getCenterY());
if(x>700){
x=700;
}
Person person = new Person(city,x,y);
personList.add(person);
}
}
并根据 ORIGINAL_COUNT=50:初始感染数量,初始化 50 个感染者(状态为 S.SHADOW 的 P):
List<Person> people = PersonPool.getInstance().getPersonList();
for(int i=0;i<Constants.ORIGINAL_COUNT;i++){
//生成人口规模范围内的随机整数
int index = new Random().nextInt(people.size()-1);
Person person = people.get(index);
//避免随机值碰撞
while (person.isInfected()){
index = new Random().nextInt(people.size()-1);
person = people.get(index);
}
//生成感染者
person.beInfected();
}
模型运行
①模拟人员流动
个人流动意愿值=流动意向平均值+随机流动意向:
public boolean wantMove(){
//流动意向平均值+随机流动意向
double value = sig*new Random().nextGaussian()+Constants.u;
return value>0;
}
若 D<1,则认为 P 已经到达 T。
若 D>1,则下一次 P 到达的坐标是 [(x2-x1)/|x2-x1|,(y2-y1)/|y2-y1|],其实就是超过了 -1,还没到 +1。
P 到达目的地后就不动了吗?不是的,P 到达目的地后会在随机产生下一个目的地,然后以同样的算法趋近目的地。
private void action(){
//已隔离,无法行动
if(state==State.FREEZE){
return;
}
//不想动,也无法行动
if(!wantMove()){
return;
}
//如果还没有行动过,或者目标地已经到达,则重新随机产生下一个目标地
if(moveTarget==null||moveTarget.isArrived()){
double targetX = targetSig*new Random().nextGaussian()+targetXU;
double targetY = targetSig*new Random().nextGaussian()+targetYU;
moveTarget = new MoveTarget((int)targetX,(int)targetY);
}
/**
* dX : 目标地与当前位置的相对x轴坐标差
* dY : 目标地与当前位置的相对y轴坐标差
* length : 目标地与当前位置的距离
*/
int dX = moveTarget.getX()-x;
int dY = moveTarget.getY()-y;
double length=Math.sqrt(Math.pow(dX,2)+Math.pow(dY,2));
//如果目标地与当前位置误差在1步长内,则视为已经到达目的地
if(length<1){
moveTarget.setArrived(true);
return;
}
//否则,缩小每次移动的步长,控制在(1,根号2)以内
int udX = (int) (dX/length);
if(udX==0&&dX!=0){
if(dX>0){
udX=1;
}else{
udX=-1;
}
}
int udY = (int) (dY/length);
if(udY==0&&dY!=0){
if(dY>0){
udY=1;
}else{
udY=-1;
}
}
//如果当前位置已经超出边界,则重新规划目的地,并往回走udx个步长
if(x>700){
moveTarget=null;
if(udX>0){
udX=-udX;
}
}
moveTo(udX,udY);
}
②模拟病毒传播与医院收治
若 S=S.FREEZE,则 P 已经被医院收治,已被隔离。状态不更新。
若 S=S.CONFIRMED,且 worldTime-confirmedTime>=Constants.HOSPITAL_RECEIVE_TIME,也即 P 已确诊且距确诊时间已经超过医院反应时间,则说明 P 应该被医院收治。
但是如果医院有床位,则将 P(x1,y1) 移动到 B(x2,y2),即表示已收容;如果医院没有床位了,则 P(x1,y1) 无法收容,依然参与人员流动过程。
若 S=S.SHADOW,且 worldTime-infectedTime>Constants.SHADOW_TIME,也即 P 是已被感染者,且感染期限超出潜伏期,则此时应转为 CONFIRMED(确诊)状态。
BROAD_RATE,这个是我们上面提到过的传播率参数,表示人是否被感染有一定概率。
SAFE_DIST,表示正常人和疑似者/感染者/确诊者等之间的安全距离。
当概率随机值超过 BROAD_RATE,且正常人和疑似者/感染者/确诊者等之间的距离小于 SAFE_DIST 时,正常人会被成为感染者,状态 S=S.SHADOW(潜伏者):
public void update(){
//已隔离,状态不更新
if(state>=State.FREEZE){
return;
}
//若已确诊时长超过医院反应时间,则表示此确诊者已被隔离到医院
if(state==State.CONFIRMED&&MyPanel.worldTime-confirmedTime>=Constants.HOSPITAL_RECEIVE_TIME){
Bed bed = Hospital.getInstance().pickBed();
if(bed==null){
System.out.println("隔离区没有空床位");
}else{
//被隔离起来了
state=State.FREEZE;
x=bed.getX();
y=bed.getY();
bed.setEmpty(false);
}
}
//若已感染时长超过潜伏期,则潜伏者就会确诊,确诊时间就是当前时间
if(MyPanel.worldTime-infectedTime>Constants.SHADOW_TIME&&state==State.SHADOW){
state=State.CONFIRMED;
confirmedTime = MyPanel.worldTime;
}
action();
List<Person> people = PersonPool.getInstance().personList;
if(state>=State.SHADOW){
return;
}
for(Person person:people){
if(person.getState()== State.NORMAL){
continue;
}
/**
* Random().nextFloat()
* 用于获取下一个从这个伪随机数生成器的序列中均匀分布的0.0和1.0之间的float值
*/
float random = new Random().nextFloat();
//随机float值小于传播率,且与感染者安全距离小于SAFE_DIST时,此人就会别感染
if(random<Constants.BROAD_RATE&&distance(person)<SAFE_DIST){
this.beInfected();
}
}
}
调节参数来模拟效果
我们上面提到了启动仿真所需的那些参数:
public class Constants {
public static int ORIGINAL_COUNT=50;//初始感染数量
public static float BROAD_RATE = 0.8f;//传播率
public static float SHADOW_TIME = 140;//潜伏时间
public static int HOSPITAL_RECEIVE_TIME=10;//医院收治响应时间
public static int BED_COUNT=1000;//医院床位
public static float u=0.99f;//流动意向平均值
}
模型优化
这里模拟的是一个城市,且城市模型是理想化的。
人群分布是伪随机正态分布的。
人的流动模型很简单,就是一个点向另一个点以小步长趋近。
病毒传播模型就是根据一定概率加上安全距离的限定来模拟人传人。
医院收治模型就是根据感染时长和确诊时长来模拟收治。
多个城市中心(这也是程序作者的意见之一)。
人群分布可以调参,可以根据实际情况来确定分布密度。
在加上收治病人治愈出院的情况,更加符合实际。
病毒传染更加科学准确的模型(因为一个人染上病是多方面因素的综合叠加)。
作者:熊饲
编辑:陶家龙、孙淑娟
出处:https://juejin.im/post/5e3ab6d3518825491d320a38
精彩文章推荐: