Ele实验室发布 病毒传播模型代码解读

说是代码解读,其实就是看到视频里面用线程跑起来的代码挺有意思的,于是在Ele实验室的公众号中把代码clone了下来并读了读把相关内容大致读通

事先声明:本文代码版权是ele实验室的哈哈哈哈哈哈,我只是一个菜菜的代码+注释搬运工

下面我们来讲解一下这个看上去十分有意思的项目是怎么简单实现的
首先附代码

//变量类
public class Constants {
    public static int ORIGINAL_COUNT=50;//初始感染数量
    public static float BROAD_RATE = 0.8f;//传播率
    public static float SHADOW_TIME = 10;//潜伏时间
    public static int HOSPITAL_RECEIVE_TIME=10;//医院收治响应时间
    public static int BED_COUNT=100;//医院床位
    public static float u=0f;//流动意向平均值
}
//床位类
public class Bed extends Point{
    public Bed(int x, int y) {
        super(x, y);
    }
    private boolean isEmpty=true;
    public boolean isEmpty() {
        return isEmpty;
    }
    public void setEmpty(boolean empty) {
        isEmpty = empty;
    }
}
//城市类
public class City {
    //城市有中心位置,设置中心的X,Y坐标参数
    private int centerX;
    private int centerY;
    public City(int centerX, int centerY) {
        this.centerX = centerX;
        this.centerY = centerY;
    }
    public int getCenterX() {
        return centerX;
    }
    public void setCenterX(int centerX) {
        this.centerX = centerX;
    }
    public int getCenterY() {
        return centerY;
    }
    public void setCenterY(int centerY) {
        this.centerY = centerY;
    }
}
//医院类
public class Hospital {
    //医院中心位置
    private int x=800;
    private int y=110;
    //医院的平面位置
    private int width;
    private int height=606;
    //get,set方法
    public int getWidth() {
        return width;
    }
    public int getHeight() {
        return height;
    }
    public int getX() {
        return x;
    }
    public int getY() {
        return y;
    }
    //单例
    private static Hospital hospital = new Hospital();
    public static Hospital getInstance(){
        return hospital;
    }
    //床位的初始点
    private Point point = new Point(800,100);
    //医院的床位
    private List<Bed> beds = new ArrayList<>();
    //构造方法初始化
    private Hospital() {
    	//根据初始化床位设置
        if(Constants.BED_COUNT==0){
            width=0;
            height=0;
        }
        //设置床位位置
        //每列100个床位,保存床位的列数
        int column = Constants.BED_COUNT/100;
        width = column*6;
        //循环设置床位
        for(int i=0;i<column;i++){
            for(int j=10;j<=610;j+=6){
                Bed bed = new Bed(point.getX()+i*6,point.getY()+j);
                beds.add(bed);
            }
        }
    }
    //设置挑选空床位的方法
    public Bed pickBed(){
        for(Bed bed:beds){
            if(bed.isEmpty()){
                return bed;
            }
        }
        return null;
    }
}
//移动目标点类
public class MoveTarget {
    //坐标
    private int x;
    private int y;
    //是否到达参数
    private boolean arrived=false;
    //构造方法
    public MoveTarget(int x, int y) {
        this.x = x;
        this.y = y;
    }
    //get,set方法
    public int getX() {
        return x;
    }
    public void setX(int x) {
        this.x = x;
    }
    public int getY() {
        return y;
    }
    public void setY(int y) {
        this.y = y;
    }
    public boolean isArrived() {
        return arrived;
    }
    public void setArrived(boolean arrived) {
        this.arrived = arrived;
    }
}
import javax.swing.*;
import java.awt.*;
import java.util.List;

/**
 * @ClassName: MyPanel
 * @Description: TODO
 * @author: Bruce Young
 * @date: 2020年02月02日 17:03
 */
