DefaultContactModel.java
/*-
* #%L
* MATSim Episim
* %%
* Copyright (C) 2020 matsim-org
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.matsim.episim.model;
import com.google.inject.Inject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.matsim.api.core.v01.Scenario;
import org.matsim.core.config.Config;
import org.matsim.core.config.ConfigUtils;
import org.matsim.episim.*;
import java.util.ArrayList;
import java.util.List;
import java.util.SplittableRandom;
import static org.matsim.episim.EpisimPerson.DiseaseStatus;
/**
* Default contact model executed, when a person ends his activity.
* Infections probabilities calculations are delegated to a {@link InfectionModel}.
*/
public final class DefaultContactModel extends AbstractContactModel {
private static final Logger log = LogManager.getLogger(DefaultContactModel.class);
/**
* Flag to enable tracking, which is considerably slower.
*/
private final int trackingAfterDay;
/**
* In order to avoid recreating a the list of other persons in the container every time it is stored as instance variable.
*/
List<EpisimPerson> otherPersonsInContainer = new ArrayList<>();
/**
* This buffer is used to store the infection type.
*/
private final StringBuilder buffer = new StringBuilder();
@Inject
/* package */
DefaultContactModel(SplittableRandom rnd, Config config,
EpisimReporting reporting, InfectionModel infectionModel, Scenario scenario) {
// (make injected constructor non-public so that arguments can be changed without repercussions. kai, jun'20)
super(rnd, config, infectionModel, reporting, scenario);
this.trackingAfterDay = ConfigUtils.addOrGetModule(config, TracingConfigGroup.class).getPutTraceablePersonsInQuarantineAfterDay();
}
DefaultContactModel(SplittableRandom rnd, Config config,
EpisimReporting reporting, InfectionModel infectionModel) {
this(rnd, config, reporting, infectionModel, null);
}
@Override
public void infectionDynamicsVehicle(EpisimPerson personLeavingVehicle, InfectionEventHandler.EpisimVehicle vehicle, double now) {
infectionDynamicsGeneralized(personLeavingVehicle, vehicle, now);
}
@Override
public void infectionDynamicsFacility(EpisimPerson personLeavingFacility, InfectionEventHandler.EpisimFacility facility, double now) {
infectionDynamicsGeneralized(personLeavingFacility, facility, now);
}
private void infectionDynamicsGeneralized(EpisimPerson personLeavingContainer, EpisimContainer<?> container, double now) {
// no infection possible if there is only one person
if (iteration == 0 || container.getPersons().size() == 1) {
return;
}
if (!personRelevantForTrackingOrInfectionDynamics(now, personLeavingContainer, container, getRestrictions(), rnd)) {
return;
}
// start tracking late as possible because of computational costs
boolean trackingEnabled = iteration >= trackingAfterDay;
otherPersonsInContainer.addAll(container.getPersons());
otherPersonsInContainer.remove(personLeavingContainer);
// For the time being, will just assume that the first 10 persons are the ones we interact with. Note that because of
// shuffle, those are 10 different persons every day.
// persons are scaled to number of agents with sample size, but at least 3 for the small development scenarios
// int contactWith = Math.min(otherPersonsInContainer.size(), Math.max((int) (episimConfig.getSampleSize() * 10), 3));
int contactWith = Math.min(otherPersonsInContainer.size(), (int)episimConfig.getMaxContacts());
for (int ii = 0; ii < contactWith; ii++) {
// we are essentially looking at the situation when the person leaves the container. Interactions with other persons who have
// already left the container were treated then. In consequence, we have some "circle of persons around us" (yyyy which should
// depend on the density), and then a probability of infection in either direction.
// Draw the contact person and remove it -> we don't want to draw it multiple times
EpisimPerson contactPerson = otherPersonsInContainer.remove(rnd.nextInt(otherPersonsInContainer.size()));
if (!personRelevantForTrackingOrInfectionDynamics(now, contactPerson, container, getRestrictions(), rnd)) {
continue;
}
// we have thrown the random numbers, so we can bail out in some cases if we are not tracking:
if (!trackingEnabled) {
if (personLeavingContainer.getDiseaseStatus() == DiseaseStatus.infectedButNotContagious) {
continue;
}
if (contactPerson.getDiseaseStatus() == DiseaseStatus.infectedButNotContagious) {
continue;
}
if (personLeavingContainer.getDiseaseStatus() == contactPerson.getDiseaseStatus()) {
continue;
}
}
// activity params of the contact person and leaving person
EpisimConfigGroup.InfectionParams leavingParams = getInfectionParams(container, personLeavingContainer, container.getPerformedActivity(personLeavingContainer.getPersonId()));
EpisimConfigGroup.InfectionParams contactParams = getInfectionParams(container, contactPerson, container.getPerformedActivity(contactPerson.getPersonId()));
String leavingPersonsActivity = leavingParams == qhParams ? "home" : leavingParams.getContainerName();
String otherPersonsActivity = contactParams == qhParams ? "home" : contactParams.getContainerName();
StringBuilder infectionType = getInfectionType(buffer, container, leavingPersonsActivity, otherPersonsActivity);
double containerEnterTimeOfPersonLeaving = container.getContainerEnteringTime(personLeavingContainer.getPersonId());
double containerEnterTimeOfOtherPerson = container.getContainerEnteringTime(contactPerson.getPersonId());
double jointTimeInContainer = calculateJointTimeInContainer(now, leavingParams, containerEnterTimeOfPersonLeaving, containerEnterTimeOfOtherPerson);
//forbid certain cross-activity interactions, keep track of contacts
if (container instanceof InfectionEventHandler.EpisimFacility) {
//home can only interact with home, leisure or work
if (infectionType.indexOf("home") >= 0 && infectionType.indexOf("leis") == -1 && infectionType.indexOf("work") == -1
&& !(leavingPersonsActivity.startsWith("home") && otherPersonsActivity.startsWith("home"))) {
continue;
} else if (infectionType.indexOf("edu") >= 0 && infectionType.indexOf("work") == -1 && !(leavingPersonsActivity.startsWith("edu") && otherPersonsActivity.startsWith("edu"))) {
//edu can only interact with work or edu
continue;
}
if (trackingEnabled) {
trackContactPerson(personLeavingContainer, contactPerson, now, jointTimeInContainer, infectionType);
}
// Only a subset of contacts are reported at the moment
// tracking has to be enabled to report more contacts
reporting.reportContact(now, personLeavingContainer, contactPerson, container, infectionType, jointTimeInContainer);
}
if (!AbstractContactModel.personsCanInfectEachOther(personLeavingContainer, contactPerson)) {
continue;
}
// person can only infect others 4 days after being contagious
if ((personLeavingContainer.hadDiseaseStatus(DiseaseStatus.contagious) &&
personLeavingContainer.daysSince(DiseaseStatus.contagious, iteration) > episimConfig.getDaysInfectious())
|| (contactPerson.hadDiseaseStatus(DiseaseStatus.contagious) &&
contactPerson.daysSince(DiseaseStatus.contagious, iteration) > episimConfig.getDaysInfectious()))
continue;
// persons leaving their first-ever activity have no starting time for that activity. Need to hedge against that. Since all persons
// start healthy (the first seeds are set at enterVehicle), we can make some assumptions.
if (containerEnterTimeOfPersonLeaving < 0 && containerEnterTimeOfOtherPerson < 0) {
throw new IllegalStateException("should not happen");
// should only happen at first activity. However, at first activity all persons are susceptible. So the only way we
// can get here is if an infected person entered the container and is now leaving again, while the other person has been in the
// container from the beginning. ???? kai, mar'20
}
if (jointTimeInContainer < 0 || jointTimeInContainer > 86400 * 7) {
log.warn(containerEnterTimeOfPersonLeaving);
log.warn(containerEnterTimeOfOtherPerson);
log.warn(now);
throw new IllegalStateException("joint time in container is not plausible for personLeavingContainer=" + personLeavingContainer.getPersonId() + " and contactPerson=" + contactPerson.getPersonId() + ". Joint time is=" + jointTimeInContainer);
}
double contactIntensity = Math.min(leavingParams.getContactIntensity(), contactParams.getContactIntensity());
// need to differentiate which person might be the infector
if (personLeavingContainer.getDiseaseStatus() == DiseaseStatus.susceptible) {
double prob = infectionModel.calcInfectionProbability(personLeavingContainer, contactPerson, getRestrictions(),
leavingParams, contactParams, contactIntensity, jointTimeInContainer);
if (rnd.nextDouble() < prob)
infectPerson(personLeavingContainer, contactPerson, now, infectionType, prob, container);
} else {
double prob = infectionModel.calcInfectionProbability(contactPerson, personLeavingContainer, getRestrictions(),
contactParams, leavingParams, contactIntensity, jointTimeInContainer);
if (rnd.nextDouble() < prob)
infectPerson(contactPerson, personLeavingContainer, now, infectionType, prob, container);
}
}
// Clear cached container
otherPersonsInContainer.clear();
}
}