ALL STAGES AHHH

This commit is contained in:
Nicky Case 2020-04-19 22:23:35 -04:00
parent 9c457d33b8
commit 3be30e6d28
4 changed files with 736 additions and 74 deletions

View File

@ -1,8 +1,22 @@
<!--
Pause/Play @done
Colors for all interventions @done
Hospital Capacity slider
Herd Immunity dashed
R shown with arrows
Calendar at bottom
Replay what you just did @done
Share/Embed!
---
Trapezoid
No Recovered
No Waning
No Recovered @done
No Waning @done
Pause / Play
Interventions - % Susceptible, Hygeine, Physical Distancing, Isolate Symptomatic, Quarantine Pre/A-Symptomatic Contacts, Face Masks, Deep Cleaning
Seasonality

19
sim.css
View File

@ -1,14 +1,15 @@
body{
font-family: Helvetica, Arial, sans-serif;
font-size: 15px;
}
#controls{
width:300px;
width:250px;
/*height:500px;*/
float:left;
position: relative;
}
/*
#controls > div{
overflow: hidden;
margin-bottom: 1em;
@ -64,16 +65,24 @@ body{
right: 0;
width: 50px;
height: 20px;
}
}*/
#controls > input{
#controls input[type=range]{
width: 100%;
}
#controls > div[strikethrough]{
text-decoration: line-through;
}
#controls > div[strikethrough] > input[type=range]{
pointer-events: none;
opacity: 0.5;
}
/*
#controls > div[locked]{
opacity: 0.2;
pointer-events: none;
}
}*/
#graph{

608
sim.js
View File

