Published
Edited
May 14, 2020
4 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
popParamDefs = [
{name: 'popSize', title: 'Population Size', min: 100, max: 5000, step: 50, value: 2500, description: 'total number of people in simulation<br />'},
{name: 'peoplePerHouse', title: 'People Per House', min: 0.1, max: 5, step: 0.1, value: 2.6, description: 'average number of people in each house<br />'},
{name: 'peoplePerDestination', title: 'People Per Destination', min: 1, max: 100, step: 1, value: 20, description: 'for every how many people is there a destination in the community<br />'},
]
Insert cell
diseaseParamDefs = [
{name: 'seedInfectionRate', title: 'Seed Infection Rate', min: 0, max: 1, step: 0.01, value: 0.1, description: 'what portion of the population starts out infected<br />'},
{name: 'susceptibility', title: 'Susceptibility', min: 0, max: 1, step: 0.01, value: 0.04, description: 'chance that susceptible individual contracts the infection from single exposure<br />'},
{name: 'symptomaticityRate', title: 'Symptomaticity Rate', min: 0, max: 1, step: 0.01, value: 0.8, description: 'portion of infected individuals who develop symptoms<br />'},
{name: 'hospitalizationRate', title: 'Hospitalization Rate', min: 0, max: 1, step: 0.01, value: 0.05, description: 'portion of infected individuals who require hospitalization<br />'},
{name: 'mortalityRate', title: 'Mortality Rate', min: 0, max: 1, step: 0.01, value: 0.01, description: 'portion of infected individuals who die<br />'},
{name: 'householdExposure', title: 'Househould Exposure Rate', min: 1, max: 10, step: 1, value: 5, description: 'what is the relative weight of exposure to someone in the same house compared to exposure from being at a shared destination<br />', format: v => `${v}to1`},
]


Insert cell
simParamDefs = [
{name: 'chanceOfLeavingHome', title: 'Daily chance of Going Out', min: 0, max: 1, step: 0.05, value: 0.85, description: 'how likely are people to leave their home any given day, potentially exposing themselves or others<br />'},
{name: 'chanceOfLeavingHomeSick', title: 'Daily chance of Going Out When Symptomatic', min: 0, max: 1, step: 0.05, value: 0.55, description: 'how likely are symptomatic people to leave their home any given day, potentially exposing others<br />'},
{name: 'chanceOfLeavingHomeSevere', title: 'Daily chance of Going Out When Severly Symptomatic', min: 0, max: 1, step: 0.05, value: 0.15, description: 'how likely are severly sick people to leave their home any given day, potentially exposing others<br />'},
{name: 'exposureIntensityReduction', title: 'Reducing Chance of exposure', min: 0, max: 1, step: 0.05, value: 0.0, description: 'how much is each chance of exposure reduced (e.g. from hand-washing, keeping distance when out, etc)<br />'},
// {name: 'householdQuarantineSymptomatic', title: 'Household Quarantine of Symptomatic People', min: 0, max: 1, step: 0.05, value: 0.55, description: 'what portion of the population starts out infected<br />'},
// {name: 'householdQuarantineNonSymptomatic', title: 'Household Quarantine of Pre/a-symptomatic People', min: 0, max: 1, step: 0.05, value: 0.55, description: 'what portion of the population starts out infected<br />'},
]
Insert cell
viewof diseaseCourse = {
return ({
sim1:
{infectious: {start: 4, end: 13},
incubation: {start: 0, end: 5}, // 100% chance of becoming infectious if infected
symptomatic: {start: 6, end: 13}, // chance of developing symptoms defined in disease parameters
hospitalization: {start: 7, end: 13}, // chance of being hospitalized defined in disease parameters
recoveryOrMortalityDay: 14
},
sim2:
{infectious: {start: 4, end: 13},
incubation: {start: 0, end: 5}, // 100% chance of becoming infectious if infected
symptomatic: {start: 6, end: 13}, // chance of developing symptoms defined in disease parameters
hospitalization: {start: 7, end: 13}, // chance of being hospitalized defined in disease parameters
recoveryOrMortalityDay: 14
}
})
}
Insert cell
popSliders = makeSliders(popParamDefs)
Insert cell
viewof popParams = new View(nameParams(pop, popParamDefs))
Insert cell
diseaseSliders = makeSliders(diseaseParamDefs)
Insert cell
viewof diseaseParams = new View(nameParams(disease, diseaseParamDefs))
Insert cell
simSliders = makeSliders(simParamDefs)
Insert cell
viewof simParams = new View(nameParams(sim, simParamDefs))
Insert cell
// these params get derived from the user-entered Global Population params.
mutable derivedParams = {
let params = ({})
simNames.forEach(function(simN,i){
params[simN] = ({})
params[simN].numHouses = Math.round(popParams[simN].popSize / popParams[simN].peoplePerHouse)
params[simN].numDestinations = Math.round(popParams[simN].popSize / popParams[simN].peoplePerDestination)
});
return params
}

