Week 1: Monte Carlo COVID Simulation in Python
Inspired by this article in the Washington Post
Click here to see the solution
Overview
In this mini exercise, students will perform a Monte Carlo simulation of a model of a spreading epidemic. This strategy of simulating complex behavior from simple rules is sometimes used to learn models in AI when we don't have a closed for expression for them.
This particular COVID model is very simple, but it is enough to illustrate the concept of "flattening the curve" (as you will see below) which is why social distancing was our best option before we had a vaccine. The simulation runs hour by hour in a loop, and each person is a dot which is either INFECTED
, UNINFECTED
, or RECOVERED
. Regardless of infection status, some people are moving, and some people are staying still. As the simulation proceeds and each moving point moves in a particular direction, the following two rules apply
Rules
-
If a person who is
UNINFECTED
becomes close enough to a person who'sINFECTED
, they becomeINFECTED
as well. -
A person who has been
INFECTED
for long enough becomesRECOVERED
. ARECOVERED
person does not get sick again, and does not infect anyone.
Code
Starter code has been provided at this link. It uses vpython to create an animation of "people" (cylinders with colors) moving around. If this code runs properly as given, you should see a bunch of blue cylinders (healthy people) milling about, as shown below
Your job is to extend the code to do a simulation realizing the rules given above. In particular, you should take the following steps
- Create a local variable for storing state, and make everyone in the simulation start off healthy except for the first person in the list
-
Color people who are healthy
BLUE
, people who are sickRED
, and people who are recoveredMAGENTA
-
Create a local method to the
Person
class that accepts another person object, and which causes the calling person to become sick if they are currently healthy but the other person is sick and close enough. By close enough, we mean that the distance should be within theinfect_radius
parameter passed to the simulation. Recall that the formula for the distance between the coordinate pair (x1, y1) and the coordinate pair (x2, y2) is\[ \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2} \]
-
Modify code in the
timestep
method to keep track of how long someone has been sick, and to change them toRECOVERED
if they have been sick longer than the variableRECOVERY_TIME
-
Finally, put everything together in the simulation loop to make the simulation run. An efficient implementation would use a KDTree data structure to rule out people who are too far away to infect, but it's OK to do a brute force solution here where for each person, you check every other person to see if they will infect. Once this is working, you should see something like this:
- The infection spreads very quickly in the above simulation, and all but one people get infected! Modify the code so that only a certain number of people are moving at any given time. What differences do you see?
Other Things To Try
If you finish this and are interested in exploring more, here are some things you can try for fun:
- Try playing around with the number of people and the size of the grid and see what curves you come up with. To speed things up and to just see the curves in the end, you can set
draw
to be false. - See what happens if more than one person starts off infected in different locations
- See what happens if you try to do a "lossy quarantine" of a square region; that is, start the infected person inside the square. Then, don't let people inside of the square leave, and don't let people out of the square enter, but a small percentage of the time let one through. Do we still need social distancing in this case?
- Add a feature so that people die with a certain probability before recovering. People who have died should not move or infect anymore, and they should be drawn in a different color.
- For those who know a little more math, it is often said that epidemics have exponential growth towards the beginning. Is that the case in this model? Why or why not?