10  Groups Plugin

The groups plugin manages the assignment people to various groups such as homes, schools, work places and other social organizations. As such, it is dependent on the people plugin. Like people, groups can have property definitions. Unlike people, groups have a modeler-defined group type that dictates which group properties apply.

10.1 Plugin Data Dependency

The groups plugin depends on the people plugin and the stochastics plugin. The stochastics plugin is used for random sampling of people from groups.

10.2 Plugin Data initialization

The plugin is initialized using a GroupsPluginData object that:

  • adds group types
  • defines group properties for each group type
  • sets group property value per group
  • adds groups
  • initializes the person membership in groups

10.3 Plugin Behavior

The plugin adds a single data manager to the simulation as an instance of the GroupsDataManager that is initialized with the GroupsPluginData.

10.4 Data Manager

The data manager manages group memberships and group property values. The data manager provides public methods that:

  • Add/Remove a group
  • Add a group type
  • Add/Remove a person to/from a group
  • Define a group property
  • Set a group property value
  • Sample a random person from a group
  • Provide many queries about the state of groups

The data manager also produces observable events:

  • GroupAdditionEvent – when a group is added
  • GroupImminentRemovalEvent – when a group is about to be removed
  • GroupMembershipAdditionEvent – when a person is added to a group
  • GroupMembershipRemovalEvent – when a person is removed from a group
  • GroupPropertyDefinitionEvent – when a new group property is defined
  • GroupPropertyUpdateEvent – when a group property value is updated
  • GroupTypeAdditionEvent – when a new group type is added

10.5 Example Code (Lesson 17)

Example_17.java shows the use of the groups plugin. In it we will examine:

  • The initialization of the groups plugin
  • The movement of people in and out of groups

The example includes seven plugins:

  • Groups plugin – (GCM core plugin) used to manage groups
  • People plugin – (GCM core plugin) used to manage people
  • Person properties plugin – (GCM core plugin) used to decorate properties onto people
  • Global properties plugin – (GCM core plugin) used to store policies and initial conditions affecting groups and disease transmission
  • Stochastics plugin – (GCM core plugin) used to generate random numbers used in various decisions
  • Regions Plugin – (GCM core plugin) used for various reports
  • Model plugin – (local plugin) used to introduce four actors that will:
    • Load the population
    • Manage the spread of disease through transmission in groups
    • Manage group-based mitigation strategies in schools
    • Manage group-based mitigation strategies in work places

10.6 Model

The example’s model represents a disease that is not treatable but can be mitigated through social distancing, school closures and the use of telework arrangements. People at the start of the simulation are either immune or susceptible. Infection is spread through personal contact in groups and is subject to public policies triggered by the number of infectious people present in the total population or in specific groups. The population is set to 10,000 people and the groups are synthetically derived using fixed proportions (global properties) for school aged children (0-18 yrs), working adults (19 to 64 yrs) and non-working seniors (65+yrs). All people are assigned a home group. School aged children are assigned to a single school group and working age adults are assigned to a single work group. The number of each group type is determined by fixed expected groups sizes (global properties).

At the beginning of the model, a small number of adults are infected at random. Each infected person is immediately infectious and will infect one person per day over a random period from 3 to 12 days inclusive. Without mitigations, each transmission is successful. Public policies are reviewed on a weekly basis. As the number of total public infections increase, some work groups may elect to move to a telework status, reducing transmissions by 50% in those groups. Similarly, when a school’s level of infection increases it will move to a split cohort mode, reducing transmission success to 50%. If infection within a cohort continues to rise, the cohort is closed and the children are no longer assigned to a school group. For simplicity of modeling, telework arrangements, cohort schools and school closures do not revert as infection levels subside.

10.7 Model Execution

The example’s execution is shown in Code Block 10.1 and Code Block 10.2.

Code Block 10.1: Executing example 17 with an output directory.
public static void main(String[] args) throws IOException {
    if (args.length == 0) {
        throw new RuntimeException("One output directory argument is required");
    }
    Path outputDirectory = Paths.get(args[0]);
    if (!Files.exists(outputDirectory)) {
        Files.createDirectory(outputDirectory);
    } else {
        if (!Files.isDirectory(outputDirectory)) {
            throw new IOException("Provided path is not a directory");
        }
    }

    new Example_17(outputDirectory).execute();
}
Code Block 10.2: The various plugins are gathered from their initial data, dimensions are added and the experiment is executed over 36 scenarios using 8 threads.
private void execute() {

    ExperimentParameterData experimentParameterData = ExperimentParameterData.builder()//
            .setThreadCount(8)//
            .build();

    Experiment.builder()

            .addPlugin(getGlobalPropertiesPlugin())//
            .addPlugin(getPersonPropertiesPlugin())//
            .addPlugin(getRegionsPlugin())//
            .addPlugin(getPeoplePlugin())//
            .addPlugin(getGroupsPlugin())//
            .addPlugin(getStochasticsPlugin())//
            .addPlugin(ModelPlugin.getModelPlugin())//

            .addDimension(getTeleworkProbabilityDimension())//
            .addDimension(getTeleworkInfectionThresholdDimension())//
            .addDimension(getSchoolDimension())//

            .addExperimentContextConsumer(getNIOReportItemHandler())//
            .setExperimentParameterData(experimentParameterData)//              
            .build()//
            .execute();//
}

The first action is to load the global properties plugin (Code Block 10.3). The fifteen global properties are marked as immutable since they will not change over the course of the simulation. Further, eleven of the properties are fixed and their values are set in the plugin data. The four remaining properties participate in the dimensions of the experiment and are not directly set in the global plugin.

  • SUSCEPTIBLE_POPULATION_PROPORTION – The fraction of the population that is susceptible
  • INITIAL_INFECTIONS – The number of adults initially infected
  • MIN_INFECTIOUS_PERIOD – The minimum number of days a person is infectious
  • MAX_INFECTIOUS_PERIOD – The maximum number of days a person is infectious
  • POPULATION_SIZE – The initial size of the population
  • CHILD_POPULATION_PROPORTION – The fraction of the population between the ages of 0 and 18, inclusive
  • SENIOR_POPULATION_PROPORTION – The fraction of the population 65 and older
  • R0 – The expected number of people a single person will infect if all contacts are susceptible and transmission success is 100% likely
  • AVERAGE_HOME_SIZE – The average number of people per household
  • AVERAGE_SCHOOL_SIZE – The average number of students in a school
  • AVERAGE_WORK_SIZE – The average number of people per work place
  • TELEWORK_INFECTION_THRESHOLD – The total infection density that triggers some work places to institute telework mode
  • TELEWORK_PROBABILTY – The probability that a work place will convert to telework mode once telework is allowed
  • SCHOOL_COHORT_INFECTION_THRESHOLD – The infection density within a school that triggers the school moving to a split cohort
  • SCHOOL_CLOSURE_INFECTION_THRESHOLD – The infection density within a school cohort that triggers the closure of that cohort.
Code Block 10.3: The global properties plugin is initialized with several properties
private Plugin getGlobalPropertiesPlugin() {
    GlobalPropertiesPluginData.Builder builder = GlobalPropertiesPluginData.builder();//

    PropertyDefinition propertyDefinition = PropertyDefinition.builder()//
            .setType(Double.class)//
            .setPropertyValueMutability(false)//
            .setDefaultValue(0.0).build();

    builder.defineGlobalProperty(GlobalProperty.SUSCEPTIBLE_POPULATION_PROPORTION, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.AVERAGE_HOME_SIZE, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.AVERAGE_SCHOOL_SIZE, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.AVERAGE_WORK_SIZE, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.CHILD_POPULATION_PROPORTION, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.SENIOR_POPULATION_PROPORTION, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.R0, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.TELEWORK_INFECTION_THRESHOLD, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.TELEWORK_PROBABILITY, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.SCHOOL_COHORT_INFECTION_THRESHOLD, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.SCHOOL_CLOSURE_INFECTION_THRESHOLD, propertyDefinition, 0);

    propertyDefinition = PropertyDefinition.builder()//
            .setType(Integer.class)//
            .setPropertyValueMutability(false)//
            .build();
    builder.defineGlobalProperty(GlobalProperty.INITIAL_INFECTIONS, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.MIN_INFECTIOUS_PERIOD, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.MAX_INFECTIOUS_PERIOD, propertyDefinition, 0);
    builder.defineGlobalProperty(GlobalProperty.POPULATION_SIZE, propertyDefinition, 0);

    builder.setGlobalPropertyValue(GlobalProperty.POPULATION_SIZE, 10_000, 0);
    builder.setGlobalPropertyValue(GlobalProperty.SUSCEPTIBLE_POPULATION_PROPORTION, 1.0, 0);
    builder.setGlobalPropertyValue(GlobalProperty.INITIAL_INFECTIONS, 10, 0);
    builder.setGlobalPropertyValue(GlobalProperty.MIN_INFECTIOUS_PERIOD, 3, 0);
    builder.setGlobalPropertyValue(GlobalProperty.MAX_INFECTIOUS_PERIOD, 12, 0);
    builder.setGlobalPropertyValue(GlobalProperty.R0, 2.0, 0);
    builder.setGlobalPropertyValue(GlobalProperty.CHILD_POPULATION_PROPORTION, 0.235, 0);
    builder.setGlobalPropertyValue(GlobalProperty.SENIOR_POPULATION_PROPORTION, 0.169, 0);
    builder.setGlobalPropertyValue(GlobalProperty.AVERAGE_HOME_SIZE, 2.5, 0);
    builder.setGlobalPropertyValue(GlobalProperty.AVERAGE_SCHOOL_SIZE, 250.0, 0);
    builder.setGlobalPropertyValue(GlobalProperty.AVERAGE_WORK_SIZE, 30.0, 0);

    GlobalPropertiesPluginData globalPropertiesPluginData = builder.build();

    return GlobalPropertiesPlugin.builder().setGlobalPropertiesPluginData(globalPropertiesPluginData)
            .getGlobalPropertiesPlugin();
}