Insert cell
paramDifferences = {
let paramDifferences = []
let sim1DifferencesHtml= ''
let sim2DifferencesHtml= ''
Object.keys(popParams.sim1).forEach(param => {
if(popParams.sim1[param] !== popParams.sim2[param] ){
paramDifferences.push({[param]:{sim1: popParams.sim1[param], sim2: popParams.sim2[param]}} )
sim1DifferencesHtml += (param + ': ' + popParams.sim1[param] + '<br />')
sim2DifferencesHtml += (param + ': ' + popParams.sim2[param] + '<br />')
}
})
Object.keys(diseaseParams.sim1).forEach(param => {
if(diseaseParams.sim1[param] !== diseaseParams.sim2[param] ){
paramDifferences.push({[param]:{sim1: diseaseParams.sim1[param], sim2: diseaseParams.sim2[param]}} )
sim1DifferencesHtml += (param + ': ' + diseaseParams.sim1[param] + '<br />')
sim2DifferencesHtml += (param + ': ' + diseaseParams.sim2[param] + '<br />')
}
})
Object.keys(simParams.sim1).forEach(param => {
if(simParams.sim1[param] !== simParams.sim2[param] ){
paramDifferences.push({[param]:{sim1: simParams.sim1[param], sim2: simParams.sim2[param]}} )
sim1DifferencesHtml += (param + ': ' + simParams.sim1[param] + '<br />')
sim2DifferencesHtml += (param + ': ' + simParams.sim2[param] + '<br />')
}
})
return [ paramDifferences, sim1DifferencesHtml, sim2DifferencesHtml]
}
Insert cell
Insert cell
simNames = ['sim1','sim2'] // global definition of sim-names. Changing this does nothing interesting.
Insert cell
// array of people in population -- rebuilt when population params are updated
people = {
let people = ({})
const sims = ['sim1','sim2']
sims.forEach(function(simN) {
people[simN] = [...Array(popParams[simN].popSize)].map(function(p, i){
let curRandom = Math.random();
return {index: i,
houseIndex: Math.floor( derivedParams[simN].numHouses * ( Math.random() )) ,
susceptibility: diseaseParams[simN].susceptibility,
symptomaticIfInfected: curRandom < diseaseParams[simN].symptomaticityRate ? 1:0,
hospitalIfInfected: curRandom < diseaseParams[simN].hospitalizationRate ? 1:0,
dieIfInfected: curRandom < diseaseParams[simN].mortalityRate ? 1:0
}
})
})
return people
}

Insert cell
// this is where the data for the main simulation lives.
mutable mainSim = {
//updateParams; // rebuild when updateParams is run.
resetSimButton; // rebuild when simulation is reset
pop, disease // rebuild when pop or disease global variables change
let mainSim = {}
simNames.forEach(d=>
mainSim[d] = {infections :{},
curInfections: [0],
cumulInfections: [0],
curHospitalizations: [0],
cumulHospitalizations: [0]}
)
return mainSim
}

