Modeling Epidemics

Due: Saturday, Oct 12th at 4pm

The goal of this assignment is to give you practice with the basics of Python and to get you to think about how to translate a few simple algorithms into code. You will be allowed to work in pairs on some of the later assignments, but you must work alone on this assignment.

Epidemics and contagion are incredibly complex phenomena, involving both biological and social factors. Computer models, though imperfect, can offer insight into disease spread, and can represent infection with varying degrees of complexity.

SIR is a simple, but commonly used, model for epidemics. In the SIR model, a person can be in one of three states: Susceptible to the disease, Infected with the disease, or Recovered from the disease after infection (the model is named after these three states: S-I-R). In this model, we focus on a network of people, such as a community that could be experiencing an epidemic. Although simple, the SIR model captures that both social factors (like the shape of the network, e.g., how often people in the network interact with each other) and biological factors (like the probability and duration of infection) that mediate disease spread.

In this assignment, you will write code to simulate a simplified version of the SIR epidemic model. Your code will model how infection spreads through a city over time, where time is measured in days. At a high level, your code will iteratively calculate the disease states in a city, keeping track of the state of each person within a city until the end of the simulation. In addition, you will see how to use functions that build on one another to simplify a complex modeling process.

To begin building our model, we must specify the model’s details:

  • the health of each person during the simulation, which we will call a person’s disease state,
  • the starting state of a community of people or city,
  • the neighbors of a given individual in a city,
  • the transmission rules for disease spread within the city,
  • the rules for recovering and acquiring immunity to disease,
  • the method for keeping track of time in a city,
  • and the stopping conditions for the model.

We specify each of these details below.

Disease state: all people in the simulation can exist in one of three states, Susceptible, Infected, or Recovered.

  • Susceptible: the individual is healthy but may become infected in the future. We will use 'S' to represent susceptible individuals.
  • Infected: the individual has an infection currently. We will represent these individuals with 'I0', 'I1', 'I2', etc. 'I0' represents the day the person is infected. 'I1' represents a person one day into the infection, 'I2' represents a person two days into the infection, etc. In general, 'Ix', where x is an integer, will be used to represent a person x days into the infection.
  • Recovered: the individual has recovered from an infection and will be immune to the infection for the rest of the simulation. We represent these individuals with 'R'. (Some versions of the SIR model remove recovered people from the model. In our model, recovered people will remain in the city.)

Cities: a city in this simulation is represented as a list of people, each represented by a disease state. For example, a city of ['S', 'I1', 'R'] is composed of three people, the first of whom is susceptible, the second of whom is infected (and specifically, is one day into the infection), and the third of whom is recovered.

You can assume that every city has at least one person.

Neighbors: a person in our simplified model has up to two neighbors, the person immediately before them in the list (known as their left neighbor) and the person immediately after them in the list (known as their right neighbor). The first person in the list does not have a left neighbor and the last person in the list does not have a right neighbor. For example, consider the following list of people: ['Mark', 'Sarah', 'Lorraine', 'Marshall']:

  • Mark has one neighbor: Sarah.
  • Sarah has two neighbors: Mark and Lorraine.
  • Lorraine has two neighbors: Sarah and Marshall
  • Marshall has one neighbor: Lorraine.

Transmission rules: infection spreads from infected people to susceptible people ('S') based on infection rate r, the disease states of the susceptible person’s neighbors, and the person’s immune level.

  • Infection rate r: infection rate r is a value between 0.0 and 1.0 that models how quickly a given infection rate spreads through a city. A high infection rate indicates that the infection is highly contagious, while a low infection rate indicates that the infection is not very contagious.
  • Neighbors: a susceptible person will only become infected if at least one of their neighbors is infected.

You can think about infection transmission as being similar to flipping a weighted coin. If a susceptible person has at least one infected neighbor, we flip a coin to determine the person’s immune level. (It does not matter which neighbor (the left or right) is infected.) This value and the infection rate will be used to determine whether the susceptible person will get infected as well. Note that, in general, the coin will not be fair (unless r is 0.5). For example, an infection rate of 1.0 can be thought of as a coin that always lands on one side. If the person’s immune level, as determined by a random number generator (which we will explain later on), is strictly less than r, then the person becomes infected.

Contagion rules: The number of days a person is infected and remains contagious is a parameter to the simulation. We will track the number of days a person has been infected as part of their state. People who become infected start out in state 'I0'. For each day a person is infected, we increment the counter by one: 'I0' becomes 'I1', 'I1' becomes 'I2', etc. When the counter reaches the specified number of days contagious, we will declare them to be recovered (R) and no longer contagious. At that point, they are immune to the disease and cannot become re-infected. For example, if we are simulating an infection in which people are contagious for three days, a newly infected person will start in state 'I0', move to 'I1' after one day, to 'I2' after two days, and to state R, where they will remain for the rest of the simulation, after three days.

Stopping conditions: the simulation should stop after a specified maximum number of days or when the state of the city does not change from the start of a day to the end of the day.

Getting started

In the first lab, you learned the basics of how to use git and our git server. We will use git for all the programming assignments and labs in this course.

We have seeded your repository with a directory for this assignment. To pick it up, change to your cmsc12100-aut-19-username directory (where the string username should be replaced with your username) and then run the command git pull upstream master. You should also run the command git pull to make sure your local copy of your repository is in sync with the server. (We refer to the process of getting your local copy of a repository and the copy on the server in sync as syncing the repository.)

At the first lab, you ran this command, and it pulled the pa1 sub-directory into your cmsc12100-aut-19-username directory. It is, however, good practice to always run git pull upstream master before you start working, since we may occasionally update files (e.g., if we notice bugs in our code, add helpful new test cases, etc.). For example, some of the files for this assignment may have changed since you downloaded the initial distribution. After you have synced your repository, you can proceed as described in the lab: work on your code and then run git add <filename> for each file you change, followed by git commit -m"some message" and git push to upload your changes to the server before you log out.

You should always upload the latest version of your work to the server using the commands described above before you log out, then run git pull and git pull upstream master before you resume work to retrieve the latest files. This discipline will guarantee that you always have the latest version, no matter which machine you are using. Also, it will be easier for us to help you recover from git and chisubmit problems if you consistently push updates to the server.

As you will see below, we strongly encourage you to experiment with library functions and try out your own functions by hand in ipython3. Let’s get you set up to do that before we describe your tasks for this assignment. Open up a new terminal window and navigate to your pa1 directory. Then, fire up ipython3 from the Linux command-line, set up autoreload, and import your code as follows:

$ ipython3

In [1]: %load_ext autoreload

In [2]: %autoreload 2

