GitHub 地址如下:https://github.com/KikiLetGo/VirusBroadcast
源碼結構
源碼結構比較簡單,我們來一起看一下:
模型講解
我對仿真模型做了一個抽象和概括,我們一起對照着源碼分析模型的整個模擬過程和思路。
模型前提設置
首先,假設 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;//流動意向平均值 }
根據模擬效果可以明顯看出來,流動意願平均值是一個很重要的參數,即使是傳播率較大,醫院資源緊缺,潛伏期較長的情況下,只要大家都不出門,有效控制人羣流動,那麼疫情很快就可以被消滅。
所以“防疫的中堅力量其實是廣大的人民羣衆,忍一時風平浪靜,別在往出去跑給國家添麻煩了!”
模型優化
其實這個模型並不複雜,簡單總結一下:
- 這裏模擬的是一個城市,且城市模型是理想化的。
- 人羣分佈是僞隨機正態分佈的。
- 人的流動模型很簡單,就是一個點向另一個點以小步長趨近。
- 病毒傳播模型就是根據一定概率加上安全距離的限定來模擬人傳人。
- 醫院收治模型就是根據感染時長和確診時長來模擬收治。
針對這幾個點,想到的優化思路:
- 多個城市中心(這也是程序作者的意見之一)。
- 人羣分佈可以調參,可以根據實際情況來確定分佈密度。
- 在加上收治病人治癒出院的情況,更加符合實際。
- 病毒傳染更加科學準確的模型(因爲一個人染上病是多方面因素的綜合疊加),作者嘗試使用DNN對病毒進行建模(包括病毒變異)。