Insert cell
md `#### SVG Handling`
Insert cell
clearSVG = {
resetSimButton; pop; disease; // run when any of these things change
let svg = d3.selectAll("svg")
svg.selectAll('path').remove()
svg.selectAll('line').remove()
svg.selectAll('g').remove()
svg.selectAll('text').remove()

}
Insert cell
addLegendSVG = function(){ d3.select('#legend').selectAll('line').data(['CumulativeInfections','CurrentInfections']).enter().append('line').attr('x1',10).attr('x2',30).attr('y1',(d,i)=> 17*(i+1)).attr('y2',(d,i)=> 17*(i+1)).attr('stroke-width',2).attr('stroke',(d,i) => ['steelblue','orange'][i])
d3.select('#legend').selectAll('text').data(['CumulativeInfections','CurrentInfections']).enter().append('text').attr('x',35).attr('y',(d,i)=> 5+17*(i+1)).text(d => d).attr('fill',(d,i) => ['steelblue','orange'][i])
}
Insert cell
updateSVG = function(simGroup){
let svg = d3.selectAll("svg")
svg.selectAll('path').remove()
svg.selectAll('line').remove()
svg.selectAll('g').remove()
svg.selectAll('text').remove()

addLegendSVG()
simNames.forEach( (simN) => {
let daysIn = simGroup[simN].cumulInfections.length
let xScale = d3.scaleLinear()
.domain([0, simGroup[simN].cumulInfections.length < 40 ? 40 : 10 * Math.ceil(daysIn / 10 )])
.range([55, 440])
let yScale = d3.scaleLinear().domain([0,popParams[simN].popSize]).range([215, 25])
let data = simGroup[simN]
d3.select("#line-chart-" + simN).selectAll('line')
.data([0,1,2,3]).enter().append('line').attr('stroke','#CCC').attr('x1', 50).attr('x2', 440)
.attr('y1', d=> 25 + d*(190/4)).attr('y2', d=> 25 + d*(190/4))
d3.select("#line-chart-" + simN).append("g").selectAll('line')
.data([...Array(Math.ceil(daysIn/ (daysIn > 60 ? 28 : 7)))]).enter().append('line')
.attr('stroke','#CCC').attr('x1', (d,i) => xScale(i*(daysIn > 60 ? 28 : 7))).attr('x2', (d,i) => xScale(i*(daysIn > 60 ? 28 : 7))).attr("stroke-dasharray","5,5")
.attr('y1','215').attr('y2', '25')
d3.select("#line-chart-" + simN).append("path")
.datum(data.cumulInfections)
.attr("fill", "none").attr("stroke", "steelblue").attr("stroke-width", 2)
.attr("d", d3.line()
.x((d,i) => xScale(i) )
.y(d =>yScale(d))
)
d3.select("#line-chart-" + simN).append("path")
.datum(data.curInfections)
.attr("fill", "none").attr("stroke", "orange").attr("stroke-width", 2)
.attr("d", d3.line()
.x((d,i) => xScale(i) )
.y(d =>yScale(d))
)
/* THis was for hospitalizations, but should probably be broken out to different svg, or just have count show up somewhere
d3.select("#line-chart-" + simN).append("path")
.datum(data.cumulHospitalizations)
.attr("fill", "none").attr("stroke", "green").attr("stroke-width", 2)
.attr("d", d3.line()
.x((d,i) => xScale(i) )
.y(d =>yScale(d))
)
d3.select("#line-chart-" + simN).append("path")
.datum(data.curHospitalizations)
.attr("fill", "none").attr("stroke", "purple").attr("stroke-width", 2)
.attr("d", d3.line()
.x((d,i) => xScale(i) )
.y(d =>yScale(d))
)
*/
d3.select("#line-chart-"+simN).append("g").attr("transform", "translate(0,215)").call(d3.axisBottom(xScale))
d3.select("#line-chart-"+simN).append("text").attr("transform","translate(250,245)").style("text-anchor", "middle").style("font-size","12").text("Days");
d3.select("#line-chart-"+simN).append("g").attr("transform", "translate(55,0)").call(d3.axisLeft(yScale))
d3.select("#line-chart-"+simN).append("text").attr("transform", "translate(14,125) rotate(-90)").style("text-anchor", "middle").style("font-size","12").text("Count");
});

}
Insert cell
Insert cell
mutable triggerSim = 0
Insert cell
runSim2 = {
if ( triggerSim === 0) {
return
} else {
console.log('running sim for ' + numDays + ' days')
let sameSeed = 0 // TO-DO: Add functionality to control whether the seed for sim 2 is a deep copy of the sim 1 as defined below. I removed the functionality to toggle this with checkbox, since I didn't yet decide how to handle parameter setting when this happens -- since global params for sim2 to should be reset to those as sim1 if we're using a deep copy and parameters aren't the same.
for (let i = 0 ; i < numDays; i++) {
console.log('day ' + i + ' of numDays')
simNames.forEach( (simN, simI) => {
// if we're at beginning of simulation seedInfect, otherwise infect
if(Object.keys(mainSim[simN].infections).length == 0){ // set up both seeds if at start of simulation.
//console.log('seeding sim on day 0:')
if(simI === 0 || !(sameSeed)) {
mutable mainSim[simN].infections = seedInfect(simN) ;
} else {
mutable mainSim[simN].infections = JSON.parse(JSON.stringify(mainSim[simNames[simI-1]].infections));
}
//console.log(mutable mainSim)
} else {
mutable mainSim[simN].infections = infect(mainSim[simN].infections, simN)
};
mutable mainSim[simN].cumulInfections.push(Object.keys( mainSim[simN].infections).length);
mutable mainSim[simN].curInfections.push(Object.keys( mainSim[simN].infections).filter(d=> mainSim[simN].infections[d].daysSinceInfection < viewof diseaseCourse[simN].recoveryOrMortalityDay).length);
mutable mainSim[simN].curHospitalizations.push(Object.keys( mainSim[simN].infections).filter(d=> mainSim[simN].infections[d].health === "hospitalized").length);
mutable mainSim[simN].cumulHospitalizations.push(Object.keys( mainSim[simN].infections).filter(d=> mainSim[simN].infections[d].daysSinceInfection >= viewof diseaseCourse[simN].hospitalization.start && people[simN][d].hospitalIfInfected === 1).length);
})
mutable daysToDouble = runDaysToDouble(mainSim)
}
updateSVG(mainSim)
mutable triggerSim = 0
}
};