In [3]: import sir

In [4]: import random

(Note: In [<number>] represents the ipython3 prompt. Your prompts may look different. Do not type the prompt when issuing commands.)

The commands %load_ext autoreload and %autoreload 2 tell ipython3 to reload your code automatically whenever it changes. We encourage you to use this package whenever you are developing and testing code. The random module is part of the Python standard library, and we will be using it in Task 3.

Getting help

If, after carefully reading the details of any part of the assignment, you are still confused about how to get started or make progress:

  • post a question on Piazza to ask for help, or
  • come to office hours, or
  • make sure to attend your discussion session for additional guidance on the programming assignment, or
  • see the CS “Harper Tutors” who are available in North Reading Room of the Arley D. Cathey Learning Center on Sunday through Thursday evenings from 7pm to 11pm.

Before you post a question on Piazza, please check to see if someone else has already asked the same question. We especially encourage you to check the “Must read posts for PA #1” post, which we will update over time to be a compendium of important questions and answers. Also, please read the pinned post on “Asking effective questions”. Please note that you should never post code or screenshots to Piazza. Finally, always remember to add, commit, and push the most recent version of your code to the server (as described above) before you ask your question. Syncing your code will allow us to look at it, which may speed up the process of helping you.

Style

Following a consistent style is important because it makes it easier for others to read your code; imagine if you were collaborating on a large software project with 30 other developers, and everyone used their own style of writing code!

To help you understand what constitutes good style, we have put together a style guide for the course: Python Style Guide for Computer Science with Applications. We expect you to use good style (that is, style that matches this guide), and will take this expectation into account when grading

For this assignment, you may assume that the input passed to your functions has the correct format. You may not change any of the input that is passed to your functions. In general, it is bad style to modify a data structure passed as input to a function, unless that is the explicit purpose of the function. Your function’s client might have other uses for the data and should not be surprised by unexpected changes.

Your tasks

For this assignment, we will specify a set of functions that you must implement. You will start with basic functions and work your way up to more complex tasks. We will also supply extensive test code. Over the course of the term, we will provide less and less guidance on the appropriate structure for your code.

Task 1: Count the number of people ever infected in a city

In Python, it is common to write helper functions that encapsulate key definitions and are only a few lines long. Your first task is to complete one such function: count_ever_infected.

This function should take a city as input and return the number of infected plus the number of recovered people in that city. For example, given city ['I0', 'I0', 'I2', 'S', 'R'], the function would return 4 (notice how we have to account for the fact that there multiple infected states). Given a city such as ['S', 'S', 'S', 'S'], the function would return 0.

Testing for Task 1

We have provided an extensive suite of automated tests for this assignment. You may be tempted to do the following: write some code, run the automated tests to find a test that fails, modify your code, and then repeat the process until all of the tests pass. This is a very bad way to debug your code; it typically takes much longer than taking a methodical step-by-step approach and often yields messy code that passes the tests without actually matching the specification of the problem. Instead, you should try your code out on some test cases by hand in ipython3 to get a sense of whether it is working before you try the automated tests.

Here, for example, are some sample calls to count_ever_infected:

In [6]: sir.count_ever_infected(['I0', 'I0', 'I2', 'S', 'R'])
Out[6]: 4

In [7]: sir.count_ever_infected(['S', 'S', 'S', 'S'])
Out[7]: 0

If you get the wrong answer for some sample input, stop to reason why your code is behaving the way it is and think about how to modify it to get the correct result. If you still can’t determine the problem after reasoning about the code, make a hypothesis about what might be wrong and use print statements to print out key values to help you verify or disprove your hypothesis.

Now on to the automated tests. The file test_sir.py contains automated test code for the tasks in this assignment. The test code contains one test function for each task. The names of the test functions share a common pattern: the word test_ followed by the name of the function being tested. For example, the name of the function for testing count_ever_infected is named test_count_ever_infected. The actual test cases are read from files, with one file per function being tested.

For count_ever_infected, we have provided 15 test cases. The tested cities vary in size from one person to twenty people and have different mixes of disease states (e.g., all susceptible, all recovered, some infected with different number of days infected, etc).

Tests for count_ever_infected
City Expected result Description
[‘I0’] 1 One person city with an infected person.
[‘I2000’] 1 One person city with an infected person who has a large days-infected count.
[‘R’] 1 One person city with a recovered person
[‘S’] 0 One person city with susceptible person
[‘S’, ‘S’, ‘S’, ‘S’] 0 Small city with all susceptible
[‘R’, ‘R’, ‘R’, ‘R’] 4 Small city with all recovered
20 person city 10 Larger city with mix of susceptible and recovered
[‘I1’, ‘S’, ‘S’, ‘S’] 1 Small city with one infected in slot 0, rest susceptible
[‘S’, ‘I1’, ‘S’, ‘S’] 1 Small city with one infected in slot 1, rest susceptible
[‘S’, ‘S’, ‘I1’, ‘S’] 1 Small city with one infected in slot 2, rest susceptible
[‘S’, ‘S’, ‘S’, ‘I1’] 1 Small city with one infected in slot 3, rest susceptible
[‘I1’, ‘R’, ‘R’, ‘R’] 4 Small city with one infected in slot 0, rest recovered
[‘I0’, ‘S’, ‘I1’, ‘R’] 3 Small city with mixed types
20 person city 20 Larger city with all in state ‘I0’
20 person city 20 Larger city with a mix of different infection states

Our goal is to ensure sufficient test coverage, meaning that our tests account for as many different cases as possible in our code. For example, we could be tempted to write tests just for the following two cities:

  • ['S', 'I0', 'I0', 'S', 'R']
  • ['S', 'S', 'S', 'S']

However, what if we wrote a solution that forgot to account for infected states other than I0 or that assumed that the number of days infected would always be in the single digits. Neither of the above tests would cover such cases.

We will be using the pytest Python testing framework for this and subsequent assignments. To run our automated tests, you will use the py.test command from the Linux command line (not from within ipython3). We recommend opening a new terminal window for running this command, which will allow you to go back and forth easily between testing code by hand in ipython3 and running the test suite using py.test. (When we work on assignments, we usually have three windows open: one for editing, one for experimenting in ipython3, and one for running the automated tests.)

Pytest, which is available on both the lab machines and your VM, has many options. We’ll use three of them: -v, which means run in verbose mode, -x, which means that pytest should stop running tests after a single test failure, and -k, which allows you to describe a subset of the test functions to run. You can see the rest of the options by running the command py.test -h.

For example, running the following command from the Linux command-line:

$ py.test -v -x -k test_count_ever_infected test_sir.py

