PJ02 - Cluster Funk v2


Overview

This project 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 project involves support code that was introduced in Lesson 40. If you have not completed Lesson 40 and established the pj02 directory already, please do that first before continuing on.

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

python -m projects.pj02

Office Hours Expectations

As this is the final project of the course, we expect you to come to office hours with a level of preparation that demonstrates effort.

Do not begin your office hours session with the screen shared. Begin a session face-to-face and provide some verbal context to where you are at in the project and what you are getting hung up on before attempting to dive into specific code. Verbalizing your intent versus your code’s outcomes. Gaining confidence and progressing as a programmer is the process of reducing this disconnect.

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.

For the final, open-ended portion (worth 5 points) of this project, the TAs cannot look at your code. This challenge segment is intended to be a challenge and to give you practice attempting to solve a problem that you might encounter out in the real world after you’ve completed COMP110.

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 project 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 projects/pj02/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 project, 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 - 20pts

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.

Part 5) Challenge Extension - 5pts

Reminder: TAs are not permitted to look at your code for this extension. Before the due date, you are able to talk with TAs at a conceptual level about this challenge. On the due date, we cannot provide any help whatsoever for this final extension.

You should not begin on this extension until fully completing the earlier requirements.

The challenge extension for this project is to write a module that will run a simulation as fast as possible, without visualizing it, and produce a chart of the infection and immunization curves once the simulation completes.

Add a new Python module to your pj02 directory named chart.py. It should have a main function using the standard Python idiom to call main when this file is run as a module.

Your program should make use of at least 3 command-line arguments, as were used in the last project. These arguments should allow the person running the module to decide:

  1. how many cells the simulation will use
  2. the starting number of infected cells
  3. the starting number of immune cells

Python’s built-in argparse library can help you manage these options in a handy way. You are not required to use argparse, but you are encouraged to try learning it and applying it on your own! Find the documentation here: https://docs.python.org/3/library/argparse.html

Once you have your arguments, construct a Model and repeatedly call its tick method until the simulation completes. You will not see a visualization of your cells moving around (and should not make use of the ViewController class). When the simulation completes, produce a line chart with the following properties:

  • A meaningful title
  • X-axis is time ticks in the simulation
  • Y-axis is number of cells
  • Two lines plotted:
    1. Number of Infected Cells
    2. Number of Immune Cells
  • Labeled axes

You will need to think about how to store the information needed to produce this chart while you are running your simulation.

For full credit on this extension, consider a hypothesis you think would be interesting to simulate the infection curve of. Write your hypothesis down in the top docstring section of chart.py. Modify any of the necessary constants of your simulation’s constants.py, and choose whatever command-line arguments make sense per your hypothesis, to produce a chart you find interesting in response to your hypothesis. You are encouraged to tinker with constants as necessary, such as increasing the size of your bounds and the number of Cells to something larger than we began with. Once you produce a meaningful chart, save the resulting chart in your pj02 directory. Add some additional commentary to your docstring in response to your hypothesis on what you found interesting about the process of using simulation to test your hypothesis.

Submission Instructions

To create a submission, run the submission script:

python -m tools.submission projects/pj02

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

Future Extensions

After you submit a final version of your project 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!