Categories
Coding

A COVID-19 / Coronavirus Simulator Tutorial in JavaScript

I saw a terrific Coronavirus simulation about transmission called “Simulating an Epidemic”. If you haven’t seen it, you should watch it now: it communicates science well and answers a lot of my questions about social isolation, PPE and travel restrictions.

Coronavirus Simulation basic simple outputThis looked like something I would like to play with. I looked at the source code and quickly realized that it was hacked together to make a video. It was too messy to salvage. I decided to write my own version from scratch. I wound up with something that isn’t terribly accurate, but lets you play around with things like tipping point and measure total confinement time and maximum hospitalization. Unlike the other simulations, this one runs on your desktop or phone – all you need is a Web Browser.

Planning

Questions

Some questions I would like to answer are:

  • How does reinfection change the result?
  • We typically measure the effects of this in deaths, but what is the economic impact of How much misery is incurred in terms of total time that a population is sick?
  • Is there a strategy that results in less than “all” the population from getting infected?
  • Do the people who “break the rules” suffer more or the same as the population?
  • Can some regions “break the rules” without imperiling others?
  • What effect do imperfect tests have on the total sick time?

Requirements

The user should be able to:

  • Visualize the simulation and summarize the results,
  • Control the speed of the simulation,
  • Modify the simulation, and
  • Expect the results to be meaningful without being precise.

Technologies

I chose pure ES6-compliant JavaScript and HTML5 Canvas for this exercise for several reasons:

  1. JavaScript is popular and easy to understand.
  2. Anybody can run it: it is available everywhere and doesn’t depend on a back-end server.
  3. There are interactive scripting tools like JSFiddle where the layperson can mess with the code, like I learned to code in BASIC.
  4. I need to brush up on my ES6 JavaScript and animation skills.

I chose to use one file rather than spreading it across many class files in order to make the simulation more portable and easier to fiddle with.

Licensing

I want other people to be free to use this work, talk about it and build upon it. I’m going to use a Creative Commons license, specifically the Creative Commons Attribution-ShareAlike CC BY-SA license.

This license lets others remix, adapt, and build upon your work even for commercial purposes, as long as they credit you and license their new creations under the identical terms…. All new works based on yours will carry the same license, so any derivatives will also allow commercial use.

Design

The code is going to be broken into sections, and we also proceed somewhat linearly through the sections.

  1. Configuration – this is where we make our settings.
  2. Model – lay out the data structures for the critters and the infection, and include some processing that determines how they behave.
  3. View – these are classes that help us display data, charts and visualizations.
  4. Controller – this runs the simulation.
    1. A simulation loop that is driven by a timer. This advances with each cycle (the equivalent of a day in our simulation) and advances each critter by triggering its model’s functionality.
    2. A visualization loop that is also driven by a timer. This updates the displays. Separating these loops lets us animate the simulation independently from the speed of the simulation.
  5. Initialization – sets up the model and views, and activates the controller.
  6. Conclusion – produces a final summary.

Project Plan

  1. Set up a visualization and demonstrate that we can animate it.
  2. Build a simple 2D model for a community and demonstrate that we can animate it.
  3. Model a population with an object that will support our simulation, e.g. location in physical space, social distancing, infection rates, etc.
  4. Build a loop that with each cycle, iterates over the array of critters to update their properties.
  5. With each cycle, calculate statistics for the population.
  6. Provide a visualization to keep track of what’s going on.
  7. Validate the simulation by plugging in values and confirming that the results match the expectations.
  8. Program the generic simulator to answer specific questions.

Getting Started

Basic Toolbox

Let’s using JSFiddle to sketch out the solution. Go to jsfiddle.net and start a new project. Close the “framework” window – you’re using pure JavaScript! This is going to look like Pong.

I’m going to write this in a way that sets up its own HTML and CSS, so you don’t have to put anything in the HTML or CSS boxes.

Critter in a Box

To get to model people in a community, let’s start with one critter in a box. It’s less stressful to perform these kinds of experiments on critters.

To get confidence in modeling and drawing, I’ll start by defining a critter and plotting them in a box.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Creative Commons Attribution-ShareAlike CC BY-SA https://creativecommons.org/licenses/by-sa/4.0/
 
// make a box
const h=300, w=300; // h is xmax, w is ymax
 