public class MyPanel extends JPanel implements Runnable {
    //存储当前更新到的下标位置
    private int pIndex=0;
    //无参构造方法
    public MyPanel() { this.setBackground(new Color(0x444444));
    }
    @Override
    //重绘方法实现,具体请见下面的长解析
    public void paint(Graphics arg0) {
        //调用父类重绘方法
        super.paint(arg0);
        //draw border
        //边缘绘画
        arg0.setColor(new Color(0x00ff00));
        arg0.drawRect(Hospital.getInstance().getX(),Hospital.getInstance().getY(),
                Hospital.getInstance().getWidth(),Hospital.getInstance().getHeight());
        //人口
        List<Person> people = PersonPool.getInstance().getPersonList();
        if(people==null){
            return;
        }
        //这句话我没懂到底有啥意义,下面每一次都对当前人口进行状态的迭代更新了还要一个个再更新一次做什么
        people.get(pIndex).update();
        //对人口列表中的人进行颜色的设置
        for(Person person:people){
            //根据不同的状态进行颜色设置
            switch (person.getState()){
                case Person.State.NORMAL:{
                    arg0.setColor(new Color(0xdddddd));

                }break;
                case Person.State.SHADOW:{
                    arg0.setColor(new Color(0xffee00));

                }break;
                case Person.State.CONFIRMED:
                case Person.State.FREEZE:{
                    arg0.setColor(new Color(0xff0000));

                }break;
            }
            //更新
            person.update();
            //画点
            arg0.fillOval(person.getX(), person.getY(), 3, 3);
        }
        pIndex++;
        if(pIndex>=people.size()){
            pIndex=0;
        }
    }
    //时间初始化
    public static int worldTime=0;
    @Override
    public void run() {
        while (true) {
            //重绘
            this.repaint();
            //线程睡眠,可以使变化可视化
            try {
                Thread.sleep(100);
                worldTime++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
import java.util.List;
import java.util.Random;

/**
 * @ClassName: Person
 * @Description: TODO
 * @author: Bruce Young
 * @date: 2020年02月02日 17:05
 */
//人类
public class Person {
    //所在城市类
    private City city;
    //所在位置
    private int x;
    private int y;
    //移动目标
    private MoveTarget moveTarget;
    //是否染病的标志位
    int sig=1;
    //目标变量设置
    double targetXU;
    double targetYU;
    double targetSig=50;
    //创建一个状态接口存储相关参数,其为一个病例从疑似到治愈(这里使用到了状态机的概念,具体请看下面的长解析)
    public interface State{
        int NORMAL = 0;
        //疑似
        int SUSPECTED = NORMAL+1;
        //潜伏
        int SHADOW = SUSPECTED+1;
        //确诊
        int CONFIRMED = SHADOW+1;
        //隔离(冰点)
        int FREEZE = CONFIRMED+1;
        //治愈
        int CURED = FREEZE+1;
    }
    //构造方法
    public Person(City city, int x, int y) {
        this.city = city;
        this.x = x;
        this.y = y;
        //设置目标初始位置
        targetXU = 100*new Random().nextGaussian()+x;
        targetYU = 100*new Random().nextGaussian()+y;
    }
    //设置移动意向类
    public boolean wantMove(){
        double value = sig*new Random().nextGaussian()+Constants.u;
        return value>0;
    }
    //设置状态参数,初始化为正常
    private int state=State.NORMAL;
    //设置get,set方法
    public int getState() {
        return state;
    }
    public void setState(int state) {
        this.state = state;
    }
    public int getX() {
        return x;
    }
    public void setX(int x) {
        this.x = x;
    }
    public int getY() {
        return y;
    }
    public void setY(int y) {
        this.y = y;
    }
    //设置感染时间与确诊时间
    int infectedTime=0;
    int confirmedTime=0;
    //设置方法判断是否感染
    public boolean isInfected(){
        return state>=State.SHADOW;
    }
    //设置传染方法
    public void beInfected(){
        state = State.SHADOW;
        infectedTime=MyPanel.worldTime;
    }
    //设置求距离方法
    public double distance(Person person){
        return Math.sqrt(Math.pow(x-person.getX(),2)+Math.pow(y-person.getY(),2));
    }
    //设置隔离方法
    private void freezy(){
        state = State.FREEZE;
    }
    //设置移动方法
    private void moveTo(int x,int y){
        this.x+=x;
        this.y+=y;
    }
    //设置动作
    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);
        }
        //变量存储目标点距离当前点的水平距离、垂直距离和距离
        int dX = moveTarget.getX()-x;
        int dY = moveTarget.getY()-y;
        double length=Math.sqrt(Math.pow(dX,2)+Math.pow(dY,2));
        //设置是否到达
        if(length<1){
            moveTarget.setArrived(true);
            return;
        }
        //设置每一步的位移变量
        //水平距离平均
        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&&udY!=0){
            if(dY>0){
                udY=1;
            }else{
                udY=-1;
            }
        }
        //边缘条件判断
        if(x>700){
            moveTarget=null;
            if(udX>0){
                udX=-udX;
            }
        }
        //移动
        moveTo(udX,udY);
//        if(wantMove()){
//        }
    }
    
    //设置安全距离
    private float SAFE_DIST = 2f;
    
