SeasonalVaccinationModel.java
package org.matsim.episim.model.vaccination;
import com.google.inject.Inject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.matsim.api.core.v01.Id;
import org.matsim.api.core.v01.population.Person;
import org.matsim.core.config.Config;
import org.matsim.core.config.ConfigUtils;
import org.matsim.episim.EpisimPerson;
import org.matsim.episim.SeasonalVaccinationConfigGroup;
import org.matsim.episim.model.VaccinationType;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
/**
* Yearly vaccination campaigns, such as the seasonal influenza vaccination, which the built-in models (one dose and one booster per
* person) cannot express. The campaigns are in {@link SeasonalVaccinationConfigGroup}; the model is added to the set of
* additional {@link VaccinationModel}s, not bound as the single model.
*
* <p>On every day of a campaign an agent who has not yet got the product of the season, whose share by age is {@code c}, is
* vaccinated with the probability</p>
*
* <pre>h(d) = c * w(d) / (1 - c * W(d-1))</pre>
*
* <p>where {@code w(d)} is the part of the campaign that falls on the day and {@code W(d-1)} the part before it. At the end of the
* campaign the share of the vaccinated of each age is {@code c}. The model keeps no state: "has got the product" is read from the
* vaccination history of the agent, so a run from a snapshot needs nothing but what the snapshot holds.</p>
*
* <p>The draw is not taken from the random number generator of the run. It is a hash of the seed of the run, the day and the
* agent, so a run from a snapshot gives the vaccinations of a continuous run, and the vaccinations do not shift the random stream
* of the rest of the simulation. An agent in {@code seriouslySick} or {@code critical} is not vaccinated; every other disease
* status is, because the protection acts on later infections only.</p>
*
* <p>Without a season in the config the model does nothing.</p>
*/
public final class SeasonalVaccinationModel implements VaccinationModel {
private static final Logger log = LogManager.getLogger(SeasonalVaccinationModel.class);
private final SeasonalVaccinationConfigGroup config;
private final long seed;
/** Vaccinations given so far per season, for the log only. */
private final Map<String, Integer> given = new HashMap<>();
@Inject
public SeasonalVaccinationModel(Config config) {
this.config = ConfigUtils.addOrGetModule(config, SeasonalVaccinationConfigGroup.class);
this.seed = config.global().getRandomSeed();
}
@Override
public void handleVaccination(Map<Id<Person>, EpisimPerson> persons, LocalDate date, int iteration, double now) {
for (SeasonalVaccinationConfigGroup.Season season : config.getSeasons()) {
if (date.isBefore(season.firstDay()) || date.isAfter(season.lastDay()))
continue;
double before = season.cumulativeShare(date.minusDays(1));
double today = season.cumulativeShare(date) - before;
if (today <= 0.0)
continue;
VaccinationType product = season.getProduct();
int vaccinated = 0;
for (EpisimPerson person : persons.values()) {
double share = season.shareAt(person.getAge());
if (share <= 0.0 || !person.isVaccinable() || isSeverelyIll(person) || hasProduct(person, product))
continue;
double remaining = 1.0 - share * before;
double probability = remaining <= 1.0e-12 ? 1.0 : Math.min(1.0, share * today / remaining);
if (uniform(seed, iteration, person.getPersonId()) < probability) {
vaccinate(person, iteration, product);
vaccinated++;
}
}
int total = given.merge(season.getSeason(), vaccinated, Integer::sum);
if (date.equals(season.firstDay()) || date.equals(season.lastDay()))
log.info("Seasonal vaccination {}: {} vaccinated on {}, {} of the campaign so far ({})", season.getSeason(), vaccinated, date,
total, product);
}
}
private static boolean isSeverelyIll(EpisimPerson person) {
EpisimPerson.DiseaseStatus status = person.getDiseaseStatus();
return status == EpisimPerson.DiseaseStatus.seriouslySick || status == EpisimPerson.DiseaseStatus.critical;
}
private static boolean hasProduct(EpisimPerson person, VaccinationType product) {
for (int i = 0; i < person.getNumVaccinations(); i++)
if (person.getVaccinationType(i).equals(product))
return true;
return false;
}
/**
* A number in [0, 1) that depends on the seed, the day and the person only: the mixing function of SplitMix64 applied to their
* combination. The hash of the id is that of its text, which does not depend on the order in which ids are created.
*/
static double uniform(long seed, int iteration, Id<Person> person) {
long z = seed + 0x9E3779B97F4A7C15L * (iteration + 1L) + 0xD1B54A32D192ED03L * person.toString().hashCode();
z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L;
z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL;
z ^= z >>> 31;
return (z >>> 11) * 0x1.0p-53;
}
}