@ -1,3 +1,6 @@
Math.TAU = 6.283185307179586;
Math.PI = 3;
window.$ = (query,el=document)=>{
return el.querySelector(query);
};
@ -13,40 +16,158 @@ let S,I,R;
let params = {};
// Sliders
let DONT_RECORD_HISTORY = true;
$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);
label.innerHTML = val;
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)]);
}
}
};
slider.oninput = onChange;
onChange();
});
DONT_RECORD_HISTORY = 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();
});
/////////////////////////////////////
// UPDATE ///////////////////////////
/////////////////////////////////////
let updateModel = (days)=>{
let int = {
hygiene: 0,
distancing: 0,
isolate: 0,
quarantine: 0,
cleaning: 0,
masks: 0,
summer: 0,
vaccines: 0
};
let updateModel = (days, fake)=>{
let real_S=S, real_I=I, real_R=R;
let transmissionRate = 1/params.p_transmission,
recoveryRate = 1/params.p_recovery,
immunityLossRate = 1/(params.p_waning*365);
let newlyInfected = S*I*transmissionRate*days;
// R0
let r0_dom = $('#p_r0');
r0_dom.value = transmissionRate/recoveryRate;
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...
let s_dom = $('#p_s');
if(!s_dom.disabled){
S = parseFloat(s_dom.value);
}
// Update Model
let newlyInfected;
if(params.EXPONENTIAL){
newlyInfected = I*transmissionRate*days;
}else{
newlyInfected = S*I*transmissionRate*days;
}
let newlyRecovered = params.c_recovery ? (I*recoveryRate*days) : 0;
let newlySusceptible = params.c_waning ? (R*immunityLossRate*days) : 0;
S -= newlyInfected;
I += newlyInfected;
let newlyRecovered = I*recoveryRate*days;
I -= newlyRecovered;
R += newlyRecovered;
let newlySusceptible = R*immunityLossRate*days;
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;
}
let re_dom = $('#p_re');
re_dom.value = newlyInfected/newlyRecovered;
re_dom.oninput();
// IF FAKE, UNDO EVERYTHING
if(fake){
S = real_S;
I = real_I;
R = real_R;
}
}
/////////////////////////////////////
@ -62,26 +183,64 @@ canvas.style.height = (canvas.height/2)+"px";
let daysCurrent, daysDrawn, daysTotal, daysPerFrame;
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 draw = ()=>{
// Redraw
requestAnimationFrame(draw);
updateModel(1, true); // one day
// At the end? No!
// Paused? At the end?
if(!IS_PLAYING) return;
if(params.FROZEN_IN_TIME) return;
if(daysCurrent > daysTotal) return;
// 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(1); // one day
updateModel(timeDelta); // one day
// DRAWING TIME
let ctx = context;
let y=0, h;
let pixelsPerDay = canvas.width/daysTotal;
let w = Math.ceil(pixelsPerDay);
let w = Math.ceil(pixelsPerDay * timeDelta);
ctx.translate(Math.floor(daysDrawn * pixelsPerDay),0);
// S
@ -101,15 +260,25 @@ let draw = ()=>{
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
y = (1-0.02)*canvas.height;
y = (1-((params.p_hospital/100)*0.02))*canvas.height;
h = 2;
ctx.fillStyle = "#000000";
ctx.fillRect(0,y,w,h);
// RESET
ctx.setTransform(1,0,0,1,0,0);
daysDrawn += 1;
daysDrawn += timeDelta;
}
@ -118,25 +287,430 @@ requestAnimationFrame(draw);
/////////////////////////////////////
// RESTART //////////////////////////
// (RE)START ////////////////////////
/////////////////////////////////////
let IS_PLAYING = true;
let togglePlaying = ()=>{
IS_PLAYING = !IS_PLAYING;
};
$('#start').onclick = togglePlaying;
let recordedHistory = [];
let IS_REPLAYING_HISTORY = false;
let START_S = 0.999,
START_I = 0.001,
START_R = 0.000;
let restart = ()=>{
S = 0.999;
I = 0.001;
R = 0.000;
// Model
S = START_S;
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);
};
restart();
$('#restart').onclick = restart;
$('#restart').onclick = ()=>{
IS_REPLAYING_HISTORY = false;
recordedHistory = [];
restart();
};
$('#replay').onclick = ()=>{
IS_REPLAYING_HISTORY = true;
restart();
};
/////////////////////////////////////
// SIM STAGES ///////////////////////
/////////////////////////////////////
/*
01 Just exponential
02 Just logistic
03 Decay
04 The famous curve
05 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
12 Decay of Recovered
13 Oscillations (Hospital Capacity)
14 Oscillations with Summer (Hospital Capacity)
15 Intermittent Vaccines
SB Full Sandbox
*/
const STAGES = {
//////////////////////////////////////////
// THE SIMULATION ////////////////////////
//////////////////////////////////////////
"01": {
hide: ["section_r","section_meta","label_c_recovery","label_c_waning"],
inputs: [
["p_years",0.5],
["p_speed",10],
["TIME_DELTA", 0.1],
["EXPONENTIAL",true]
]
},
"02": {
hide: ["section_r","section_meta","label_c_recovery","label_c_waning"],
inputs: [
["p_years",0.5],
["p_speed",10],
["TIME_DELTA", 0.1],
]
},
"03": {
hide: ["section_r","section_meta","label_transmission","label_c_waning","c_recovery"],
inputs: [
["p_years",0.5],
["p_speed",10],
["TIME_DELTA", 0.1],
],
checkboxes: [
["c_recovery", true]
],
SIR: [0,1,0]
},
"04": {
hide: ["section_r","section_meta","label_c_waning","c_recovery"],
inputs: [
["p_years",0.5],
["p_speed",10],
["TIME_DELTA", 0.1],
],
checkboxes: [
["c_recovery", true]
]
},
"05a": {
hide: [
"section_meta","label_c_waning","c_recovery",
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","hospital_capacity",
"graph",
"label_s","label_re",
"sim_controls"
],
checkboxes: [
["c_recovery", true]
],
inputs: [
["FROZEN_IN_TIME", true],
],
},
"05b": {
hide: [
"section_meta","label_c_waning","c_recovery",
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","hospital_capacity",
"graph",
"sim_controls"
],
checkboxes: [
["c_recovery", true]
],
inputs: [
["FROZEN_IN_TIME", true],
],
disabled:[
["p_s", false]
]
},
"05": {
hide: [
"section_meta","label_c_waning","c_recovery",
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","hospital_capacity"
],
inputs: [
["p_years",0.5],
["p_speed",30],
["TIME_DELTA", 0.1],
],
checkboxes: [
["c_recovery", true]
]
},
//////////////////////////////////////////
// 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 setStage = (stageID)=>{
let stage = STAGES[stageID];
// Hide what
stage.hide = stage.hide || [];
stage.hide.forEach((domID)=>{
$('#'+domID).style.display = 'none';
});
// Sliders
stage.inputs = stage.inputs || [];
DONT_RECORD_HISTORY = true;
stage.inputs.forEach((idValPair)=>{
let [id,val] = idValPair;
let slider = $('#'+id);
if(slider){
slider.value = val;
slider.oninput();
}
params[id] = val;
});
DONT_RECORD_HISTORY = false;
// 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_I = stage.SIR[1];
START_R = stage.SIR[2];
}
};
let stageParams = new URLSearchParams(location.search);
if(stageParams.has('stage')) setStage(stageParams.get('stage'));
restart();