Insert cell
// function to run the main simulation for some number of days triggered by the main button above the svgs
runSim = function(numDays){
console.log('running sim for ' + numDays + ' days')
let sameSeed = 0 // TO-DO: Add functionality to control whether the seed for sim 2 is a deep copy of the sim 1 as defined below. I removed the functionality to toggle this with checkbox, since I didn't yet decide how to handle parameter setting when this happens -- since global params for sim2 to should be reset to those as sim1 if we're using a deep copy and parameters aren't the same.
for (let i = 0 ; i < numDays; i++) {
console.log('day ' + i + ' of numDays')
simNames.forEach( (simN, simI) => {
// if we're at beginning of simulation seedInfect, otherwise infect
if(Object.keys(mainSim[simN].infections).length == 0){ // set up both seeds if at start of simulation.
//console.log('seeding sim on day 0:')
if(simI === 0 || !(sameSeed)) {
mutable mainSim[simN].infections = seedInfect(simN) ;
} else {
mutable mainSim[simN].infections = JSON.parse(JSON.stringify(mainSim[simNames[simI-1]].infections));
}
//console.log(mutable mainSim)
} else {
mutable mainSim[simN].infections = infect(mainSim[simN].infections, simN)
};
mutable mainSim[simN].cumulInfections.push(Object.keys( mainSim[simN].infections).length);
mutable mainSim[simN].curInfections.push(Object.keys( mainSim[simN].infections).filter(d=> mainSim[simN].infections[d].daysSinceInfection < viewof diseaseCourse[simN].recoveryOrMortalityDay).length);
mutable mainSim[simN].curHospitalizations.push(Object.keys( mainSim[simN].infections).filter(d=> mainSim[simN].infections[d].health === "hospitalized").length);
mutable mainSim[simN].cumulHospitalizations.push(Object.keys( mainSim[simN].infections).filter(d=> mainSim[simN].infections[d].daysSinceInfection >= viewof diseaseCourse[simN].hospitalization.start && people[simN][d].hospitalIfInfected === 1).length);
})
mutable daysToDouble = runDaysToDouble(mainSim)
}
updateSVG(mainSim)
};


