This commit is contained in:
Nicky Case 2020-04-24 12:59:53 -04:00
parent b7f254f151
commit b4cc21a6b3
8 changed files with 629 additions and 1248 deletions

View File

@ -36,7 +36,7 @@
<div id="label_c_recovery">
<input class="sim_checkbox" type="checkbox" id="c_recovery">
Recovers <icon r></icon> in <span id="label_p_recovery">N</span> days
Becomes <icon r></icon> in <span id="label_p_recovery">N</span> days
<br>
<input class="sim_input" type="range" id="p_recovery" min="1" max="30" step="1" value="11">
</div>
@ -54,20 +54,23 @@
<div id="section_r">
R0 is <span id="label_p_r0" toFixed="2"></span>
<input class="sim_input" type="range" id="p_r0" min="0" max="6" step="0.01" disabled>
R<sub>0</sub> is <span id="label_p_r0" toFixed="2"></span>
<canvas class="r_canvas" id="canvas_r0"></canvas>
<!--input class="sim_input" type="range" id="p_r0" min="0" max="6" step="0.01" disabled-->
<br>
<span id="label_s">
<br>
But R is changed by...
<br>
Susceptible population
<input class="sim_input" type="range" id="p_s" min="0" max="1" step="0.001" value="1" disabled>
% of people who are <i>NOT</i> <icon s></icon>
<input class="sim_input" type="range" id="p_s" min="0" max="1" step="0.001" value="0" disabled>
<div class="herd"></div>
<br>
</span>
<span id="int_block_0">
Personal Hygiene
Increased Hygiene
<br>
<input class="sim_input recordable" type="range" id="p_hygiene" min="0" max="1" step="0.001" value="0">
<br>
@ -89,14 +92,14 @@
<br>
</span>
<span id="int_block_3">
Deep Cleaning
<br>
<input class="sim_input recordable" type="range" id="p_cleaning" min="0" max="1" step="0.001" value="0">
<br>
Face Masks
<br>
<input class="sim_input recordable" type="range" id="p_masks" min="0" max="1" step="0.001" value="0">
<br>
Deep Cleaning
<br>
<input class="sim_input recordable" type="range" id="p_cleaning" min="0" max="1" step="0.001" value="0">
<br>
</span>
<span id="int_block_4">
Summer
@ -111,11 +114,10 @@
<br>
</span>
<br>
<span id="label_re">
R is now <span id="label_p_re" toFixed="2"></span>
<input class="sim_input" type="range" id="p_re" min="0" max="6" step="0.01" disabled>
<canvas class="r_canvas" id="canvas_re"></canvas>
<!--<input class="sim_input" type="range" id="p_re" min="0" max="6" step="0.01" disabled>-->
</span>
<span id="hospital_capacity">
@ -124,7 +126,7 @@
<input class="sim_input recordable" type="range" id="p_hospital" min="0" max="500" step="1" value="100">
</span>
<hr>
<hr id="divider">
</div>
@ -171,15 +173,15 @@
<span id="label_susceptible">
<icon s></icon> Susceptible<span id="show_percent_s"></span>
<br>
<!--<br>-->
</span>
<span id="label_exposed">
<icon e></icon> Exposed<span id="show_percent_e"></span>
<br>
<!--<br>-->
</span>
<span id="label_infectious">
<icon i></icon> Infectious<span id="show_percent_i"></span>
<br>
<!--<br>-->
</span>
<span id="label_removed">
<icon r></icon> Removed<span id="show_percent_r"></span>
@ -191,10 +193,10 @@
- - - Herd Immunity
</span>
<br>
<!--<br>-->
<span id="label_capacity">
&nbsp; Healthcare Capacity
Healthcare Capacity
</span>
</div>

View File

