AntibodyDependentTransitionModel.java

package org.matsim.episim.model.progression;

import com.google.inject.Inject;
import org.matsim.episim.EpisimPerson;
import org.matsim.episim.EpisimPerson.DiseaseStatus;
import org.matsim.episim.Immunizable;
import org.matsim.episim.PathogenConfigGroup;
import org.matsim.episim.VaccinationConfigGroup;
import org.matsim.episim.VirusStrainConfigGroup;
import org.matsim.episim.model.ImmunityModel;
import org.matsim.episim.model.VirusStrain;

import java.util.NavigableMap;

import org.matsim.episim.util.EpisimSplittableRandom;

/**
 * Disease-status transition model that accounts for antibody levels.
 */
public class AntibodyDependentTransitionModel implements DiseaseStatusTransitionModel {

	private final EpisimSplittableRandom rnd;
	private final ImmunityModel immunityModel;
	private final VaccinationConfigGroup vaccinationConfig;
	private final VirusStrainConfigGroup strainConfig;
	private final PathogenConfigGroup pathogenConfig;

	/**
	 * Whether this model reads the age-dependent pathogen configuration.
	 */
	private final boolean ageDependent;

	@Inject
	public AntibodyDependentTransitionModel(EpisimSplittableRandom rnd, ImmunityModel immunityModel,
	                                        VaccinationConfigGroup vaccinationConfig,
	                                        VirusStrainConfigGroup strainConfigGroup, PathogenConfigGroup pathogenConfig) {
		this(rnd, immunityModel, vaccinationConfig, strainConfigGroup, pathogenConfig, false);
	}

	/**
	 * Constructor for subclasses with age-dependent transitions.
	 *
	 * @param ageDependent whether the age-dependent pathogen configuration is used
	 * @throws IllegalStateException if a configured strain lacks this progression variant, so that this does not fail at
	 *                               the first transition during the simulation
	 */
	protected AntibodyDependentTransitionModel(EpisimSplittableRandom rnd, ImmunityModel immunityModel,
	                                           VaccinationConfigGroup vaccinationConfig,
	                                           VirusStrainConfigGroup strainConfigGroup, PathogenConfigGroup pathogenConfig,
	                                           boolean ageDependent) {
		this.rnd = rnd;
		this.immunityModel = immunityModel;
		this.vaccinationConfig = vaccinationConfig;
		this.strainConfig = strainConfigGroup;
		this.pathogenConfig = pathogenConfig;
		this.ageDependent = ageDependent;

		pathogenConfig.checkProgressionConfigured(strainConfigGroup.getConfiguredStrains(), ageDependent, getClass().getSimpleName());
	}

	/**
	 * Whether this model reads the age-dependent pathogen configuration.
	 */
	protected final boolean isAgeDependentTransition() {
		return ageDependent;
	}

	/**
	 * Base disease-progression probabilities of the pathogen the person is currently infected with.
	 */
	protected final PathogenConfigGroup.ProgressionParams progressionParams(Immunizable person) {
		return pathogenConfig.getProgressionParams(person.getVirusStrain().getPathogen(), isAgeDependentTransition());
	}

	/**
	 * Age used for the probability lookup in the given profile. A profile with a single bucket does not depend on
	 * age, so the age is not read at all (persons without age are fine, as for the previously constant probabilities).
	 * Otherwise the age-dependent variant requires a real age, while the age-independent variant falls back to
	 * {@code 0} for a missing age.
	 */
	private int lookupAge(Immunizable person, NavigableMap<Integer, Double> profile) {
		if (profile.size() <= 1){
			return 0;}

		if (isAgeDependentTransition()){
			return person.getAge();}

		return person instanceof EpisimPerson ? ((EpisimPerson) person).getAgeOrDefault(0) : person.getAge();
	}