will run the function in test_sir.py named test_count_ever_infected. (Recall that the $ represents the prompt and is not included in the command.)

Here is (slightly-modified) output from using this command to test our reference implementation of test_count_ever_infected:

$ py.test -v -x -k test_count_ever_infected test_sir.py
  =============================== test session starts ===============================
  platform linux -- Python 3.5.2, pytest-3.1.2, py-1.4.34, pluggy-0.4.0 -- /usr/bin/python3
  cachedir: .cache
  metadata: {'Python': '3.5.2', 'Plugins': {'html': '1.16.0', 'metadata': '1.5.1', 'json': '0.4.0'}, 'Platform': 'Linux-4.4.0-135-generic-x86_64-with-Ubuntu-16.04-xenial', 'Packages': {'pluggy': '0.4.0', 'py': '1.4.34', 'pytest': '3.1.2'}} rootdir: /home/student/cmsc12X00-instructors/30121/pa-sphinx/pa1-sir/soln, inifile: pytest.ini
  plugins: metadata-1.5.1, json-0.4.0, html-1.16.0
  collected 82 items

 test_sir.py::test_count_ever_infected[params0] PASSED
 test_sir.py::test_count_ever_infected[params1] PASSED
 test_sir.py::test_count_ever_infected[params2] PASSED
 test_sir.py::test_count_ever_infected[params3] PASSED
 test_sir.py::test_count_ever_infected[params4] PASSED
 test_sir.py::test_count_ever_infected[params5] PASSED
 test_sir.py::test_count_ever_infected[params6] PASSED
 test_sir.py::test_count_ever_infected[params7] PASSED
 test_sir.py::test_count_ever_infected[params8] PASSED
 test_sir.py::test_count_ever_infected[params9] PASSED
 test_sir.py::test_count_ever_infected[params10] PASSED
 test_sir.py::test_count_ever_infected[params11] PASSED
 test_sir.py::test_count_ever_infected[params12] PASSED
 test_sir.py::test_count_ever_infected[params13] PASSED
 test_sir.py::test_count_ever_infected[params14] PASSED

  generated json report: /home/student/cmsc12X00-instructors/30121/pa-sphinx/pa1-sir/soln/tests.json
  =============================== 75 tests deselected ===============================
  ==================== 15 passed, 75 deselected in 0.08 seconds =====================

This output shows that our code passed all fifteen tests in the test_count_ever_infected suite. It also shows that there were 75 tests that were deselected (that is, were not run) because they did not match the test selection criteria specified by the argument to -k.

If you fail a test, pytest will tell you the name of the test function that failed and the line in the test code at which the failure was detected. This information can help you determine what is wrong with your program. Read it carefully to understand the test inputs and why the test failed! Then, switch back to testing your function in ipython3 until you have fixed the problem.

For this assignment, we have added information to the error messages to tell you how to rerun the test by hand in ipython3. For example, if you wrote a solution that did not account for the R state, you would pass the first two tests, but would fail the third test:

$ py.test -v -x -k test_count_ever_infected test_sir.py

====================================== test session starts ======================================
platform linux -- Python 3.5.2, pytest-3.1.2, py-1.4.34, pluggy-0.4.0 -- /usr/bin/python3
cachedir: .cache
metadata: {'Plugins': {'json': '0.4.0', 'html': '1.16.0', 'metadata': '1.5.1'}, 'Packages': {'pytest': '3.1.2', 'py': '1.4.34', 'pluggy': '0.4.0'}, 'Platform': 'Linux-4.4.0-135-generic-x86_64-with-Ubuntu-16.04-xenial', 'Python': '3.5.2'}
rootdir: /home/student/cmsc12X00-instructors/30121/pa-sphinx/pa1-sir/soln, inifile: pytest.ini
plugins: metadata-1.5.1, json-0.4.0, html-1.16.0
collected 82 items

test_sir.py::test_count_ever_infected[params0] PASSED
test_sir.py::test_count_ever_infected[params1] PASSED
test_sir.py::test_count_ever_infected[params2] FAILED

 generated json report: /home/student/cmsc12X00-instructors/30121/pa-sphinx/pa1-sir/soln/tests.json
=========================================== FAILURES ============================================
_______________________________ test_count_ever_infected[params2] _______________________________

params = {'city': ['R'], 'expected_num_infected': 1, 'purpose': 'One person city with a recovered person'}

    @pytest.mark.parametrize(
        "params",
        read_config_file("count_ever_infected.json"))
    def test_count_ever_infected(params):
        '''
        Test harness for count_ever_infected function.

        Inputs:
          params (dictionary): the test parameters:
            city and the expected number of infected folks in the city.
        '''

        actual_num_infected = sir.count_ever_infected(params["city"])

        recreate_msg = "To recreate this test in ipython3 run:\n"
        recreate_msg += "  sir.count_ever_infected({})".format(params["city"])

        assert actual_num_infected is not None, \
            gen_none_error(recreate_msg)

        expected_num_infected = params["expected_num_infected"]
        assert isinstance(actual_num_infected, type(expected_num_infected)), \
            gen_type_error(recreate_msg,
                           expected_num_infected,
                           actual_num_infected)

>       assert actual_num_infected == expected_num_infected, \
            gen_mismatch_error(recreate_msg,
                               expected_num_infected,
                               actual_num_infected)
E       AssertionError:
E         Actual (0) and expected (1) values do not match.
E         To recreate this test in ipython3 run:
E           sir.count_ever_infected(['R'])
E       assert 0 == 1

test_sir.py:183: AssertionError
!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
====================================== 67 tests deselected ======================================
======================= 1 failed, 2 passed, 67 deselected in 0.24 seconds =======================

The volume of output can be a bit overwhelming. You should focus on the lines towards the end that start with E. These lines will usually contain a helpful message telling you why the test failed:

E         Actual (0) and expected (1) values do not match.
E         To recreate this test in ipython3 run:
E           sir.count_ever_infected(['R'])

This information can help us narrow down the issue with our code. This error message, in particular, tells us that the test code expected a return value of one, but got a return value of zero. It also shows you how to run this test in ipython3.

Take into account that, because we specified the -x option, pytest exited as soon as the third test failed (without running the remaining tests ). Omitting the -x option makes sense when you want to get a sense of which tests are passing and which ones aren’t; however, when debugging your code, you should always use the -x option so that you can focus on one error at a time.

Finally, pytest will run any function that starts with test_. You can limit the tests that get run using the -k option along with any string that uniquely identifies the desired tests. The string is not required to be a prefix. For example, if you specify -k count, pytest will run test functions that start with test_ and include the word count.

