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).
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.
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:
- Determine whether the person has an infected neighbor.
- If and only if the person has an infected neighbor, compute the immune level of the person and determine whether they will become infected.
- 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.
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 togets_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:
- Susceptible (
'S'
): you need to determine whether they will become infected ('I0'
) or remain susceptible ('S'
) for another day using yourgets_infected_at_position
function. - 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\). - 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
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.
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 asmax_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.
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 tosimulate_one_day
, and the disease states after the call tosimulate_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.
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.
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.