Overview

For Project 2, the infection model is significantly more complicated than for Project 1. It includes randomness, the possibility of "break-through" infections for immune cells, and vaccines. This model is implemented in the model.rkt file.

Model Parameters

The behavior of the model is controled by a number of tunable parameters. These are represented by the following type:

;; The parameters that specify the model's behavior
(define-struct Model-Params
  ([inf-rate : Real]        ;; The scaling factor used to compute the probability of
                            ;; infection [0..1].
   [imm-adj : Real]         ;; The immunity adjustment factor [0..1]
   [vac-adj : Real]         ;; The vaccination adjustment factor [0..1]
   [vac-rate : Real]        ;; The rate of vaccination [0..1]
   [ill-rate : Real]        ;; The initial rate of infection [0..1]
   [ill-length : Natural])) ;; The length of the recovery time in steps

We impose certain requirements on the parameters so that the model is sensible. Specifically,

  • All of the real-valued parameters should be in the range latemath:[0..1] (inclusive).

  • The sum of the initial infection rate (ill-rate) and the vaccination rate (vac-rate) should be no greater than one.

  • The recovery time (ill-length)should be at least one.

To enforce these limits, we define a function for creating the model parameters:

(: make-model-params : Real Real Real Real Real Natural -> Model-Params)
;; (make-model-params inf i-adj v-adj v-rate i-rate n) deines the model
;; parameters as follows:
;;    inf-rat           == inf
;;    imm-adj           == (- 1.0 i-adj)
;;    vac-adj           == (- 1.0 v-adj)
;;    vac-rate          == v-rate
;;    ill-rate          == i-rate
;;    ill-length        == n
;; Furthermore, it checks the validity of the fields and signals an error
;; if any field is invalid.

Your implementation of this function should signal an error with the string

"invalid model parameters"

An example of a valid model is

(make-model-params 0.15  ;; the "infection rate" is 15%
                   0.8   ;; immunity is 80% effective
                   0.75  ;; the vaccine is 75% effective
                   0.6   ;; 60% of cells are vaccinated
                   0.1   ;; 10% of cells are initially infected
                   5)    ;; the length of the illness is five steps

The Model

Given a cell in the grid, let \(n\) be the number of ill neighbors. Then we update the state of the cell as follows:

  • If the cell is ill, we decrement its recovery-time by one. When the recovery-time field becomes zero, the cell’s health changes to immune.

  • Otherwise, we compute the base probability that the cell will become ill as \(p = n \cdot I \cdot V \cdot R\), where

    • \(I\) is the immunity adjustment if the cell is immune and one otherwise

    • \(V\) is the vaccination adjustment if the cell is vaccinated and one otherwise

    • \(R\) is the infection rate

  • If \(p = 0\), then there is no change in the cell’s health, and if \(p \geq 1\), then the cell becomes ill. Asuming that \(0 < p < 1\), we generate a random number \(0 < r < 1\) using the random function. If \(r \leq p\), then the cell becomes ill and otherwise it’s health is unchanged.

When a cell becomes ill, its recovery time is set to the value of the ill-length field in the model parameters. Note that unlike in Project 1, a cell can become ill multiple times.

Implementing the Model

Cells

The first step is to define a representation of cells in the grid: Add the following definitions to your code:

;; the health of a cell
(define-type Health (U 'Well 'Ill 'Immune))

;; A (Cell health ticks vac?) represents the state of a grid cell, where health
;; is the current health of the cell, ticks is the number of simulation steps
;; until an ill cell turns immune, and vac? is #t for cells that have been
;; vaccinated.  If the cell is not ill, then ticks should be 0.
(define-struct Cell
  ([health : Health]
   [recovery-time : Integer]
   [vaccinated? : Boolean]))

Updating the Grid

Because the model involves non-determinism, it is difficult to accurately test. To solve this problem, we factor out as much of the the deterministic (i.e., non-random) components to standalone testing.

First, implement a function for counting the number of ill neighbors of a cell. This function takes the grid as input and returns a function for counting the ill neighbors of a specified cell.

(: count-ill-neighbors : (Grid Cell) -> Coord -> Natural)

The other piece of the puzzle is computing the probability that a cell will become ill based on the model parameters, the cell’s state, and the number of ill neighbors.

(: compute-probability : Model-Params -> Cell Natural -> Real)

For this function, you should return a zero probability when the cell is already ill, since its status will not change.

We can then combine these functions, along with code for updating ill cells, to implement a cell update function.

(: update-cell : Model-Params -> (Grid Cell) -> Coord Cell -> Cell)

Note that the vaccination status of a cell should be preserved when the cell’s health changes.

With this function in hand, it is easy to define a function for updating the whole grid:

(: update-grid : Model-Params -> (Grid Cell) -> (Grid Cell))
Tip

Recall from Homework 2 that the Racket’s random function is a function of no arguments that returns a random number between zero and one. You can call it using the syntax

(random)

Note that every time that you call it, you will get a different result, so if you want to use the result of a single call multiple times, you will need to bind it to a variable.

Checking for a Stable Grid

For this model, we define a stable grid as one that has no ill cells. Define a function for checking the stability of a grid:

(: stable? : (Grid Cell) -> Boolean)

Building an Initial Grid

The initial model is also generated using randomness. Let \(v\) be the vaccination rate and \(c\) be the initial rate of infection as specified by the model parameters. Recall that for a valid set of model parameters, we have \(0 \leq v + c \leq 1\).

To create the initial population of cells, we generate a random number \(0 \leq r \leq 1\) and use it to determine the initial state of cells as follows:

  • if \(0 \leq r \leq v\), then we initialize the cell to (Cell 'Well 0 #t).

  • if \(v < r \leq v + c\), then we initialize the cell to (Cell 'Ill n #f), where n is the value of the ill-length field in the model parameters.

  • otherwise, \(v + c < r \leq 1\), and we initialize the cell to (Cell 'Well 0 #f).

Again, we structure the code to enable testing. Write a function

(: init-cell : Model-Params -> Real -> Cell)

that takes in the model parameters and returns a function for mapping a number \(0 \leq r \leq 1\) to a cell as described above.

We can then supply this function with random numbers to generate random cells for the initial grid:

(: initial-grid : Model-Params -> Natural Natural -> (Grid Cell))

When implementing this function, be careful to generate a fresh random number for each cell.

Building a Model

We then put all of the above mechanisms together in a function for creating the model from model parameters.

(: make-model : Model-Params -> (Model Cell))

The name of the model should just be "Random Model".

Rendering

The model.rkt file also defines support for rendering cells in the model. Draw the cells as circles with black outlines and where the fill color depends on the health of the cell and its vaccination status. Implement a function for creating the rendering information for the model from the cell radius and the refresh rate:

(: render-model : Integer Positive-Real -> (Render-Info Cell))

Exports

Once you have implemented and tested the code described above, you should add the following exports to the end of your model.rkt file:

;; ===== Exports =====

(provide Model-Params
         (struct-out Cell))

(provide make-model-params
         make-model
         render-model)