================== 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: 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 |repo_name| 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 |repo_name| 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 `` 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: .. code:: $ ipython3 In [1]: %load_ext autoreload In [2]: %autoreload 2 In [3]: import sir In [4]: import random (Note: ``In []`` 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: .. include:: getting-help-course-specific.rst 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``: .. code:: 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). .. csv-table:: Tests for ``count_ever_infected`` :header: City, Expected result, Description :widths: 40, 5, 20 :delim: | ['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**: .. code:: $ 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``: .. code:: $ 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: .. code:: $ 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: .. code:: 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 :ref:`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 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``: .. code:: 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. .. csv-table:: Tests for ``has_an_infected_neighbor`` :header: City, Position, Expected result, Description :widths: 30, 3, 5, 30 :delim: | ['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:** .. code:: $ 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``: .. code:: 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 :ref:`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: .. code:: 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. .. csv-table:: Tests for ``gets_infected_at_position`` :header: Seed, City, Position, Infection rate, Expected result, Description :widths: 8, 30, 5, 5, 8, 20 :delim: | 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**: .. code:: $ 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: #. 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. #. Infected (``'Ix'``, where *x* is an integer): determine whether the person remains infected (that is, :math:`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 :math:`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``: .. code:: 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: .. code:: 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. .. code:: 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 .. csv-table:: Tests for ``advance_person_at_position`` :header: Seed, City, Position, Infection rate, Days Contagious, Result, Description :widths: 10, 30, 2, 5, 5, 5, 30 :delim: | 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``: .. code:: 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. .. csv-table:: Tests for ``simulate_one_day`` :header: Seed, City, Infection rate, Days contagious, Expected result, Description :widths: 10, 30, 5, 5, 30, 30 :delim: | 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**: .. code:: $ 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: .. code:: 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. .. csv-table:: Tests for ``run_simulation`` :header: Starting City, Seed, Maximum number of days, Infection rate, Days contagious, "Expected Result: city, number of days simulated", Description :widths: 30, 5, 5, 5, 5, 40, 40 :delim: | ['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**. .. code:: $ 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: .. code:: 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. .. csv-table:: Intermediate values from ``calc_avg_num_newly_infected`` :header: Simulation number, Seed, Starting state for simulation run, Final state for simulation run, Number of people newly infected :widths: 5, 15, 30, 30, 5 :delim: | 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 :math:`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. .. csv-table:: Tests for ``calc_avg_num_newly_infected`` :header: Starting Seed, Starting City, Maximum number of days, Infection rate, Days contagious, Number of Trials, Expected result, Description :widths: 10, 30, 5, 5, 5, 5, 5, 40 :delim: | 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**. .. code:: $ 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. .. code:: $ 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: .. code:: $ 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: .. code:: 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: .. code:: $ 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 <../rubric.html>`__.) 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.) .. code:: $ 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. .. include:: course-specific-values.rst