PathogenConfigGroup.java
package org.matsim.episim;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import org.matsim.core.config.Config;
import org.matsim.core.config.ConfigGroup;
import org.matsim.core.config.ReflectiveConfigGroup;
import org.matsim.episim.model.ContactTransmissionType;
import org.matsim.episim.model.Pathogen;
import org.matsim.episim.model.TransmissionWeights;
import org.matsim.episim.model.VirusStrain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
/**
* Configuration of the properties of a {@link Pathogen}.
*
* <p>There is exactly one {@link PathogenParams} parameter set per pathogen. It holds everything that is a property
* of the pathogen itself, independent of how disease progression is modelled:</p>
* <ul>
* <li>{@code routeTransmissibility} — how well the pathogen transmits per {@link ContactTransmissionType}
* route (respiratory / direct contact), in the {@code route=weight;route=weight} notation of
* {@link ContactTransmissionConfigGroup};</li>
* <li>the <b>isolation at symptom onset</b> ({@code symptomaticIsolationProbabilityByAge},
* {@code symptomaticIsolationStatus});</li>
* <li>an optional <b>infectivity profile</b> ({@code infectivityProfile}): relative infectivity by day, see
* {@link PathogenParams#getInfectivity(double)};</li>
* <li>one or two nested {@link ProgressionParams} with the base disease-progression probabilities, one per
* variant ({@code ageDependent=false} for {@code DefaultDiseaseStatusTransitionModel} /
* {@code AntibodyDependentTransitionModel}, {@code ageDependent=true} for
* {@code AgeDependentDiseaseStatusTransitionModel}).</li>
* </ul>
*
* <p>Route weights are independent, <b>additive</b> channels: they are relative and not normalised, so raising one
* route's transmissibility does not lower another's. The infection model multiplies them, route by route, with the
* contact-side split from {@link ContactTransmissionConfigGroup} and sums the products under the exponent
* ({@code via[route] = contactWeight[route] · routeTransmissibility[route] · routeModifiers}).
* {@code respiratory=1.0} is the conventional anchor — with it (and the default contact-side weights) the
* formula reduces to the previous respiratory-only model, so SARS-CoV-2 behaves exactly as before. A pathogen that
* also spreads by direct contact is written as e.g. {@code respiratory=1.0;directContact=0.4} (a sum above 1 is
* allowed). A pathogen whose weights are all zero is rejected on config finalisation.</p>
*
* <p>A person who starts showing symptoms goes into {@code symptomaticIsolationStatus} ({@code full}: no contacts at
* all; {@code atHome}: home activities only) with probability {@code symptomaticIsolationProbabilityByAge}. The
* decision is drawn once per person at symptom onset and holds until recovery. The defaults ({@code 0=1.0},
* {@code full}) reproduce the previously hard-coded behaviour, including the random number stream.</p>
*
* <p>Each {@link ProgressionParams} describes the conditional probability of moving on to the more severe branch
* of a disease-status transition, given the preceding state and the person's age:</p>
* <ul>
* <li>{@code P(showingSymptoms | contagious, age)}</li>
* <li>{@code P(seriouslySick | showingSymptoms, age)}</li>
* <li>{@code P(critical | seriouslySick, age)}</li>
* <li>{@code P(deceased | critical, age)}</li>
* </ul>
* <p>The remaining probability mass corresponds to the existing alternative branch (usually {@code recovered},
* or {@code seriouslySickAfterCritical} for the {@code critical} transition). Strain-relative differences,
* vaccination and antibody effects, the {@code hospitalFactor} and every other modifier stay in the models.</p>
*
* <p>All age profiles use the {@code age=value;age=value} bucket notation of {@link VirusStrainConfigGroup}. A value
* applies from the given age (inclusive) up to the next boundary; a single entry such as {@code 0=0.8} yields a
* constant for all ages.</p>
*/
public class PathogenConfigGroup extends ReflectiveConfigGroup {
static final String GROUPNAME = "pathogen";
private static final Splitter.MapSplitter SPLITTER = Splitter.on(";").withKeyValueSeparator("=");
private static final Joiner.MapJoiner JOINER = Joiner.on(";").withKeyValueSeparator("=");
/**
* Parameters per pathogen.
*/
private final Map<Pathogen, PathogenParams> params = new LinkedHashMap<>();
/**
* Default constructor. Registers the SARS-CoV-2 defaults so that existing COVID configurations without an
* explicit {@code pathogen} section keep behaving exactly as before.
*/
public PathogenConfigGroup() {
super(GROUPNAME);
addParameterSet(PathogenParams.sarsCov2Default());
}
/**
* Get the parameter set of a pathogen.
*
* @throws IllegalStateException if the pathogen has not been configured
*/
public PathogenParams getParams(Pathogen pathogen) {
PathogenParams p = params.get(pathogen);
if (p == null)
throw new IllegalStateException("No pathogen configuration for '" + pathogen.getName() + "'. Add a '"
+ PathogenParams.SET_TYPE + "' parameter set for this pathogen to the '" + GROUPNAME + "' config group.");
return p;
}
/**
* Get the disease-progression probabilities of a pathogen in the requested variant.
*
* @throws IllegalStateException if the pathogen or the requested variant has not been configured
*/
public ProgressionParams getProgressionParams(Pathogen pathogen, boolean ageDependent) {
return getParams(pathogen).getProgressionParams(ageDependent);
}
/**
* Whether a parameter set for the pathogen is present.
*/
public boolean hasParams(Pathogen pathogen) {
return params.containsKey(pathogen);
}
/**
* Get an existing or add a new parameter set for the given pathogen. A new set carries the defaults
* (respiratory-only, isolation of everybody) and no progression variant yet.
*/
public PathogenParams getOrAddParams(Pathogen pathogen) {
PathogenParams existing = params.get(pathogen);
if (existing != null)
return existing;
PathogenParams p = new PathogenParams();
p.setPathogen(pathogen);
addParameterSet(p);
return p;
}
/**
* Checks that the pathogens of all given strains are configured with the requested progression variant. Transition
* models call this when they are created, i.e. before the simulation starts, because only they know which variant
* they need.
*
* @param strains strains that can infect persons
* @param ageDependent the progression variant used by the model
* @param model name of the model for the error message
* @throws IllegalStateException listing all pathogens without parameter set or without this variant
*/
public void checkProgressionConfigured(Collection<VirusStrain> strains, boolean ageDependent, String model) {
Map<Pathogen, List<String>> byPathogen = new TreeMap<>(Comparator.comparing(Pathogen::getName));
for (VirusStrain strain : strains)
byPathogen.computeIfAbsent(strain.getPathogen(), k -> new ArrayList<>()).add(strain.getVirusStrainName());
List<String> problems = new ArrayList<>();
byPathogen.forEach((pathogen, names) -> {
if (!hasParams(pathogen))
problems.add("pathogen '" + pathogen.getName() + "' of strains " + names + " has no '" + PathogenParams.SET_TYPE + "'");
else if (!params.get(pathogen).hasProgressionParams(ageDependent))
problems.add("pathogen '" + pathogen.getName() + "' of strains " + names + " has no '" + ProgressionParams.SET_TYPE
+ "' with ageDependent=" + ageDependent);
});
if (!problems.isEmpty())
throw new IllegalStateException(model + " needs the " + (ageDependent ? "age-dependent" : "age-independent")
+ " disease progression of every configured strain in config group '" + GROUPNAME + "', but "
+ String.join("; ", problems) + ".");
}
@Override
protected void checkConsistency(Config config) {
super.checkConsistency(config);
for (PathogenParams p : params.values()) {
p.validateComplete(getName());
}
}
@Override
public ConfigGroup createParameterSet(String type) {
if (PathogenParams.SET_TYPE.equals(type))
return new PathogenParams();
throw new IllegalArgumentException("Unknown parameter set type: " + type);
}
@Override
public void addParameterSet(final ConfigGroup set) {
if (!PathogenParams.SET_TYPE.equals(set.getName()))
throw new IllegalStateException("Unknown parameter set type: " + set.getName());
PathogenParams p = (PathogenParams) set;
PathogenParams previous = params.put(p.getPathogen(), p);
// Replace a previously registered set (e.g. the auto-added SARS-CoV-2 default) instead of keeping a stale duplicate.
if (previous != null)
super.removeParameterSet(previous);
super.addParameterSet(set);
}
/**
* Value of an age profile for the given age; ages below the first bucket use the first bucket.
*/
private static double forAge(String param, NavigableMap<Integer, Double> map, int age) {
Map.Entry<Integer, Double> entry = map.floorEntry(age);
if (entry == null)
entry = map.firstEntry();
if (entry == null)
throw new IllegalStateException("No age profile configured for '" + param + "'.");
return entry.getValue();
}
private static void replace(NavigableMap<Integer, Double> target, NavigableMap<Integer, Double> values) {
target.clear();
target.putAll(values);
}
private static NavigableMap<Integer, Double> parse(String param, String config) {
NavigableMap<Integer, Double> parsed = new TreeMap<>();
for (Map.Entry<String, String> e : SPLITTER.split(config).entrySet()) {
parsed.put(Integer.parseInt(e.getKey().trim()), Double.parseDouble(e.getValue().trim()));
}
return validated(param, parsed);
}
private static NavigableMap<Integer, Double> validated(String param, Map<Integer, Double> values) {
if (values.isEmpty())
throw new IllegalArgumentException("'" + param + "' needs at least one age bucket.");
NavigableMap<Integer, Double> out = new TreeMap<>();
for (Map.Entry<Integer, Double> e : values.entrySet()) {
int age = e.getKey();
double p = e.getValue();
if (age < 0)
throw new IllegalArgumentException("'" + param + "' age bucket must not be negative but was " + age + ".");
if (Double.isNaN(p) || p < 0.0 || p > 1.0)
throw new IllegalArgumentException("'" + param + "' probability for age " + age
+ " must be within [0, 1] but was " + p + ".");
out.put(age, p);
}
return out;
}
private static void requireNonEmpty(String owner, String param, NavigableMap<Integer, Double> map) {
if (map.isEmpty())
throw new IllegalStateException(owner + " is missing an age profile for '" + param + "'.");
}
/**
* Properties of a single {@link Pathogen}: transmission routes, isolation at symptom onset and the nested
* disease-progression variants.
*/
public static final class PathogenParams extends ReflectiveConfigGroup {
static final String SET_TYPE = "pathogenParams";
private static final String PATHOGEN = "pathogen";
private static final String ROUTE_TRANSMISSIBILITY = "routeTransmissibility";
private static final String SYMPTOMATIC_ISOLATION = "symptomaticIsolationProbabilityByAge";
private static final String SYMPTOMATIC_ISOLATION_STATUS = "symptomaticIsolationStatus";
private static final String INFECTIVITY_PROFILE = "infectivityProfile";
/**
* Pathogen this parameter set applies to.
*/
private Pathogen pathogen = Pathogen.SARS_COV_2;
/**
* Per-route transmissibility of this pathogen. Relative, additive weights (no sum constraint);
* the infection model multiplies them route by route with the contact-side split from
* {@link ContactTransmissionConfigGroup}. Defaults to respiratory-only.
*/
private TransmissionWeights routeTransmissibility = TransmissionWeights.RESPIRATORY_ONLY;
/**
* Probability that a person isolates at symptom onset, by age. Defaults to everybody (the previous behaviour).
*/
private final NavigableMap<Integer, Double> symptomaticIsolationProbabilityByAge = new TreeMap<>(Map.of(0, 1.0));
/**
* Quarantine status of a person who isolates at symptom onset: {@code full} or {@code atHome}.
*/
private EpisimPerson.QuarantineStatus symptomaticIsolationStatus = EpisimPerson.QuarantineStatus.full;
/**
* Relative infectivity by signed day offset; empty means not configured.
*/
private final NavigableMap<Integer, Double> infectivityProfile = new TreeMap<>();
/**
* Disease-progression variants; index 0 is age-independent, index 1 age-dependent.
*/
private final ProgressionParams[] progression = new ProgressionParams[2];
PathogenParams() {
super(SET_TYPE);
}
/**
* SARS-CoV-2 defaults: respiratory-only, isolation of everybody, and both progression variants with the values
* that used to be hard-coded in the transition models.
*/
static PathogenParams sarsCov2Default() {
PathogenParams p = new PathogenParams();
p.setPathogen(Pathogen.SARS_COV_2);
p.addParameterSet(ProgressionParams.sarsCov2AgeIndependentDefault());
p.addParameterSet(ProgressionParams.sarsCov2AgeDependentDefault());
return p;
}
@StringGetter(PATHOGEN)
public String getPathogenName() {
return pathogen.getName();
}
@StringSetter(PATHOGEN)
public void setPathogen(String pathogen) {
setPathogen(new Pathogen(pathogen));
}
public Pathogen getPathogen() {
return pathogen;
}
public void setPathogen(Pathogen pathogen) {
this.pathogen = Objects.requireNonNull(pathogen, "Pathogen must not be null");
}
/**
* Disease-progression probabilities in the requested variant.
*
* @throws IllegalStateException if this variant has not been configured
*/
public ProgressionParams getProgressionParams(boolean ageDependent) {
ProgressionParams p = progression[index(ageDependent)];
if (p == null)
throw new IllegalStateException("No " + (ageDependent ? "age-dependent" : "age-independent")
+ " disease progression for pathogen '" + pathogen.getName() + "'. Add a '" + ProgressionParams.SET_TYPE
+ "' parameter set with ageDependent=" + ageDependent + " to its '" + SET_TYPE + "'.");
return p;
}
/**
* Whether the progression variant is present.
*/
public boolean hasProgressionParams(boolean ageDependent) {
return progression[index(ageDependent)] != null;
}
/**
* Get an existing or add a new (empty) progression variant.
*/
public ProgressionParams getOrAddProgressionParams(boolean ageDependent) {
ProgressionParams existing = progression[index(ageDependent)];
if (existing != null)
return existing;
ProgressionParams p = new ProgressionParams();
p.setAgeDependent(ageDependent);
addParameterSet(p);
return p;
}
@Override
public ConfigGroup createParameterSet(String type) {
if (ProgressionParams.SET_TYPE.equals(type))
return new ProgressionParams();
throw new IllegalArgumentException("Unknown parameter set type: " + type);
}
@Override
public void addParameterSet(final ConfigGroup set) {
if (!ProgressionParams.SET_TYPE.equals(set.getName()))
throw new IllegalStateException("Unknown parameter set type: " + set.getName());
ProgressionParams p = (ProgressionParams) set;
int idx = index(p.isAgeDependent());
ProgressionParams previous = progression[idx];
progression[idx] = p;
// replace a previously registered variant instead of keeping a stale duplicate
if (previous != null)
super.removeParameterSet(previous);
super.addParameterSet(set);
}
private static int index(boolean ageDependent) {
return ageDependent ? 1 : 0;
}
@StringGetter(ROUTE_TRANSMISSIBILITY)
String getRouteTransmissibilityString() {
return routeTransmissibility.toToken();
}
@StringSetter(ROUTE_TRANSMISSIBILITY)
void setRouteTransmissibilityString(String config) {
this.routeTransmissibility = TransmissionWeights.parse(config);
}
/**
* Per-route transmissibility of this pathogen (respiratory / direct contact) — relative,
* additive weights, {@code respiratory=1.0} by default.
*/
public TransmissionWeights getRouteTransmissibility() {
return routeTransmissibility;
}
/**
* Set the per-route transmissibility of this pathogen, replacing the previous value.
*/
public void setRouteTransmissibility(TransmissionWeights routeTransmissibility) {
this.routeTransmissibility = Objects.requireNonNull(routeTransmissibility, ROUTE_TRANSMISSIBILITY);
}
@StringGetter(SYMPTOMATIC_ISOLATION)
String getSymptomaticIsolationProbabilityByAgeString() {
return JOINER.join(symptomaticIsolationProbabilityByAge);
}
@StringSetter(SYMPTOMATIC_ISOLATION)
void setSymptomaticIsolationProbabilityByAge(String config) {
replace(symptomaticIsolationProbabilityByAge, parse(SYMPTOMATIC_ISOLATION, config));
}
/**
* Set the probability of isolating at symptom onset by age, replacing all previous entries.
*/
public void setSymptomaticIsolationProbabilityByAge(Map<Integer, Double> values) {
replace(symptomaticIsolationProbabilityByAge, validated(SYMPTOMATIC_ISOLATION, values));
}
public NavigableMap<Integer, Double> getSymptomaticIsolationProbabilityByAge() {
return symptomaticIsolationProbabilityByAge;
}
/**
* Probability that a person of the given age isolates at symptom onset.
*/
public double getSymptomaticIsolationProbability(int age) {
return forAge(SYMPTOMATIC_ISOLATION, symptomaticIsolationProbabilityByAge, age);
}
@StringGetter(SYMPTOMATIC_ISOLATION_STATUS)
String getSymptomaticIsolationStatusString() {
return symptomaticIsolationStatus.name();
}
@StringSetter(SYMPTOMATIC_ISOLATION_STATUS)
void setSymptomaticIsolationStatusString(String status) {
setSymptomaticIsolationStatus(EpisimPerson.QuarantineStatus.valueOf(status.trim()));
}
/**
* Quarantine status of a person who isolates at symptom onset.
*/
public EpisimPerson.QuarantineStatus getSymptomaticIsolationStatus() {
return symptomaticIsolationStatus;
}
/**
* Set the quarantine status used for isolation at symptom onset; only {@code full} and {@code atHome} are
* allowed. Disable isolation with a probability of 0 instead.
*/
public void setSymptomaticIsolationStatus(EpisimPerson.QuarantineStatus status) {
Objects.requireNonNull(status, SYMPTOMATIC_ISOLATION_STATUS);
if (status != EpisimPerson.QuarantineStatus.full && status != EpisimPerson.QuarantineStatus.atHome)
throw new IllegalArgumentException("'" + SYMPTOMATIC_ISOLATION_STATUS + "' must be 'full' or 'atHome' but was '"
+ status + "'; set '" + SYMPTOMATIC_ISOLATION + "' to 0 to disable isolation.");
this.symptomaticIsolationStatus = status;
}
@StringGetter(INFECTIVITY_PROFILE)
String getInfectivityProfileString() {
return JOINER.join(infectivityProfile);
}
@StringSetter(INFECTIVITY_PROFILE)
void setInfectivityProfileString(String config) {
// an unset profile is written as an empty value, which must be read back as unset
if (config == null || config.isBlank()) {
infectivityProfile.clear();
return;
}
Map<Integer, Double> parsed = new TreeMap<>();
for (Map.Entry<String, String> e : SPLITTER.split(config).entrySet())
parsed.put(Integer.parseInt(e.getKey().trim()), Double.parseDouble(e.getValue().trim()));
setInfectivityProfile(parsed);
}
/**
* Set the infectivity profile, replacing the previous one; an empty map unsets it.
*
* @param values relative infectivity by signed day offset, see {@link #getInfectivity(double)}. Values must be
* finite and non-negative, and a non-empty profile needs at least one positive value.
*/
public void setInfectivityProfile(Map<Integer, Double> values) {
boolean positive = false;
for (Map.Entry<Integer, Double> e : values.entrySet()) {
double v = e.getValue();
if (!Double.isFinite(v) || v < 0.0)
throw new IllegalArgumentException("'" + INFECTIVITY_PROFILE + "' value for day " + e.getKey()
+ " must be finite and non-negative but was " + v + ".");
positive |= v > 0.0;
}
if (!values.isEmpty() && !positive)
throw new IllegalArgumentException("'" + INFECTIVITY_PROFILE + "' of pathogen '" + pathogen.getName()
+ "' is zero everywhere; leave it empty to use the infection model's built-in curve.");
infectivityProfile.clear();
infectivityProfile.putAll(values);
}
public NavigableMap<Integer, Double> getInfectivityProfile() {
return infectivityProfile;
}
/**
* Whether an infectivity profile is configured. Without one the infection model uses its built-in curve.
*/
public boolean hasInfectivityProfile() {
return !infectivityProfile.isEmpty();
}
/**
* Relative infectivity at a signed day offset from the reference point: symptom onset for an infection that
* becomes symptomatic (negative before onset), the middle of the contagious period for one that does not.
* Linear between configured days, zero outside the configured range. The values are relative: with a peak of
* 1.0, {@code infectiousness} and {@code calibrationParameter} keep their meaning.
*
* @throws IllegalStateException if no profile is configured
*/
public double getInfectivity(double daysFromReference) {
if (infectivityProfile.isEmpty())
throw new IllegalStateException("No '" + INFECTIVITY_PROFILE + "' configured for pathogen '" + pathogen.getName() + "'.");
Map.Entry<Integer, Double> lower = infectivityProfile.floorEntry((int) Math.floor(daysFromReference));
Map.Entry<Integer, Double> upper = infectivityProfile.ceilingEntry((int) Math.ceil(daysFromReference));
if (lower == null || upper == null)
return 0.0;
if (lower.getKey().equals(upper.getKey()))
return lower.getValue();
double t = (daysFromReference - lower.getKey()) / (upper.getKey() - lower.getKey());
return lower.getValue() + t * (upper.getValue() - lower.getValue());
}
/**
* Ensure the pathogen can transmit, has an isolation profile and at least one complete progression variant.
* Called on config finalisation, not on every {@code addParameterSet}, so that sets can still be filled in after
* being registered.
*/
void validateComplete(String groupName) {
String owner = "pathogen '" + pathogen.getName() + "' in config group '" + groupName + "'";
requireNonEmpty(owner, SYMPTOMATIC_ISOLATION, symptomaticIsolationProbabilityByAge);
if (routeTransmissibility.isZero())
throw new IllegalStateException(owner + " has an all-zero '" + ROUTE_TRANSMISSIBILITY
+ "'; it cannot transmit by any route.");
if (progression[0] == null && progression[1] == null)
throw new IllegalStateException(owner + " has no '" + ProgressionParams.SET_TYPE + "' parameter set.");
for (ProgressionParams p : progression) {
if (p != null)
p.validateComplete(owner);
}
}
}
/**
* Base disease-progression probabilities of a pathogen in one variant (age-dependent or age-independent).
*/
public static final class ProgressionParams extends ReflectiveConfigGroup {
static final String SET_TYPE = "progressionParams";
private static final String AGE_DEPENDENT = "ageDependent";
private static final String SHOWING_SYMPTOMS = "showingSymptomsProbabilityByAge";
private static final String SERIOUSLY_SICK = "seriouslySickProbabilityByAge";
private static final String CRITICAL = "criticalProbabilityByAge";
private static final String DEATH = "deathProbabilityByAge";
/**
* Whether this variant is used by the age-dependent transition model.
*/
private boolean ageDependent = false;
private final NavigableMap<Integer, Double> showingSymptomsProbabilityByAge = new TreeMap<>();
private final NavigableMap<Integer, Double> seriouslySickProbabilityByAge = new TreeMap<>();
private final NavigableMap<Integer, Double> criticalProbabilityByAge = new TreeMap<>();
private final NavigableMap<Integer, Double> deathProbabilityByAge = new TreeMap<>();
ProgressionParams() {
super(SET_TYPE);
}
/**
* SARS-CoV-2 defaults for the age-independent transition models
* ({@code DefaultDiseaseStatusTransitionModel}, {@code AntibodyDependentTransitionModel}).
*/
static ProgressionParams sarsCov2AgeIndependentDefault() {
ProgressionParams p = new ProgressionParams();
p.setAgeDependent(false);
p.setShowingSymptomsProbabilityByAge(Map.of(0, 0.8));
p.setSeriouslySickProbabilityByAge(Map.of(0, 0.05625));
p.setCriticalProbabilityByAge(Map.of(0, 0.25));
p.setDeathProbabilityByAge(Map.of(0, 0.0));
return p;
}
/**
* SARS-CoV-2 defaults for {@code AgeDependentDiseaseStatusTransitionModel}. The values and age boundaries
* are taken verbatim from that model; the {@code hospitalFactor} multiplier stays in the model.
*/
static ProgressionParams sarsCov2AgeDependentDefault() {
ProgressionParams p = new ProgressionParams();
p.setAgeDependent(true);
p.setShowingSymptomsProbabilityByAge(Map.of(0, 0.8));
NavigableMap<Integer, Double> seriouslySick = new TreeMap<>();
seriouslySick.put(0, 4.0 / 100);
seriouslySick.put(5, 1.1 / 100);
seriouslySick.put(15, 2.4 / 100);
seriouslySick.put(35, 5.6 / 100);
seriouslySick.put(60, 23. / 100);
seriouslySick.put(80, 36. / 100);
p.setSeriouslySickProbabilityByAge(seriouslySick);
NavigableMap<Integer, Double> critical = new TreeMap<>();
critical.put(0, 7.0 / 100);
critical.put(5, 0.0 / 100);
critical.put(15, 15.0 / 100);
critical.put(35, 30.0 / 100);
critical.put(60, 41. / 100);
critical.put(80, 27. / 100);
p.setCriticalProbabilityByAge(critical);
p.setDeathProbabilityByAge(Map.of(0, 0.0));
return p;
}
@StringGetter(AGE_DEPENDENT)
public boolean isAgeDependent() {
return ageDependent;
}
/**
* Set the variant. Must be set before the parameter set is added to its {@link PathogenParams}.
*/
@StringSetter(AGE_DEPENDENT)
public void setAgeDependent(boolean ageDependent) {
this.ageDependent = ageDependent;
}
@StringGetter(SHOWING_SYMPTOMS)
String getShowingSymptomsProbabilityByAgeString() {
return JOINER.join(showingSymptomsProbabilityByAge);
}
@StringSetter(SHOWING_SYMPTOMS)
void setShowingSymptomsProbabilityByAge(String config) {
replace(showingSymptomsProbabilityByAge, parse(SHOWING_SYMPTOMS, config));
}
/**
* Set the {@code showingSymptoms} age profile, replacing all previous entries.
*/
public void setShowingSymptomsProbabilityByAge(Map<Integer, Double> values) {
replace(showingSymptomsProbabilityByAge, validated(SHOWING_SYMPTOMS, values));
}
@StringGetter(SERIOUSLY_SICK)
String getSeriouslySickProbabilityByAgeString() {
return JOINER.join(seriouslySickProbabilityByAge);
}
@StringSetter(SERIOUSLY_SICK)
void setSeriouslySickProbabilityByAge(String config) {
replace(seriouslySickProbabilityByAge, parse(SERIOUSLY_SICK, config));
}
/**
* Set the {@code seriouslySick} age profile, replacing all previous entries.
*/
public void setSeriouslySickProbabilityByAge(Map<Integer, Double> values) {
replace(seriouslySickProbabilityByAge, validated(SERIOUSLY_SICK, values));
}
@StringGetter(CRITICAL)
String getCriticalProbabilityByAgeString() {
return JOINER.join(criticalProbabilityByAge);
}
@StringSetter(CRITICAL)
void setCriticalProbabilityByAge(String config) {
replace(criticalProbabilityByAge, parse(CRITICAL, config));
}
/**
* Set the {@code critical} age profile, replacing all previous entries.
*/
public void setCriticalProbabilityByAge(Map<Integer, Double> values) {
replace(criticalProbabilityByAge, validated(CRITICAL, values));
}
@StringGetter(DEATH)
String getDeathProbabilityByAgeString() {
return JOINER.join(deathProbabilityByAge);
}
@StringSetter(DEATH)
void setDeathProbabilityByAge(String config) {
replace(deathProbabilityByAge, parse(DEATH, config));
}
/**
* Set the {@code deceased} age profile, replacing all previous entries.
*/
public void setDeathProbabilityByAge(Map<Integer, Double> values) {
replace(deathProbabilityByAge, validated(DEATH, values));
}
public NavigableMap<Integer, Double> getShowingSymptomsProbabilityByAge() {
return showingSymptomsProbabilityByAge;
}
public NavigableMap<Integer, Double> getSeriouslySickProbabilityByAge() {
return seriouslySickProbabilityByAge;
}
public NavigableMap<Integer, Double> getCriticalProbabilityByAge() {
return criticalProbabilityByAge;
}
public NavigableMap<Integer, Double> getDeathProbabilityByAge() {
return deathProbabilityByAge;
}
/**
* Probability of transitioning {@code contagious -> showingSymptoms} for a person of the given age.
*/
public double getShowingSymptomsProbability(int age) {
return forAge(SHOWING_SYMPTOMS, showingSymptomsProbabilityByAge, age);
}
/**
* Probability of transitioning {@code showingSymptoms -> seriouslySick} for a person of the given age.
*/
public double getSeriouslySickProbability(int age) {
return forAge(SERIOUSLY_SICK, seriouslySickProbabilityByAge, age);
}
/**
* Probability of transitioning {@code seriouslySick -> critical} for a person of the given age.
*/
public double getCriticalProbability(int age) {
return forAge(CRITICAL, criticalProbabilityByAge, age);
}
/**
* Probability of transitioning {@code critical -> deceased} for a person of the given age.
*/
public double getDeathProbability(int age) {
return forAge(DEATH, deathProbabilityByAge, age);
}
/**
* Ensure every transition has an age profile.
*/
void validateComplete(String owner) {
String variant = owner + " (" + SET_TYPE + " ageDependent=" + ageDependent + ")";
requireNonEmpty(variant, SHOWING_SYMPTOMS, showingSymptomsProbabilityByAge);
requireNonEmpty(variant, SERIOUSLY_SICK, seriouslySickProbabilityByAge);
requireNonEmpty(variant, CRITICAL, criticalProbabilityByAge);
requireNonEmpty(variant, DEATH, deathProbabilityByAge);
}
}
}