	@Override
	public final EpisimPerson.DiseaseStatus decideNextState(EpisimPerson person, EpisimPerson.DiseaseStatus status, int day) {

		switch (status) {
			case infectedButNotContagious:
				return EpisimPerson.DiseaseStatus.contagious;
			case contagious:

				if (rnd.nextDouble() < getProbaOfTransitioningToShowingSymptoms(person) * immunity(person, EpisimPerson.DiseaseStatus.showingSymptoms, day))
					return EpisimPerson.DiseaseStatus.showingSymptoms;
				else
					return EpisimPerson.DiseaseStatus.recovered;

			case showingSymptoms:
				if (rnd.nextDouble() < getProbaOfTransitioningToSeriouslySick(person)
						* (person.getVaccinationStatus() == EpisimPerson.VaccinationStatus.yes ?
						strainConfig.getParams(person.getVirusStrain()).getFactorSeriouslySickVaccinated() :
						strainConfig.getParams(person.getVirusStrain()).getFactorSeriouslySick())
						* immunity(person, EpisimPerson.DiseaseStatus.seriouslySick, day))
//						* (person.getNumInfections() > 1 ? getFactorRecovered(person, day) : 1.0))
					return EpisimPerson.DiseaseStatus.seriouslySick;
				else
					return EpisimPerson.DiseaseStatus.recovered;

			case seriouslySick:
				if (!person.hadDiseaseStatus(EpisimPerson.DiseaseStatus.critical)
						&& (rnd.nextDouble() < getProbaOfTransitioningToCritical(person) * strainConfig.getParams(person.getVirusStrain()).getFactorCritical()
						* immunity(person, EpisimPerson.DiseaseStatus.critical, day)))
					return EpisimPerson.DiseaseStatus.critical;
				else
					return EpisimPerson.DiseaseStatus.recovered;

			case critical:
				double proba = getProbaOfTransitioningToDeceased(person);
				if (proba != 0 && rnd.nextDouble() < proba)
					return DiseaseStatus.deceased;
				else
					return EpisimPerson.DiseaseStatus.seriouslySickAfterCritical;

			case seriouslySickAfterCritical:
				return EpisimPerson.DiseaseStatus.recovered;

			case recovered:
				return EpisimPerson.DiseaseStatus.susceptible;

			default:
				throw new IllegalStateException("No state transition defined for " + person.getDiseaseStatus());
		}
	}

	/**
	 * Probability that a person transitions from {@code showingSymptoms} to {@code seriouslySick} when person was already infected.
	 */
	protected double getFactorRecovered(EpisimPerson person, int day) {

		int daysSince = person.daysSince(DiseaseStatus.recovered, day);

		//we assume about 20% loss of protection against severe progression every year
		return Math.min(0.2 * (daysSince / 365), 1.0);
	}

	/**
	 * Probability that a persons transitions from {@code showingSymptoms} to {@code seriouslySick}.
	 */
	public double getProbaOfTransitioningToSeriouslySick(Immunizable person) {
		PathogenConfigGroup.ProgressionParams params = progressionParams(person);
		return params.getSeriouslySickProbability(lookupAge(person, params.getSeriouslySickProbabilityByAge()));
	}

	/**
	 * Probability that a persons transitions from {@code seriouslySick} to {@code critical}.
	 */
	public double getProbaOfTransitioningToCritical(Immunizable person) {
		PathogenConfigGroup.ProgressionParams params = progressionParams(person);
		return params.getCriticalProbability(lookupAge(person, params.getCriticalProbabilityByAge()));
	}

	protected double getProbaOfTransitioningToShowingSymptoms(EpisimPerson person) {
		PathogenConfigGroup.ProgressionParams params = progressionParams(person);
		return params.getShowingSymptomsProbability(lookupAge(person, params.getShowingSymptomsProbabilityByAge()));
	}

	/**
	 * Remaining risk of the transition, given what the person's immune history protects them against.
	 */
	private double immunity(EpisimPerson person, EpisimPerson.DiseaseStatus target, int day) {
		return immunityModel.getFactor(person, person.getVirusStrain(), target, day);
	}

	protected double getProbaOfTransitioningToDeceased(EpisimPerson person) {
		PathogenConfigGroup.ProgressionParams params = progressionParams(person);
		return params.getDeathProbability(lookupAge(person, params.getDeathProbabilityByAge()));
	}


}