病毒擴散仿真程序火了,其實模型很簡單

前幾天,天氣回暖,陽光和煦,雖然疫情依然嚴重,但是已經震懾不住某些人蠢蠢欲動的內心了,這不 B 站上的一大佬就用技術來告訴人們 “為什麼現在還沒到出門的時候?”,然後這位大佬就火了。

病毒擴散仿真程序火了,其實模型很簡單

圖片來自 Pexels

事情是這樣的,B 站 UP 主 @ele實驗室,用了一夜的時間,寫了一個簡單的疫情傳播仿真程序,告訴大家在家待著的重要性,視頻如下:

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:

<code>/***以(400,400)為城市中心,在方圓100單位長度以內,*偽隨機(近似正態分佈)出5000人;*如果person的x軸座標超過了700,則就按700算(為了限制到一定範圍內)*/privatePersonPool(){Citycity=newCity(400,400);for(inti=0;i<5000;i++){/***random.nextGaussian()*返回均值0.0和標準差1的偽隨機(近似)正態分佈的double。*/Randomrandom=newRandom();intx=(int)(100*random.nextGaussian()+city.getCenterX());inty=(int)(100*random.nextGaussian()+city.getCenterY());if(x>700){x=700;}Personperson=newPerson(city,x,y);personList.add(person);}}/<code>

並根據 ORIGINAL_COUNT=50:初始感染數量,初始化 50 個感染者(狀態為 S.SHADOW 的 P):

<code>List<person>people=PersonPool.getInstance().getPersonList();for(inti=0;i<constants.original>/<person>/<code>


模型運行

啟動之後模型就開始模擬人員流動,模擬病毒隨人群如何傳播,以及醫院如何收治,我這裡著重講解一下。

①模擬人員流動

首先要知道,P 是否流動與 P 的狀態 S 和流動意願值有關係,如果 S=S.FREEZE(也即被醫院隔離)則無法流動,如果 P 不想動則也不會流動。其中這裡流動意願值如何計算的呢?

個人流動意願值=流動意向平均值+隨機流動意向:

<code>publicbooleanwantMove(){//流動意向平均值+隨機流動意向doublevalue=sig*newRandom().nextGaussian()+Constants.u;returnvalue>0;}/<code>

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 到達目的地後會在隨機產生下一個目的地,然後以同樣的算法趨近目的地。

<code>privatevoidaction(){//已隔離,無法行動if(state==State.FREEZE){return;}//不想動,也無法行動if(!wantMove()){return;}//如果還沒有行動過,或者目標地已經到達,則重新隨機產生下一個目標地if(moveTarget==null||moveTarget.isArrived()){doubletargetX=targetSig*newRandom().nextGaussian()+targetXU;doubletargetY=targetSig*newRandom().nextGaussian()+targetYU;moveTarget=newMoveTarget((int)targetX,(int)targetY);}/***dX:目標地與當前位置的相對x軸座標差*dY:目標地與當前位置的相對y軸座標差*length:目標地與當前位置的距離*/intdX=moveTarget.getX()-x;intdY=moveTarget.getY()-y;doublelength=Math.sqrt(Math.pow(dX,2)+Math.pow(dY,2));//如果目標地與當前位置誤差在1步長內,則視為已經到達目的地if(length<1){moveTarget.setArrived(true);return;}//否則,縮小每次移動的步長,控制在(1,根號2)以內intudX=(int)(dX/length);if(udX==0&&dX!=0){if(dX>0){udX=1;}else{udX=-1;}}intudY=(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);}/<code> 

因為有沒有感染病毒,有沒有隔離病毒,其實都是和人有關係,所以模擬病毒傳播其實就是模擬 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(潛伏者):

<code>publicvoidupdate(){//已隔離,狀態不更新if(state>=State.FREEZE){return;}//若已確診時長超過醫院反應時間,則表示此確診者已被隔離到醫院if(state==State.CONFIRMED&&MyPanel.worldTime-confirmedTime>=Constants.HOSPITAL_RECEIVE_TIME){Bedbed=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(Personperson:people){if(person.getState()==State.NORMAL){continue;}/***Random().nextFloat()*用於獲取下一個從這個偽隨機數生成器的序列中均勻分佈的0.0和1.0之間的float值*/floatrandom=newRandom().nextFloat();//隨機float值小於傳播率,且與感染者安全距離小於SAFE_DIST時,此人就會別感染if(random<constants.broad>/<person>/<code>


調節參數來模擬效果

我們上面提到了啟動仿真所需的那些參數:

<code>publicclassConstants{publicstaticintORIGINAL_COUNT=50;//初始感染數量publicstaticfloatBROAD_RATE=0.8f;//傳播率publicstaticfloatSHADOW_TIME=140;//潛伏時間publicstaticintHOSPITAL_RECEIVE_TIME=10;//醫院收治響應時間publicstaticintBED_COUNT=1000;//醫院床位publicstaticfloatu=0.99f;//流動意向平均值}/<code>

根據模擬效果可以明顯看出來,流動意願平均值是一個很重要的參數,即使是傳播率較大,醫院資源緊缺,潛伏期較長的情況下,只要大家都不出門,有效控制人群流動,那麼疫情很快就可以被消滅。

所以“防疫的中堅力量其實是廣大的人民群眾,忍一時風平浪靜,別在往出去跑給國家添麻煩了!”

模型優化

其實這個模型並不複雜,簡單總結一下:

  • 這裡模擬的是一個城市,且城市模型是理想化的。
  • 人群分佈是偽隨機正態分佈的。
  • 人的流動模型很簡單,就是一個點向另一個點以小步長趨近。
  • 病毒傳播模型就是根據一定概率加上安全距離的限定來模擬人傳人。
  • 醫院收治模型就是根據感染時長和確診時長來模擬收治。

針對這幾個點,想到的優化思路:

  • 多個城市中心(這也是程序作者的意見之一)。
  • 人群分佈可以調參,可以根據實際情況來確定分佈密度。
  • 在加上收治病人治癒出院的情況,更加符合實際。
  • 病毒傳染更加科學準確的模型(因為一個人染上病是多方面因素的綜合疊加)。


分享到:


相關文章: