文章目录
憋疯了?在家宅不下去了?想出去透透气了?
千万别!
虽然你对疫情已经麻木了,觉得没什么大碍了。
但现实并非如此,抗疫战斗仍在继续,还没有达到钟南山等专家所说的爆发期。
如果大家现在要是出门,真的是在疫情防控添乱!
不信?一名程序员,连夜打造的计算机仿真程序,模拟新冠病毒传播,在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 * new Random().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) {
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;
}
}
}
最后,希望大家能够多点耐心,身体健康最重要。等疫情过去,再撒欢儿玩~