解析B站UP主Ele实验室的疫情传播仿真程序

2,981 阅读8分钟

物料准备

最近天气回暖,阳光和煦,虽然疫情依然严重,但是已经震慑不住某些人蠢蠢欲动的内心了,这不B站上的一大佬就用技术来告诉人们 “为什么现在还没到出门的时候?”,然后这位大佬就火了。我放个链接,不知道读者可以看看蛮有趣的:

  1. 计算机仿真程序告诉你为什么现在还没到出门的时候!!!(B站UP主Ele实验室)

  2. Github地址

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

源码结构

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

模型讲解

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

模型前提设置

首先,假设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)就表示位于医院内的床位;

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

  • 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;//流动意向平均值
}

根据模拟效果可以明显看出来,流动意愿平均值是一个很重要的参数,即使是传播率较大,医院资源紧缺,潜伏期较长的情况下,只要大家都不出门,有效控制人群流动,那么疫情很快就可以被消灭。

所以“防疫的中坚力量其实是广大的人民群众,忍一时风平浪静,别在往出去跑给国家添麻烦了!”

模型优化

其实这个模型并不复杂,简单总结一下

  • 这里模拟的是一个城市,且城市模型是理想化的
  • 人群分布是伪随机正态分布的
  • 人的流动模型很简单,就是一个点向另一个点以小步长趋近
  • 病毒传播模型就是根据一定概率加上安全距离的限定来模拟人传人
  • 医院收治模型就是根据感染时长和确诊时长来模拟收治

针对这几个点,想到的优化思路:

  • 多个城市中心(这也是程序作者的意见之一)
  • 人群分布可以调参,可以根据实际情况来确定分布密度
  • 在加上收治病人治愈出院的情况,更加符合实际
  • 病毒传染更加科学准确的模型(因为一个人染上病是多方面因素的综合叠加)