Also, by default, if you do not supply the name of a specific test file, pytest will look in the current directory tree for Python files that have names that start with test_.

In subsequent examples, we will leave out the name of the file with the test code (test_sir.py), use short substrings to describe the desired tests, and combine the option flags (-v -x -k) into a single string (-xvk). For example, the tests for count_ever_infected can also be run with the following command:

$ py.test -xvk count

Debugging suggestions and hints for Task 1

Remember to save any changes you make to your code in your editor as you are debugging. Skipping this step is a common error. Fortunately, we’ve eliminated another common error – forgetting to reload code after it changes – by using the autoreload package. (If you skipped the Getting started section, please go back and follow the instructions to set up autoreload and import sir and random)

Task 2: Is a neighbor infected?

Next, you will write a function called has_an_infected_neighbor that will determine whether a susceptible person at a given position in a list has at least one neighbor who is infected.

More specifically, given the city and the person’s position, your code will compute the positions of the specified person’s left and right neighbors in the city, if they exist, and determine whether either one is in an infected state.

Recall that the first person in the city has a right neighbor, but not a left neighbor and the last person in the city has a left neighbor, but not a right neighbor. Your code will need to handle these special cases.

When you look at the code, you will see that we included the following line:

assert city[position] == "S"

to verify that the function has been called on a person who is susceptible to infection. In general, assertions have the following form:

assert <boolean expression>

Assertions are a useful way to check that your code is receiving valid inputs: if the boolean expression specified as the assertion’s condition evaluates to False, the assertion statement will make the function fail. Simple assertions can greatly simplify the debugging process by highlighting cases where a function is being called incorrectly.

Testing for Task 2

As in the previous task, we suggest you start by trying out your code in ipython3 before you run the automated tests. Here, for example, are some sample calls to has_an_infected_neighbor:

In [8]: sir.has_an_infected_neighbor(['I1', 'S', 'S'], 1)
Out[8]: True

In [9]: sir.has_an_infected_neighbor(['S', 'I1', 'IO'], 0)
Out[9]: True

In [9]: sir.has_an_infected_neighbor(['S', 'R', 'IO'], 0)
Out[9]: False

In [10]: sir.has_an_infected_neighbor(['S', 'I0', 'S'], 2)
Out[10]: True

In [10]: sir.has_an_infected_neighbor(['S'], 0)
Out[10]: False

In the first sample call, we are checking whether the susceptible person in position 1 has an infected neighbor. Since their left neighbor (at position 0) is infected, the result should be True.

The next call checks whether the susceptible person in position 0 has an infected neighbor. This person does not have a left neighbor. Their right neighbor, at position 1, though, is infected and so, the result should be True.

The third call also checks the person at position 0. In this case, the person at position 1 is not infected, and so the expected result is False.

The fourth call checks the person at position 2. This person does not have a right neighbor. Their left neighbor, at position 1, is infected, though, and so, the expected result is True.

Finally, the last call will return False. Why? Because, the lone person in this city has no neighbors and so, by definition has no infected neighbors. (You should not need special code to handle this specific case.)

The table below provides information about the tests for has_an_infected_neighbor. Each row contains the values that will be passed for the city and position arguments for that test, the expected result, and a brief description of the tests purpose.

Tests for has_an_infected_neighbor
City Position Expected result Description
[‘I0’, ‘S’, ‘S’] 1 True Left neighbor infected.
[‘I1000’, ‘S’, ‘S’] 1 True Left neighbor infected w/ multi-digit days infected.
[‘R’, ‘S’, ‘I0’] 1 True Right neighbor infected.
[‘R’, ‘S’, ‘I1000’] 1 True Right neighbor infected w/ multi-digit days infected.
[‘I1’, ‘S’, ‘I0’] 1 True Both neighbors infected
[‘S’, ‘S’, ‘R’] 1 False Neither neighbor infected.
[‘R’, ‘S’, ‘S’, ‘I1’] 2 True City with more than three people. Right neighbor infected.
[‘R’, ‘I200’, ‘S’, ‘R’] 2 True City with more than three people. Left neighbor infected.
[‘I0’, ‘S’, ‘S’, ‘R’] 2 False City with more than three people. Neither neighbor infected.
[‘S’, ‘S’, ‘S’, ‘I1’] 0 False First person, Single neighbor (right) not infected.
[‘S’, ‘I1’, ‘S’, ‘I1’] 0 True First person, Single neighbor (right) infected.
[‘I0’, ‘S’, ‘S’, ‘S’] 3 False Last person, Single neighbor (left) not infected
[‘I0’, ‘S’, ‘I10’, ‘S’] 3 True Last person, Single neighbor (left) infected
[‘S’] 0 False Solo person in city.

You can run these tests by running the following command from the Linux command-line:

$ py.test -xvk has

Debugging suggestions and hints for Task 2

There is a lot going on in this function and, when you are debugging, it can be helpful to know exactly what is happening inside the function. print statements are among the most intuitive ways to identify what your code is actually doing and will become your go-to debugging method. If you are struggling to get started or to return the correct values from your function, consider the following debugging suggestions:

  • Print which neighbors exist;
  • Print the positions you calculated for those neighbors; and
  • Print the values you extracted for those neighbors.

Is your code behaving as expected given these values?

Also, make sure that you are returning, not printing, the desired value from your function.

Don’t forget to remove your debugging code (i.e., the print statements) before you submit your solution.

Task 3: Determine infection for a given person

Your next task is to complete the function gets_infected_at_position.

This function will determine whether someone at a given position in a list will become infected on the next day of the simulation. More specifically, given a city, a specified susceptible person’s location within that city, and an infection rate r, your code should:

  1. Determine whether the person has an infected neighbor.
  2. If and only if the person has an infected neighbor, compute the immune level of the person and determine whether they will become infected.
  3. Return whether the person becomes infected as a boolean.

You must use your has_an_infected_neighbor function to determine whether a susceptible person has an infected neighbor. Do not repeat the logic for determining infection transmission from a neighbor in this function!

Earlier, we described infection transmission as being similar to flipping a weighted coin. In this function, if (and only if) the person has an infected neighbor, you will compute the person’s current immune level, a value between 0.0 and 1.0, by flipping that weighted coin. We will use a random number generator to obtain that value and, more specifically, you will call random.random(), a function that returns a random floating point number between 0.0 and 1.0. If the resulting immune level is strictly less than the infection rate, the person will become infected. Another way to think about it is that having an immune level greater than or equal to the infection rate allows a person to fight off the infection.