The person properties plugin is loaded in (Code Block 10.4). The properties are:

  • AGE – An integer value for the age of a person: child (0-18), adult (19-64), senior (65+)
  • DISEASE_STATE – The state for a person: IMMUNE, SUSCEPTIBLE, INFECTIOUS, RECOVERED
  • INFECTED_COUNT – The number of people infected by each particular person
Code Block 10.4: The person properties plugin is initialized with several properties.
private Plugin getPersonPropertiesPlugin() {

    PersonPropertiesPluginData.Builder builder = PersonPropertiesPluginData.builder();

    PropertyDefinition propertyDefinition = PropertyDefinition.builder()//
            .setType(Integer.class)//
            .build();

    builder.definePersonProperty(PersonProperty.AGE, propertyDefinition, 0, false);//

    propertyDefinition = PropertyDefinition.builder()//
            .setType(DiseaseState.class)//
            .setDefaultValue(DiseaseState.SUSCEPTIBLE).build();

    builder.definePersonProperty(PersonProperty.DISEASE_STATE, propertyDefinition, 0, false);//

    propertyDefinition = PropertyDefinition.builder()//
            .setType(Integer.class)//
            .setDefaultValue(0).build();

    builder.definePersonProperty(PersonProperty.INFECTED_COUNT, propertyDefinition, 0, false);//

    PersonPropertiesPluginData personPropertiesPluginData = builder.build();

    PersonPropertyReportPluginData personPropertyReportPluginData = PersonPropertyReportPluginData.builder()//
            .setReportLabel(ModelReportLabel.PERSON_PROPERTY)//
            .setReportPeriod(ReportPeriod.DAILY)//
            .includePersonProperty(PersonProperty.DISEASE_STATE)//
            .build();

    return PersonPropertiesPlugin.builder()//
            .setPersonPropertiesPluginData(personPropertiesPluginData)//
            .setPersonPropertyReportPluginData(personPropertyReportPluginData)//
            .getPersonPropertyPlugin();
}

The groups plugin (Code Block 10.5) loads the group types and their corresponding group properties. The creation of groups is left to the Population Loader.

  • HOME
  • SCHOOL
    • SCHOOL_STATUS : OPEN, COHORT (50% transmission reduction), CLOSED (100% transmission reduction)
  • WORK
    • TELEWORK – Boolean designating work place as telework, reducing transmission by 50%
Code Block 10.5: The groups plugin includes a tele-work property for work places and open status properties for schools.
private Plugin getGroupsPlugin() {
    GroupsPluginData.Builder builder = GroupsPluginData.builder();
    for (GroupType groupType : GroupType.values()) {
        builder.addGroupTypeId(groupType);
    }
    PropertyDefinition propertyDefinition = PropertyDefinition.builder()//
            .setType(Boolean.class)//
            .setDefaultValue(false)//
            .build();

    builder.defineGroupProperty(GroupType.WORK, GroupProperty.TELEWORK, propertyDefinition);

    propertyDefinition = PropertyDefinition.builder()//
            .setType(SchoolStatus.class)//
            .setDefaultValue(SchoolStatus.OPEN)//
            .build();

    builder.defineGroupProperty(GroupType.SCHOOL, GroupProperty.SCHOOL_STATUS, propertyDefinition);

    GroupsPluginData groupsPluginData = builder.build();

    GroupPopulationReportPluginData groupPopulationReportPluginData = //
            GroupPopulationReportPluginData.builder()//
                    .setReportLabel(ModelReportLabel.GROUP_POPULATION)//
                    .setReportPeriod(ReportPeriod.END_OF_SIMULATION)//
                    .build();//
    return GroupsPlugin.builder()//
            .setGroupsPluginData(groupsPluginData)//
            .setGroupPopulationReportPluginData(groupPopulationReportPluginData)//
            .getGroupsPlugin();
}

The stochastics plugin is initialized with a random seed and all simulations will start in the same stochastic state. The regions plugin has five regions and the people plugin is loaded in an empty state. People are added by an actor in the model plugin.

The model plugin adds four actors

  • PopulationLoader – Adds people and groups to the simulation and initializes immunity.
  • InfectionManager– Infects a small number of randomly chosen adults. Manages the progress of infections and the spread of infection from one per to another via their shared groups.
  • TeleworkManager – Reviews the status of infections within the greater population every week and triggers randomly selected work places to move to telework mode.
  • SchoolManager – Reviews the status of infections per school every week and splits school groups into cohorts. If infections increase it can close individual schools.

