程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

憋瘋了?在家宅不下去了?想出去透透氣了?


千萬別!


雖然你對疫情已經麻木了,覺得沒什麼大礙了。


但現實並非如此,抗疫戰鬥仍在繼續,還沒有達到鍾南山等專家所說的爆發期。


程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

如果大家現在要是出門,真的是在疫情防控添亂!


不信?一名程序員,連夜打造的計算機仿真程序,模擬新冠病毒傳播,在B站上播放量已達到數百萬:


程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

視頻鏈接:https://www.bilibili.com/video/av86478875


它告訴我們:


如果現在出門逛,迎接我們的,就是疫情越來越難控制的局面。


本文對火爆B站的仿真程序進行了深度解讀:


一、JFrame面板組件佈局


程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?


二、數學概念:高斯(正態)分佈


為什麼要講高斯分佈?


病毒傳播代碼最精華部分,最精彩部分,最能體現仿真模擬的就是這個高斯分佈的代碼,在本章儘量也最簡單的話語為大家簡單的介紹一下高斯分佈的理論。


2.1 高斯分佈概念


一個非常常見的連續概率分佈。正態分佈在統計學上十分重要,經常用在自然和社會科學來代表一個不明的隨機變量。


例如說人的體重、身高、某種疾病的患病年齡、城市人口的分佈,基本都是符合高斯分佈的統計學應用。高斯分佈的公式為:

程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

縱觀整個高斯公式,有兩個很重要的參數:μ(可以諧音為 "謬"),σ(發音為sigma);那麼跟大家解釋一下μ為平均值,σ為標準差。


標準差決定了整個樣本數據的分佈密度,標準差越小,數據越集中,如下圖非常直觀的描述的μ與σ的關係,以及σ對整個概率分佈的影響。


程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?


另外,如果μ=0, σ=1的時候,就稱之為標準高斯分佈。而X軸落在整個⾼斯曲線內的值,專業的說法為服從高斯分佈。


2.2 正態變量的標準化


正態變量的標準化是高斯公式的一個重要推導,這裡直接給出結論:


v 為服從高斯分佈的數據

σ 為標準差

μ 為平均值

W 對v進行標準化處理後的數據,依然是服從高斯分佈的


正態變量的標準化公式:


W = (V - μ) / σ


那麼V值的計算公式為:


V = W * σ + μ


2.3 高斯分佈在Java中的應用


java.util.Random函數有個方法叫做nextGaussian()函數,定義如下:


/*

* @return the next pseudorandom, Gaussian ("normally") distributed

* {@code double} value with mean {@code 0.0} and

* standard deviation {@code 1.0} from this random number

* generator's sequence

*/

synchronized public double nextGaussian() {

// See Knuth, ACP, Section 3.4.1 Algorithm C. if(haveNextNextGaussian) {

haveNextNextGaussian = false; return nextNextGaussian;

} else {


double v1, v2, s;

do {

v1 = 2 * nextDouble() - 1; // between -1 and 1

v2 = 2 * nextDouble() - 1; // between -1 and 1 s = v1 * v1 + v2 *v2;

} while (s >= 1 || s == 0);

double multiplier = StrictMath.sqrt(-2 * StrictMath.log(s)/s);nextNextGaussian = v2 * multiplier;

haveNextNextGaussian = true;

return v1 * multiplier;

}

}


根據API的描述:返回一個double類型的值,這個值服從均值為0,均方差為1的標準正態分佈。


然後根據正態變量的標準化推導公式:


double value = sigma * new Random().nextGaussian() + 0.99;


三、核心代碼解讀


3.1啟動類函數:


程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?


3.2畫布相關代碼


初始化畫布:


程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

如上圖所示的MyPanel 類實現了Runnable接口:


程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

重寫的run方法如下:


程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

如上圖所示的MyPanel.this.repaint()方法為一個鉤子函數,會調用MyPanel類中法paint方法,paint方法會重新設置人員的狀態,數據的變更,醫院的床位等。