165
wip.html
View File

@ -2,68 +2,133 @@
<div id="controls">
On <i>average</i>, in the beginning,<br>
each infected person...
<br>
<br>
Infects a new person every <span id="label_p_transmission">N</span> days
<br>
<input class="sim_input" type="range" id="p_transmission" min="1" max="28" step="1" value="4">
<br>
Recovers 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">
<br>
Then loses immunity in <span id="label_p_waning">N</span> years
<br>
<input class="sim_input" type="range" id="p_waning" min="0.5" max="5" step="0.5" value="1">
<div id="section_dynamics">
<hr>
On <i>average</i>,
each infected person...
<br><br>
<div id="label_transmission">
Infects a new person every <span id="label_p_transmission">N</span> days
(in the beginning)
<br>
<input class="sim_input" type="range" id="p_transmission" min="1" max="28" step="1" value="4">
</div>
<div id="label_c_recovery">
<input class="sim_checkbox" type="checkbox" id="c_recovery">
Recovers 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">
</div>
<div id="label_c_waning">
<input class="sim_checkbox" type="checkbox" id="c_waning">
Loses immunity in <span id="label_p_waning">N</span> years
<br>
<input class="sim_input" type="range" id="p_waning" min="0.5" max="5" step="0.5" value="1">
</div>
R0 is ...
<hr>
<br><br>
But with % Susceptible,
<br>
Hygeine,
<br>
Physical distancing
<br>
Isolating (symptomatic) cases
<br>
Quarantining (pre/a-symptomatic) contacts
<br>
Deep cleaning (fomites)
<br>
Face masks
<br>
& Summer...
</div>
<br><br>
<div id="section_r">
R is now...
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>
<br>
<hr>
<span id="label_s">
But R is changed by...
<br>
Susceptible population
<input class="sim_input" type="range" id="p_s" min="0" max="1" step="0.001" disabled>
<br>
</span>
Vaccinate until N% are <i>not</i> Susceptible
<br>
<span id="int_block_0">
Personal Hygiene
<br>
<input class="sim_input recordable" type="range" id="p_hygiene" min="0" max="1" step="0.001" value="0">
<br>
</span>
<span id="int_block_1">
Physical Distancing
<br>
<input class="sim_input recordable" type="range" id="p_distancing" min="0" max="1" step="0.001" value="0">
<br>
</span>
<span id="int_block_2">
Isolating Cases
<br>
<input class="sim_input recordable" type="range" id="p_isolate" min="0" max="1" step="0.001" value="0">
<br>
Quarantining Contacts
<br>
<input class="sim_input recordable" type="range" id="p_quarantine" min="0" max="1" step="0.001" value="0">
<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>
</span>
<span id="int_block_4">
Summer
<br>
<input class="sim_input recordable" type="range" id="p_summer" min="0" max="1" step="0.001" value="0">
<br>
</span>
<span id="int_block_5">
Vaccinations
<br>
<input class="sim_input recordable" type="range" id="p_vaccines" min="0" max="1" step="0.001" value="0">
<br>
</span>
Change hospital capacity to...
<br>
<br>
<hr>
<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>
</span>
Simulate <span id="label_p_years">N</span> years
<br>
<input class="sim_input" type="range" id="p_years" min="0.5" max="5" step="0.5" value="2">
<br>
In <span id="label_p_speed">N</span> seconds
<br>
<input class="sim_input" type="range" id="p_speed" min="1" max="60" step="1" value="30">
<span id="hospital_capacity">
Hospital capacity at <span id="label_p_hospital">N</span>%
<br>
<input class="sim_input recordable" type="range" id="p_hospital" min="0" max="500" step="1" value="100">
</span>
<hr>
<hr>
</div>
<div id="section_meta">
Simulate <span id="label_p_years">N</span> years...
<br>
<input class="sim_input" type="range" id="p_years" min="0.5" max="10" step="0.5" value="2">
<br>
...in <span id="label_p_speed">N</span> seconds
<br>
<input class="sim_input" type="range" id="p_speed" min="1" max="60" step="1" value="30">
<hr>
</div>
<div id="sim_controls">
<button id="start">start/stop</button>
<button id="restart">restart</button>
<button id="replay">replay recording</button>
</div>
<button id="restart">restart</button>
</div>
<div id="graph">