Each time a random number generator, like random.random(), is called, it returns a new random number. This behavior complicates debugging because the sequence of random numbers generated will impact the simulation. Two calls to gets_infected_at_position with the exact same parameters, for example, can produce different results.

Fortunately, we can ensure that random.random() returns the same sequence of numbers when it is called by initializing it with a seed value. It is common to set the seed value for a random number generator when debugging. If we do not actively set the seed, random number generators will usually derive one from the system clock.

Since many of our tests use the same seed (20170217), we have defined a constant, TEST_SEED, with this value in sir.py for your convenience. This value should be used for testing only; it should not appear anywhere in the code you write.

Let’s try out setting the seed using the value of sir.TEST_SEED and then making some calls to the random number generator in ipython3:

In [11]: sir.TEST_SEED
Out[11]: 20170217

In [12]: random.seed(sir.TEST_SEED)

In [13]: random.random()
Out[13]: 0.48971492504609215

In [14]: random.random()
Out[14]: 0.23010566619210782

In [15]: random.seed(sir.TEST_SEED)

In [16]: random.random()
Out[16]: 0.48971492504609215

In [17]: random.random()
Out[17]: 0.23010566619210782

(If your attempt to try out these commands in ipython3 fails with a name error, you probably skipped the set up steps described in the Getting started section. Exit ipython3 and restart it following the instructions above.)

Notice that the third and fourth calls to random.random() generate exactly the same values as the first two calls. Why? Because we set the seed to the exact same value before the first and third calls.

This behavior of random has another implication: it is crucial that you only compute a person’s immune level when they have at least one infected neighbor. If you call the random number generator more often than necessary, your code may generate different answers than ours on subsequent tasks.

Testing for Task 3

As in Task 1 and 2, we strongly encourage you to do some testing by hand in ipython3 before you start using the automated tests. Unlike previous tasks, you have to be careful to initialize the random seed before calling gets_infected_at_position, to make sure you get the expected results. For example:

In [18]: random.seed(sir.TEST_SEED)

In [19]: sir.gets_infected_at_position(['S', 'I1', 'I1'], 0, 0.5)
Out[19]: True

In [20]: random.seed(sir.TEST_SEED)

In [21]: sir.gets_infected_at_position(['S', 'I1', 'I1'], 0, 0.3)
Out[21]: False

The table below provides information about the automated tests for gets_infected_at_position. Each row contains the seed used to initialize the random number generator, the values that will be passed for the city, position, and infection_rate arguments for that test, and the expected result. The last column briefly describes the test.

Tests for gets_infected_at_position
Seed City Position Infection rate Expected result Description
20170217 [‘I1’, ‘S’, ‘S’] 1 0.5 True Left neighbor is infected, susceptible person gets infected
20170218 [‘I1’, ‘S’, ‘S’] 1 0.65 True Left neighbor is infected, susceptible person gets infected. Different seed
20170217 [‘I1’, ‘S’, ‘S’] 1 0.2 False Left neighbor is infected, susceptible person does not get infected
20170217 [‘S’, ‘S’, ‘I0’] 1 0.5 True Right neighbor is infected, susceptible person gets infected
20170217 [‘S’, ‘S’, ‘I0’] 1 0.2 False Right neighbor is infected, susceptible person does not get infected
20170217 [‘I20’, ‘S’, ‘I0’] 1 1.0 True Both neighbors are infected, susceptible person gets infected
20170217 [‘I20’, ‘S’, ‘I0’] 1 0.2 False Both neighbors are infected, susceptible person does not get infected
20170217 [‘R’, ‘S’, ‘R’] 1 1.0 False Neither neighbor is infected, susceptible person does not get infected. No calls to random.random()
20170217 [‘I1’, ‘S’, ‘S’, ‘S’] 2 1.0 False Neither neighbor is infected, susceptible person does not get infected. No calls to random.random()
20170217 [‘S’, ‘S’, ‘I0’] 0 1.0 False Right neighbor only, susceptible person does not get infected. No calls to random.random()
20170217 [‘S’, ‘I1500’, ‘I0’] 0 0.5 True Right neighbor only, susceptible person gets infected.
20170217 [‘S’, ‘I1500’, ‘I0’] 0 0.2 False Right neighbor only, susceptible person does not get infected.
20170217 [‘I1’, ‘S’, ‘S’] 2 0.5 False Left neighbor only, susceptible person does not get infected. No calls to random.random().
20170217 [‘I1’, ‘I1500’, ‘S’] 2 0.5 True Left neighbor only, susceptible person gets infected.
20170217 [‘I1’, ‘I1500’, ‘S’] 2 0.2 False Left neighbor only, susceptible person does not get infected.

You can run these tests by executing the following command from the Linux command-line:

$ py.test -xvk gets

Debugging suggestions and hints for Task 3

If you are struggling to get started or to return the correct values in your function, consider the following suggestions to debug your code:

  • Print the result you are getting from has_an_infected_neighbor.
  • Print the immune level, if needed.
  • Make sure that you are making the right number of calls to random.random (zero or one).
  • When testing in ipython3, ensure that you have reset the seed for the random number generator before each test call to gets_infected_at_position.

Task 4: Advance person

Your fourth task is to complete the function advance_person_at_position. The goal of this function is advance the state of a person from one day to the next. Given a city, a person’s location within that city, an infection rate r, and the number of days the infection is contagious c, your function should, determine the next state for the person. Specifically, if the person is:

  1. Susceptible ('S'): you need to determine whether they will become infected ('I0') or remain susceptible ('S') for another day using your gets_infected_at_position function.
  2. Infected ('Ix', where x is an integer): determine whether the person remains infected (that is, \(x + 1 < c\)) and moves to the next infected state (e.g. 'I0' becomes 'I1', 'I1' becomes 'I2', etc) or switches to the recovered state ('R'). To compute the new state of an infected person, you will need to extract the number of days infected from the state as a string, convert it to an integer, and then compare it to the number of days contagious c. If you determined the person will remain infected, you’ll need to construct a new string from 'I' and \(x+1\).
  3. Recovered ('R'): you should do nothing. Recovered people remain in that state.

As an example, consider the following calls to advance_person_at_position:

In [22]: sir.advance_person_at_position(['I0', 'I1', 'R'], 0, 0.3, 2)
Out[22]: "I1"

In [22]: sir.advance_person_at_position(['I0', 'I1', 'R'], 1, 0.3, 2)
Out[22]: "R"

In [22]: sir.advance_person_at_position(['I0', 'I1', 'R'], 2, 0.3, 2)
Out[22]: "R"