let theBox = document.createElement('canvas');
  theBox.id = "theBox";
  theBox.width=w.toString();
  theBox.height=h.toString();
  document.body.appendChild(theBox);
 
var context = theBox.getContext("2d");
  context.fillStyle = "white";
  context.strokeStyle = "green";
  context.lineWidth = "5";
  context.fillRect(0, 0, w, h);
  context.strokeRect(0, 0, w, h);
 
// a bit of utility
// this is a good random number generator that gives a uniform distribution over the range
function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
 
// define a simple critter
let critter = {x:getRandomInt(0,w), y:getRandomInt(0,h), w:5, h:5, c:"red", dx:1, dy:1};
 
// put it in the box
context.fillStyle = critter.c;
context.fillRect(critter.x, critter.y, critter.w, critter.h);

I’m using “let” instead of “var” when declaring my variables. I had to look this up. Var is function-scoped, but Let is block-scoped, which reduces the chances of leakage.

Make sure this works for you before you move on.

Let the Critter Move

Now, let’s move the critter around to see what animation looks like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Creative Commons Attribution-ShareAlike CC BY-SA https://creativecommons.org/licenses/by-sa/4.0/
let fps=15; // at 15 fps it should take 20 seconds to traverse 300 px
let duration=40; // 40 seconds at 15fps, it should come back to start
var updateIntervalID = window.setInterval(updateCritter, 1000/fps, critter);
var clearIntervalID = window.setTimeout(clearInterval, duration*1000, updateIntervalID);
 
