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 thesickness
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 thesickness
attribute directly. In doing so, within theCell
class you could change your mind about how you representsickness
, such as by making itbool
, or astr
, or even another object, and any code outside of theCell
relying on the methods theCell
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 Cell
s 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 Cell
s 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
- Establish a constant in
constants.py
namedIMMUNE
that is assigned-1
. We will use the value -1 in thesickness
attribute to represent the state of an immune Cell. - Establish a constant in
constants.py
namedRECOVERY_PERIOD
that will be assigned anint
representing the number of ticks aCell
will be infected for before recovering. Eachtick
is approximately 1/30th of a second, so let’s simulate aCell
recovers after 3 seconds. For now, assign a value of90
ticks toRECOVERY_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
- Your algorithm in the
check_contacts
method. You should be sure per given call tocheck_contacts
you do not find the distance between the same pair ofCell
objects twice and you do not find the distance between a cell and itself. - Your
contact_with
method being defined in terms of other methods and not accessing thesickness
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:
- Make an infection probabilistic
- Try and model shelter-in-place orders
- Make the cells “bounce” when they come in contact with each other
- Add another kind of object to the simulation (like a health clinic) that convey immunity when a cell comes in touch
- Model infected cells quarantining in place
- Look to the Washington Post article for more ideas!