The first call determines that the person at position 0 moves from state 'I0' to 'I1'. The second call determines that the person at position 1 shifts to state 'R', because the parameters specify that a person is only contagious for two days. And finally, the third call returns 'R' because the person at position 2 is already in state 'R'.

When it encounters a susceptible person, your function will call gets_infected_at_position, which may involve calls to the random number generator. So, when testing this function on a susceptible person in ipython3, make sure to set the seed (using random.seed()) before calling get_infected_at_position.

For example:

In [23]: random.seed(sir.TEST_SEED)

In [24]: sir.advance_person_at_position(['I0', 'I1', 'S'], 2, 0.5, 2)
Out[24]: 'I0'

In [23]: random.seed(sir.TEST_SEED)

In [24]: sir.advance_person_at_position(['I0', 'I1', 'S'], 2, 0.3, 2)
Out[24]: 'S'

The person at position 2 has an infected neighbor. The function will call random.random() to get the person’s immune level. Because we set the seed to sir.TEST_SEED, we know that random.random() will return a value (0.48971492504609215) less than the infection rate (0.5) specified in the first call. Consequently, the first call returns 'I0' as the new state for the person at position 2. The second call to advance_person_at_position uses an infection rate of 0.3, which is less than the value returned from random.random() (again, 0.48971492504609215, because we reset the seed), and so, the function will return 'S'.

If we do not reset the seed, the second call will yield a different result.

In [23]: random.seed(sir.TEST_SEED)

In [24]: sir.advance_person_at_position(['I0', 'I1', 'S'], 2, 0.5, 2)
Out[24]: 'I0'

In [24]: sir.advance_person_at_position(['I0', 'I1', 'S'], 2, 0.3, 2)
Out[24]: 'I0'

Why? Because the call random.random() in the second call will return 0.23010566619210782, the second value in the random sequence generated using sir.TEST_SEED as the starting seed. Since this value is less than the infection rate of 0.3, the person at position 2 will become infected.

Testing for Task 4

The table below provides information about the tests for advance_person_at_position. Each row contains the seed, the values that will be passed for the city, position, infection_rate, and days_contagious arguments for that test, the expected result, and a brief description of the test

Tests for advance_person_at_position
Seed City Position Infection rate Days Contagious Result Description
20170217 [‘I1’, ‘S’, ‘S’] 1 0.5 3 I0 Left neighbor is infected, susceptible person gets infected.
20170217 [‘I1’, ‘S’, ‘S’] 1 0.2 3 S Left neighbor is infected, susceptible person does not get infected.
20170218 [‘I1’, ‘S’, ‘S’] 1 0.5 3 S Left neighbor is infected, susceptible person does not get infected. Different seed.
20170217 [‘S’, ‘S’, ‘I0’] 1 0.5 3 I0 Right neighbor is infected, susceptible person gets infected.
20170217 [‘S’, ‘S’, ‘I0’] 1 0.2 3 S Right neighbor is infected, susceptible person does not get infected.
20170217 [‘I20’, ‘S’, ‘I0’] 1 1.0 3 I0 Both neighbors are infected, susceptible person gets infected.
20170217 [‘I20’, ‘S’, ‘I0’] 1 0.2 3 S Both neighbors are infected, susceptible person does not get infected.
20170217 [‘R’, ‘S’, ‘R’] 1 1.0 3 S Neither neighbor is infected, susceptible person does not get infected. No calls to random.random().
20170217 [‘I1’, ‘S’, ‘S’, ‘S’] 2 1.0 3 S Neither neighbor is infected, susceptible person does not get infected. No calls to random.random().
20170217 [‘S’, ‘S’, ‘I0’] 0 1.0 3 S Right neighbor only, susceptible person does not get infected. No calls to random.random().
20170217 [‘S’, ‘I1500’, ‘I0’] 0 0.5 3 I0 Right neighbor only, susceptible person gets infected.
20170217 [‘S’, ‘I1500’, ‘I0’] 0 0.2 3 S Right neighbor only, susceptible person does not get infected.
20170217 [‘I1’, ‘R’, ‘S’] 2 0.5 3 S Left neighbor only, susceptible person does not get infected. No calls to random.random().
20170217 [‘I1’, ‘I1500’, ‘S’] 2 0.5 3 I0 Left neighbor only, susceptible person gets infected.
20170217 [‘I1’, ‘I1500’, ‘S’] 2 0.2 3 S Left neighbor only, susceptible person does not get infected.
20170217 [‘I1’, ‘I1500’, ‘S’] 0 0.2 3 I2 Infected should be incremented. No calls to random.random().
20170217 [‘I2’, ‘I1500’, ‘S’] 0 0.2 3 R Infected should be converted to recovered. No calls to random.random().
20170217 [‘I2’, ‘I1500’, ‘S’] 1 0.2 2000 I1501 Infected should be incremented. Large number of days contagious. No calls to random.random().
20170217 [‘I2’, ‘I1500’, ‘S’] 1 0.2 1501 R Infected person recovers. Large number of days contagious. No calls to random.random().
20170217 [‘I2’, ‘I1500’, ‘R’] 2 0.2 2000 R Recovered, no change. No calls to random.random().

You can run these tests by executing the following command from the Linux command-line:

$ py.test -xvk advance

Task 5: Move the simulation forward a single day

Your fifth task is to complete the function simulate_one_day. In layman’s terms, this function will model one day in a simulation and will act as a helper function to run_simulation. More concretely, simulate_one_day should take the city’s state at the start of the day, the infection rate r, and the number of days contagious c and return a new list of disease states (i.e., the state of the city after one day).

Your implementation for this function must use advance_person_at_position to determine the new state of each person in the city.

As in the previous task, you will need to set the seed when you are testing your function in ipython3:

In [23]: random.seed(sir.TEST_SEED)

In [24]: sir.simulate_one_day(['S', 'I0', 'S'], 0.3, 2)
Out[24]: ['S', 'I1', 'I0']

Testing for Task 5

The table below provides information about the tests for simulate_one_day. Each row contains the seed, the values that will be passed for the city and infection_rate arguments for that test, the expected result, and a brief description of the test.

