19

病毒扩散仿真程序火了,其实模型很简单!

 4 years ago
source link: http://developer.51cto.com/art/202002/610174.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前几天,天气回暖,阳光和煦,虽然疫情依然严重,但是已经震慑不住某些人蠢蠢欲动的内心了,这不 B 站上的一大佬就用技术来告诉人们 “为什么现在还没到出门的时候?”,然后这位大佬就火了。

u2qy2aE.jpg!web

图片来自 Pexels

事情是这样的,B 站 UP 主 @ele实验室,用了一夜的时间,写了一个简单的疫情传播仿真程序,告诉大家在家待着的重要性,视频如下:

GitHub 地址如下:https://github.com/KikiLetGo/VirusBroadcast

我在家闲着无聊便去把代码下载下来研究了一下,这里算是做个解析,废话不多说,我们开始。

源码结构

源码结构比较简单,我们来一起看一下:

3EZzu2Q.jpg!web

模型讲解

我对仿真模型做了一个抽象和概括,我们一起对照着源码分析模型的整个模拟过程和思路。

模型前提设置

首先,假设 C(400,400) 是城市的中心,整个城市是以 C 为中心的圆,L=100 是圆的半径。

假设 P(x,y) 就表示城市中的人,人受疫情影响有不同的状态 S:

  • S.NORMAL=0:正常。
  • S.SUSPECTED=1:疑似。
  • S.SHADOW=2:病毒携带潜伏者。
  • S.CONFIRMED=3:确诊。
  • S.FREEZE=4:隔离。
  • S.CURED=5:治愈。

对应于感染者,和确诊者分别设置 infectedTime(被感染的时刻)和 confirmedTime(确诊的时刻)。

其次,假设医院是高为 H,宽为 W 的长方形区域,其中矩形左下角坐标为 H(800,110)。

为了表示医院容量的大小,我们把 H=606 设为常量,则 W 越大表示医院的可容纳量越大(也即床位越多);然后,假设 B(x,y) 就表示位于医院内的床位。

NZJV7jE.gif

最后我们要设置一些启动参数:

  • 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(); 
        } 

模型运行

启动之后模型就开始模拟人员流动,模拟病毒随人群如何传播,以及医院如何收治,我这里着重讲解一下。

①模拟人员流动

首先要知道,P 是否流动与 P 的状态 S 和流动意愿值有关系,如果 S=S.FREEZE(也即被医院隔离)则无法流动,如果 P 不想动则也不会流动。其中这里流动意愿值如何计算的呢?

个人流动意愿值=流动意向平均值+随机流动意向:

public boolean wantMove(){ 
       //流动意向平均值+随机流动意向 
       double value = sig*new Random().nextGaussian()+Constants.u; 
       return value>0; 
   } 

P(x1,y1) 初次流动时会随机产生一个 T(x2,y2) 目标地,且 T 是限制在以 P 为圆心的一定范围内的。

那么 P 是如何向 T 流动的呢?这里不是简单的直接 moveTo(T),为了更真实模拟实际情况,P 其实是逐渐靠近 T 的。

假设 D 是 P 到 T 之间的距离,则 D = sqrt(pow(x1-x2,2)+pow(y1-y2,2)) :

  • 若 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); 
    } 

②模拟病毒传播与医院收治

因为有没有感染病毒,有没有隔离病毒,其实都是和人有关系,所以模拟病毒传播其实就是模拟 P 的状态 S 的变迁。

这里有一个前提说明:设置 worldTime 表示当前时刻,初始化为 0,JPanel 面板每刷新一次,worldTime+1。

  • 若 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

IZ7zUjY.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK