public static void main(String[] args) throws IOException {
if (args.length == 0) {
throw new RuntimeException("One output directory argument is required");
}
= Paths.get(args[0]);
Path outputDirectory if (!Files.exists(outputDirectory)) {
.createDirectory(outputDirectory);
Files} else {
if (!Files.isDirectory(outputDirectory)) {
throw new IOException("Provided path is not a directory");
}
}
new Example_17(outputDirectory).execute();
}
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.
private void execute() {
= ExperimentParameterData.builder()//
ExperimentParameterData experimentParameterData .setThreadCount(8)//
.build();
.builder()
Experiment
.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.
private Plugin getGlobalPropertiesPlugin() {
.Builder builder = GlobalPropertiesPluginData.builder();//
GlobalPropertiesPluginData
= PropertyDefinition.builder()//
PropertyDefinition propertyDefinition .setType(Double.class)//
.setPropertyValueMutability(false)//
.setDefaultValue(0.0).build();
.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);
builder
= PropertyDefinition.builder()//
propertyDefinition .setType(Integer.class)//
.setPropertyValueMutability(false)//
.build();
.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);
builder
= builder.build();
GlobalPropertiesPluginData globalPropertiesPluginData
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
private Plugin getPersonPropertiesPlugin() {
.Builder builder = PersonPropertiesPluginData.builder();
PersonPropertiesPluginData
= PropertyDefinition.builder()//
PropertyDefinition propertyDefinition .setType(Integer.class)//
.build();
.definePersonProperty(PersonProperty.AGE, propertyDefinition, 0, false);//
builder
= PropertyDefinition.builder()//
propertyDefinition .setType(DiseaseState.class)//
.setDefaultValue(DiseaseState.SUSCEPTIBLE).build();
.definePersonProperty(PersonProperty.DISEASE_STATE, propertyDefinition, 0, false);//
builder
= PropertyDefinition.builder()//
propertyDefinition .setType(Integer.class)//
.setDefaultValue(0).build();
.definePersonProperty(PersonProperty.INFECTED_COUNT, propertyDefinition, 0, false);//
builder
= builder.build();
PersonPropertiesPluginData personPropertiesPluginData
= PersonPropertyReportPluginData.builder()//
PersonPropertyReportPluginData personPropertyReportPluginData .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%
private Plugin getGroupsPlugin() {
.Builder builder = GroupsPluginData.builder();
GroupsPluginDatafor (GroupType groupType : GroupType.values()) {
.addGroupTypeId(groupType);
builder}
= PropertyDefinition.builder()//
PropertyDefinition propertyDefinition .setType(Boolean.class)//
.setDefaultValue(false)//
.build();
.defineGroupProperty(GroupType.WORK, GroupProperty.TELEWORK, propertyDefinition);
builder
= PropertyDefinition.builder()//
propertyDefinition .setType(SchoolStatus.class)//
.setDefaultValue(SchoolStatus.OPEN)//
.build();
.defineGroupProperty(GroupType.SCHOOL, GroupProperty.SCHOOL_STATUS, propertyDefinition);
builder
= builder.build();
GroupsPluginData groupsPluginData
= //
GroupPopulationReportPluginData groupPopulationReportPluginData .builder()//
GroupPopulationReportPluginData.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.
public void init(ActorContext actorContext) {
= actorContext.getDataManager(PersonPropertiesDataManager.class);
personPropertiesDataManager = actorContext.getDataManager(GroupsDataManager.class);
groupsDataManager = actorContext.getDataManager(StochasticsDataManager.class);
StochasticsDataManager stochasticsDataManager = stochasticsDataManager.getRandomGenerator();
randomGenerator = actorContext.getDataManager(PeopleDataManager.class);
peopleDataManager = actorContext
GlobalPropertiesDataManager globalPropertiesDataManager .getDataManager(GlobalPropertiesDataManager.class);
= actorContext.getDataManager(RegionsDataManager.class);
regionsDataManager
int populationSize = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.POPULATION_SIZE);
= globalPropertiesDataManager
susceptibleProbability .getGlobalPropertyValue(GlobalProperty.SUSCEPTIBLE_POPULATION_PROPORTION);
= globalPropertiesDataManager
childPopulationProportion .getGlobalPropertyValue(GlobalProperty.CHILD_POPULATION_PROPORTION);
= globalPropertiesDataManager
seniorPopulationProportion .getGlobalPropertyValue(GlobalProperty.SENIOR_POPULATION_PROPORTION);
= globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.AVERAGE_HOME_SIZE);
averageHomeSize = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.AVERAGE_SCHOOL_SIZE);
averageSchoolSize = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.AVERAGE_WORK_SIZE);
averageWorkSize
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.
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;
= FastMath.min(homeCount, adultCount);
homeCount int seniorCount = (int) (n * seniorPopulationProportion);
= FastMath.min(seniorCount, adultCount);
seniorCount 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) {
= randomGenerator.nextInt(25) + 65;
age } else if (i < adultCount) {
= randomGenerator.nextInt(18) + (65 - 18);
age } else {
= randomGenerator.nextInt(18);
age }
= new PersonPropertyValueInitialization(
PersonPropertyValueInitialization ageInitialization .AGE, age);
PersonProperty
= DiseaseState.IMMUNE;
DiseaseState diseaseState if (randomGenerator.nextDouble() < susceptibleProbability) {
= DiseaseState.SUSCEPTIBLE;
diseaseState }
= new PersonPropertyValueInitialization(
PersonPropertyValueInitialization diseaseInitialization .DISEASE_STATE, diseaseState);
PersonProperty= PersonConstructionData.builder()//
PersonConstructionData personConstructionData .add(ageInitialization)//
.add(diseaseInitialization)//
.add(regionId)//
.build();
.addPerson(personConstructionData);
peopleDataManager}
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.
// create the home groups
List<GroupId> homeGroupIds = new ArrayList<>();
for (int i = 0; i < homeCount; i++) {
= GroupConstructionInfo.builder().setGroupTypeId(GroupType.HOME)
GroupConstructionInfo groupConstructionInfo .build();
= groupsDataManager.addGroup(groupConstructionInfo);
GroupId groupId .add(groupId);
homeGroupIds}
// create the work groups
List<GroupId> workGroupIds = new ArrayList<>();
for (int i = 0; i < workCount; i++) {
= GroupConstructionInfo.builder().setGroupTypeId(GroupType.WORK)
GroupConstructionInfo groupConstructionInfo .build();
= groupsDataManager.addGroup(groupConstructionInfo);
GroupId groupId .add(groupId);
workGroupIds}
// create the school groups
List<GroupId> schoolGroupIds = new ArrayList<>();
for (int i = 0; i < schoolCount; i++) {
= GroupConstructionInfo.builder()
GroupConstructionInfo groupConstructionInfo .setGroupTypeId(GroupType.SCHOOL).build();
= groupsDataManager.addGroup(groupConstructionInfo);
GroupId groupId .add(groupId);
schoolGroupIds}
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) {
.add(personId);
children} else {
.add(personId);
adultsif (age < 65) {
.add(personId);
workingAdults}
}
}
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++) {
= adults.get(i);
PersonId personId = homeGroupIds.get(i);
GroupId groupId .addPersonToGroup(personId, groupId);
groupsDataManager}
// assign the remaining adults at random to homes
for (int i = homeGroupIds.size(); i < adults.size(); i++) {
= adults.get(i);
PersonId personId = homeGroupIds.get(randomGenerator.nextInt(homeGroupIds.size()));
GroupId groupId .addPersonToGroup(personId, groupId);
groupsDataManager}
// assign working age adults to work groups
for (int i = 0; i < workingAdults.size(); i++) {
= workingAdults.get(i);
PersonId personId = workGroupIds.get(randomGenerator.nextInt(workGroupIds.size()));
GroupId groupId .addPersonToGroup(personId, groupId);
groupsDataManager}
// assign children to school groups
for (int i = 0; i < children.size(); i++) {
= children.get(i);
PersonId personId = schoolGroupIds.get(randomGenerator.nextInt(schoolGroupIds.size()));
GroupId groupId .addPersonToGroup(personId, groupId);
groupsDataManager}
// assign children to home groups
for (int i = 0; i < children.size(); i++) {
= children.get(i);
PersonId personId = homeGroupIds.get(randomGenerator.nextInt(homeGroupIds.size()));
GroupId groupId .addPersonToGroup(personId, groupId);
groupsDataManager}
}
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.
public void init(ActorContext actorContext) {
this.actorContext = actorContext;
scheduleNextReview();
}
private void scheduleNextReview() {
double planTime = actorContext.getTime() + reviewInterval;
= new ConsumerActorPlan(planTime,false,this::reviewTeleworkStatus);
ActorPlan plan .addPlan(plan);
actorContext}
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.
private void reviewTeleworkStatus(ActorContext actorContext) {
= actorContext.getDataManager(StochasticsDataManager.class);
StochasticsDataManager stochasticsDataManager = stochasticsDataManager.getRandomGenerator();
RandomGenerator randomGenerator = actorContext.getDataManager(PeopleDataManager.class);
PeopleDataManager peopleDataManager = actorContext
PersonPropertiesDataManager personPropertiesDataManager .getDataManager(PersonPropertiesDataManager.class);
= actorContext.getDataManager(GroupsDataManager.class);
GroupsDataManager groupsDataManager = actorContext
GlobalPropertiesDataManager globalPropertiesDataManager .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,
.INFECTIOUS);
DiseaseStateint populationCount = peopleDataManager.getPopulationCount();
double infectiousFraction = infectiousCount;
/= populationCount;
infectiousFraction
if (infectiousFraction >= threshold) {
List<GroupId> workGroupIds = groupsDataManager.getGroupsForGroupType(GroupType.WORK);
for (GroupId groupId : workGroupIds) {
if (randomGenerator.nextDouble() < teleworkProbability) {
.setGroupPropertyValue(groupId, GroupProperty.TELEWORK, true);
groupsDataManager}
}
} 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.
public void init(ActorContext actorContext) {
this.actorContext = actorContext;
= actorContext.getDataManager(PersonPropertiesDataManager.class);
personPropertiesDataManager = actorContext.getDataManager(GroupsDataManager.class);
groupsDataManager = actorContext
GlobalPropertiesDataManager globalPropertiesDataManager .getDataManager(GlobalPropertiesDataManager.class);
= globalPropertiesDataManager
cohortThreshold .getGlobalPropertyValue(GlobalProperty.SCHOOL_COHORT_INFECTION_THRESHOLD);
= globalPropertiesDataManager
closureThreshold .getGlobalPropertyValue(GlobalProperty.SCHOOL_CLOSURE_INFECTION_THRESHOLD);
planNextReview();
}
private void planNextReview() {
double planTime = actorContext.getTime() + reviewInterval;
= new ConsumerActorPlan(planTime,false,this::reviewSchools);
ActorPlan plan .addPlan(plan);
actorContext}
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.
private void reviewSchool(GroupId groupId) {
int infectiousCount = 0;
List<PersonId> peopleForGroup = groupsDataManager.getPeopleForGroup(groupId);
for (PersonId personId : peopleForGroup) {
= personPropertiesDataManager.getPersonPropertyValue(personId,
DiseaseState diseaseState .DISEASE_STATE);
PersonPropertyif (diseaseState == DiseaseState.INFECTIOUS) {
++;
infectiousCount}
}
double infectiousFraction = infectiousCount;
if (!peopleForGroup.isEmpty()) {
/= peopleForGroup.size();
infectiousFraction }
= groupsDataManager.getGroupPropertyValue(groupId, GroupProperty.SCHOOL_STATUS);
SchoolStatus schoolStatus
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);
}
}
private void splitSchoolIntoCohorts(GroupId groupId) {
= GroupConstructionInfo.builder().setGroupTypeId(GroupType.SCHOOL)
GroupConstructionInfo groupConstructionInfo .build();
= groupsDataManager.addGroup(groupConstructionInfo);
GroupId newGroupId
List<PersonId> peopleForGroup = groupsDataManager.getPeopleForGroup(groupId);
for (int i = 0; i < peopleForGroup.size(); i++) {
if (i % 2 == 0) {
= peopleForGroup.get(i);
PersonId personId .removePersonFromGroup(personId, groupId);
groupsDataManager.addPersonToGroup(personId, newGroupId);
groupsDataManager}
}
.setGroupPropertyValue(newGroupId, GroupProperty.SCHOOL_STATUS, SchoolStatus.COHORT);
groupsDataManager.setGroupPropertyValue(groupId, GroupProperty.SCHOOL_STATUS, SchoolStatus.COHORT);
groupsDataManager
}
private void closeSchool(GroupId groupId) {
.setGroupPropertyValue(groupId, GroupProperty.SCHOOL_STATUS, SchoolStatus.CLOSED);
groupsDataManagerList<PersonId> people = groupsDataManager.getPeopleForGroup(groupId);
for (PersonId personId : people) {
.removePersonFromGroup(personId, groupId);
groupsDataManager}
}
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.
public void init(ActorContext actorContext) {
this.actorContext = actorContext;
= actorContext.getDataManager(StochasticsDataManager.class);
StochasticsDataManager stochasticsDataManager = stochasticsDataManager.getRandomGenerator();
randomGenerator Random random = new Random(randomGenerator.nextLong());
= actorContext.getDataManager(GroupsDataManager.class);
groupsDataManager = actorContext
GlobalPropertiesDataManager globalPropertiesDataManager .getDataManager(GlobalPropertiesDataManager.class);
= actorContext.getDataManager(PersonPropertiesDataManager.class);
personPropertiesDataManager 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) {
.add(personId);
susceptibleAdults}
}
Collections.shuffle(susceptibleAdults, random);
= globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.MIN_INFECTIOUS_PERIOD);
minInfectiousPeriod = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.MAX_INFECTIOUS_PERIOD);
maxInfectiousPeriod double r0 = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.R0);
= (double) (minInfectiousPeriod + maxInfectiousPeriod) / (2 * r0);
infectionInterval
int initialInfections = globalPropertiesDataManager.getGlobalPropertyValue(GlobalProperty.INITIAL_INFECTIONS);
= FastMath.min(initialInfections, susceptibleAdults.size());
initialInfections
for (int i = 0; i < initialInfections; i++) {
= susceptibleAdults.get(i);
PersonId personId double planTime = randomGenerator.nextDouble() * 0.5 + 0.25;
.addPlan((c) -> infectPerson(personId), planTime);
actorContext}
}
private void infectPerson(PersonId personId) {
.setPersonPropertyValue(personId, PersonProperty.DISEASE_STATE,
personPropertiesDataManager.INFECTIOUS);
DiseaseStateint 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++) {
+= infectionInterval;
planTime .addPlan((c) -> infectContact(personId), planTime);
actorContext}
.addPlan((c) -> endInfectiousness(personId), planTime);
actorContext}
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%.
private void infectContact(PersonId personId) {
List<GroupId> groupsForPerson = groupsDataManager.getGroupsForPerson(personId);
= groupsForPerson.get(randomGenerator.nextInt(groupsForPerson.size()));
GroupId groupId
// work groups doing telework have a 50% contact mitigation
= groupsDataManager.getGroupType(groupId);
GroupTypeId groupTypeId 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)) {
= groupsDataManager.getGroupPropertyValue(groupId, GroupProperty.SCHOOL_STATUS);
SchoolStatus schoolStatus switch (schoolStatus) {
case COHORT:
if (randomGenerator.nextBoolean()) {
return;
}
break;
case CLOSED:
return;
default:
// no mitigation
break;
}
}
= GroupSampler.builder().setExcludedPersonId(personId).build();
GroupSampler groupSampler <PersonId> optional = groupsDataManager.sampleGroup(groupId, groupSampler);
Optionalif (optional.isPresent()) {
= optional.get();
PersonId contactedPerson = personPropertiesDataManager.getPersonPropertyValue(contactedPerson,
DiseaseState diseaseState .DISEASE_STATE);
PersonPropertyif (diseaseState == DiseaseState.SUSCEPTIBLE) {
int infectedCount = personPropertiesDataManager.getPersonPropertyValue(personId,
.INFECTED_COUNT);
PersonProperty++;
infectedCount.setPersonPropertyValue(personId, PersonProperty.INFECTED_COUNT,
personPropertiesDataManager);
infectedCountinfectPerson(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.