Tests for simulate_one_day
Seed City Infection rate Days contagious Expected result Description
20170217 [‘I0’, ‘I1’, ‘I100’] 0.0 200 [‘I1’, ‘I2’, ‘I101’] Are the I values are incremented correctly?
20170217 [‘I2’, ‘I2’, ‘I2’] 0.0 3 [‘R’, ‘R’, ‘R’] Are the I values are converted to R correctly?
20170217 [‘R’, ‘R’, ‘R’] 0.0 3 [‘R’, ‘R’, ‘R’] R values should not change.
20170217 [‘I1’, ‘S’, ‘I1’] 0.2 3 [‘I2’, ‘S’, ‘I2’] Susceptible person does not become infected (low infection rate)
20170217 [‘I1’, ‘S’, ‘I1’] 0.5 3 [‘I2’, ‘I0’, ‘I2’] Susceptible person becomes infected (higher infection rate)
20170218 [‘I1’, ‘S’, ‘I1’] 0.5 3 [‘I2’, ‘S’, ‘I2’] Different seed. Susceptible person does not get infected
20170217 [‘I1’, ‘S’, ‘I1’] 0.5 2 [‘R’, ‘I0’, ‘R’] A susceptible person becomes infected, even when its neighbors recover in that same day.
20170217 [‘S’, ‘I0’, ‘S’] 0.9 2 [‘I0’, ‘I1’, ‘I0’] Two susceptible persons become infected.
20170217 [‘S’, ‘I0’, ‘S’] 0.3 2 [‘S’, ‘I1’, ‘I0’] Two susceptible persons, only one of them becomes infected.
20170217 [‘S’, ‘S’, ‘S’] 1.0 2 [‘S’, ‘S’, ‘S’] None of the susceptible persons become infected
20170218 30 person city 0.4 2 See test Large city w/ medium infection rate

You can run these tests by executing the following command from the Linux command-line:

$ py.test -xvk one

Debugging suggestions for Task 5

If you are struggling to get started or to return the correct values in your function, consider the following suggestions to debug your code:

  • Use simple infection rates that will not rely on the random number generator (like 0.0 and 1.0) to verify that the states change as expected.
  • Print out each person’s old and new disease states. Ensure that the new disease states are correct in all cases.

Task 6: Run the simulation

Your sixth task is to complete the function run_simulation, which takes the starting state of the city, the random seed, the maximum number of days to simulate (max_num_days), the infection rate, and the number of days a person is contagious as arguments and returns both the final state of the city and the number days simulated as a tuple.

To clarify:

  • Your function should run one whole simulation
  • Before starting the work of the simulation, your function should set the seed using the provided argument.
  • Your simulation must count the number of the days simulated.
  • Recall that there are two stopping conditions for this simulation: that max_num_days days have passed or that the state of the city remains unchanged after simulating a day. You should check the second condition after you simulate a day. Thus, as long as max_num_days is greater than zero, you should always simulate at least one day.

Your implementation must call simulate_one_day. Do not repeat the logic for simulating a day in this function.

Here is an example use of this function:

In [67]: sir.run_simulation(['S', 'S', 'I1'], 20170217, 10, 0.4, 3)
Out[67]: (['S', 'R', 'R'], 6)

Notice that our sample use did not include a call to set the random seed. Your run_simulation function should set the seed based on the random seed parameter, so you will not need to reset the seed manually to test this function.

Testing Task 6

We have provided five tests for this task.

Tests for run_simulation
Starting City Seed Maximum number of days Infection rate Days contagious Expected Result: city, number of days simulated Description
[‘S’, ‘S’, ‘I0’] 20170217 10 0.4 3 ([‘S’, ‘R’, ‘R’], 6) One of two susceptible persons gets infected. Second stopping condition.
[‘S’, ‘S’, ‘I0’] 20170218 10 0.4 2 ([‘S’, ‘S’, ‘R’], 3) Different seed.
[‘S’, ‘S’, ‘I0’] 20170217 100 0.2 1 ([‘S’, ‘S’, ‘R’], 2) Neither susceptible person gets infected. Second stopping condition.
[‘R’, ‘S’, ‘S’] 20170217 10 1.0 2 ([‘R’, ‘S’, ‘S’], 1) No changes. Second stopping condition.
[‘R’, ‘I0’, ‘S’, ‘S’, ‘S’, ‘S’, ‘S’] 20170217 3 1.0 10 ([‘R’, ‘I3’, ‘I2’, ‘I1’, ‘I0’, ‘S’, ‘S’], 3) First stopping condition
30 person city 20170218 20 0.4 2 (See test for city, 6) Large city w/ medium infection rate. Second stopping condition.

You can run these tests by executing the following command from the Linux command-line.

$ py.test -xvk run

Debugging hints for Task 6

If you are struggling to get started or to return the correct values in your function, consider the following suggestions to debug your code:

  • If your function returns one fewer or one more day than our test function, check your code for the stopping conditions.
  • If you are generating the wrong final state for the city, try printing the day (0, 1, 2, etc.), the disease states before the call to simulate_one_day, and the disease states after the call to simulate_one_day.

From this point on, we will not be providing explicit debugging hints. In general, it is a good idea to use print statements to uncover what your code is doing.

Task 7: Determining average infection spread

Your last task is to complete the function calc_avg_num_newly_infected, which computes the average number of newly infected people over num_trials trials for a given city, infection rate, and number of days contagious. This function takes the starting state of the city, the random seed, the maximum number of days to simulate, the infection rate, the number of days contagious, and the number of trials to run as arguments and returns the average number of people who become infected over the num_trials different trial runs. The number of newly infected people per trial is simply the number of people who start the simulation as susceptible and end it as infected or recovered.

Each time you run a trial simulation, you should increase the random seed by 1. It is important that you increment your random seed. If you forget to increment your seed, all trials will be identical, and if you increment your seed in a different way than specified, your code may produce a different result (and thereby, not pass our tests).

Your implementation should call run_simulation, which sets the seed, so unlike some of the earlier tasks, you do not need to call random.seed before running this function in ipython3.

Is there another function you have written that could be useful for this task?

Here’s a sample use of this function:

In [7]: sir.calc_avg_num_newly_infected(["S", "I1", "S", "I0"],
   ...:                                 20170217, 10, 0.3, 2, 5)
Out[7]: 0.8

How did the function arrive at an average of 0.8 newly infected people? Here’s a table that shows, for each trial, the seed used, the starting state, the end state, and the number of people newly infected during the trial.

Intermediate values from calc_avg_num_newly_infected
Simulation number Seed Starting state for simulation run Final state for simulation run Number of people newly infected
0 20170217 [‘S’, ‘I1’, ‘S’, ‘I0’] [‘S’, ‘R’, ‘R’, ‘R’] 1
1 20170218 [‘S’, ‘I1’, ‘S’, ‘I0’] [‘S’, ‘R’, ‘S’, ‘R’] 0
2 20170219 [‘S’, ‘I1’, ‘S’, ‘I0’] [‘S’, ‘R’, ‘R’, ‘R’] 1
3 20170220 [‘S’, ‘I1’, ‘S’, ‘I0’] [‘R’, ‘R’, ‘R’, ‘R’] 2
4 20170221 [‘S’, ‘I1’, ‘S’, ‘I0’] [‘S’, ‘R’, ‘S’, ‘R’] 0