在paint方法中會調用這個person.update()方法,這個方法至關重要。在後續有一節專門介紹。


3.3初始化感染人員


程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

代碼中會看到PersonPool這個類,這個類中有人員池這樣一個個靜態變量,在加載時候去初始化城市人口:

程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

PersonPool的構造函數:

程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

3.4Person類

程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

Person的構造方法:

程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

為了方便大家理解,給出下面這張圖:

程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

Person類中的wantMove法的實現:

程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

Person 類中distance 方法的實現,用以判斷是否能被感染:

程序員用代碼程序建立模型讓你知道 特殊時期要如何防護?

3.5Person中的action方法


action方法是一個至關重要的方法,故單獨提出為一個章節,該方法決定了用戶座標的移動,方法如下:


/**

* 不同狀態下的單個人實例運動行為

*/

private void action() {

if (state == State.FREEZE || state == State.DEATH) { 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;

}

/**

* 如果沒有到達目標,一步一步的走,根據座標方向,如果為正方向,就往前移動

1,

* 如果座標軸的反方向,就移動

-1

* udx的結果只可能為兩種三種情況:

-1 1 0

*/

int udX = (int) (dX / length);

//x軸移動步,符號為沿x軸前進方向

if (udX == 0 && dX != 0) {

if (dX > 0) {

udX = 1;

} else { udX = -1;

}

}

int udY = (int) (dY / length);

//y軸移動步,符號為沿x軸前進方向

if (udY == 0 && dY != 0) {

if (dY > 0) { udY = 1;

} else { udY = -1;

}

}

// 如果超過邊界,就往回走

if (x > 700) {

//這個700也許是x方向邊界的意思,因為畫布大小

1000x800

//TODO:如果是邊界那麼似乎邊界判斷還差一個y方向

moveTarget = null;

if (udX > 0) { udX = -udX;

}

}


3.6Person中的update方法


update方法是一個至關重要的方法,故單獨提出為一個章節,該方法決定了根據用戶不同的狀態決定如何處理,方法如下:


/**

* 對各種狀態的人進行不同的處理

*/

public void update() {

//@TODO找時間改為狀態機

if (state == State.FREEZE || state == State.DEATH) { return;

//如果已經隔離或者死亡了,就不需要處理了

}

//處理已經確診的感染者(即患者)

if (state == State.CONFIRMED && dieMoment == 0) {

int destiny = new Random().nextInt(10000)+1;

//命運數字[1,10000]隨機數

if (1 <= destiny && destiny <= (int)(Constants.FATALITY_RATE* 10000))

{

//如果命運數字落在死亡區間

int dieTime = (int) (Constants.DIE_VARIANCE * newRandom().nextGaussian()+Constants.DIE_TIME);

dieMoment = confirmedTime + dieTime;

//發病後確定死亡時刻

//System.out.printf("%d,%f,%d\\n",destiny,Constants.FATALITY_RATE * 10000,dieTime);

}

else {

dieMoment = -1;

//逃過了死神的魔手

}

}

//*/

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((state == State.CONFIRMED || state == State.FREEZE )&&MyPanel.worldTime >= dieMoment && dieMoment > 0) {

state = State.DEATH;

//患者死亡

}

//處理發病的潛伏期感染者

if (MyPanel.worldTime - infectedTime > Constants.SHADOW_TIME &&state == State.SHADOW) {

state = State.CONFIRMED;

//潛伏者發病

confirmedTime = MyPanel.worldTime;

//刷新時間

}

//處理病死者

if((state == State.CONFIRMED || state == State.FREEZE )&&MyPanel.worldTime >= dieMoment && dieMoment > 0) {

state = State.DEATH;

//患者死亡

}

//處理發病的潛伏期感染者

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) {/<person>

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

// 如果被感染了,就繼續判斷下一個

break;

}

}

}


最後,希望大家能夠多點耐心,身體健康最重要。等疫情過去,再撒歡兒玩~


分享到:


相關文章: