nice stuff

This commit is contained in:
Nicky Case 2020-04-23 12:21:18 -04:00
parent fe74ab9f56
commit b7f254f151
10 changed files with 1434 additions and 13 deletions

BIN
icons/hand.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -19,25 +19,26 @@
<br><br>
<div id="label_transmission">
Infects a <icon s></icon> every <span id="label_p_transmission">N</span> days
Infects 1 <icon s></icon> per <span id="label_p_transmission">N</span> days
<br>
(when almost everyone's still <icon s></icon>)
<br>
<input class="sim_input" type="range" id="p_transmission" min="1" max="28" step="1" value="4">
<span id="label_transmission_caveat">
(at the start of the epidemic)<br>
</span>
<input class="sim_input" type="range" id="p_transmission" min="1" max="30" step="1" value="4">
</div>
<div id="label_c_exposed">
<input class="sim_checkbox" type="checkbox" id="c_exposed">
Takes <span id="label_p_exposed">N</span> days to go from <icon e></icon> to <icon i></icon>
<br>
<input class="sim_input" type="range" id="p_exposed" min="1" max="28" step="1" value="3">
<input class="sim_input" type="range" id="p_exposed" min="1" max="30" step="1" value="3">
</div>
<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
<br>
<input class="sim_input" type="range" id="p_recovery" min="1" max="28" step="1" value="14">
<input class="sim_input" type="range" id="p_recovery" min="1" max="30" step="1" value="11">
</div>
<div id="label_c_waning">
@ -168,10 +169,21 @@
</div>
<div id="legend">
<icon s></icon> Susceptible<span id="show_percent_s"></span>
<span id="label_exposed"><br><icon e></icon> Exposed</span>
<br><icon i></icon> Infectious<span id="show_percent_i"></span>
<span id="label_removed"><br><icon r></icon> Removed</span>
<span id="label_susceptible">
<icon s></icon> Susceptible<span id="show_percent_s"></span>
<br>
</span>
<span id="label_exposed">
<icon e></icon> Exposed<span id="show_percent_e"></span>
<br>
</span>
<span id="label_infectious">
<icon i></icon> Infectious<span id="show_percent_i"></span>
<br>
</span>
<span id="label_removed">
<icon r></icon> Removed<span id="show_percent_r"></span>
</span>
<br>
@ -188,6 +200,25 @@
</div>
</div>
<div id="pointer">
<div id="hand_container">
<div id="hand"></div>
</div>
<div id="pointer_words">
<span id="pointer_params">
Try re-running the simulation
with different numbers!
<span id="pointer_params_2">
<b>(note: you can change the numbers <i>while</i> the sim is running)</b>
</span>
</span>
<span id="pointer_scroll">
Once you're done playing around,
scroll down to keep reading!
</span>
</div>
</div>
</div>
<span id="month_names">
@ -207,4 +238,8 @@
</body>
<script src="sim.js"></script>
<script src="js/helpers.js"></script>
<script src="js/Model.js"></script>
<script src="js/Controls.js"></script>
<script src="js/Stages.js"></script>
<script src="js/main.js"></script>

373
sim/js/Controls.js Normal file
View File

@ -0,0 +1,373 @@
/////////////////////////////////////
// PARAMETERS ///////////////////////
/////////////////////////////////////
let S,E,I,R;
let params = {};
// Update Month Ticks!
let mn = [...$('#month_names').children].map((dom)=>{
return dom.innerHTML;
});
let updateMonthTicks = ()=>{
let ticks;
switch(params.p_years){
case 0.5:
ticks = [
mn[0]+" 2020",
mn[1]+" 2020",
mn[2]+" 2020",
mn[3]+" 2020",
mn[4]+" 2020",
mn[5]+" 2020",
null
];
break;
case 1.0:
ticks = [
mn[0]+" 2020",
mn[3]+" 2020",
mn[6]+" 2020",
mn[9]+" 2020",
null
];
break;
default:
ticks = [];
for(let time=0; time<=params.p_years; time+=0.5){
if(time%1 != 0){
ticks.push(null);
}else{
ticks.push(time+2020);
}
}
ticks[ticks.length-1] = null;
break;
}
let monthTicksDOM = $('#month_ticks');
monthTicksDOM.innerHTML = '';
ticks.forEach((tick,i)=>{
if(i==ticks.length-1 || !tick) return;
let tickDOM = document.createElement('div');
let tickSpanDOM = document.createElement('span');
tickSpanDOM.innerHTML = tick;
tickDOM.style.left = ((i/(ticks.length-1))*500)+"px";
tickDOM.appendChild(tickSpanDOM);
monthTicksDOM.appendChild(tickDOM);
});
}
// Sliders
let DONT_RECORD_HISTORY = true;
let _DO_NOT_RECURSE = true;
let yearsSlider = $('#p_years');
$all('.sim_input').forEach((slider)=>{
let id = slider.id;
let label = $('#label_'+id);
let isRecordable = slider.classList.contains('recordable');
let onChange = ()=>{
// Change label & param
let val = parseFloat(slider.value);
if(label){
let digits = parseInt(label.getAttribute('toFixed'));
label.innerHTML = digits ? val.toFixed(digits) : val;
}
params[id] = val;
// Record history (@ this day)
if(isRecordable){
if(!DONT_RECORD_HISTORY){
recordedHistory.push([id, val, Math.round(daysCurrent)]);
}
}
// Just to update other random crap
if(!_DO_NOT_RECURSE){
_DO_NOT_RECURSE = true;
updateModel(1, true); // one day
_DO_NOT_RECURSE = false;
}
// HACK: If this is years, RESET SIM & CHANGE THE MONTHS
if(slider==yearsSlider){
if(daysCurrent>0){
_resetTheSim();
_updateButtons();
}
updateMonthTicks();
}
// MORE HAX
if(daysCurrent==0){
sbDOM.setAttribute('label','params');
}
};
slider.oninput = onChange;
onChange();
});
DONT_RECORD_HISTORY = false;
_DO_NOT_RECURSE = false;
// Checkboxes
$all('.sim_checkbox').forEach((checkbox)=>{
let id = checkbox.id;
let label = $('#label_'+id);
let onChange = ()=>{
if(label){
if(!checkbox.checked){
label.setAttribute("strikethrough","yes");
}else{
label.removeAttribute("strikethrough");
}
}
params[id] = checkbox.checked;
};
checkbox.oninput = onChange;
onChange();
});
/////////////////////////////////////
// (RE)START ////////////////////////
/////////////////////////////////////
let IS_PLAYING = false;
let recordedHistory = [];
let IS_REPLAYING_HISTORY = false;
let START_S = 0.999,
START_E = 0.001,
START_I = 0.001,
START_R = 0.000;
let restart = ()=>{
// Model
S = START_S;
E = START_E;
I = START_I;
R = START_R;
// Drawing
daysCurrent = 0;
daysDrawn = 0;
daysTotal = params.p_years*365;
daysPerFrame = 0; // calc it on the fly
let ctx = context;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
};
/////////////////////////////////////
// BIG & SMALL BUTTONS //////////////
/////////////////////////////////////
let bbDOM = $('.big_button');
let sbDOM = $('.small_button');
let handTutorial = 0;
bbDOM.onclick = ()=>{
if(CURRENT_STAGE.SHOW_HAND=="tutorial_0"){
if(handTutorial==0){
hideHand();
handTutorial = 1;
}else if(handTutorial==1){
hideHand();
}else if(handTutorial==2){
hideHand();
setTimeout(()=>{
showHand('end');
},1);
handTutorial = 3;
}
}
if(CURRENT_STAGE.SHOW_HAND=="tutorial_1"){
if(handTutorial==0){
hideHand();
handTutorial = 1;
}else if(handTutorial==1){
hideHand();
handTutorial = 2;
}
}
if(daysCurrent>daysTotal || params._HACK_RESET_WHEN_I_100=="go" || params._HACK_RESET_WHEN_R_100=="go"){
_resetTheSim();
params._HACK_RESET_WHEN_I_100 = undefined;
params._HACK_RESET_WHEN_R_100 = undefined;
if(CURRENT_STAGE.SHOW_HAND=="tutorial_0" && handTutorial<=1){
setTimeout(()=>{
showHand('params');
},1000);
handTutorial = 2;
}
}else{
if(daysCurrent==0){
restart();
}
IS_PLAYING = !IS_PLAYING;
}
_updateButtons();
};
let defaultParams = [
["p_transmission", 4],
["p_exposed", 3],
["p_recovery", 11],
["p_waning", 1],
["p_hospital", 100],
["p_years", 2],
["p_speed", 30],
];
sbDOM.onclick = ()=>{
if(daysCurrent==0){
changeSliders(defaultParams);
changeSliders(CURRENT_STAGE.inputs);
}else if(daysCurrent>daysTotal){
_replayTheSim();
}else{
_resetTheSim();
}
_updateButtons();
};
let _updateButtons = ()=>{
if(daysCurrent > daysTotal){
bbDOM.setAttribute('label','reset');
sbDOM.setAttribute('label',params.CANNOT_REPLAY_HISTORY ? '' : 'replay');
}else if(IS_PLAYING){
bbDOM.setAttribute('label','pause');
sbDOM.setAttribute('label','reset');
}else{
if(daysCurrent==0){
bbDOM.setAttribute('label','start');
sbDOM.setAttribute('label','NONE');
}else{
bbDOM.setAttribute('label','continue');
sbDOM.setAttribute('label','reset');
}
}
};
let hofp = $('#hide_on_first_playthrough');
let _showAllControls = ()=>{
let originalHeight = hofp.getBoundingClientRect().height;
if(originalHeight>0) return;
hofp.style.height = "auto";
hofp.style.position = "absolute";
hofp.style.top = "-1000px";
setTimeout(()=>{
let newHeight = hofp.getBoundingClientRect().height;
hofp.style.position = "";
hofp.style.top = "";
hofp.style.height = originalHeight+"px";
setTimeout(()=>{
hofp.style.marginTop = "";
hofp.style.height = newHeight+"px";
},20);
},20);
};
let _hideAllControls = ()=>{
hofp.style.height = "0px";
};
let _resetTheSim = ()=>{
_showAllControls();
IS_REPLAYING_HISTORY = false;
recordedHistory = [];
restart();
IS_PLAYING = false;
};
let _replayTheSim = ()=>{
_hideAllControls();
IS_REPLAYING_HISTORY = true;
restart();
IS_PLAYING = true;
};
/////////////////////////////////////
// THE HAND /////////////////////////
/////////////////////////////////////
let pointerDOM = $('#pointer');
let handDOM = $('#hand_container');
let wordsDOM = $('#pointer_words');
let HAND_IS_VISIBLE = false;
let showHand = (position)=>{
HAND_IS_VISIBLE = true;
pointerDOM.style.display = 'block';
$all('span',pointerDOM).forEach((span)=>{
span.style.display = 'none';
});
switch(position){
case 'start':
handDOM.style.top = '111px';
handDOM.style.left = '98px';
break;
case 'params': case 'params2':
handDOM.style.top = '100px';
handDOM.style.left = '280px';
handDOM.style.transform = 'rotate(270deg)';
wordsDOM.style.top = '117px';
wordsDOM.style.left = '377px';
wordsDOM.style.textAlign = 'left';
$('#pointer_params').style.display = 'inline';
$('#pointer_params_2').style.display = (position=='params2') ? 'inline' : 'none';
break;
case 'end':
handDOM.style.top = '445px';
handDOM.style.left = '101px';
handDOM.style.transform = 'rotate(180deg) scaleX(-1)';
wordsDOM.style.top = '363px';
wordsDOM.style.left = '17px';
wordsDOM.style.textAlign = 'center';
$('#pointer_scroll').style.display = 'inline';
break;
}
};
let hideHand = ()=>{
HAND_IS_VISIBLE = false;
pointerDOM.style.display = 'none';
};

316
sim/js/Model.js Normal file
View File

@ -0,0 +1,316 @@
/////////////////////////////////////
// UPDATE ///////////////////////////
/////////////////////////////////////
let int = {
hygiene: 0,
distancing: 0,
isolate: 0,
quarantine: 0,
cleaning: 0,
masks: 0,
summer: 0,
vaccines: 0
};
let daysCurrent, daysDrawn, daysTotal, daysPerFrame;
let r0_dom = $('#p_r0');
let s_dom = $('#p_s');
let re_dom = $('#p_re');
let updateModel = (days, fake)=>{
let real_S=S, real_E=E, real_I=I, real_R=R;
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();
// Transmission affected by interventions
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)
// Vaccination...
if(S > 1-params.p_vaccines){
let newlyVaccinated = S - (1-params.p_vaccines);
S -= newlyVaccinated;
R += newlyVaccinated;
}
int.vaccines = params.p_vaccines;
// S...
if(!s_dom.disabled){
S = parseFloat(s_dom.value);
}
// Update Model
let newlyExposed;
if(params.EXPONENTIAL){
newlyExposed = I*transmissionRate*days;
}else{
newlyExposed = S*I*transmissionRate*days;
}
let newlyInfectious = params.c_exposed ? (E*incubationRate*days) : 0;
let newlyRecovered = params.c_recovery ? (I*recoveryRate*days) : 0;
let newlySusceptible = params.c_waning ? (R*immunityLossRate*days) : 0;
S -= newlyExposed;
E += newlyExposed;
if(params.c_exposed){
E -= newlyInfectious;
I += newlyInfectious;
}else{
I += E; // instant transfer
E = 0;
}
I -= newlyRecovered;
R += newlyRecovered;
R -= newlySusceptible;
S += newlySusceptible;
// bounds
if(S<0) S=0;
if(I>1) I=1;
// Susceptible & Re
if(s_dom.disabled){
s_dom.value = S;
}
re_dom.value = newlyExposed/newlyRecovered;
if(re_dom.oninput) re_dom.oninput();
// IF FAKE, UNDO EVERYTHING
if(fake){
S = real_S;
E = real_E;
I = real_I;
R = real_R;
}
}
/////////////////////////////////////
// DRAW /////////////////////////////
/////////////////////////////////////
let canvas = $('#graphCanvas');
let context = canvas.getContext('2d');
canvas.width = 1000;
canvas.height = 1000;
canvas.style.width = (canvas.width/2)+"px";
canvas.style.height = (canvas.height/2)+"px";
let interventionColors = [
['hygiene', 'hsl(230,100%,63%)', 0.1],
['distancing', 'hsl(200,100%,63%)', 0.2],
['isolate', 'hsl(140,100%,63%)', 0.2],
['quarantine', 'hsl(100,100%,63%)', 0.2],
['cleaning', 'hsl(290,100%,63%)', 0.2],
['masks', 'hsl(260,100%,63%)', 0.2],
['summer', 'hsl(20,100%,63%)', 0.3],
['vaccines', 'hsl(53, 100%, 73%)', 0.6],
];
let _isItPastHerd = false;
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)+'%';
}
// Paused? At the end?
if(!IS_PLAYING) return;
if(params.FROZEN_IN_TIME) return;
if(daysCurrent > daysTotal){
IS_PLAYING = false;
_updateButtons();
if(params._HACK_RESET_WHEN_R_100=="ready"){
params._HACK_RESET_WHEN_R_100 = "go";
if(CURRENT_STAGE.SHOW_HAND=="tutorial_1" && handTutorial==1){
if(!HAND_IS_VISIBLE){
showHand('start');
}
}
}
return;
}
// HACK
if(params._HACK_RESET_WHEN_I_100=="ready" && I>=0.999999){
params._HACK_RESET_WHEN_I_100 = "go";
bbDOM.setAttribute('label','reset');
sbDOM.setAttribute('label',params.CANNOT_REPLAY_HISTORY ? '' : 'replay');
if(CURRENT_STAGE.SHOW_HAND=="tutorial_0" && handTutorial==1){
if(!HAND_IS_VISIBLE){
setTimeout(()=>{
showHand('start');
},2500);
}
}
if(CURRENT_STAGE.SHOW_HAND=="tutorial_1" && handTutorial==1){
if(!HAND_IS_VISIBLE){
showHand('start');
}
}
}
// Replay History!
if(IS_REPLAYING_HISTORY){
let keepLooping = true;
while(keepLooping){
let record = recordedHistory[0];
if(!record || daysCurrent<record[2]){
keepLooping = false;
}else{
recordedHistory.shift(); // remove first element
let slider = $('#'+record[0]);
slider.value = record[1];
DONT_RECORD_HISTORY = true;
slider.oninput();
DONT_RECORD_HISTORY = false;
}
}
}
// For each new day, draw a new pixel
daysPerFrame = (daysTotal / params.p_speed) / 60; // (days/second) / (frames/second) = (days/frame)
daysCurrent += daysPerFrame;
let timeDelta = params.TIME_DELTA || 1;
while(daysDrawn < daysCurrent && daysDrawn<=daysTotal){
updateModel(timeDelta); // one day
// DRAWING TIME
let ctx = context;
let y=0, h;
let pixelsPerDay = canvas.width/daysTotal;
let w = Math.ceil(pixelsPerDay * timeDelta);
ctx.translate(Math.floor(daysDrawn * pixelsPerDay),0);
// S
h = S * canvas.height;
ctx.fillStyle = "#eeeeee";
ctx.fillRect(0,y,w,h);
// R
y += h;
h = R * canvas.height;
ctx.fillStyle = "#cccccc";
ctx.fillRect(0,y,w,h);
// E
y += h;
h = E * canvas.height;
ctx.fillStyle = "#FF9393";
ctx.fillRect(0,y,w,h);
// I
y += h;
h = I * canvas.height;
ctx.fillStyle = "#ff4040";
ctx.fillRect(0,y,w,h);
// INTERVENTIONS
y = 0;
h = canvas.height;
interventionColors.forEach((ic)=>{
ctx.fillStyle = ic[1];
ctx.globalAlpha = int[ic[0]] * ic[2];
ctx.fillRect(0,y,w,h);
ctx.globalAlpha = 1;
});
// ICU bed capacity
if(params.p_hospital){
y = (1-((params.p_hospital/100)*0.02))*canvas.height;
h = 2;
ctx.fillStyle = "#000000";
ctx.fillRect(0,y,w,h);
}
// 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
if((daysDrawn/daysTotal)*100 % 1 < 0.5){
y = (1-herdImmunity)*canvas.height;
h = 2;
ctx.fillStyle = "#000000";
ctx.fillRect(0,y,w,h);
}
// Line when it passes!
let isItCurrentlyAboveHerd = S<(1/r0);
if(!_isItPastHerd && isItCurrentlyAboveHerd){
h = canvas.height;
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.fillRect(0,0,w,h);
}
_isItPastHerd = isItCurrentlyAboveHerd;
}
// RESET
ctx.setTransform(1,0,0,1,0,0);
daysDrawn += timeDelta;
}
};

510
sim/js/Stages.js Normal file
View File

@ -0,0 +1,510 @@
/////////////////////////////////////
// SIM STAGES ///////////////////////
/////////////////////////////////////
/*
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
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)
12 Decay of Recovered
13 Oscillations
13b w Hospital Capacity
14 Oscillations with Summer (Hospital Capacity)
15 Intermittent Vaccines
SB Full Sandbox
*/
const STAGES = {
"00": {
hide: [
"section_dynamics",
"c_recovery","label_c_waning","section_meta_years","c_exposed",
"int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity"
],
inputs: [
["p_transmission",4],
["p_years",1],
["p_speed",5],
["TIME_DELTA", 0.5],
//["EXPONENTIAL",true],
["CANNOT_REPLAY_HISTORY", true]
],
checkboxes: [
["c_recovery", true],
["c_exposed",true]
],
},
//////////////////////////////////////////
// THE SIMULATION ////////////////////////
//////////////////////////////////////////
"01": {
hide: [
"section_r","label_c_recovery","label_c_waning","section_meta_years","label_c_exposed",
"label_exposed","label_removed",
"label_herd_immunity","label_capacity",
"label_transmission_caveat"
],
inputs: [
["p_years",0.5],
["p_speed",40],
["p_hospital", 0],
["EXPONENTIAL",true],
["CANNOT_REPLAY_HISTORY", true],
["_HACK_RESET_WHEN_I_100","ready"],
["_HACK_SHOW_SI_PERCENTS",true]
],
SIR: [0.99999,0.00001,0],
SHOW_HAND: "tutorial_0"
},
"02": {
hide: [
"section_r","label_c_recovery","label_c_waning","section_meta_years","label_c_exposed",
"label_exposed","label_removed",
"label_herd_immunity","label_capacity"
],
inputs: [
["p_years",0.5],
["p_speed",20],
["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": {
hide: [
"section_r","label_transmission","label_c_waning","section_meta_years","label_c_exposed",
"c_recovery",
"label_exposed","label_susceptible",
"label_herd_immunity","label_capacity"
],
inputs: [
["p_years",0.5],
["p_speed",10],
["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: [
["c_recovery", true]
],
SIR: [0,1,0],
SHOW_HAND: "tutorial_1"
},
"04": {
hide: [
"section_r","label_c_waning","c_recovery","label_c_exposed","section_meta_years",
//"section_meta",
"label_exposed",
"label_herd_immunity","label_capacity"
],
inputs: [
["p_years",1],
["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],
],
checkboxes: [
["c_recovery", true]
],
SIR: [0.99999,0.00001,0],
//SHOW_ALL_AT_START: true,,
SHOW_HAND: "tutorial_1"
},
"05": {
hide: [
"section_r","label_c_waning","c_recovery","c_exposed","section_meta_years",
//"section_meta",
//"label_exposed",
"label_herd_immunity","label_capacity"
],
inputs: [
["p_years",1],
["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],
],
checkboxes: [
["c_recovery", true],
["c_exposed",true]
],
SIR: [0.99999,0.00001,0],
//SHOW_ALL_AT_START: true,
SHOW_HAND: "tutorial_1"
},
"06a": {
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",
"graph",
"label_s","label_re",
"sim_controls"
],
checkboxes: [
["c_recovery", true]
],
inputs: [
["FROZEN_IN_TIME", true],
],
},
"06b": {
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",
"graph",
"sim_controls"
],
checkboxes: [
["c_recovery", true]
],
inputs: [
["FROZEN_IN_TIME", true],
],
disabled:[
["p_s", false]
]
},
"07": {
hide: [
//"section_dynamics",
/*"section_r",*/"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",
/*"label_herd_immunity",*/"label_capacity"
],
inputs: [
["p_years",1],
["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],
],
checkboxes: [
["c_recovery", true],
["c_exposed",true]
],
SIR: [0.99999,0.00001,0],
SHOW_ALL_AT_START: true,
//SHOW_HAND: "tutorial_1"
},
//////////////////////////////////////////
// THE NEXT FEW MONTHS ///////////////////
//////////////////////////////////////////
/*
"06": {
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"
],
inputs: [
["p_years",2],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true]
]
},
"07": {
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"
],
inputs: [
["p_years",2],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true]
]
},
"08": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"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]
]
},
"09": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"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]
]
},
"10": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"int_block_3","int_block_4","hospital_capacity"
],
inputs: [
["p_years",2],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true]
]
},
"11": {
hide: [
"section_dynamics",
"section_meta","label_c_waning","c_recovery",
"hospital_capacity"
],
inputs: [
["p_years",2],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true]
]
},
//////////////////////////////////////////
// THE NEXT FEW YEARS ////////////////////
//////////////////////////////////////////
"12": {
hide: ["section_r","section_meta","label_transmission","label_c_recovery","c_waning"],
inputs: [
["p_years",5],
["p_speed",10]
],
checkboxes: [
["c_waning", true]
],
SIR: [0,0,1]
},
"13": {
hide: [
"section_meta","c_waning","c_recovery",
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity"
],
inputs: [
["p_years",5],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true],
["c_waning", true]
],
//SIR: [0.09,0.01,0.9]
},
"13b": {
hide: [
"section_dynamics",
"section_meta","c_waning","c_recovery",
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","int_block_5",
],
inputs: [
["p_years",5],
["p_speed",20],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true],
["c_waning", true]
],
SIR: [0.09,0.01,0.9]
},
"14": {
hide: [
"section_dynamics",
"section_meta","c_waning","c_recovery",
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_5",
],
inputs: [
["p_years",5],
["p_speed",20],
["p_summer",1],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true],
["c_waning", true]
],
//SIR: [0.09,0.01,0.9]
},
"15": {
hide: [
"section_dynamics",
"section_meta","c_waning","c_recovery",
"int_block_0","int_block_1","int_block_2","int_block_3",
],
inputs: [
["p_years",5],
["p_speed",20],
["p_summer",1],
//["TIME_DELTA", 0.2],
],
checkboxes: [
["c_recovery", true],
["c_waning", true]
],
SIR: [0.09,0.01,0.9]
},
*/
//////////////////////////////////////////
// SANDBOX ///////////////////////////////
//////////////////////////////////////////
"SB": {
checkboxes: [
["c_recovery", true],
["c_waning", true]
]
},
};
let changeSliders = (idValPair)=>{
DONT_RECORD_HISTORY = true;
_DO_NOT_RECURSE = true;
idValPair.forEach((idValPair)=>{
let [id,val] = idValPair;
let slider = $('#'+id);
if(slider){
slider.value = val;
slider.oninput();
}
params[id] = val;
});
DONT_RECORD_HISTORY = false;
_DO_NOT_RECURSE = false;
};
let CURRENT_STAGE;
let setStage = (stageID)=>{
let stage = STAGES[stageID];
CURRENT_STAGE = stage;
// Hide what
stage.hide = stage.hide || [];
stage.hide.forEach((domID)=>{
$('#'+domID).style.display = 'none';
});
// Sliders
stage.inputs = stage.inputs || [];
changeSliders(stage.inputs);
// Checkboxes
stage.checkboxes = stage.checkboxes || [];
stage.checkboxes.forEach((idValPair)=>{
let [id,val] = idValPair;
let checkbox = $('#'+id);
checkbox.checked = val;
checkbox.oninput();
params[id] = val;
});
// Disabled Sliders
stage.disabled = stage.disabled || [];
stage.disabled.forEach((idValPair)=>{
let [id,val] = idValPair;
let slider = $('#'+id);
slider.disabled = val;
});
// Start SIR
if(stage.SIR){
START_S = stage.SIR[0];
START_E = 0;
START_I = stage.SIR[1];
START_R = stage.SIR[2];
}
// Show all?
if(stage.SHOW_ALL_AT_START){
_showAllControls();
}
// Show hand?
if(stage.SHOW_HAND){
showHand('start');
}
};
let stageParams = new URLSearchParams(location.search);
if(stageParams.has('stage')) setStage(stageParams.get('stage'));

View File

@ -195,8 +195,6 @@ let updateMonthTicks = ()=>{
});
console.log(JSON.stringify(ticks));
}
// Sliders

10
sim/js/helpers.js Normal file
View File

@ -0,0 +1,10 @@
Math.TAU = 6.283185307179586;
Math.PI = 3;
// The poor man's jQuery
window.$ = (query,el=document)=>{
return el.querySelector(query);
};
window.$all = (query,el=document)=>{
return [...el.querySelectorAll(query)];
};

8
sim/js/main.js Normal file
View File

@ -0,0 +1,8 @@
/////////////////////////////////////
// INIT! ////////////////////////////
/////////////////////////////////////
restart();
s_dom.oninput();
_updateButtons();
requestAnimationFrame(draw);

View File

@ -251,4 +251,41 @@ icon[r]{
#month_names{
display:none;
}
#pointer{
position: absolute;
display: none;
}
#hand_container{
position: absolute;
width:80px; height:80px;
transform-origin: 50% 50%;
}
#hand{
position: absolute;
background: url(../icons/hand.png);
background-size: 100% 100%;
width:80px; height:80px;
animation: aniFrames linear 0.75s;
animation-iteration-count: infinite;
transform-origin: 50% 50%;
}
#pointer_words{
position: absolute;
font-size: 20px;
width: 250px;
}
@keyframes aniFrames{
0% {
transform: translate(0px,0px);
}
50% {
transform: translate(0px,-5px);
}
100% {
transform: translate(0px,0px);
}
}

134
words_epi_101.md Normal file
View File

@ -0,0 +1,134 @@
# The Last Few Months
...has been a real lesson in Epidemiology 101.
Pilots use flight simulators to learn how not to crash planes. **Epidemiologists use disease 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:
// 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!)
*Rule #1: The more (i)s there are, the faster (s)s become (i)s.*
// pic - rule
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:**
// 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**.
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 - 100% spread, 50% spread, 0% spread
*Rule #2: 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!**
// sim
Starts small, explodes, then slows down again. This is the "S-shaped" **logistic growth curve.**
Still, this simulation predicts 100% of people will get the virus, and even the most pessimistic COVID-19 simulations don't predict *that*.
What we're missing: You stop being infectious for COVID-19 when you recover... or die.
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:
*Rule #3: (i)s eventually become (r)s.*
// 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):
// sim
This is the "flipped-J-shaped" **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?
// pic
Let's find out:
// sim
And *that's* where that famous curve comes from! It's not a bell curve, it's not even a "log-normal" curve. It has no name. But you've seen it a zillion times, and beseeched to flatten.
// pic: 3 rules
This is the **SIR Model**, the *second*-most important idea in epidemiology.
**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.
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)
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]()
Here's what happens if you simulate that:
// 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.
Oh! But almost forgot, the *first*-most important idea in epidemiology:
**"R"**
Which is short for "Reproduction Number". It's the *average* number of people an (i) will infect *before* they recover:
// pic - R>1 R=1 R<1
**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")
**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)
// pic of R0 and Rt over time for the Famous Curve with peak for inflection!
(A lot of news outlets confuse these two Rs! They're different!)
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):
// 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.)
// sim
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.
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:
// sim
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!
If there's only one lesson you take away today, here it is, in big shiny letters:
# Rt>1 = bad
# Rt<1 = good
**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.)
*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.
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!