The reports in this model are:

  • GroupPopulationReport – Shows the distribution of group sizes for each group type at the end of the simulation.
  • PersonPropertyReport – Shows the distribution of disease state values over each day by regions
  • DiseaseStateReport – Shows the distribution of disease state at the end of the simulation with a single line per scenario
  • ContagionReport – Shows the distribution of the number of infections spread per person

10.8 Experiment dimensions

Three dimensions are added to the experiment that define alternate values for telework infection thresholds, telework adoption probability, school cohort infection thresholds and school closure infection thresholds, yielding 36 scenarios. The values are:

  • Telework infection threshold – 0.1, 1, and 10 percent
  • Telework adoption probability – 10, 30, 50 and 80 percent
  • School cohort and closure infection thresholds
    • 0.1 and 1 percent
    • 1 and 2 percent
    • 10 and 20 percent

10.9 The actors

We will finish this chapter by reviewing the four actors of the model plugin and then examine the output.

10.9.1 Population Loader

The PopulationLoader actor, in Code Block 10.6, adds people to the simulation based on the number in the POPULATION_SIZE global property. The population is split evenly between the regions. For each region, the manager determines the number of homes needed to house the people based on the AVERAGE_HOME_SIZE. The people are grouped into children, working adults and seniors based on related global properties. Similar calculations determine the number of schools and workplaces. The people are randomly distributed based on their ages into homes, work places and schools. Some care is given to ensure that every household has at least one adult.

Code Block 10.6: The population loader initializes by establishing various constants from the global properties and establishing the population of each region.
public void init(ActorContext actorContext) {
    personPropertiesDataManager = actorContext.getDataManager(PersonPropertiesDataManager.class);
    groupsDataManager = actorContext.getDataManager(GroupsDataManager.class);
    StochasticsDataManager stochasticsDataManager = actorContext.getDataManager(StochasticsDataManager.class);
    randomGenerator = stochasticsDataManager.getRandomGenerator();
    peopleDataManager = actorContext.getDataManager(PeopleDataManager.class);
    GlobalPropertiesDataManager globalPropertiesDataManager = actorContext
            .getDataManager(GlobalPropertiesDataManager.class);
    regionsDataManager = actorContext.getDataManager(RegionsDataManager.class);

    int populationSize = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.POPULATION_SIZE);
    susceptibleProbability = globalPropertiesDataManager
            .getGlobalPropertyValue(GlobalProperty.SUSCEPTIBLE_POPULATION_PROPORTION);
    childPopulationProportion = globalPropertiesDataManager
            .getGlobalPropertyValue(GlobalProperty.CHILD_POPULATION_PROPORTION);
    seniorPopulationProportion = globalPropertiesDataManager
            .getGlobalPropertyValue(GlobalProperty.SENIOR_POPULATION_PROPORTION);
    averageHomeSize = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.AVERAGE_HOME_SIZE);
    averageSchoolSize = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.AVERAGE_SCHOOL_SIZE);
    averageWorkSize = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.AVERAGE_WORK_SIZE);

    Set<RegionId> regionIds = regionsDataManager.getRegionIds();
    int regionSize = populationSize / regionIds.size();
    int leftoverPeople = populationSize % regionIds.size();

    for (RegionId regionId : regionIds) {
        int regionPopulation = regionSize;
        if (leftoverPeople > 0) {
            leftoverPeople--;
            regionPopulation++;
        }
        initializeRegionPopulation(regionId, regionPopulation);
    }
}

Initializing the region population, Code Block 10.7, first establishes the number of home groups, school groups and work groups by determining the number of children, working adults and seniors that are present based on various fixed global property values. The people are added with randomly determined ages and disease immunity.

