AgeAndProgressionDependentInfectionModelWithSeasonality.java
package org.matsim.episim.model;
import com.google.inject.Inject;
import org.apache.commons.math3.distribution.NormalDistribution;
import org.apache.commons.math3.distribution.RealDistribution;
import org.matsim.core.config.Config;
import org.matsim.core.config.ConfigUtils;
import org.matsim.episim.*;
import org.matsim.episim.policy.Restriction;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import org.matsim.episim.util.EpisimSplittableRandom;
import static org.matsim.episim.model.DefaultInfectionModel.*;
/**
* Extension of the {@link DefaultInfectionModel}, with age, time and seasonality-dependen additions.
*/
public final class AgeAndProgressionDependentInfectionModelWithSeasonality implements InfectionModel {
private final FaceMaskModel maskModel;
private final ProgressionModel progression;
private final EpisimConfigGroup episimConfig;
private final EpisimReporting reporting;
private final EpisimSplittableRandom rnd;
private final VaccinationConfigGroup vaccinationConfig;
private final VirusStrainConfigGroup virusStrainConfig;
private final PathogenConfigGroup pathogenConfig;
private final Map<VirusStrain, double[]> susceptibility;// = new EnumMap<>(VirusStrain.class);
private final Map<VirusStrain, double[]> infectivity;// = new EnumMap<>(VirusStrain.class);
private final RealDistribution distribution;
/**
* Scale infectivity to 1.0.
*/
private final double scale;
private double outdoorFactor;
private int iteration;
private final ImmunityModel immunityModel;
private double lastUnVac;
@Inject
AgeAndProgressionDependentInfectionModelWithSeasonality(FaceMaskModel faceMaskModel, ProgressionModel progression,
ImmunityModel immunityModel, Config config,
EpisimReporting reporting, EpisimSplittableRandom rnd) {
this.maskModel = faceMaskModel;
this.immunityModel = immunityModel;
this.progression = progression;
this.episimConfig = ConfigUtils.addOrGetModule(config, EpisimConfigGroup.class);
this.vaccinationConfig = ConfigUtils.addOrGetModule(config, VaccinationConfigGroup.class);
this.virusStrainConfig = ConfigUtils.addOrGetModule(config, VirusStrainConfigGroup.class);
this.pathogenConfig = ConfigUtils.addOrGetModule(config, PathogenConfigGroup.class);
this.reporting = reporting;
this.rnd = rnd;
this.susceptibility = new HashMap<>();
this.infectivity = new HashMap<>();
AgeDependentInfectionModelWithSeasonality.preComputeAgeDependency(susceptibility, infectivity, virusStrainConfig, virusStrainConfig.getVirusStrains());
// based on https://arxiv.org/abs/2007.06602
distribution = new NormalDistribution(0.5, 2.6);
scale = 1 / distribution.density(distribution.getNumericalMean());
}
@Override
public void setIteration(int iteration) {
this.outdoorFactor = InfectionModelWithSeasonality.interpolateOutdoorFraction(episimConfig, iteration);
this.iteration = iteration;
reporting.reportOutdoorFraction(this.outdoorFactor, iteration);
}
@Override
public double getLastUnVacInfectionProbability() {
return lastUnVac;
}
@Override
public double calcInfectionProbability(EpisimPerson target, EpisimPerson infector, Map<String, Restriction> restrictions,
EpisimConfigGroup.InfectionParams act1, EpisimConfigGroup.InfectionParams act2,
TransmissionWeights transmissionWeights,
double contactIntensity, double jointTimeInContainer) {
//noinspection ConstantConditions // ci corr can not be null, because sim is initialized with non null value
double ciCorrection = Math.min(restrictions.get(act1.getContainerName()).getCiCorrection(), restrictions.get(act2.getContainerName()).getCiCorrection());
double susceptibility = this.susceptibility.get(infector.getVirusStrain())[target.getAge()];
double infectivity = this.infectivity.get(infector.getVirusStrain())[infector.getAge()];
// apply reduced susceptibility of vaccinated persons
VirusStrainConfigGroup.StrainParams strain = virusStrainConfig.getParams(infector.getVirusStrain());
susceptibility *= immunityModel.getFactor(target, strain.getStrain(),
EpisimPerson.DiseaseStatus.infectedButNotContagious, iteration);
double indoorOutdoorFactor = InfectionModelWithSeasonality.getIndoorOutdoorFactor(outdoorFactor, rnd, act1, act2);
double shedding = maskModel.getWornMask(infector, act2, restrictions.get(act2.getContainerName())).shedding;
double intake = maskModel.getWornMask(target, act1, restrictions.get(act1.getContainerName())).intake;
lastUnVac = calcUnVacInfectionProbability(target, infector, restrictions, act1, act2, transmissionWeights, contactIntensity, jointTimeInContainer, indoorOutdoorFactor, shedding, intake);
// route-agnostic factors
double base = episimConfig.getCalibrationParameter() * susceptibility * infectivity * contactIntensity * jointTimeInContainer
* immunityModel.getInfectivityFactor(infector, strain.getStrain(), iteration)
* target.getSusceptibility()
* getInfectivity(infector)
* strain.getInfectiousness();
// respiratory route only: ventilation / contact-intensity correction, face masks, indoor-outdoor dilution
double respiratoryModifier = ciCorrection * shedding * intake * indoorOutdoorFactor;
// contact-side route split combined with the pathogen's per-route transmissibility
TransmissionWeights pathogenWeights = pathogenConfig.getParams(strain.getPathogen()).getRouteTransmissibility();
double viaRespiratory = transmissionWeights.getRespiratory() * pathogenWeights.getRespiratory() * respiratoryModifier;
double viaDirectContact = transmissionWeights.getDirectContact() * pathogenWeights.getDirectContact();
// fomite: double viaFomite = transmissionWeights.getFomite() * pathogenWeights.getFomite();
return 1 - Math.exp(-base * (viaRespiratory + viaDirectContact /* fomite: + viaFomite */));
}
/**
* The probability the target would have had without their vaccination, reported for vaccinated persons only
* (see {@code AbstractContactModel.potentialInfection}) as a vaccine-effectiveness diagnostic. It deliberately
* still uses the static helpers: asking an {@link ImmunityModel} for "immunity without the vaccinations" would
* mean exposing which source a factor came from, which is exactly what that interface hides. Note that
* {@code InfectionModelWithAntibodies} already reads the same quantity as "without any immunity", so the two
* never agreed on what it means.
*/
private double calcUnVacInfectionProbability(EpisimPerson target, EpisimPerson infector, Map<String, Restriction> restrictions, EpisimConfigGroup.InfectionParams act1, EpisimConfigGroup.InfectionParams act2, TransmissionWeights transmissionWeights, double contactIntensity, double jointTimeInContainer,
double indoorOutdoorFactor, double shedding, double intake) {
//noinspection ConstantConditions // ci corr can not be null, because sim is initialized with non null value
double ciCorrection = Math.min(restrictions.get(act1.getContainerName()).getCiCorrection(), restrictions.get(act2.getContainerName()).getCiCorrection());
double susceptibility = this.susceptibility.get(infector.getVirusStrain())[target.getAge()];
double infectivity = this.infectivity.get(infector.getVirusStrain())[infector.getAge()];
// apply reduced susceptibility of vaccinated persons
VirusStrainConfigGroup.StrainParams strain = virusStrainConfig.getParams(infector.getVirusStrain());
// vac is reduced from this term
susceptibility *= getImmunityEffectiveness(strain, target, vaccinationConfig, iteration);
// route-agnostic factors
double base = episimConfig.getCalibrationParameter() * susceptibility * infectivity * contactIntensity * jointTimeInContainer
* DefaultInfectionModel.getInfectivity(infector, strain, vaccinationConfig, iteration)
* target.getSusceptibility()
* getInfectivity(infector)
* strain.getInfectiousness();
// respiratory route only: ventilation / contact-intensity correction, face masks, indoor-outdoor dilution
double respiratoryModifier = ciCorrection * shedding * intake * indoorOutdoorFactor;
// contact-side route split combined with the pathogen's per-route transmissibility
TransmissionWeights pathogenWeights = pathogenConfig.getParams(strain.getPathogen()).getRouteTransmissibility();
double viaRespiratory = transmissionWeights.getRespiratory() * pathogenWeights.getRespiratory() * respiratoryModifier;
double viaDirectContact = transmissionWeights.getDirectContact() * pathogenWeights.getDirectContact();
// fomite: double viaFomite = transmissionWeights.getFomite() * pathogenWeights.getFomite();
return 1 - Math.exp(-base * (viaRespiratory + viaDirectContact /* fomite: + viaFomite */));
}
/**
* Calculates infectivity of infector depending on disease progression.
*
* @apiNote package private for testing
*/
double getInfectivity(EpisimPerson infector) {
if (infector.getDiseaseStatus() == EpisimPerson.DiseaseStatus.showingSymptoms) {
int afterSymptomOnset = infector.daysSince(EpisimPerson.DiseaseStatus.showingSymptoms, iteration);
PathogenConfigGroup.PathogenParams pathogen = pathogenOf(infector);
return pathogen.hasInfectivityProfile() ? pathogen.getInfectivity(afterSymptomOnset)
: distribution.density(afterSymptomOnset) * scale;
} else if (infector.getDiseaseStatus() == EpisimPerson.DiseaseStatus.contagious) {
EpisimPerson.DiseaseStatus nextDiseaseStatus = progression.getNextDiseaseStatus(infector.getPersonId());
int transitionDays = progression.getNextTransitionDays(infector.getPersonId());
int daysSince = infector.daysSince(infector.getDiseaseStatus(), iteration);
if (nextDiseaseStatus == EpisimPerson.DiseaseStatus.showingSymptoms) {
int untilOnset = transitionDays - daysSince;
// the built-in curve is read at the distance to onset, i.e. mirrored; a configured profile is signed
PathogenConfigGroup.PathogenParams pathogen = pathogenOf(infector);
return pathogen.hasInfectivityProfile() ? pathogen.getInfectivity(-untilOnset)
: distribution.density(untilOnset) * scale;
} else if (nextDiseaseStatus == EpisimPerson.DiseaseStatus.recovered) {
// when next state is recovered the half of the interval is used
double sinceMidpoint = daysSince - transitionDays / 2.0;
PathogenConfigGroup.PathogenParams pathogen = pathogenOf(infector);
return pathogen.hasInfectivityProfile() ? pathogen.getInfectivity(sinceMidpoint)
: distribution.density(sinceMidpoint) * scale;
}
}
return 0.0;
}
private PathogenConfigGroup.PathogenParams pathogenOf(EpisimPerson infector) {
return pathogenConfig.getParams(virusStrainConfig.getParams(infector.getVirusStrain()).getPathogen());
}
public static void main(String[] args) {
// test distribution
NormalDistribution dist = new NormalDistribution(0.5, 2.6);
for(int i = -5; i <= 10; i++) {
System.out.println(i + " " + dist.density(i));
}
}
}