@ -67,13 +67,14 @@ let updateMonthTicks = ()=>{
}
// Sliders
let DONT_RECORD_HISTORY = true;
//let DONT_RECORD_HISTORY = true;
let _DO_NOT_RECURSE = true;
let INPUTS_WERE_CHANGED = false;
let yearsSlider = $('#p_years');
$all('.sim_input').forEach((slider)=>{
let id = slider.id;
let label = $('#label_'+id);
let isRecordable = slider.classList.contains('recordable');
//let isRecordable = slider.classList.contains('recordable');
let onChange = ()=>{
// Change label & param
@ -85,11 +86,16 @@ $all('.sim_input').forEach((slider)=>{
params[id] = val;
// Record history (@ this day)
/*
if(isRecordable){
if(!DONT_RECORD_HISTORY){
recordedHistory.push([id, val, Math.round(daysCurrent)]);
let lastRecordedEntry = recordedHistory[recordedHistory.length-1];
if(!lastRecordedEntry || id!=lastRecordedEntry[0] || Math.round(daysCurrent)!=lastRecordedEntry[2]){ // not same thing/time
recordedHistory.push([id, val, Math.round(daysCurrent)]);
}
}
}
*/
// Just to update other random crap
if(!_DO_NOT_RECURSE){
@ -112,11 +118,14 @@ $all('.sim_input').forEach((slider)=>{
sbDOM.setAttribute('label','params');
}
// HAX
INPUTS_WERE_CHANGED = true;
};
slider.oninput = onChange;
onChange();
});
DONT_RECORD_HISTORY = false;
//DONT_RECORD_HISTORY = false;
_DO_NOT_RECURSE = false;
// Checkboxes
@ -144,8 +153,8 @@ $all('.sim_checkbox').forEach((checkbox)=>{
let IS_PLAYING = false;
let recordedHistory = [];
let IS_REPLAYING_HISTORY = false;
//let recordedHistory = [];
//let IS_REPLAYING_HISTORY = false;
let START_S = 0.999,
START_E = 0.001,
@ -169,6 +178,9 @@ let restart = ()=>{
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
// Force refresh DOM
s_dom.oninput();
};
@ -246,7 +258,7 @@ sbDOM.onclick = ()=>{
changeSliders(CURRENT_STAGE.inputs);
}else if(daysCurrent>daysTotal){
_replayTheSim();
//_replayTheSim();
}else{
_resetTheSim();
}
@ -258,7 +270,7 @@ let _updateButtons = ()=>{
if(daysCurrent > daysTotal){
bbDOM.setAttribute('label','reset');
sbDOM.setAttribute('label',params.CANNOT_REPLAY_HISTORY ? '' : 'replay');
sbDOM.setAttribute('label','');
}else if(IS_PLAYING){
@ -305,18 +317,20 @@ let _hideAllControls = ()=>{
let _resetTheSim = ()=>{
_showAllControls();
IS_REPLAYING_HISTORY = false;
recordedHistory = [];
//IS_REPLAYING_HISTORY = false;
//recordedHistory = [];
restart();
IS_PLAYING = false;
};
/*
let _replayTheSim = ()=>{
_hideAllControls();
IS_REPLAYING_HISTORY = true;
//IS_REPLAYING_HISTORY = true;
restart();
IS_PLAYING = true;
};
*/
/////////////////////////////////////
// THE HAND /////////////////////////

View File

@ -3,6 +3,7 @@
/////////////////////////////////////
let int = {
non_s: 0,
hygiene: 0,
distancing: 0,
isolate: 0,
@ -14,47 +15,63 @@ let int = {
};
let daysCurrent, daysDrawn, daysTotal, daysPerFrame;
let r0_dom = $('#p_r0');
let r0, re;
//let r0_dom = $('#p_r0');
let s_dom = $('#p_s');
let re_dom = $('#p_re');
//let re_dom = $('#p_re');
let interventionStrengths = [
['non_s', 1.0],
['hygiene', 0.25],
['distancing', 0.7],
['isolate', 0.4],
['quarantine', 0.5],
['cleaning', 0.1],
['masks', 0.5], // 3.4 fold reduction (70%) (what CI?), subtract points for... improper usage? https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3591312/ // cloth masks...
['summer', 0.333] // 15°C diff * 0.0225 (Wang et al)
];
let updateModel = (days, fake)=>{
let real_S=S, real_E=E, real_I=I, real_R=R;
// Susceptible & Re
/*if(fake && s_dom.disabled){
s_dom.value = 1 - S;
}*/
if(fake && !s_dom.disabled){
//s_dom.value = 1 - S;
S = 1 - parseFloat(s_dom.value);
}
let transmissionRate = 1/params.p_transmission,
incubationRate = 1/params.p_exposed,
recoveryRate = 1/params.p_recovery,
immunityLossRate = 1/(params.p_waning*365);
// R0
r0_dom.value = transmissionRate/recoveryRate;
if(r0_dom.oninput) r0_dom.oninput();
r0 = transmissionRate/recoveryRate;
/*r0_dom.value = transmissionRate/recoveryRate;
if(r0_dom.oninput) r0_dom.oninput();*/
// Transmission affected by interventions
int.non_s = 1 - S;
int.hygiene = params.p_hygiene;
transmissionRate *= 1 - int.hygiene*0.5;
int.distancing = params.p_distancing;
transmissionRate *= 1 - int.distancing*0.7;
int.isolate = params.p_isolate;
transmissionRate *= 1 - int.isolate*0.4;
int.quarantine = params.p_quarantine;
transmissionRate *= 1 - int.quarantine*0.5;
int.cleaning = params.p_cleaning;
transmissionRate *= 1 - int.cleaning*0.1;
int.masks = params.p_masks;
transmissionRate *= 1 - int.masks*0.1;
int.summer = (1 - Math.cos((daysCurrent-30)/365 * Math.TAU))/2;
//int.summer = Math.max(0, int.summer);
int.summer *= params.p_summer;
transmissionRate *= 1 - int.summer* 0.333; // 15°C diff * 0.0225 (Wang et al)
interventionStrengths.forEach((isPair,i)=>{
if(i==0) return; // DON'T double count S
transmissionRate *= 1 - (int[isPair[0]] * isPair[1]);
});
// Vaccination...
if(S > 1-params.p_vaccines){
@ -66,7 +83,7 @@ let updateModel = (days, fake)=>{
// S...
if(!s_dom.disabled){
S = parseFloat(s_dom.value);
S = 1 - parseFloat(s_dom.value);
}
// Update Model
@ -103,11 +120,12 @@ let updateModel = (days, fake)=>{
if(I>1) I=1;
// Susceptible & Re
if(s_dom.disabled){
s_dom.value = S;
if(!fake && s_dom.disabled){
s_dom.value = 1 - S;
}
re_dom.value = newlyExposed/newlyRecovered;
if(re_dom.oninput) re_dom.oninput();
re = newlyExposed/newlyRecovered;
/*re_dom.value = newlyExposed/newlyRecovered;
if(re_dom.oninput) re_dom.oninput();*/
// IF FAKE, UNDO EVERYTHING
if(fake){
@ -132,6 +150,7 @@ canvas.style.height = (canvas.height/2)+"px";
let interventionColors = [
['non_s', '#bbbbbb'],
['hygiene', 'hsl(230,100%,63%)', 0.1],
['distancing', 'hsl(200,100%,63%)', 0.2],
['isolate', 'hsl(140,100%,63%)', 0.2],
@ -144,23 +163,153 @@ let interventionColors = [
let _isItPastHerd = false;
let label_p_r0 = $('#label_p_r0');
let canvas_r0 = $('#canvas_r0');
let label_p_re = $('#label_p_re');
let canvas_re = $('#canvas_re');
canvas_r0.width = 540;
canvas_r0.height = 40;
canvas_r0.style.width = (canvas_r0.width/2)+"px";
canvas_r0.style.height = (canvas_r0.height/2)+"px";
canvas_re.width = 540;
canvas_re.height = 40;
canvas_re.style.width = (canvas_re.width/2)+"px";
canvas_re.style.height = (canvas_re.height/2)+"px";
let updateRBar = (label, canvas, number, THIS_IS_RE)=>{
label.innerHTML = number.toFixed(2);
let ctx = canvas.getContext('2d');
ctx.scale(2,2);
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.fillStyle = "#ff4040";
ctx.fillRect(0, 3, 250*number/3, 14);
if(THIS_IS_RE && re<r0-0.01){
// interventionColors & interventionStrengths
let diff = r0-re;
/*
let rReductions = [];
let fakeR = r0;
for(let i=0; i<interventionStrengths.length; i++){
let isPair = interventionStrengths[i],
name = isPair[0],
maxStrength = isPair[1],
myStrength = int[name] * maxStrength;
let newFakeR = fakeR*(1 - myStrength);
rReductions.push(fakeR - newFakeR);
fakeR = newFakeR;
}
*/
let interventionsAdded = interventionStrengths.reduce((sum, isPair)=>{
let name = isPair[0],
maxStrength = isPair[1],
myStrength = int[name] * maxStrength;
return sum+myStrength;
},0);
let interventionsRelative = interventionStrengths.map((isPair)=>{
let name = isPair[0],
maxStrength = isPair[1],
myStrength = int[name] * maxStrength;
return myStrength/interventionsAdded;
});
// Go from right to left
ctx.translate( (250/3)*r0, 10 );
interventionColors.forEach((icPair, i)=>{
let name = icPair[0],
color = icPair[1],
myDiff = interventionsRelative[i] * diff,
dx = -myDiff*(250/3);
if(dx<-8){
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(dx+4, 0);
ctx.lineTo(dx+4+4, -4);
ctx.lineTo(dx+4, 0);
ctx.lineTo(dx+4+4, 4);
ctx.stroke();
}
ctx.translate(dx,0);
});
}
ctx.setTransform(1,0,0,1,0,0);
ctx.scale(2,2);
// Herd Immunity
ctx.fillStyle = "#000";
ctx.fillRect(250*1/3, 0, 1, ctx.canvas.height);
ctx.setTransform(1,0,0,1,0,0);
};
let IS_PLAYING_RECORDING = false;
let recordedHistory = [];
let show_percent_s = $('#show_percent_s'),
show_percent_e = $('#show_percent_e'),
show_percent_i = $('#show_percent_i'),
show_percent_r = $('#show_percent_r'),
herdDOM = $('.herd');
let draw = ()=>{
// Redraw
requestAnimationFrame(draw);
// Update SI
if(params._HACK_SHOW_SI_PERCENTS){
let digits = 3; //(params._HACK_SHOW_SI_PERCENTS===true) ? 5 : params._HACK_SHOW_SI_PERCENTS;
$('#show_percent_s').innerHTML = ': '+(S*100).toFixed(digits)+'%';
$('#show_percent_e').innerHTML = ': '+(I*100).toFixed(digits)+'%';
$('#show_percent_i').innerHTML = ': '+(I*100).toFixed(digits)+'%';
$('#show_percent_r').innerHTML = ': '+(R*100).toFixed(digits)+'%';
// SUCH A HACK
if(CURRENT_STAGE._HACK_MAKE_TIME_KEEP_GOING){
daysTotal = Infinity;
daysCurrent += 1;
S = 0.999;
E = 0;
I = 0.001;
R = 0;
updateModel(1);
updateRBar(label_p_r0, canvas_r0, r0);
updateRBar(label_p_re, canvas_re, re, true);
}
// Update SI
if((IS_PLAYING || INPUTS_WERE_CHANGED) && params._HACK_SHOW_SI_PERCENTS){
let digits = 3; //(params._HACK_SHOW_SI_PERCENTS===true) ? 5 : params._HACK_SHOW_SI_PERCENTS;
show_percent_s.innerHTML = ': '+(S*100).toFixed(digits)+'%';
show_percent_e.innerHTML = ': '+(I*100).toFixed(digits)+'%';
show_percent_i.innerHTML = ': '+(I*100).toFixed(digits)+'%';
show_percent_r.innerHTML = ': '+(R*100).toFixed(digits)+'%';
}
// Update R0 & Re
if(IS_PLAYING || INPUTS_WERE_CHANGED){
updateRBar(label_p_r0, canvas_r0, r0);
updateRBar(label_p_re, canvas_re, re, true);
// Herd Immunity
herdDOM.style.left = (1-(1/r0))*250 + 'px';
}
INPUTS_WERE_CHANGED = false;
// Paused? At the end?
if(!IS_PLAYING) return;
if(params.FROZEN_IN_TIME) return;
if(!IS_PLAYING) return; // STOP
if(params.FROZEN_IN_TIME) return; // STOP
if(daysCurrent > daysTotal){
IS_PLAYING = false;
@ -183,7 +332,7 @@ let draw = ()=>{
params._HACK_RESET_WHEN_I_100 = "go";
bbDOM.setAttribute('label','reset');
sbDOM.setAttribute('label',params.CANNOT_REPLAY_HISTORY ? '' : 'replay');
sbDOM.setAttribute('label','');
if(CURRENT_STAGE.SHOW_HAND=="tutorial_0" && handTutorial==1){
if(!HAND_IS_VISIBLE){
@ -202,6 +351,33 @@ let draw = ()=>{
// Replay History!
if(IS_PLAYING_RECORDING){
let keepFindingNewEntries = true;
while(keepFindingNewEntries){
let record = recordedHistory[0];
if(!record || daysCurrent<record[2]){
// Not its time yet
keepFindingNewEntries = false;
}else{
// It is!
recordedHistory.shift(); // remove first element
// Set that slider
let slider = $('#'+record[0]);
slider.value = record[1];
//DONT_RECORD_HISTORY = true;
slider.oninput();
//DONT_RECORD_HISTORY = false;
}
}
}
/*
if(IS_REPLAYING_HISTORY){
let keepLooping = true;
while(keepLooping){
@ -223,6 +399,7 @@ let draw = ()=>{
}
}
*/
// For each new day, draw a new pixel
daysPerFrame = (daysTotal / params.p_speed) / 60; // (days/second) / (frames/second) = (days/frame)
@ -266,6 +443,7 @@ let draw = ()=>{
y = 0;
h = canvas.height;
interventionColors.forEach((ic)=>{
if(ic[0]=="non_s") return; // EXCEPT Non-Susceptibles
ctx.fillStyle = ic[1];
ctx.globalAlpha = int[ic[0]] * ic[2];
ctx.fillRect(0,y,w,h);
@ -273,6 +451,7 @@ let draw = ()=>{
});
// ICU bed capacity
// Actually... just make it a generous 1%.
if(params.p_hospital){
y = (1-((params.p_hospital/100)*0.02))*canvas.height;
h = 2;
@ -283,9 +462,6 @@ let draw = ()=>{
// Herd Immunity
if(params.c_recovery && !params.DO_NOT_SHOW_HERD_IMMUNITY){
let transmissionRate = 1/params.p_transmission,
recoveryRate = 1/params.p_recovery,
r0 = transmissionRate/recoveryRate;
let herdImmunity = 1 - (1/r0);
// Dashed

View File

@ -4,27 +4,32 @@
/*
01 Just exponential
02 Just logistic
03 Decay
04 The famous curve
05 SEIR
06a - r0 calc
06b - r0 calc with S
07 SEIR with R
epi-1 Just exponential
epi-2 Just logistic
epi-3 Decay
epi-4 The famous curve
epi-5 SEIR
epi-6a r0 calc
epi-6b r0 calc with S
epi-7 SEIR with R
06 Do Nothing
07 Flatten The Curve
08 Lockdown for a while
09 Intermittent Lockdown
10 Lockdown, then Test & Trace... and with Vaccination!
11 Also deep cleaning & masks & summer (with one more lockdown)
int-1 Do Nothing
int-2a hygiene & distancing
int-2 Flatten Curve / Herd Immunity
int-3 Lockdown for a while
int-4 Intermittent Lockdown "second & third waves"
int-5 Lockdown, then Test & Trace...
int-5b and with Vaccination!
int-6a Masks
int-6b Deep Cleaning
int-6c Summer
int-7 Test+Trace+Masks + One Circuit Breaker
12 Decay of Recovered
13 Oscillations
13b w Hospital Capacity
14 Oscillations with Summer (Hospital Capacity)
15 Intermittent Vaccines
yrs-1 Decay of Recovered
yrs-2 Oscillations
yrs-2b w Hospital Capacity
yrs-3 Oscillations with Summer (Hospital Capacity)
yrs-4 Intermittent Vaccines
SB Full Sandbox
@ -44,7 +49,6 @@ const STAGES = {
["p_speed",5],
["TIME_DELTA", 0.5],
//["EXPONENTIAL",true],
["CANNOT_REPLAY_HISTORY", true]
],
checkboxes: [
["c_recovery", true],
@ -56,7 +60,7 @@ const STAGES = {
// THE SIMULATION ////////////////////////
//////////////////////////////////////////
"01": {
"epi-1": {
hide: [
"section_r","label_c_recovery","label_c_waning","section_meta_years","label_c_exposed",
"label_exposed","label_removed",
@ -68,7 +72,6 @@ const STAGES = {
["p_speed",40],
["p_hospital", 0],
["EXPONENTIAL",true],
["CANNOT_REPLAY_HISTORY", true],
["_HACK_RESET_WHEN_I_100","ready"],
["_HACK_SHOW_SI_PERCENTS",true]
],
@ -76,7 +79,7 @@ const STAGES = {
SHOW_HAND: "tutorial_0"
},
"02": {
"epi-2": {
hide: [
"section_r","label_c_recovery","label_c_waning","section_meta_years","label_c_exposed",
"label_exposed","label_removed",
@ -88,13 +91,12 @@ const STAGES = {
["p_hospital", 0],
["_HACK_RESET_WHEN_I_100","ready"],
["_HACK_SHOW_SI_PERCENTS",true],
["CANNOT_REPLAY_HISTORY", true]
],
SIR: [0.99999,0.00001,0],
SHOW_HAND: "tutorial_1"
},
"03": {
"epi-3": {
hide: [
"section_r","label_transmission","label_c_waning","section_meta_years","label_c_exposed",
"c_recovery",
@ -107,7 +109,6 @@ const STAGES = {
["p_hospital", 0],
["_HACK_RESET_WHEN_R_100","ready"],
["_HACK_SHOW_SI_PERCENTS",true],
["CANNOT_REPLAY_HISTORY", true],
["DO_NOT_SHOW_HERD_IMMUNITY", true]
],
checkboxes: [
@ -117,7 +118,7 @@ const STAGES = {
SHOW_HAND: "tutorial_1"
},
"04": {
"epi-4": {
hide: [
"section_r","label_c_waning","c_recovery","label_c_exposed","section_meta_years",
//"section_meta",
@ -129,7 +130,6 @@ const STAGES = {
["p_speed",10],
["p_hospital", 0],
//["TIME_DELTA", 0.1],
["CANNOT_REPLAY_HISTORY", true],
["DO_NOT_SHOW_HERD_IMMUNITY", true],
["_HACK_SHOW_SI_PERCENTS",true],
],
@ -141,7 +141,7 @@ const STAGES = {
SHOW_HAND: "tutorial_1"
},
"05": {
"epi-5": {
hide: [
"section_r","label_c_waning","c_recovery","c_exposed","section_meta_years",
//"section_meta",
@ -153,7 +153,6 @@ const STAGES = {
["p_speed",10],
["p_hospital", 0],
//["TIME_DELTA", 0.1],
["CANNOT_REPLAY_HISTORY", true],
["DO_NOT_SHOW_HERD_IMMUNITY", true],
["_HACK_SHOW_SI_PERCENTS",3],
],
@ -166,13 +165,16 @@ const STAGES = {
SHOW_HAND: "tutorial_1"
},
"06a": {
"epi-6a": {
hide: [
"section_meta","label_c_waning","c_recovery",
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity",
"label_c_exposed",
"int_block_0",
"int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity",
"graph",
"label_s","label_re",
"sim_controls"
"sim_controls",
"divider"
],
checkboxes: [
["c_recovery", true]
@ -180,14 +182,19 @@ const STAGES = {
inputs: [
["FROZEN_IN_TIME", true],
],
SHOW_ALL_AT_START: true
},
"06b": {
"epi-6b": {
hide: [
"section_meta","label_c_waning","c_recovery","label_c_exposed",
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity",
"section_meta","label_c_waning","c_recovery",
"label_c_exposed",
"int_block_0",
"int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity",
"graph",
"sim_controls"
//"label_s","label_re",
"sim_controls",
"divider"
],
checkboxes: [
["c_recovery", true]
@ -197,10 +204,11 @@ const STAGES = {
],
disabled:[
["p_s", false]
]
],
SHOW_ALL_AT_START: true
},
"07": {
"epi-7": {
hide: [
//"section_dynamics",
/*"section_r",*/"label_c_waning","c_recovery","c_exposed",
@ -213,7 +221,6 @@ const STAGES = {
["p_speed",30],
["p_hospital", 0],
//["TIME_DELTA", 0.1],
["CANNOT_REPLAY_HISTORY", true],
//["DO_NOT_SHOW_HERD_IMMUNITY", true],
["_HACK_SHOW_SI_PERCENTS",3],
],
@ -230,107 +237,272 @@ const STAGES = {
// THE NEXT FEW MONTHS ///////////////////
//////////////////////////////////////////
/*
"06": {
"int-1": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity"
"label_c_waning","c_recovery","c_exposed",
"section_meta_years",
"int_block_0","int_block_1",
"int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity"
],
inputs: [
["p_years",2],
["p_speed",20],
["p_speed",5],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true]
]
["c_recovery", true],
["c_exposed",true]
],
SHOW_HAND: "tutorial_1"
//SHOW_ALL_AT_START: true
},
"07": {
"int-2a": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity"
"label_c_exposed",
//"int_block_0",
/*"int_block_1",*/"int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity",
"graph",
//"label_s","label_re",
"sim_controls",
"divider"
],
checkboxes: [
["c_recovery", true]
],
inputs: [
["p_years",2],
["p_speed",20],
//["TIME_DELTA", 0.2],
["FROZEN_IN_TIME", true],
],
checkboxes: [
["c_recovery", true]
]
disabled:[
["p_s", false]
],
SHOW_ALL_AT_START: true
},
"08": {
"int-2": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"label_c_waning","c_recovery","c_exposed",
"section_meta_years",
"int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity"
],
inputs: [
["p_years",2],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true]
]
["c_recovery", true],
["c_exposed",true]
],
SHOW_ALL_AT_START: true,
PLAY_RECORDING: [
["p_distancing",0.344,84], ["p_hygiene",1,84],
["p_distancing",0,340], ["p_hygiene",0,340],
],
SIR: [0.999995,0.000005,0]
},
"09": {
"int-3": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"label_c_waning","c_recovery","c_exposed",
"section_meta_years",
"int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity"
],
inputs: [
["p_years",2],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true]
]
["c_recovery", true],
["c_exposed",true]
],
SHOW_ALL_AT_START: true,
PLAY_RECORDING: [
["p_distancing",1,84], ["p_hygiene",1,84],
["p_distancing",0,234], //["p_hygiene",0,234]
],
SIR: [0.999995,0.000005,0]
},
"10": {
"int-4": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"int_block_3","int_block_4","hospital_capacity"
"label_c_waning","c_recovery","c_exposed",
"section_meta_years",
"int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity"
],
inputs: [
["p_years",2],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true]
]
["c_recovery", true],
["c_exposed",true]
],
SHOW_ALL_AT_START: true,
PLAY_RECORDING: [
["p_distancing",1,85], ["p_hygiene",1,85],
["p_distancing",0,85+58],
["p_distancing",1,85+58+33],
["p_distancing",0,85+58+33+58],
["p_distancing",1,85+58+33+58+36],
["p_distancing",0,85+58+33+58+36+58],
["p_distancing",1,85+58+33+58+36+58+48],
["p_distancing",0,85+58+33+58+36+58+48+58],
["p_distancing",1,85+58+33+58+36+58+48+58+60],
["p_distancing",0,85+58+33+58+36+58+48+58+60+58],
["p_distancing",1,85+58+33+58+36+58+48+58+60+58+80],
],
SIR: [0.999995,0.000005,0]
},
"11": {
"int-5": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"hospital_capacity"
"label_c_waning","c_recovery","c_exposed",
"section_meta_years",
/*"int_block_2",*/"int_block_3","int_block_4","int_block_5","hospital_capacity"
],
inputs: [
["p_years",2],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true],
["c_exposed",true]
],
SHOW_ALL_AT_START: true,
PLAY_RECORDING: [
["p_distancing",1,84], ["p_hygiene",1,84],
["p_distancing",0,175], ["p_quarantine",0.65,175], ["p_isolate",0.65,175],
],
SIR: [0.999995,0.000005,0]
},
"int-5b": {
hide: [
"section_dynamics",
"label_c_waning","c_recovery","c_exposed",
"section_meta_years",
/*"int_block_2",*/"int_block_3","int_block_4",/*"int_block_5",*/"hospital_capacity"
],
inputs: [
["p_years",2],
["p_speed",10],
],
checkboxes: [
["c_recovery", true],
["c_exposed",true]
],
SHOW_ALL_AT_START: true,
PLAY_RECORDING: [
["p_distancing",1,84], ["p_hygiene",1,84],
["p_distancing",0,175], ["p_quarantine",0.65,175], ["p_isolate",0.65,175],
["p_hygiene",0,550], ["p_quarantine",0,550], ["p_isolate",0,550],
["p_vaccines",0.64,550],
["p_vaccines",0,580],
],
SIR: [0.999995,0.000005,0]
},
"int-6c": {
hide: [
"section_dynamics",
"section_meta",
"label_c_waning","c_recovery",
"label_c_exposed",
/*"int_block_0",
"int_block_1","int_block_2","int_block_3","int_block_4","int_block_5",*/
"hospital_capacity",
"graph",
//"label_s","label_re",
"sim_controls",
"divider"
],
checkboxes: [
["c_recovery", true]
]
],
inputs: [
["FROZEN_IN_TIME", true],
],
disabled:[
["p_s", false]
],
SHOW_ALL_AT_START: true,
_HACK_MAKE_TIME_KEEP_GOING: true,
},
"int-7": {
hide: [
"section_dynamics",
"label_c_waning","c_recovery","c_exposed",
"section_meta_years",
/*"int_block_2","int_block_3","int_block_4","int_block_5",*/"hospital_capacity"
],
inputs: [
["p_years",2],
["p_speed",20],
],
checkboxes: [
["c_recovery", true],
["c_exposed",true]
],
SHOW_ALL_AT_START: true,
PLAY_RECORDING: [
// Lockdown
["p_distancing",1,84], ["p_hygiene",1,84],
// Lift
["p_distancing",0,175],
["p_hygiene",0.66,84],
["p_quarantine",0.33,175], ["p_isolate",0.33,175], ["p_masks",0.33,175],
// Circuit Breaker
["p_distancing",1,60+283],
["p_distancing",0,60+283+60],
// Vaccine!
["p_hygiene",0,550], ["p_quarantine",0,550], ["p_isolate",0,550], ["p_masks",0,550],
["p_vaccines",0.64,550],
["p_vaccines",0,580],
],
SIR: [0.999995,0.000005,0]
},
//////////////////////////////////////////
// THE NEXT FEW YEARS ////////////////////
//////////////////////////////////////////
"yrs-1": {
hide: [
/*"section_dynamics",*/
"c_waning","c_recovery","c_exposed",
"section_meta_years",
"int_block_0","int_block_1",
"int_block_2","int_block_3",/*"int_block_4",*/"int_block_5","hospital_capacity"
],
inputs: [
["p_years",10],
["p_speed",20],
],
checkboxes: [
["c_recovery", true],
["c_exposed",true],
["c_waning", true],
],
SHOW_ALL_AT_START: true,
SIR: [0.999995,0.000005,0]
},
/*
"12": {
hide: ["section_r","section_meta","label_transmission","label_c_recovery","c_waning"],
inputs: [
@ -504,6 +676,17 @@ let setStage = (stageID)=>{
showHand('start');
}
// Show herd immunity?
if(params.DO_NOT_SHOW_HERD_IMMUNITY){
$('.herd').style.display = 'none';
}
// PLAY BACK?
if(stage.PLAY_RECORDING){
IS_PLAYING_RECORDING = true;
recordedHistory = JSON.parse(JSON.stringify(stage.PLAY_RECORDING));
}
};
let stageParams = new URLSearchParams(location.search);

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,5 @@
/////////////////////////////////////
restart();
s_dom.oninput();
_updateButtons();
requestAnimationFrame(draw);

View File

@ -14,7 +14,7 @@ div{
#sandbox{
width: 800px;
height: 540px;
height: 640px;
overflow: hidden;
background: #fff;
position: absolute;
@ -26,14 +26,30 @@ div{
position: absolute;
left:15px;
top:20px;
width:250px;
width:270px;
/*height:500px;*/
float:left;
position: relative;
}
#label_s{
position: relative;
display: inline-block;
}
#label_s > .herd{
display: block;
position: absolute;
border-right: 2px dotted black;
width: 0;
height: 16px;
bottom: 3px;
left: 159px;
pointer-events: none;
}
#sim_controls{
margin-top: 10px;
width: 250px;
}
.big_button{
@ -99,6 +115,7 @@ hr{
border:none;
border-bottom: 2px dashed #ddd;
margin: 15px 0;
width: 250px;
}
@ -161,7 +178,8 @@ hr{
}*/
#controls input[type=range]{
width: 100%;
/*width: 100%;*/
width: 250px;
}
#controls div[strikethrough]{
text-decoration: line-through;

View File

@ -1,68 +1,78 @@
# What Happens Next?
## COVID-19 Futures, Explained With Playable Simulations
"The only thing to fear is fear itself" was stupid advice.
If people fear fear itself, they'll deny danger because they don't want to create "mass panic". The problem's not fear, but how we *use* our fear. Fear, used well, gives you energy to deal with current dangers, and prepare for future dangers.
Honestly, the two of us (Marcel, epidemiologist + Nicky, artist/coder) are worried about the future. We bet you are, too. That's why we want to channel *our* worries into making these **playable simulations**, so that you can channel *your* worries into understanding:
* **The Last Few Months** (epidemiology 101, SEIR model, R & R<sub>0</sub>)
* **The Next Few Months** (lockdowns, contact tracing, masks)
* **The Next Few Years** (vaccines, loss of immunity?)
This guide is meant to give you hope *and* fear. To beat this virus **in a way that also protects our mental & financial health**, we need optimism to create plans, and pessimism to create backup plans. As Gladys Bronwyn Stern once said, *“The optimist invents the airplane and the pessimist the parachute.”*
So, buckle in: we're about to experience some turbulence.
---
# The Last Few Months
...has been a real lesson in Epidemiology 101.
Pilots use flight simulators to learn how not to crash planes.
Pilots use flight simulators to learn how not to crash planes. **Epidemiologists use disease simulators to learn how not to crash humanity.**
**Epidemiologists use epidemic simulators to learn how not to crash humanity.**
So, let's set up an disease "flight simulator"! First, we need some simulation rules.
Let's say you have some Infected (i) people and not-yet-infected Susceptible (s) people. One (i) infects a (s), those 2 (i) infect another 2 (s), those 4 (i) infect another 4 (s), and so on:
So, let's create a very simple "epidemic flight simulator"! Here, we have some (i) Infectious people & some not-yet-infected (s) Susceptible people. (i)s turn (s)s into more (i)s:
// pic
*On average*, COVID-19 jumps from an (i) to a (s) every 4 days.[1](source) The average # of days it takes for an (i) to infect an (s) is called the **"generation time"**[2-note](serial interval). (Click the gray circles for sources, and the blue squares for side-notes!)
At the start of a COVID-19 outbreak, it's estimated that the virus jumps from an (i) to an (s) every 4 days.[^1] (*On average.* Remember, there's lots of variation.)
*Rule #1: The more (i)s there are, the faster (s)s become (i)s.*
[^1]: source
// pic - rule
Here's a simulation of a population with *just* 0.001% (i) and 99.999% (s), over 6 months. If we simulate "double every 4 days" *and nothing else*, what happens?
If we simulate *just this rule and nothing else*, here's what it looks like over 3 months, starting with 99.9% (s) and just 0.1% (i):
**Click "Start" play the simulation! You can then change the "generation time", and see how that changes the simulation:**
**Click "Start" to play the simulation! (Afterwards, you can re-play the simulation with different settings)**
// sim
Starts small ("it's just a flu"), then explodes ("oh right, flus don't break hospitals in rich countries"). This is the "J-shaped" **exponential growth curve**.
This is the **exponential growth curve.** Starts small, then explodes. "Oh it's just a flu" to "Oh right, flus don't create *mass graves in rich cities*".
But this simulation is wrong. There are things that prevent an (i) from infecting someone else like if that other person is *already* an (i):
// pic - exponential double rice
But, this simulation is wrong. Exponential growth, thankfully, can't go on forever. One thing that stops a virus from spreading is if others *already* have the virus:
// pic - 100% spread, 50% spread, 0% spread
*Rule #2: The fewer (s)s there are, the slower (s)s become (i)s.*
**The more (i)s there are, the faster (s)s become (i)s, but the fewer (s)s there are, the *slower* (s)s become (i)s.**
// pic - rule
Now, what happens if we simulate *both* these rules?
**Again, click Start to play the simulation!**
Now, what happens if we simulate that?
// sim
Starts small, explodes, then slows down again. This is the "S-shaped" **logistic growth curve.**
This is the "S-shaped" **logistic growth curve.** Starts small, explodes, then slows down again.
Still, this simulation predicts 100% of people will get the virus, and even the most pessimistic COVID-19 simulations don't predict *that*.
But, this simulation is *still* wrong. We're missing the fact that (i) Infectious people eventually stop being infectious, either by 1) recovering, 2) "recovering" with lung damage, or 3) dying.
What we're missing: You stop being infectious for COVID-19 when you recover... or die.
For simplicity's sake, let's pretend that all (i) Infectious people become (r) Recovered. (r)s can't be infected again, and let's pretend *for now!* that they stay immune for life.
For the sake not making these simulations too depressing, let's only simulate Infected (i) becoming (r) Recovered. (The math works out the same.) And let's assume *(for now!!!)* that (r)s can't get infected again. So, new rule:
When you're infected with COVID-19, it's estimated you stay (i) infectious for 12 days.[^2] (Again, *on average.*)
*Rule #3: (i)s eventually become (r)s.*
[^2]: source
// pic - rule
Let's have (i)s become (r)s after 14 days, *on average*.[3-note](technical notes) This means some (i)s will recover *before* 14 days, and some recover *after*! This is closer to real life.
To show *only* Rule #3, here's a simulation starting with 100% (i):
Here's a simulation that starts with 100% (i). Most people recover after 12 days, then most of the remainder recover after another 12 days, then most of the remainder *of that remainder* recover after another 12 days, etc:
// sim
This is the "flipped-J-shaped" **exponential decay curve.**
This is the opposite of exponential growth, the **exponential decay curve**.
Now, what happens if you simulate all 3 rules at once? What happens when you combine an S-shaped logistic curve with a flipped-J exponential decay curve?
Now, what happens if you combine this with the S-shaped logistic curve of infection?
// pic
Let's find out:
Let's find out. Here's a simulation of an epidemic *with* recovery:
// sim
@ -70,65 +80,84 @@ And *that's* where that famous curve comes from! It's not a bell curve, it's not
// pic: 3 rules
This is the **SIR Model**, the *second*-most important idea in epidemiology.
This is the the **SIR Model**, ((s) **S**usceptible → (i) **I**nfectious → (r) **R**ecovered) the second-most important idea in Epidemiology 101.
**NOTE:** The simulations you've been hearing in the news are *far* more complex than the ones you're seeing here! But the sims you'll play with here reach the same general conclusions, even if missing the nuances.
Note: The simulations that inform policy are *far* more sophisticated than this! But the SIR model can still help us understand a lot about COVID-19, even if missing the nuances.
One nuance you could add is the **SIRS Model**, where the final "S" also stands for (s) Susceptible this is when people recover, are immune for a bit, *then lose that immunity and can be infected again.* (We'll consider this in the Next Few Years section)
Actually, let's add one more nuance: before an (s) becomes an (i), they first become an (e) Exposed person, when they're infect*ed* but not yet infect*ious* they have the virus but can't pass it on (yet).
Another nuanced version is the **SEIR Model**, where the "E" stands for (e) Exposed, a brief period of time *after* you've been infected, but *before* you can infect others. This is called the **"latent period"**, and for COVID-19 it's around 3 days.[4]()
(This variant is called the **SEIR Model**, where "E" stands for (e) Exposed. Note this *isn't* the everyday meaning of "exposed", where you might or might not have the virus. In this technical definition, "Exposed" means you definitely have it. Yeah, science terminology is bad.)
Here's what happens if you simulate that:
For COVID-19, it's estimated that you're in this "latent period" for around 3 days.[^3] What happens if we add that to the simulation?
[^3]: source
// sim
Doesn't change much, so let's stick to the vanilla SIR model. We brought (e)s up because the exact timing of contagiousness is important in "contact tracing", which we'll explain in the Next Few Months section.
Not much, actually! The "latent period" only changes *when* the peak happens, but the *height* of the peak and total people infected remain the same:
Oh! But almost forgot, the *first*-most important idea in epidemiology:
// pics
**"R"**
Why's that? Because of the *first*-most important idea in Epidemiology 101:
Which is short for "Reproduction Number". It's the *average* number of people an (i) will infect *before* they recover:
// pic - **"R"**
// pic - R>1 R=1 R<1
Which is short for "Reproduction Number". It's the *average* number of people an (i) infects *before* they recover (or die).
**R0** (pronounced R-nought) is the Reproduction Number for a virus *at the very beginning of an outbreak, before we have immunity or interventions*. (Also called "Basic Reproduction Number")
// R > 1, R = 1, R < 1 pic
**Rt** (the 't' stands for time) is the Reproduction Number *right now*, after we have some immunity or interventions. (Also called "Re", e standing for "Effective Reproduction Number". Also called just "R", to... confuse people)
**R** changes over the course of an outbreak, as we get more immunity & interventions.
// pic of R0 and Rt over time for the Famous Curve with peak for inflection!
**R<sub>0</sub>** (pronounced R-nought) is what R is *at the start of an outbreak, before immunity or interventions*. R<sub>0</sub> is also called the "basic reproduction number". R<sub>0</sub> more closely reflects the power of the virus itself, but it still changes from place to place. For example, because heat 'kills' coronaviruses, R<sub>0</sub> for COVID-19 is lower in hot places than cold ones. Not low enough to contain it, though.
(A lot of news outlets confuse these two Rs! They're different!)
(A lot of news outlets and even academic papers! confuse R and R<sub>0</sub>. Again, science terminology is bad.)
The R0 for the flu[6](more) is around 1.3. The R0 for COVID-19 is somewhere between 2 and 5.[7](source) The huge uncertainty is because R0 depends on exactly how quickly new people are infected ("generation time") vs how quickly people recover[8](technical note):
The R<sub>0</sub> for the flu[^r0_flu] is around 1.3. The R<sub>0</sub> estimates for COVID-19 are usually between 2 and 3, maybe as high as 6.[^r0_covid]
[^r0_flu]: source
[^r0_covid]: source
In our simulations, an (i) recovers in 12 days, but infects one new (s) every 4 days. That means, *on average*, an (i) infects 3 (s)s before they recover. So for our simulations, R<sub>0</sub> is 3.
**Play around with this R<sub>0</sub> calculator, to see how R<sub>0</sub> depends on recovery time & new-infection time:**
// calc
But remember, the fewer (s)s there are, the *slower* (s)s become (i)s.
R depends not just on R<sub>0</sub>, but also how many people are no longer Susceptible due to, say, having recovered & gotten natural immunity.
// calc 2
When enough people have natural immunity, R < 1, and the virus is contained! This is called **herd immunity**, and while it's *terrible* policy, (we'll explain why later it's not for the reason you may think!) it's essential to understanding Epidemiology 101.
Now, let's play the last simulation again, but showing R<sub>0</sub>, R over time, and the herd immunity threshold:
// sim
Rt for COVID-19 depends on the interventions we do (or don't) have, as well as how many people *aren't* (s) Susceptible. (because they're (r) Recovered, currently (i) Infected, or... dead.)
Note: Total cases (the gray curve) does not stop at herd immunity, but *overshoots* it! And it does this *exactly when* current cases (the pink curve) peaks. This happens no matter how you change the settings:
// sim
// pic
Note that when (s)% is low enough, you can get Rt<1 *containing the virus!* This is called **the "herd immunity" threshold**. "Herd immunity" is a terrible *policy* (TODO: explain why), but it's important for understanding epidemiology.
This is because, by definition, when there are more non-(s)s than the herd immunity threshold, you get R < 1. And, by definition, R < 1 means new cases stop growing.
Now, let's run the same SIR model simulation again, but this time showing 1) Rt changing over time, and 2) the herd immunity threshold:
If there's only one lesson you take away from this whole guide, here it is, in big shiny letters:
// sim
# R > 1 = bad
# R < 1 = good (R=1, meh)
Note how total cases ((i)+(r)) *overshoots* the herd immunity threshold! And the *exact* moment it does this is when infections peak *and* when Rt drops below 1!
**This means: we do NOT need to catch all transmissions, or even nearly all transmissions, to stop COVID-19!**
If there's only one lesson you take away today, here it is, in big shiny letters:
It's a paradox. COVID-19 is incredibly contagious, yet to contain it, we "only" need to stop 67% of infections. 67%?! If that was a school grade, that's a D+. But if R<sub>0</sub> = 3, cutting that by 67% gives us R = 0.99, which is R < 1, which means the virus is contained!
# Rt>1 = bad
# Rt<1 = good
(And even if, extreme-worst-case, R<sub>0</sub> = *6*, you still "only" need to stop 84% of transmissions. That's a B grade.)
**NOTE: We do not need to catch all transmissions, or even nearly all transmissions, to stop COVID-19.**
It's a paradox COVID-19 is incredibly contagious, yet to contain it, we "only" need to stop 72% of infections. 72%?! That's, like, a C grade. But if R0 = 3.5, then reducing that by 72% will make Rt < 1 = good.
(And even if worst-case, R0=5, you "only" need to stop 80%. That's a B.)
// calculator - custom
*Every* COVID-19 intervention you've heard of handwashing, social distancing, lockdowns, self-isolation, contact tracing & quarantining, face masks, even "herd immunity" they're *all* doing the same thing:
Reducing Rt.
Getting R < 1.
Let's see how we can get Rt<1 in a way that protects not just our physical health, but also our mental health, social health, *and* financial health!
So now, let's use our "epidemic flight simulator" to figure out the next few months! How will we get R < 1 in a way that protects not just our physical health, **but also our mental health, social health, *and* financial health?**
Brace yourselves for an emergency landing...