Code Block 10.7: The population for a region is initialized with each person being assigned an age, an immunity status and a region.
private void initializeRegionPopulation(RegionId regionId, int populationSize) {

    double n = populationSize;
    int homeCount = (int) (n / averageHomeSize) + 1;
    int childCount = (int) (n * childPopulationProportion);
    int adultCount = populationSize - childCount;
    homeCount = FastMath.min(homeCount, adultCount);
    int seniorCount = (int) (n * seniorPopulationProportion);
    seniorCount = FastMath.min(seniorCount, adultCount);
    int workingAdultCount = adultCount - seniorCount;
    int workCount = (int) ((double) workingAdultCount / averageWorkSize) + 1;
    int schoolCount = (int) ((double) childCount / averageSchoolSize) + 1;

    // create the population
    for (int i = 0; i < populationSize; i++) {
        int age;
        if (i < seniorCount) {
            age = randomGenerator.nextInt(25) + 65;
        } else if (i < adultCount) {
            age = randomGenerator.nextInt(18) + (65 - 18);
        } else {
            age = randomGenerator.nextInt(18);
        }
        PersonPropertyValueInitialization ageInitialization = new PersonPropertyValueInitialization(
                PersonProperty.AGE, age);

        DiseaseState diseaseState = DiseaseState.IMMUNE;
        if (randomGenerator.nextDouble() < susceptibleProbability) {
            diseaseState = DiseaseState.SUSCEPTIBLE;
        }

        PersonPropertyValueInitialization diseaseInitialization = new PersonPropertyValueInitialization(
                PersonProperty.DISEASE_STATE, diseaseState);
        PersonConstructionData personConstructionData = PersonConstructionData.builder()//
                .add(ageInitialization)//
                .add(diseaseInitialization)//
                .add(regionId)//
                .build();
        peopleDataManager.addPerson(personConstructionData);
    }

In Code Block 10.8, the manager continues by creating the work, home and school groups. It then organizes the people by age group in Code Block 10.9. Finally, it places the people into the appropriate groups in Code Block 10.10.

Code Block 10.8: The home, work and school groups are added to the groups data manager.
// create the home groups
List<GroupId> homeGroupIds = new ArrayList<>();
for (int i = 0; i < homeCount; i++) {
    GroupConstructionInfo groupConstructionInfo = GroupConstructionInfo.builder().setGroupTypeId(GroupType.HOME)
            .build();
    GroupId groupId = groupsDataManager.addGroup(groupConstructionInfo);
    homeGroupIds.add(groupId);
}

// create the work groups
List<GroupId> workGroupIds = new ArrayList<>();
for (int i = 0; i < workCount; i++) {
    GroupConstructionInfo groupConstructionInfo = GroupConstructionInfo.builder().setGroupTypeId(GroupType.WORK)
            .build();
    GroupId groupId = groupsDataManager.addGroup(groupConstructionInfo);
    workGroupIds.add(groupId);
}

// create the school groups
List<GroupId> schoolGroupIds = new ArrayList<>();
for (int i = 0; i < schoolCount; i++) {
    GroupConstructionInfo groupConstructionInfo = GroupConstructionInfo.builder()
            .setGroupTypeId(GroupType.SCHOOL).build();
    GroupId groupId = groupsDataManager.addGroup(groupConstructionInfo);
    schoolGroupIds.add(groupId);
}
Code Block 10.9: The people are separated into age related lists.
List<PersonId> peopleInRegion = regionsDataManager.getPeopleInRegion(regionId);
List<PersonId> adults = new ArrayList<>();
List<PersonId> children = new ArrayList<>();
List<PersonId> workingAdults = new ArrayList<>();
for (PersonId personId : peopleInRegion) {
    int age = personPropertiesDataManager.getPersonPropertyValue(personId, PersonProperty.AGE);
    if (age < 18) {
        children.add(personId);
    } else {
        adults.add(personId);
        if (age < 65) {
            workingAdults.add(personId);
        }
    }
}
Code Block 10.10: People are assigned to homes, work places and schools.
Random random = new Random(randomGenerator.nextLong());
/*
         * Randomize the adults and assign them to the home groups such that there is at
         * least one adult in each home
         */
Collections.shuffle(adults, random);
// put one adult in each home
for (int i = 0; i < homeGroupIds.size(); i++) {
    PersonId personId = adults.get(i);
    GroupId groupId = homeGroupIds.get(i);
    groupsDataManager.addPersonToGroup(personId, groupId);
}

// assign the remaining adults at random to homes
for (int i = homeGroupIds.size(); i < adults.size(); i++) {
    PersonId personId = adults.get(i);
    GroupId groupId = homeGroupIds.get(randomGenerator.nextInt(homeGroupIds.size()));
    groupsDataManager.addPersonToGroup(personId, groupId);
}

// assign working age adults to work groups
for (int i = 0; i < workingAdults.size(); i++) {
    PersonId personId = workingAdults.get(i);
    GroupId groupId = workGroupIds.get(randomGenerator.nextInt(workGroupIds.size()));
    groupsDataManager.addPersonToGroup(personId, groupId);
}

// assign children to school groups
for (int i = 0; i < children.size(); i++) {
    PersonId personId = children.get(i);
    GroupId groupId = schoolGroupIds.get(randomGenerator.nextInt(schoolGroupIds.size()));
    groupsDataManager.addPersonToGroup(personId, groupId);
}

// assign children to home groups
for (int i = 0; i < children.size(); i++) {
    PersonId personId = children.get(i);
    GroupId groupId = homeGroupIds.get(randomGenerator.nextInt(homeGroupIds.size()));
    groupsDataManager.addPersonToGroup(personId, groupId);
}
    }

10.9.2 Telework manager

The TeleworkManager actor, in Code Block 10.11, schedules a weekly review of school disease mitigation strategies. Note that the scheduling is through a passive plan, which indicates to the simulation that this plan should only be executed if there are active plans still remaining. Passive plans are generally used when there is a continuing task that is not driven directly by events. When active plans cease and only passive plans remain, the simulation halts since the passive plans are not reason enough to continue time flow.