function updateCritter(critter) {
  // clear
  context.fillStyle = "blue";
  context.fillRect(critter.x, critter.y, critter.w, critter.h);
  // move
  critter.x += critter.dx;
  if ((critter.x < 0)||(critter.x > w) {
    critter.x -= critter.dx; // undo
    critter.dx = -critter.dx; // reverse
    critter.x += critter.dx; // do
  }
  if ((critter.y < 0)||(critter.y > h) {
    critter.y -= critter.dy; // undo
    critter.dy = -critter.dy; // reverse
    critter.y += critter.dy; // do
  }
  // draw
  context.fillStyle = critter.c;
  context.fillRect(critter.x, critter.y, critter.w, critter.h);
}

You can see that there are some issues with the boundary on the right side and the bottom because of the way that we draw the critter (which is intended to be quick), so we are going to eventually need to fix this. Try changing some of the values like the frames per second to see how fast you can get the animation to go, and try setting a random dx and/or dy and/or color.

Drawing the objects isn’t the point of the simulation, but it sure helps understand what the visualization is doing.

That wraps up a proof-of-concept of our basic building blocks. The next step is to generalize and compartmentalize it so we can expand on it easily in our simulation.

Objectifying

Here is a version that does the same thing, but puts the code into a few JavaScript classes and enables some things like creating more than one critter. Based on the tests so far, I’m also going to separate the animation from the calculation.

If you’re following along in our design, this is where we are building out the model, views and controllers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Creative Commons Attribution-ShareAlike CC BY-SA https://creativecommons.org/licenses/by-sa/4.0/
const settings = {
  xmax: 1200,
  ymax: 300,
  cycles: 3000,
  fps: 15
}
 
class Utility {
  static getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
  static log(message) {
    console.log(message);
  }
}
 
// Models
 
class Critter {
  constructor() {
    this.x = Utility.getRandomInt(0, settings.xmax);
    this.y = Utility.getRandomInt(0, settings.ymax);
    this.c = "red";
    this.dx = Utility.getRandomInt(-2, 2);
    this.dy = Utility.getRandomInt(-2, 2);
  }
  
  update() {
    this.x += this.dx;
    if ((this.x < 0) || (this.x > settings.xmax)) {
      this.dx = -this.dx; // reverse
      this.x += this.dx; // do
    }
    this.y += this.dy;
    if ((this.y < 0) || (this.y > settings.ymax)) {
      this.dy = -this.dy; // reverse
      this.y += this.dy; // do
    }
  }
}
 
// Views
 
class Visualization {
  constructor(divID) {
    this.elementID = divID;
    this.width = settings.width || 400;
    this.height = settings.height || 600;
    this.xmax = settings.xmax || settings.width;
    this.ymax = settings.ymax ||settings.height;
    this.fillColor = "white";
    this.borderColor = settings.borderColor || "green";
    this.borderSize = 5;
    this.pointWidth = 5; // lets you make the points big enough to see
    this.pointHeight = 5; // lets you make the points big enough to see
 
    this.canvas = document.getElementById(this.elementID);
    if (!this.canvas) {
      this.canvas = this.createCanvas();
    }
 
    this.context = this.canvas.getContext("2d");
    this.clearCanvas();
    return this;
  }
  createCanvas() {
    let theBox = document.createElement('canvas');
    theBox.id = this.elementID;
    theBox.width = this.width.toString();
    theBox.height = this.height.toString();
    document.body.appendChild(theBox);
    return theBox;
  }
  clearCanvas() {
    this.context.fillStyle = this.fillColor;
    this.context.fillRect(0, 0, this.width, this.height);
 
    this.context.strokeStyle = this.borderColor;
    this.context.lineWidth = this.borderSize;
    this.context.strokeRect(0, 0, this.width, this.height); // strokeRectangle
  }
  /* Quickly plots a point into the visualization.
   * Transforms the points so they fit in the boundries.
   * Assumes that all the elements that are ever drawn have the same w&amp;h
   */
  drawPoint(color, x, y) {
    let w=this.pointWidth, h=this.pointHeight;
    let cx = Math.floor(this.borderSize + ( (this.width - 2*this.borderSize - this.pointWidth) * x / this.xmax ));
    let cy = Math.floor(this.borderSize + ( (this.height - 2*this.borderSize - this.pointHeight) * y / this.ymax ));
    this.context.fillStyle = color;
    this.context.fillRect(cx, cy, this.pointWidth, this.pointHeight);
  }
  drawCritter(critter) {
    this.drawPoint(critter.c, critter.x, critter.y);
  }
  eraseCritter(critter) {
    this.drawPoint(this.fillColor, critter.x, critter.y);
  }
}
 
// Controllers
 
// definitions
let visualization = new Visualization("visualization");
 
let critters = [];
 
function animateCritters(critters) {
  critters.forEach(function(critter) {
    visualization.eraseCritter(critter);
    critter.update();
    visualization.drawCritter(critter);
    graph.drawCritter(critter);
  });
}
 
// start
critters.push(new Critter());
 
animateCritters(critters);
 
// animate
var animationIntervalID = window.setInterval(animateCritters, 1000 / fps, critters);
 
// calculate
for (let cycle = 0; cycle &lt; settings.cycles; cycle++) {
  critters.forEach(function(critter) {
    critter.update();
  });
}
 
// stop animation
clearInterval(animationIntervalID);
 
// present final state
animateCritters(critters);

At this point, you should have a good idea of the framework that we’re going to be using going forward.

Assembly

Well, now that that’s working, it’s time to give it some personality.

Configuration

Some of the things that you can set in the global configuration section are the length and speed of the simulation, how often to update the display.

Infection!

The heart of the infection modeling component is a JavaScript object that, for each state you can be in, maps out the various states you can get to. So if a critter is infected, on every cycle it has a probability of becoming symptomatic, hospitalized, removed (dead), or recovered.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
this.advancements = {
  infected: [
      { atLeast: 3, noMore: 14, chance: 25, next: "symptomatic" },
      { atLeast: 7, noMore: 14, chance: 75, next: "recovered" },
  ],
  symptomatic: [
      { atLeast: 7, noMore: 14, chance: 20, next: "hospitalized" },
      { atLeast: 7, noMore: 14, chance: 5, next: "dead" },
      { atLeast: 2, noMore: 14, chance: 75, next: "recovered" },
  ],
  hospitalized: [
      { atLeast: 7, noMore: 14, chance: 25, next: "dead" },
      { atLeast: 7, noMore: 14, chance: 75, next: "recovered" },
  ]
};

This matrix is implemented the update() method.

150
151
152
153
154
155
Object.keys(chances).forEach(next => {
   if (Utility.coinToss(chances[next])) { // congratulations, you advance a state
      this.status = next;
      return;
   }
}

The Code

Here’s our baseline code. After the code listing there’s a fiddle so you can play with it right here, and a final section that handles the conclusion.

The Simulator

Here’s my finished fiddle: https://jsfiddle.net/jtw90210/a4Ld25n9/.

Subscribe
Notify of
guest


0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x