DefaultAntibodyModel.java

package org.matsim.episim.model;


import com.google.inject.Inject;
import org.matsim.episim.EpisimPerson;
import org.matsim.episim.EpisimUtils;
import org.matsim.episim.VirusStrainConfigGroup;

import java.util.Collection;
import java.util.Map;

import org.matsim.episim.util.EpisimSplittableRandom;

/**
 * Default implementation of the antibody model.
 */
public class DefaultAntibodyModel implements AntibodyModel {

	public static final double HALF_LIFE_DAYS = 60; // todo: would 40 work better?

	// optimistic: 46
	//

	private final AntibodyModel.Config antibodyConfig;
	private final EpisimSplittableRandom localRnd;

	private final Collection<VirusStrain> virusStrains;


	@Inject
	DefaultAntibodyModel(AntibodyModel.Config antibodyConfig, VirusStrainConfigGroup virusStrainConfig) {
		this.antibodyConfig = antibodyConfig;
		this.virusStrains = virusStrainConfig.getVirusStrains();
		localRnd = new EpisimSplittableRandom(2938); // todo: should it be a fixed seed, i.e not change btwn snapshots


	}

	@Override
	public void init(Collection<EpisimPerson> persons, int iteration) {

		for (EpisimPerson person : persons) {

			// mu = log(median); log(1)=0


			// we assume immune response multiplier follows log-normal distribution, bounded by 0.01 and 10.
			double immuneResponseMultiplier = 0;
			while (immuneResponseMultiplier < 0.1 || immuneResponseMultiplier > 10) {

				immuneResponseMultiplier = EpisimUtils.nextLogNormal(localRnd, 0, antibodyConfig.getImmuneResponseSigma());
			}

			person.setImmuneResponseMultiplier(immuneResponseMultiplier);

		}


	}

	@Override
	public void recalculateAntibodiesAfterSnapshot(Collection<EpisimPerson> persons, int iteration) {
		for (EpisimPerson person : persons) {

			// reset to 0.0
			for (VirusStrain strain : virusStrains) {
				person.setAntibodies(strain, 0.0);
			}

			// recalculate antibodies from immune history
			for (int it = 1; it < iteration; it++) {
				updateAntibodies(person, it);

			}
		}
	}

	/**
	 * Updates the antibody levels for person. If an immunity event occurs (vaccination or infection) on the previous
	 * day, antibodies will increase. If not, they will decrease. This method was designed to also recalculate antibodies
	 * when the simulation is started from snapshot.
	 *
	 * @param person person whose antibodies to update
	 * @param day    current day / iteration
	 */
	@Override
	public void updateAntibodies(EpisimPerson person, int day) {

		//handle vaccination
		if (person.getVaccinationDates().contains(day - 1)) {
			int vaccinationIndex = person.getVaccinationDates().indexOf(day - 1);
			VaccinationType vaccinationType = person.getVaccinationType(vaccinationIndex);
			handleImmunization(person, vaccinationType);
			return;
		}

		// handle infection
		for (int infectionIndex = 0; infectionIndex < person.getNumInfections(); infectionIndex++) {
			if (person.daysSinceInfection(infectionIndex, day) == 1) {
				VirusStrain virusStrain = person.getVirusStrain(infectionIndex);
				handleImmunization(person, virusStrain);
				return;
			}
		}

		double halflifeDays = HALF_LIFE_DAYS;

		if ((person.getNumInfections() > 0 && person.getNumVaccinations() > 0)
			|| person.getNumInfections() > 4
			|| person.getNumVaccinations() > 3) {

			halflifeDays *= antibodyConfig.getHlMultiplierForInfected(); // 1, 2, 5
		}

		// if no immunity event: exponential decay, day by day:
		for (VirusStrain strain : virusStrains) {
			double oldAntibodyLevel = person.getAntibodies(strain);
			person.setAntibodies(strain, oldAntibodyLevel * Math.pow(0.5, 1 / halflifeDays));
		}

	}

	private void handleImmunization(EpisimPerson person, ImmunityEvent immunityEventType) {

		boolean firstImmunization = checkFirstImmunization(person);

		// resolve the per-event maps once instead of rebuilding the whole nested map per strain
		Map<VirusStrain, Double> initialAntibodiesForEvent = antibodyConfig.getInitialAntibodies(immunityEventType);

		// 1st immunization:
		if (firstImmunization) {

			for (VirusStrain strain2 : virusStrains) {
				// no entry: this immunity event does not induce antibodies against the strain (e.g. another pathogen)
				double antibodies = initialAntibodiesForEvent.getOrDefault(strain2, 0.0);

				antibodies = Math.min(150., antibodies * person.getImmuneResponseMultiplier());

				person.setAntibodies(strain2, antibodies);

				// if antibodies against a strain2 are higher than previous maximum, replace maximum
				// should always be the case for initial immunization
				person.updateMaxAntibodies(strain2, antibodies);
			}


		} else {
			Map<VirusStrain, Double> refreshFactorsForEvent = antibodyConfig.getAntibodyRefreshFactors(immunityEventType);

			for (VirusStrain strain2 : virusStrains) {
				// no entry: the existing antibody level against the strain is not refreshed
				double refreshFactor = refreshFactorsForEvent.getOrDefault(strain2, 1.0);

				// antibodies before refresh
				double antibodies = person.getAntibodies(strain2);

				// refresh antibodies
				antibodies = antibodies * refreshFactor;

				// check that new antibody level at least as high as initial antibodies
				double initialAntibodies = initialAntibodiesForEvent.getOrDefault(strain2, 0.0) * person.getImmuneResponseMultiplier();
				antibodies = Math.max(antibodies, initialAntibodies);

				// check that new antibody level is at most 150
				antibodies = Math.min(150., antibodies);

				person.setAntibodies(strain2, antibodies);

				// if antibodies against a strain2 are higher than previous maximum, replace maximum
				person.updateMaxAntibodies(strain2, antibodies);
			}
		}
	}

	private boolean checkFirstImmunization(EpisimPerson person) {
		boolean firstImmunization = true;
		for (double abLevel : person.getAntibodies().values()) {
			if (abLevel > 0) {
				firstImmunization = false;
				break;
			}
		}
		return firstImmunization;
	}

}