A Starting Point for a Campus COVID Spread Model

Return to Week 10 of course schedule.

We will add rendering to this model as Assignment 7 (due March 28).

We will add realistic associations, places, and have people do different things on breaks than during terms as Assignment 8 (probably due April 4 unless we need more cellular automata assignments instead).

Implementation

// The main concepts are person, place, and association.
// A person can belong to many associations.
// An association has a meeting schedule, which with the help
// of the academic calendar will allow us to determine if it is meeting.
// If an association is meeting, and a person belongs to that association,
// then that is what that person's place will become.
// If there is a conflict (because the person has multiple associations
// that are meeting) then the first association in the list wins.
// If the person is in no associations that are meeting, then they
// are at home. Not all of this is implemented in this early version.

// The thing that manages the entire list of places, the entire list
// of people, and the entire list of assciations, is a singleton called
// community.

AcademicCalendar ac;
Community community;

class AcademicCalendar {
  static final int HOURS_IN_DAY = 24;
  static final int HOURS_IN_WEEK = 168;
  static final int HOURS_IN_YEAR = 8760;
  // let us begin our simulation with the beginning of COVID
  int hours = 12 * HOURS_IN_WEEK; // COVID really got going in the US on the 12th week of 2020
  int[] breakWeeks = {
                        0, /* third week of winter break */
                        /* Term 4 */
                        8, 9, /* two-week break */
                        /* Term 5 */
                        17, /* one-week break */
                        /* Term 6 */
                        25,
                        /* Term 1 */
                        33, 34, /* two-week break */
                        /* Term 2 */
                        42, /* Term 3 */
                        50, 51, /* first two weeks of three-week break */
                        52 /* there is actually only one day in week 52 */
                     };

  void increment() {
    hours++;
  }

  boolean isAssociationMeeting(Association a) {
    int h = hours % a.frequency;
    return h >= a.meetingTime && h < a.meetingTime + a.duration;
  }

  int year() {
    return 2020 + hours / HOURS_IN_YEAR;
  }

  int week() {
    int h = hours % HOURS_IN_YEAR;
    return h / HOURS_IN_WEEK;
  }

  boolean isCampusOnBreak() {
    int w = week();
    for (int i = 0; i < breakWeeks.length; ++i) {
      if (w == breakWeeks[i]) return true;
    }
    return false;
  }
}

class Infection {  
  static final int INCUBATION_PERIOD = 1 * AcademicCalendar.HOURS_IN_DAY;
  static final int WORST_OF_IT_ENDS = 4 * AcademicCalendar.HOURS_IN_DAY;
  static final int INFECTION_ENDS = 14 * AcademicCalendar.HOURS_IN_DAY;
  static final float HOURLY_R_VALUE = 0.5;

  int infectionHours;

  Infection() {
    infectionHours = ac.hours;
  }

  float infectiousness() {
    // during day 0, not very infectious (incubating)
    // real infectious during days 1, 2, 3 (inclusive)
    // steadily declining infectiousness during days 4-13 (inclusive)
    int delta = ac.hours - infectionHours;
    float infectiousness;
    if (delta < AcademicCalendar.HOURS_IN_DAY) {
      // incubating, low infectiousness
      infectiousness = 0.2;
    } else if (delta >= INCUBATION_PERIOD && delta < WORST_OF_IT_ENDS) {
      // worst of it
      infectiousness = 1.0;
    } else if (delta < INFECTION_ENDS) {
      // recovering
      infectiousness = (delta - WORST_OF_IT_ENDS) / (INFECTION_ENDS - WORST_OF_IT_ENDS);
    } else {
      // recovered
      infectiousness = 0.0;
    }
    return infectiousness;
  }

  boolean over() {
    return ac.hours - infectionHours >= INFECTION_ENDS;
  }
}

static int last_id = -1;

class Person {
  Infection infection = null;
  float immunity = 0.0; // a number between 0 and 1, with 1 being immune
  float fastiduousness = 0.5; // a number between 0 and 1 with 1 being ultra-hygienic
  int id;

  Person() {
    id = ++last_id;
  }

  boolean isInfected() {
    return infection != null;
  }

  float infectiousness() {
    return infection != null ? infection.infectiousness() : 0.0;
  }

  float slovenliness() {
    return 1.0 - fastiduousness;
  }

  float susceptibility() {
    return 1.0 - immunity;
  }

  void catchCOVID() {
    infection = new Infection();
    immunity = 1.0;
  }

  void expose(Person otherPerson, float closeness) {
    if (this != otherPerson && isInfected() && !otherPerson.isInfected()) {
      float exposureIntensity = closeness * infectiousness() * slovenliness() * otherPerson.slovenliness();
      if (random(1.0) < exposureIntensity * Infection.HOURLY_R_VALUE) {
        otherPerson.catchCOVID();
      }
    }
  }

  void age() {
    if (infection != null && infection.over()) {
      infection = null;
    }
    // 1.00007912952 is 2^(1/8760). Using this number
    // gives immunity a half-life of one year.
    immunity /= 1.00007912952;
  }
}

class Staffulty extends Person {
}

class Student extends Person {
}

class Association {
  int meetingTime;
  int duration; // either DAILY OR WEEKLY
  int frequency; // either DAILY OR WEEKLY
  float closeness = 0.5;
  ArrayList<Person> members = new ArrayList<Person>();

  void expose() {
    for (Person person : members) {
      for (Person otherPerson : members) {
        person.expose(otherPerson, closeness);
      }
    }
  }
}

class Community {
  ArrayList<Person> people = new ArrayList<Person>();
  ArrayList<Association> associations = new ArrayList<Association>();

  Community() {
    Association leftAssociation = new Association();
    Association rightAssociation = new Association();
    associations.add(leftAssociation);
    associations.add(rightAssociation);
    for (int i = 0; i < 26; i++) {
      Person person = new Person();
      people.add(person);
      if (i < 13) {
        leftAssociation.members.add(person);
      } else {
        rightAssociation.members.add(person);
      }
    }
    Person patient0 = people.get(13);
    patient0.catchCOVID();
  }

  void increment() {
    for (Association association : associations) {
      association.expose();
    }
    for (Person person : people) {
      person.age();
    }
  }

  void report() {
    int covidCount = 0;
    for (Person person : people) {
      if (person.isInfected()) {
        ++covidCount;
      }
    }
    println("The community has " + covidCount + " cases.");
  }

  void draw() {
    background(255);
    for (int i = 0; i < people.size(); ++i) {
      ellipseMode(CENTER);
      stroke(0);
      Person person = people.get(i);
      if (person.isInfected()) {
        fill(255, 0, 0);
      } else {
        fill(255);
      }
      ellipse(10 + i * 20 + 10, 15, 10, 10);
    }
  }
}

void setup() {
  size(540, 30);
  ac = new AcademicCalendar();
  community = new Community();
}

void draw() {
  ac.increment();
  community.increment();
  // community.report();
  community.draw();
}