Code Block 10.11: The telework manager initializes by scheduling a telework status review for seven days from the start of the simulation.
public void init(ActorContext actorContext) {
    this.actorContext = actorContext;
    scheduleNextReview();
}

private void scheduleNextReview() {
    double planTime = actorContext.getTime() + reviewInterval;
    ActorPlan plan = new ConsumerActorPlan(planTime,false,this::reviewTeleworkStatus);
    actorContext.addPlan(plan);     
}

The review process (Code Block 10.12) continues until a disease threshold has been reached. At that point, the telework manager randomly determines which work groups will be in telework mode. Note that there is no returning from this mode and there is no further evolution of the telework mode even after the disease wanes.

Code Block 10.12: The telework manager schedules a review every seven days until a threshold of infections is reached. Once the threshold is achieved, work places are randomly selected to use telework until the end of the simulation.
private void reviewTeleworkStatus(ActorContext actorContext) {
    StochasticsDataManager stochasticsDataManager = actorContext.getDataManager(StochasticsDataManager.class);
    RandomGenerator randomGenerator = stochasticsDataManager.getRandomGenerator();
    PeopleDataManager peopleDataManager = actorContext.getDataManager(PeopleDataManager.class);
    PersonPropertiesDataManager personPropertiesDataManager = actorContext
            .getDataManager(PersonPropertiesDataManager.class);
    GroupsDataManager groupsDataManager = actorContext.getDataManager(GroupsDataManager.class);
    GlobalPropertiesDataManager globalPropertiesDataManager = actorContext
            .getDataManager(GlobalPropertiesDataManager.class);
    double threshold = globalPropertiesDataManager
            .getGlobalPropertyValue(GlobalProperty.TELEWORK_INFECTION_THRESHOLD);
    double teleworkProbability = globalPropertiesDataManager
            .getGlobalPropertyValue(GlobalProperty.TELEWORK_PROBABILITY);

    int infectiousCount = personPropertiesDataManager.getPersonCountForPropertyValue(PersonProperty.DISEASE_STATE,
            DiseaseState.INFECTIOUS);
    int populationCount = peopleDataManager.getPopulationCount();

    double infectiousFraction = infectiousCount;
    infectiousFraction /= populationCount;

    if (infectiousFraction >= threshold) {
        List<GroupId> workGroupIds = groupsDataManager.getGroupsForGroupType(GroupType.WORK);
        for (GroupId groupId : workGroupIds) {
            if (randomGenerator.nextDouble() < teleworkProbability) {
                groupsDataManager.setGroupPropertyValue(groupId, GroupProperty.TELEWORK, true);
            }
        }
    } else {
        scheduleNextReview();
    }
}

10.9.3 School manager

The SchoolManager actor (Code Block 10.13) initializes by establishing various values derived from fixed global properties and then scheduling a weekly review of all schools.

Code Block 10.13: The school manager initializes by establishing some property constants and planning school status review for seven days after the simulation starts.
public void init(ActorContext actorContext) {
    this.actorContext = actorContext;
    personPropertiesDataManager = actorContext.getDataManager(PersonPropertiesDataManager.class);
    groupsDataManager = actorContext.getDataManager(GroupsDataManager.class);
    GlobalPropertiesDataManager globalPropertiesDataManager = actorContext
            .getDataManager(GlobalPropertiesDataManager.class);
    cohortThreshold = globalPropertiesDataManager
            .getGlobalPropertyValue(GlobalProperty.SCHOOL_COHORT_INFECTION_THRESHOLD);
    closureThreshold = globalPropertiesDataManager
            .getGlobalPropertyValue(GlobalProperty.SCHOOL_CLOSURE_INFECTION_THRESHOLD);
    planNextReview();
}

private void planNextReview() {
    double planTime = actorContext.getTime() + reviewInterval;
    ActorPlan plan = new ConsumerActorPlan(planTime,false,this::reviewSchools);             
    actorContext.addPlan(plan);
}

private void reviewSchools(ActorContext actorContext) {
    List<GroupId> schoolGroupIds = groupsDataManager.getGroupsForGroupType(GroupType.SCHOOL);
    for (GroupId groupId : schoolGroupIds) {
        reviewSchool(groupId);
    }
    planNextReview();
}

The review of each school group is shown in Code Block 10.14. The fraction of the school’s students who are infectious is calculated. Depending on the current state of the group (OPEN, COHORT, CLOSED) the group may move to the next state. If the group moves from OPEN to COHORT (Code Block 10.15), a new group is formed and half of the students are moved into the new group. Both groups are then marked as COHORT groups. If the group moves from COHORT to CLOSED (Code Block 10.16), all students are removed from the group.