Insert cell
health = function(person, daysIn, simNum) {
let dc = viewof diseaseCourse[simNum]
let infectiousStatus = (daysIn >= dc.infectious.start &&
daysIn <= dc.infectious.end) ? 1 : 0
let healthStatus
if ( daysIn >= dc.recoveryOrMortalityDay ) {
healthStatus = person.dieIfInfected ? 'dead' : 'recovered'
} else if ( person.hospitalIfInfected &&
daysIn >= dc.hospitalization.start &&
daysIn <= dc.hospitalization.end
) {
healthStatus = 'hospitalized'
} else if ( daysIn >= dc.symptomatic.start &&
daysIn <= dc.symptomatic.end
) {
healthStatus = person.symptomaticIfInfected ? 'sick' : 'asymptomatic'
} else if (daysIn <= dc.incubation.end) {
healthStatus = 'presymptomatic'
} else {
healthStatus = 'notDefined'
}
return { infectious : infectiousStatus,health: healthStatus }
}


Insert cell
// function to assign initial infections at start of simulation
seedInfect = function(simNum) {
console.log('running seed infect function on ' + simNum)
let seedInfections = {}
while ( Object.keys(seedInfections).length < (popParams[simNum].popSize * diseaseParams[simNum].seedInfectionRate || 2) ){
let curPersonIndex = Math.floor( popParams[simNum].popSize * ( Math.random() ));
let daysSinceInfection = Math.floor( 6 * ( Math.random() ) )
let curHealth = health(people[simNum][curPersonIndex], daysSinceInfection, simNum)
//console.log(curHealth)
seedInfections[ curPersonIndex ] = {
//personIndex: curPersonIndex,
daysSinceInfection: daysSinceInfection ,
health: curHealth.health,
infectious: curHealth.infectious
}
//console.log(seedInfections[curPersonIndex])
}

return seedInfections
}
Insert cell
assignDestination = function (prevInfections, simNum, personIndex, numDestinations) {
let goOutProbability = viewof simParams.value[simNum].chanceOfLeavingHome;
if(prevInfections[personIndex]){
if(prevInfections[personIndex].health === 'sick'){
goOutProbability = viewof simParams.value[simNum].chanceOfLeavingHomeSick
} else if(prevInfections[personIndex].health === 'hospitalized'){
goOutProbability = viewof simParams.value[simNum].chanceOfLeavingHomeSevere
}
}
return (Math.random() > goOutProbability) ? -1 : Math.floor( numDestinations * Math.random() )
} // would need to change this to potentially have certain houses with certain destinations.

