EX09 - Contagion Simulation


Overview

This exercise is inspired by the Washington Post’s article that simulated infectiousness with 2D cells in a graphical visualization. Simulations such as this are great sandboxes to practice and learn object-oriented programming concepts such as classes, objects, constructors, and methods.

Learning Objectives

  • Gain experience modelling concepts with object-oriented programming techniques
  • Practice implementing methods and utilizing method calls
  • Gain comfort modelling more complex data relationships through object graphs

Getting Started

This exercise involves support code that was introduced in the lesson for Tuesday, November 1st. If you have not completed that lesson, you will need to do so first before continuing on. Watch and complete the follow-along exercises of the video to get started here: https://www.youtube.com/watch?v=hTU2Fr58RO0

As a reminder, to begin running your simulation you can run the following command in the terminal:

python -m exercises.ex09

Office Hours Expectations

As we round out the final weeks of the course, we expect you to come to office hours with a level of preparation that demonstrates effort.

You must be able to explain the code you have written. If you do not understand the code you have written, the TAs are instructed to help you understand that and then turn you free to try solving the next problem you are up against.

Part 0) Attribution, Style Linting, and Types - 10pts

Be sure you fill in the global __author__ variable of model.py with your PID.

As is standard in COMP110, your exercise will be linted to follow standard Python style conventions for a part of your grade. Additionally, your program’s annotated types and value uses will be statically checked by mypy, so you should annotate all methods and constructors with the correct static types.

Part 1) Infection - 30pts

Now that you have a simulation where Cell objects are moving around a 2D Model environment, model infectiousness of close contacts by making it possible for a Cell to contract disease and spread it to other Cell objects.

In our modelling, we will choose to use the sickness attribute of a Cell to determine its state as either VULNERABLE or INFECTED. These two states will be represented as integers: VULNERABLE is 0 and INFECTED is 1.

Constants

In exercises/ex09/constants.py add two additional named constants: VULNERABLE and INFECTED. Establish their values as described above. In the Cell’s attribute definitions, replace 0 with a reference to constants.VULNERABLE.

Cell#contract_disease

Define a method of Cell named contract_disease. It should assign the INFECTED constant you defined above to the sickness attribute of the Cell object the method is called on. The method has no formal parameters and returns nothing.

Why define a method in this case?

For such a simple concept, why not just expect anyone using a Cell object to assign directly to the sickness attribute? Fantastic question! The motivation here is that by defining methods for modelling the “verbs” or actions of an object, we have more flexibility as the designer of a class to change our minds on how the data inside the class, specifically its defined attributes, are represented.

This design principle is called encapsulation and is a bit beyond the scope of COMP110. We’ll continue practicing it in the next few method implementations that will feel very simple. The implication of defining such methods is that we expect any code outside of the Cell class to use is the methods and not the sickness attribute directly. In doing so, within the Cell class you could change your mind about how you represent sickness, such as by making it bool, or a str, or even another object, and any code outside of the Cell relying on the methods the Cell exposes would still work just fine!

Cell#is_vulnerable

Define a method of a Cell named is_vulnerable. It should return True when the cell’s sickness attribute is equal to VULNERABLE and False otherwise. The method has no formal parameters.

Cell#is_infected

Define a method of a Cell named is_infected. It should return True when the cell’s sickness attribute is equal to INFECTED and False otherwise. The method has no formal parameters.

Cell#color

The ViewController support code responsible for visualizing the state of your simulation will ask each cell for the color to fill it with via this method.

To practice calling a method on self, be sure to implement this method in terms of Cell#is_infected and/or Cell#is_vulnerable. In other words: do not access the attribute directly from within the definition of this method.

Define a method of a Cell named color. It should return "gray" if the Cell is vulnerable, and any other color string of your choosing if the Cell is infected.

Model#__init__ - Number of Infected Cells

Now that a Cell can be modelled as infected, let’s be sure the simulation begins with some number of infected Cell objects. Simulating contagion is the purpose of this exercise, after all.

Add a third, formal parameter to the Model constructor. This int parameter will establish the number of infected Cell objects a simulation begins with. If this parameter’s value is equal to or exceeds the value of the cells parameter, or is 0 or negative, raise a ValueError with a message that indicates some number of the Cell objects must begin infected.

You will need to change the code in the constructor to infect the correct number of cells according to your new parameter. This parameter’s value should not increase the total number of cells in your simulation beyond the first parameter’s value.

After modifying the constructor, you will need to update the __main__.py’s use of it to have the correct arguments. You should define a constant, of any name you’d like, to represent the number of infected cells to begin the simulation with.

Functionality checkpoint: When you begin your simulation, the number of cells you specified as infected should be visualized in the color you choose to represent infected cells with in Cell#color.

Part 2) Contagion - 30 points

Now that your simulation is modelling infected Cell objects in the Model’s population, it’s time to simulate what happens when two Cell objects come into contact with one another when one is infected and the other is vulnerable: disease contraction.

Since your cells are modelled as simple circles with a radius defined in constants.py, we’ll define “contact” as meaning when the two cells touch. To know when two cells touch, it helps to know how far apart they are, so let’s begin with defining a helper method on the Point class that determines the distance between two points.

Point#distance

Define a distance method on the Point class that returns the distance between the Point object the method was called on and some other Point object passed in as a parameter. You should use the formula for computing the distance between two points that involves summing the squared difference of each component and taking the square root. Feel free to Khan Academy the distance between two points formula if it’s been a second since you last had to compute this. To calculate a square root in Python, import the sqrt function from the math library.

Cell#contact_with

Before you test every pair of Cells for a contact, let’s go ahead and setup a method that will be called when two Cell objects do make contact.