Since a total of 4 people where infected over five trials, the average number of people newly infected per trial works out to \(0.8\).

Testing Task 7

We have provided nine tests for this task. The first three can be checked easily with print statements. The fourth and fifth tasks use a large number of trials (100) and different seeds. You will see that as the number of trials increases, the starting seed matters less. The sixth and seventh tests uses 30 person cities. And the last pair of tests check edge cases: one trial and a city with without any susceptible people.

Tests for calc_avg_num_newly_infected
Starting Seed Starting City Maximum number of days Infection rate Days contagious Number of Trials Expected result Description
20170217 [‘S’, ‘I1’, ‘S’, ‘I0’] 10 0.3 2 5 0.8 Test case that can be hand-computed.
20170217 [‘S’, ‘I1’, ‘S’, ‘I0’] 10 0.3 4 5 1.4 Increasing the number of days contagious causes the infection to spread more.
20170220 [‘S’, ‘I1’, ‘S’, ‘I0’] 10 0.3 4 5 1.6 Different seed
20170217 [‘S’, ‘I1’, ‘S’, ‘I0’] 10 0.3 4 100 1.41 Large number of trials.
20170220 [‘S’, ‘I1’, ‘S’, ‘I0’] 10 0.3 4 100 1.43 Large number of trials with a different seed.
20170218 30 person city 20 0.1 2 10 0.8 30 person city, slow rate of infection, and few days contagious
20170218 49 person city 20 0.4 2 100 5.47 49 person city, medium infection rate, few days contagious.
20170217 [‘S’, ‘S’, ‘I1’, ‘I1’, ‘I1’, ‘I1’, ‘I1’, ‘S’] 2 0.8 2 1 3.0 Edge case: 1 trial
20170217 [‘R’, ‘I1’, ‘R’, ‘I1’] 10 1.0 2 5 0.0 Edge case: no one is susceptible to start, so no one can become infected during the simulation.

You can run these tests by executing the following command from the Linux command-line.

$ py.test -xvk avg

Putting it all together

We have included code in sir.py that calls your function run a simulation or calculate an approximation for the average number of people newly infected in a given scenario.

Running this program with the --help flag shows the flags to use for different arguments.

$ python3 sir.py --help
Usage: sir.py [OPTIONS] CITY

  Process the command-line arguments and do the work.

Options:
  --random_seed INTEGER
  --max-num-days INTEGER
  --infection-rate FLOAT
  --days-contagious INTEGER
  --num-trials INTEGER
  --task-type [single|average]
  --help                     Show this message and exit.

Cities are specified as a comma separated string, such as, “S, S, I0”.

Here is a sample use of this program that runs a single simulation:

$ python3 sir.py "S, S, I0" --random_seed=20170217 --max-num-days=10  --infection-rate=0.4 --days-contagious=3 --task-type=single

and here is the output that it should print:

Running one simulation...
Final city: ['S', 'R', 'R']
Days simulated: 6

Here is a sample use of this program that calculates the average number of newly infected people:

$ python3 sir.py "S, S, I0" --random_seed=20170217 --max-num-days=10 --infection-rate=0.4 --days-contagious=3 --num-trials=5 --task-type=average
Running multiple trials...
Over 5 trial(s), on average, 1.4 people were infected

Grading

Programming assignments will be graded according to a general rubric. Specifically, we will assign points for completeness, correctness, design, and style. (For more details on the categories, see our PA Rubric page.)

The exact weights for each category will vary from one assignment to another. For this assignment, the weights will be:

  • Completeness: 75%
  • Correctness: 15%
  • Design: 0%
  • Style: 10%

Obtaining your test score

The completeness part of your score will be determined using automated tests. To get your score for the automated tests, simply run the following from the Linux command-line. (Remember to leave out the $ prompt when you type the command.)

$ py.test
$ ../common/grader.py

Notice that we’re running py.test without the -k or -x options: we want it to run all the tests. If you’re still failing some tests, and don’t want to see the output from all the failed tests, you can add the --tb=no option when running py.test:

$ py.test --tb=no
$ python3 ../common/grader.py

Take into account that the grader.py program will look at the results of the last time you ran py.test so, if you make any changes to your code, you need to make sure to re-run py.test. You can also just run py.test followed by the grader on one line by running this:

$ py.test --tb=no; ../common/grader.py

After running the above, you should see something like this (of course, your actual scores may be different!):

===================================== 86 passed in 0.29 seconds =====================================
Category                                                       Passed / Total       Score  / Points
----------------------------------------------------------------------------------------------------
Task 1: Count the number of infected people in a city          15     / 15          7.50   / 7.50
Task 2: Is one of our neighbors infected?                      14     / 14          7.50   / 7.50
Task 3: Determine infection for a given person                 15     / 15          10.00  / 10.00
Task 4: Advance person at position                             20     / 20          15.00  / 15.00
Task 5: Move the simulation forward a single day               11     / 11          10.00  / 10.00
Task 6: Run the simulation                                     6      / 6           10.00  / 10.00
Task 7: Determining average infection spread                   9      / 9           15.00  / 15.00
----------------------------------------------------------------------------------------------------
                                                                               TOTAL = 75.00  / 75
====================================================================================================

Cleaning up

Before you submit your final solution, you should, remove

  • any print statements that you added for debugging purposes and
  • all in-line comments of the form: “YOUR CODE HERE” and “REPLACE …”

Also, check your code against the style guide. Did you use good variable names? Do you have any lines that are too long, etc.

Do not remove header comments, that is, the triple-quote strings that describe the purpose, inputs, and return values of each function.

As you clean up, you should periodically save your file and run your code through the tests to make sure that you have not broken it in the process.

Submission

To submit your assignment, make sure that you have:

  • put your name at the top of your file,
  • registered for the assignment using chisubmit (if you have not done so already),
  • added, committed, and pushed your code to the git server, and
  • run the chisubmit submission command.

Here are the relevant commands to run on the Linux command-line. (Remember to leave out the $ prompt when you type the command.)

$ chisubmit student assignment register pa1

$ git add sir.py
$ git commit -m"final version of PA #1 ready for submission"
$ git push

$ chisubmit student assignment submit pa1

We recommend copying and pasting these commands rather than re-typing them!

Remember to push your code to the server early and often!

Acknowledgments: This assignment was inspired by a discussion of the SIR model in the book Networks, Crowds, and Markets by Easley and Kleinberg. Emma Nechamkin wrote the original version of this assignment.