    //状态机更新
    public void update(){
        //@TODO找时间改为状态机
        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;
        //如果确定被感染(状态在shadow之上)
        if(state>=State.SHADOW){
            return;
        }
        //对人口资源表中的人进行迭代
       for(Person person:people){
           //判断状态
           if(person.getState()== State.NORMAL){
               continue;
           }
           //设置一个随机概率阈值
           float random = new Random().nextFloat();
           //判断是否被感染
           if(random<Constants.BROAD_RATE&&distance(person)<SAFE_DIST){
               this.beInfected();
           }
       }
    }
}
//人口资源类
public class PersonPool {
    //单例模式:具体讲解请见下面的长解读第二条
    private static PersonPool personPool = new PersonPool();
    //对象创建方法
    public static PersonPool getInstance(){
        return personPool;
    }
    List<Person> personList = new ArrayList<Person>();
    //人口列表创建方法
    public List<Person> getPersonList() {
        return personList;
    }
    //构造方法设置
    private PersonPool() {
        //城市类
        City city = new City(400,400);
        //使用正态分布创建散点
        //循环5000次
        for (int i = 0; i < 5000; i++) {
            Random random = new Random();
            //nextGaussion方法生成正态分布的数,让散点以城市中心为心随机散开
            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);
        }
    }
}
//点类
public class Point {
    private int x;
    private int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public int getX() {
        return x;
    }
    public void setX(int x) {
        this.x = x;
    }
    public int getY() {
        return y;
    }
    public void setY(int y) {
        this.y = y;
    }
}
//main函数
public class Main {
    public static void main(String[] args) {
        //面板类
        MyPanel p = new MyPanel();
        //使用构造方法中的Runable单参构造,具体内容请看长解读第一条
        Thread panelThread = new Thread(p);
        //JFrame窗口类
        JFrame frame = new JFrame();
        //将MyPanel这个JPanel加到frame窗口上
        frame.add(p);
        //设置相关内容
        //大小
        frame.setSize(1000, 800);
        //居中
        frame.setLocationRelativeTo(null);
        //可视化
        frame.setVisible(true);
        //关闭设置
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //线程开启
        panelThread.start();

        //获取人口列表
        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();
        }
    }
}

整体思路

本模型在人群分布与人的移动上都使用了正态分布(高斯分布) 来进行模拟,是一个相对较为简单的模型

		//nextGaussion方法生成正态分布的数,让散点以城市中心为心随机散开
        int x = (int) (100 * random.nextGaussian() + city.getCenterX());
        int y = (int) (100 * random.nextGaussian() + city.getCenterY());
        //设置目标点位置,使用正态随机模拟去设置目标点
        double targetX = targetSig*new Random().nextGaussian()+targetXU;
        double targetY = targetSig*new Random().nextGaussian()+targetYU;
        moveTarget = new MoveTarget((int)targetX,(int)targetY);

1、每个人都分配城市,位置,状态等相关信息,使用线程 进行5000例人口的模拟,同时减速可以让大家对过程看的更为清晰;

2、人口都存到一个列表中,使用PersonPool来设置这个人口池的方法与参数;

3、个人使用状态机的概念设置不同状态(疑似,潜伏,感染,隔离,治愈),同时根据不同的状态分配不同的方法;潜伏一段时间会升级成确诊,确诊之后分配医院Hospital,床位Bed;

//有关状态修改的代码
    //创建一个状态接口存储相关参数,其为一个病例从疑似到治愈(这里使用到了状态机的概念,具体请看下面的长解析)
    public interface State{
        int NORMAL = 0;
        //疑似
        int SUSPECTED = NORMAL+1;
        //潜伏
        int SHADOW = SUSPECTED+1;
        //确诊
        int CONFIRMED = SHADOW+1;
        //隔离(冰点)
        int FREEZE = CONFIRMED+1;
        //治愈
        int CURED = FREEZE+1;
    }
    //设置状态参数,初始化为正常
    private int state=State.NORMAL;
    //设置方法判断是否感染
    public boolean isInfected(){
        return state>=State.SHADOW;
    }
    //设置传染方法
    public void beInfected(){
        state = State.SHADOW;
        infectedTime=MyPanel.worldTime;
    }
    //设置隔离方法
    private void freezy(){
        state = State.FREEZE;
    }
    //设置动作
    private void action(){
        //如果是在隔离中或者不想移动
        if(state==State.FREEZE){
            return;
        }
   }
    //设置安全距离
    private float SAFE_DIST = 2f;
    //状态机更新
    public void update(){
        //@TODO找时间改为状态机
        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;
        //如果确定被感染(状态在shadow之上)
        if(state>=State.SHADOW){
            return;
        }
        //对人口资源表中的人进行迭代
       for(Person person:people){
           //判断状态
           if(person.getState()== State.NORMAL){
               continue;
           }
           //设置一个随机概率阈值
           float random = new Random().nextFloat();
           //判断是否被感染
           if(random<Constants.BROAD_RATE&&distance(person)<SAFE_DIST){
               this.beInfected();
           }
       }
    }
}