Code Block 10.14: Each school is reviewed on a weekly basis. As the fraction of students who are infected increases, the school transitions from OPEN to COHORT to CLOSED.
private void reviewSchool(GroupId groupId) {

    int infectiousCount = 0;
    List<PersonId> peopleForGroup = groupsDataManager.getPeopleForGroup(groupId);
    for (PersonId personId : peopleForGroup) {
        DiseaseState diseaseState = personPropertiesDataManager.getPersonPropertyValue(personId,
                PersonProperty.DISEASE_STATE);
        if (diseaseState == DiseaseState.INFECTIOUS) {
            infectiousCount++;
        }
    }

    double infectiousFraction = infectiousCount;
    if (!peopleForGroup.isEmpty()) {
        infectiousFraction /= peopleForGroup.size();
    }

    SchoolStatus schoolStatus = groupsDataManager.getGroupPropertyValue(groupId, GroupProperty.SCHOOL_STATUS);

    switch (schoolStatus) {
    case OPEN:
        if (infectiousFraction >= cohortThreshold) {
            splitSchoolIntoCohorts(groupId);
        }
        break;
    case COHORT:
        if (infectiousFraction >= closureThreshold) {
            closeSchool(groupId);
        }
        break;
    case CLOSED:
        // do nothing
        break;
    default:
        throw new RuntimeException("unhandled case " + schoolStatus);
    }
}
Code Block 10.15: When a school moves to COHORT status, a new group is added to the simulation and half of the students move to this new group.
private void splitSchoolIntoCohorts(GroupId groupId) {
    GroupConstructionInfo groupConstructionInfo = GroupConstructionInfo.builder().setGroupTypeId(GroupType.SCHOOL)
            .build();
    GroupId newGroupId = groupsDataManager.addGroup(groupConstructionInfo);

    List<PersonId> peopleForGroup = groupsDataManager.getPeopleForGroup(groupId);
    for (int i = 0; i < peopleForGroup.size(); i++) {
        if (i % 2 == 0) {
            PersonId personId = peopleForGroup.get(i);
            groupsDataManager.removePersonFromGroup(personId, groupId);
            groupsDataManager.addPersonToGroup(personId, newGroupId);
        }
    }

    groupsDataManager.setGroupPropertyValue(newGroupId, GroupProperty.SCHOOL_STATUS, SchoolStatus.COHORT);
    groupsDataManager.setGroupPropertyValue(groupId, GroupProperty.SCHOOL_STATUS, SchoolStatus.COHORT);

}
Code Block 10.16: When a school is closed, all the students are removed from the school group so that infection can no longer spread via school-based contact.
private void closeSchool(GroupId groupId) {
    groupsDataManager.setGroupPropertyValue(groupId, GroupProperty.SCHOOL_STATUS, SchoolStatus.CLOSED);
    List<PersonId> people = groupsDataManager.getPeopleForGroup(groupId);
    for (PersonId personId : people) {
        groupsDataManager.removePersonFromGroup(personId, groupId);
    }
}

10.9.4 Infection Manager

The InfectionManager actor, in Code Block 10.17, first determines various values from the fixed global properties that will be used to manage infections. The most important calculation is to determine the length of time between infectious contacts as a function of the global R0 value and the expected number of days a person will be infectious. It then selects a small set of adults to infect during first day of the simulation. Infecting a person (Code Block 10.18) first requires the manager to determine the number of days of the infection for the particular person and then schedule infectious contact times for that person. At the end of the infectious contacts the manager schedules the transition of the person from infectious to recovered.

Code Block 10.17: The infection manager initializes by infecting the initially infected people in the first day.
public void init(ActorContext actorContext) {
    this.actorContext = actorContext;

    StochasticsDataManager stochasticsDataManager = actorContext.getDataManager(StochasticsDataManager.class);
    randomGenerator = stochasticsDataManager.getRandomGenerator();
    Random random = new Random(randomGenerator.nextLong());

    groupsDataManager = actorContext.getDataManager(GroupsDataManager.class);
    GlobalPropertiesDataManager globalPropertiesDataManager = actorContext
            .getDataManager(GlobalPropertiesDataManager.class);
    personPropertiesDataManager = actorContext.getDataManager(PersonPropertiesDataManager.class);
    List<PersonId> susceptiblePeople = personPropertiesDataManager
            .getPeopleWithPropertyValue(PersonProperty.DISEASE_STATE, DiseaseState.SUSCEPTIBLE);
    List<PersonId> susceptibleAdults = new ArrayList<>();
    for (PersonId personId : susceptiblePeople) {
        int age = personPropertiesDataManager.getPersonPropertyValue(personId, PersonProperty.AGE);
        if (age > 18) {
            susceptibleAdults.add(personId);
        }
    }

    Collections.shuffle(susceptibleAdults, random);

    minInfectiousPeriod = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.MIN_INFECTIOUS_PERIOD);
    maxInfectiousPeriod = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.MAX_INFECTIOUS_PERIOD);
    double r0 = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.R0);
    infectionInterval = (double) (minInfectiousPeriod + maxInfectiousPeriod) / (2 * r0);

    int initialInfections = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.INITIAL_INFECTIONS);
    initialInfections = FastMath.min(initialInfections, susceptibleAdults.size());

    for (int i = 0; i < initialInfections; i++) {
        PersonId personId = susceptibleAdults.get(i);
        double planTime = randomGenerator.nextDouble() * 0.5 + 0.25;
        actorContext.addPlan((c) -> infectPerson(personId), planTime);
    }
}
Code Block 10.18: When a person is infected, the number of possible infectious contacts is determined and planned. After the last infectious contact, the person is scheduled to become recovered.
private void infectPerson(PersonId personId) {
    personPropertiesDataManager.setPersonPropertyValue(personId, PersonProperty.DISEASE_STATE,
            DiseaseState.INFECTIOUS);
    int infectiousDays = randomGenerator.nextInt(maxInfectiousPeriod - minInfectiousPeriod) + minInfectiousPeriod;
    int infectionCount = (int) FastMath.round(((double) infectiousDays / infectionInterval));
    double planTime = actorContext.getTime();
    for (int j = 0; j < infectionCount; j++) {
        planTime += infectionInterval;
        actorContext.addPlan((c) -> infectContact(personId), planTime);
    }
    actorContext.addPlan((c) -> endInfectiousness(personId), planTime);
}