Insert cell
// function to generate new infections daily
infect = function(prevInfections, simNum) {
let updatedInfections = prevInfections // this is where we'll store updated infections
let destinationExposures = {}
let houseExposures = {}
let expMultiplier = 1 - viewof simParams.value[simNum].exposureIntensityReduction;
let houseExp = diseaseParams[simNum].householdExposure * expMultiplier;
//console.log(houseExp)
let destExp = 1 * expMultiplier;
Object.keys(prevInfections).forEach(function(d){
if ( prevInfections[d].infectious ){ // replace this with infectious boolean
let curHouse = people[simNum][d].houseIndex;
houseExposures[curHouse] = houseExposures[curHouse] ? houseExposures[curHouse] + houseExp : houseExp ;
let curDest = assignDestination(prevInfections, simNum, d, derivedParams[simNum].numDestinations)
if(curDest != -1) {
destinationExposures[curDest] = destinationExposures[curDest] ? destinationExposures[curDest] + destExp : destExp ;
}
}
})
people[simNum].forEach(function(p,i){
let myDestination = assignDestination(prevInfections, simNum, i, derivedParams[simNum].numDestinations)
let numExposures = destinationExposures[myDestination] || 0 + houseExposures[p.houseIndex] || 0
// if already infected or exposure turned into infection then put in updated infections.
if ( updatedInfections[i] || ( Math.random() < 1 - (Math.pow(1 - p.susceptibility, numExposures))) ){
let daysIn = updatedInfections[i] ? updatedInfections[i].daysSinceInfection + 1 : 0
let curHealth = health(p, daysIn, simNum)
updatedInfections[i] = {
daysSinceInfection: daysIn,
health: curHealth.health,
infectious: curHealth.infectious
}
}
});
return updatedInfections;
}
Insert cell
mutable daysToDouble = runDaysToDouble(mainSim)
Insert cell
Insert cell
md `#### Form Handling Functions`
Insert cell
makeSliders = function(paramDefs) {
return paramDefs.map(({name, title, description, min, max, step, value}) => {
// for each parameter, create two sliders with set defaults
return {
title, description, name,
sliders: simNames.map(simName => {
const id = name + simName
return {
id,
input: html`<input type=range min=${min} max=${max} step=${step} value=${value}></input>`,
output: html`<output></output>`,
}
})
}
})
}
Insert cell
makeSliderForm = function(paramDefs, sliderDefs) {
//let mySliders = makeSliders(paramDefs)
const form = html`
<form>
${
sliderDefs.map(({title, description, sliders}, i) => {
// for each parameter, create title and description
// and wrap the corresponding sliders in divs
return html`
<div>
<div><strong>${title}</strong> ${description}</div>
${
sliders.map(({input, output}) => html`
<span style='display:inline-block; min-width:325px; margin: 4px'>
${input}
${output}
</span>
`)
}
</div>
`
})
}
</form>
`
form.oninput = () => {
form.value = sliderDefs.map(({sliders}) => {
return sliders.map(({input, output}) => {
output.value = input.value
return +input.value
})
})
}
form.oninput()
return form
}
Insert cell
// this takes array of params output by slider and names them based on paramDefs
nameParams = function(paramArray, paramDefs){
let params = ({})
simNames.forEach(function(simN,i){
params[simN] = ({})
paramArray.forEach(function(input, j){
params[simN][paramDefs[j].name] = paramArray[j][i];
})
});
return params
}
Insert cell
setParam = function(paramGroup, params, sliderGroup, paramDefs, paramName, values){
console.log('setting param')
sliderGroup[ paramDefs.findIndex((p) => p.name == paramName) ].sliders.map(({input},i) => {
input.value = values[i]
})
paramGroup.oninput()
paramGroup.dispatchEvent(new CustomEvent("input"))
params.value = nameParams(paramGroup.value, paramDefs)
params.dispatchEvent(new CustomEvent("input"))

}
Insert cell
resetParams = function(paramGroup, sliderName, paramDefs){
sliderName.map(({sliders}, i) => {
return sliders.map(({input}) => {
input.value = paramDefs[i].value
})
})
paramGroup.oninput()
paramGroup.dispatchEvent(new CustomEvent("input"))
}
Insert cell
resetAllParams = function() {
resetParams(viewof pop, popSliders, popParamDefs);
resetParams(viewof disease, diseaseSliders, diseaseParamDefs);
resetParams(viewof sim, simSliders, simParamDefs);
}
Insert cell
resetButton = function(resetFunction, resetParamArray, text) {
const form = html`
<form onsubmit="return false;">
<button name=reset style="font-size: 18px">${text}</button>
</form>`
form.reset.onclick = () => {resetFunction.apply(null,resetParamArray)}
return form
}
Insert cell
md `#### Imports`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more