4、床位设置大小6*6,医院设置宽根据床位数判定,每列100个床位,高度600;医院由平面区域进行模拟;医院遍历找到没有占用的床位来进行床位的分配直到床位无剩余

5、每个时间循环,感染的人都会找到附近的还没有感染的人进行感染,如果找到的人已经感染,则链式寻找这个找到的人的附近未感染人,直到找到一个正常状态的人并进行感染

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

6、使用重绘实现颜色的绘制与移动图像的更新。每次重绘时调用update方法更新当前情况并予以显示

7、更新方法的实现思路:
①针对有关状态进行设置:
针对隔离人群不进行更新直到治愈
针对确诊病例而且医院的周转时间已过,设置医院床位进行隔离,如果没有空床位则有感染他人危险
针对潜伏人群,如果潜伏时间已到,则确诊发病
②进行正态移动模拟
③进行进一步的感染模拟

8、动作方法的实现思路:
根据正态模拟设置一个目标点,并计算与当前点的水平,垂直位移差;根据其正负进行八个方向的动作实现

9、治愈的具体实现在代码中没有参悟到,会有空接着读一读,也请大家帮忙理解一下,如果有同学理解到了请告诉我,感激不尽

参数设置

	public static int ORIGINAL_COUNT=50;//初始感染数量
    public static float BROAD_RATE = 0.8f;//传播率
    public static float SHADOW_TIME = 10;//潜伏时间
    public static int HOSPITAL_RECEIVE_TIME=10;//医院收治响应时间
    public static int BED_COUNT=1000;//医院床位
    public static float u=0f;//流动意向平均值

1、初始感染数量用来在Main函数中进行最开始的感染设置

2、传播率用于在感染判断时与安全距离一起作为判断条件判定这个人是否在这个时间点上被感染

 //设置一个随机概率阈值
           float random = new Random().nextFloat();
           //判断是否被感染
           if(random<Constants.BROAD_RATE&&distance(person)<SAFE_DIST){
               this.beInfected();
           }

3、潜伏时间用于在更新当前情况时判定是否经过潜伏期之后确诊

 //如果当前时间距感染时超过了观察所需时间而且当前还在潜伏期
        if(MyPanel.worldTime-infectedTime>Constants.SHADOW_TIME&&state==State.SHADOW){
            //确诊发病
            state=State.CONFIRMED;
            confirmedTime = MyPanel.worldTime;
        }

4、医院收治时间用于更新当前情况时模拟确诊后医院的周转是否完成

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

5、医院床位作为Hospital类中的硬指标,存储当前床位数量

6、流动意向用于在wantMove方法中模拟当前人们的流动概率,移动意向也是采用的正态随机模拟

   //设置移动意向类
    public boolean wantMove(){
        double value = sig*new Random().nextGaussian()+Constants.u;
        return value>0;
    }

使用到的技术细节

1、Runable与Thread
在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口;Thread类和Runnable接口都是在java.lang包中定义的。

通过继承Thread类,同时重写run()方法时,由于java中只允许单继承,也就是一个类只能继承一个父类,使得该方式具有一定的局限性,而实现Runnable类接口的run()方法,再结合Thread类来实现多线程。从而便于资源的共享与实现。

具体可见Runnable和Thread比较

2、单例模式
单例模式(Singleton),也叫单子模式,是一种常用的设计模式

在应用这个模式时,单例对象的类必须保证只有一个实例存在。 许多时候,整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,显然,这种方式简化了在复杂环境下的配置管理。

特别地,在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。事实上,这些应用都或多或少具有资源管理器的功能。例如,每台计算机可以有若干个打印机,但只能有一个 Printer Spooler(单例) ,以避免两个打印作业同时输出到打印机中。再比如,每台计算机可以有若干通信端口,系统应当集中 (单例)管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

综上所述,单例模式就是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种方法。

具体实现与原理参照java单例模式

3、重绘的实现
由于当对界面进行放大,缩小,平移等操作时,界面都会重新更新,而如果不再将图形重新绘制上去的话,就会导致图形的中断,因此需要在进行动作时重绘。

重绘需要对paint方法进行重写, 其中paint方法super父类的paint方法,之后在单位时间内都会调用paint方法进行重绘,具体请见java之图形重绘

4、状态机
状态机不是一个机器,而是一个状态转移模型的构建,是一种比较常见的设计模式,由现态,条件,动作,次态等过程进行状态转移,类似于数字模拟电路中状态转移方程由次态到现态的不断转变,这样,在进行状态的更新之后,我们就可以较为正式地模拟出一个动态的过程;状态机类似的概念也是这个病毒传播模型的灵魂
具体:什么是状态机?

以上

发布了36 篇原创文章 · 获赞 11 · 访问量 2889

猜你喜欢

转载自blog.csdn.net/yeweij226/article/details/104173920