Define a method on the Cell class that can be given another Cell object as a parameter. If either of the Cell objects is infected and the other is vulnerable, then the other should become infected. You should implement this method in terms of the Cell#is_vulnerable, Cell#is_infected, and Cell#contract_disease methods defined above (in other words: do not directly access the sickness attribute from within contact_with, rely upon those methods instead). This method does not return any value.

Model#check_contacts

Now it’s time to test whether any two Cell values come in “contact” with one another. This logic will be primarily defined inside of the Model class.

Write a method named check_contacts in the Model class. It should return None. Its purpose is to compare the distance between every two Cell objects’ location attributes in the population. If any distance between two Cells is less than the constant CELL_RADIUS, then call the Cell#contact_with method on one of the two Cell objects, giving a reference to the other as an argument.

Before attempting to write code, try thinking through a similar problem. Imagine a sequence of playing cards lying face down. You can only check two at a time and you can’t use any memory to cleverly recall where you saw some other number. How would you turn over pairs at a time in an algorithmic fashion such that you could find all pairs of the same suit without checking the same two cards twice?

Hint: you should loop based on indices. You will need to make use of nested loops to address this algorithmic challenge, as well. For full credit, your nested loops should not compute the distance between a pair of Cell objects twice (or with itself).

You want to call this function once each time the Model#tick method is called. In Model#tick, after your for loop for calling the Cell#tick method on every Cell in the population completes, call the Model#check_contacts method. Be sure to unindent this call so that it is not inside of the for loop!

Functionality checkpoint: Once you have this part working, you should be able to run your simulation and see vulnerable Cell objects become infected as they come into contact with infected cells. For debugging purposes, try slowing your CELL_SPEED down to 1.0 and your CELL_COUNT up to 50 to observe contacts more easily. Turn your speed back up to something more fun than 1.0 when complete.

Part 3) Immunity - 25pts

Now that you can simulate a Cell infecting another Cell, let’s simulate the concept of recovery and immunity. After a Cell is infected for some period of time, it will recover and become immune. The instructions in this part are intentionally more succinct than in the previous part as an exercise in translating requirements into code.

Constants

  1. Establish a constant in constants.py named IMMUNE that is assigned -1. We will use the value -1 in the sickness attribute to represent the state of an immune Cell.
  2. Establish a constant in constants.py named RECOVERY_PERIOD that will be assigned an int representing the number of ticks a Cell will be infected for before recovering. Each tick is approximately 1/30th of a second, so let’s simulate a Cell recovers after 3 seconds. For now, assign a value of 90 ticks to RECOVERY_PERIOD.

Cell#immunize

Add a method to Cell named immunize that assigns the constant IMMUNE to the sickness attribute of the Cell.

Cell#is_immune

Add a method to Cell named is_immune that returns True when the Cell object’s sickness attribute is equal to the IMMUNE constant.

Cell#color

Modify your implementation of the color method to return a color of your choosing when the cell is immune. You should implement this method in terms of the Cell#is_immune method.

Model#__init__

Add a fourth parameter to Model#__init__ that is the number of immune cells the simulation should begin with. Define this parameter as one which has a default value of 0, thus making it an optional parameter. The syntax for a default parameter is param_name: param_type = default_value

Update your main function to begin the simulation with at least 1 immune cell. However many immune cells you begin your simulation with, define that as a constant like you did the number of infecteds. You should be able to run your program and see the immune cells show up. Note that this should not increase the total number of cells in your simulation beyond the number specified in the first parameter. Additionally, you will need to handle edge cases around the number of infected and immune cells such that it does not equal or exceed the total number of cells in the simulation. Raise a ValueError in the edge cases where there is an improper number of immune or infected cells in the call to model’s constructor.

Cell#tick

Infected cells should become immune after RECOVERY_PERIOD ticks. To model this concept, use the sickness attribute of a Cell to count the number of ticks it has been infected for. Thus, in the tick method, if a Cell is infected, increase its sickness attribute by 1. If a Cell’s sickness attribute is greater than RECOVERY_PERIOD, be sure the cell becomes immunized. You will need to update the logic of your is_infected method to return True for any sickness attribute value greater than or equal to INFECTED.

When this is working you should see each infected Cell change to your immune color after ~3 seconds. Your immune cells should not become reinfected. If they do, be sure to fix this logic!

Model#is_complete

The state of the simulation is complete when there are no remaining infected cells. Implement the Model#is_complete method such that it will return True when all Cell objects in the population are either vulnerable or immune and False when any of the Cell objects are infected. The ViewController will stop animating the model when is_complete returns True.

You should now be able to run a simulation through to completion! You are encouraged to experiment with some of the constants such that you get a feel for impacts of speed, population density, percent immune, and so on.

Part 4) Style Concerns - 5pts

  1. Your algorithm in the check_contacts method. You should be sure per given call to check_contacts you do not find the distance between the same pair of Cell objects twice and you do not find the distance between a cell and itself.
  2. Your contact_with method being defined in terms of other methods and not accessing the sickness attribute directly.

Submission Instructions

To create a submission, run the submission script:

python -m tools.submission exercises/ex09

This will produce a zip file with all of your exercise’s directory’s files in it. Autograding will open shortly.

Future Extensions

After you submit a final version of your exercise to Gradescope for the courses’ purposes, you are encouraged to try modelling different ideas which interest you if you’d like an additional challenge. This is a great sandbox for practicing bringing little ideas to life. Here are a few:

  1. Make an infection probabilistic
  2. Try and model shelter-in-place orders
  3. Make the cells “bounce” when they come in contact with each other
  4. Add another kind of object to the simulation (like a health clinic) that convey immunity when a cell comes in touch
  5. Model infected cells quarantining in place
  6. Look to the Washington Post article for more ideas!
Contributor(s): Kris Jordan