An infectious contact , Code Block 10.19, first selects a group at random from the person’s groups. A person to infect is selected from the group, excluding the person who is infecting. If such a person can be found and that person is susceptible then, barring mitigation, the susceptible person becomes infected and is immediately infectious. Cohort schools are mitigated by 50% and closed schools are 100% mitigated. Telework groups are mitigated by 50%.

Code Block 10.19: The infection manager attempts to infect a susceptible person found in a randomly selected group associated with the currently infected person.
private void infectContact(PersonId personId) {
    List<GroupId> groupsForPerson = groupsDataManager.getGroupsForPerson(personId);
    GroupId groupId = groupsForPerson.get(randomGenerator.nextInt(groupsForPerson.size()));

    // work groups doing telework have a 50% contact mitigation
    GroupTypeId groupTypeId = groupsDataManager.getGroupType(groupId);
    if (groupTypeId.equals(GroupType.WORK)) {
        boolean teleworkGroup = groupsDataManager.getGroupPropertyValue(groupId, GroupProperty.TELEWORK);
        if (teleworkGroup) {
            if (randomGenerator.nextBoolean()) {
                return;
            }
        }
    }

    // school groups in COHORT mode have a 50% contact mitigation
    // school groups in CLOSED mode have a 100% contact mitigation
    if (groupTypeId.equals(GroupType.SCHOOL)) {
        SchoolStatus schoolStatus = groupsDataManager.getGroupPropertyValue(groupId, GroupProperty.SCHOOL_STATUS);
        switch (schoolStatus) {
        case COHORT:
            if (randomGenerator.nextBoolean()) {
                return;
            }
            break;
        case CLOSED:
            return;
        default:
            // no mitigation
            break;
        }
    }

    GroupSampler groupSampler = GroupSampler.builder().setExcludedPersonId(personId).build();
    Optional<PersonId> optional = groupsDataManager.sampleGroup(groupId, groupSampler);
    if (optional.isPresent()) {
        PersonId contactedPerson = optional.get();
        DiseaseState diseaseState = personPropertiesDataManager.getPersonPropertyValue(contactedPerson,
                PersonProperty.DISEASE_STATE);
        if (diseaseState == DiseaseState.SUSCEPTIBLE) {
            int infectedCount = personPropertiesDataManager.getPersonPropertyValue(personId,
                    PersonProperty.INFECTED_COUNT);
            infectedCount++;
            personPropertiesDataManager.setPersonPropertyValue(personId, PersonProperty.INFECTED_COUNT,
                    infectedCount);
            infectPerson(contactedPerson);
        }
    }
}

10.10 Inspecting the output

10.10.1 Person Property Report

The 36 scenarios result in a large amount of output in the person property report. It is effectively a trace log of the initial state and changes to each person.

10.10.2 Group Population Report

The group population report shows the distribution of people at the end of the simulation. Since there is no stochastics dimension, each scenario has an identical distribution of people into homes. The report shows all the expected patterns as a response to initial policies. For example, higher school closure infection thresholds drive the ending school populations to less than 1/3 their initial values.

10.10.3 Disease State Report

The disease state report shows the ending counts for each disease state. By analyzing the number of people recovered over the 36 scenarios, we observe that the disease threshold for triggering work places into a telework status is significant. If that threshold is set to 10%, the working population does not telework and the number of recovered is significantly higher that at the lower levels. School cohort and closure policies exhibit a similar pattern.

10.10.4 Contagion Report

The contagion report shows then number of people infected by each infectious person as counts. The report shows that early decisions to close schools and institute telework policies can significantly reduce the spread of the disease.