EpisimConfigGroup.java

/*-
 * #%L
 * MATSim Episim
 * %%
 * Copyright (C) 2020 matsim-org
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.matsim.episim;

import com.google.common.annotations.Beta;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.magnos.trie.Trie;
import org.magnos.trie.TrieMatch;
import org.magnos.trie.Tries;
import org.matsim.core.config.ConfigGroup;
import org.matsim.core.config.ReflectiveConfigGroup;
import org.matsim.episim.model.VirusStrain;
import org.matsim.episim.policy.Restriction;
import org.matsim.episim.policy.ShutdownPolicy;

import javax.validation.constraints.NotNull;
import java.io.File;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Main config for episim.
 */
public final class EpisimConfigGroup extends ReflectiveConfigGroup {

	private static final Splitter.MapSplitter SPLITTER = Splitter.on(";").withKeyValueSeparator("=");
	private static final Joiner.MapJoiner JOINER = Joiner.on(";").withKeyValueSeparator("=");

	private static final String WRITE_EVENTS = "writeEvents";
	private static final String CALIBRATION_PARAMETER = "calibrationParameter";
	private static final String HOSPITAL_FACTOR = "hospitalFactor";
	private static final String INITIAL_INFECTIONS = "initialInfections";
	private static final String INITIAL_INFECTION_DISTRICT = "initialInfectionDistrict";
	private static final String INFECTIONS_PER_DAY = "infectionsPerDay";
	private static final String LOWER_AGE_BOUNDARY_FOR_INIT_INFECTIONS = "lowerAgeBoundaryForInitInfections";
	private static final String UPPER_AGE_BOUNDARY_FOR_INIT_INFECTIONS = "upperAgeBoundaryForInitInfections";
	private static final String MAX_CONTACTS = "maxContacts";
	private static final String SAMPLE_SIZE = "sampleSize";
	private static final String START_DATE = "startDate";
	private static final String SNAPSHOT_INTERVAL = "snapshotInterval";
	private static final String START_FROM_SNAPSHOT = "startFromSnapshot";
	private static final String START_FROM_IMMUNIZATION = "startFromImmunization";
	private static final String SNAPSHOT_PREFIX = "snapshotPrefix";
	private static final String SNAPSHOT_SEED = "snapshotSeed";
	private static final String LEISUREOUTDOORFRACTION = "leisureOutdoorFraction";
	private static final String INPUT_DAYS = "inputDays";
	private static final String DAYS_INFECTIOUS = "daysInfectious";
	private static final String ACTIVITY_HANDLING = "activityHandling";
	private static final String THREADS = "threads";
	private static final String CURFEW_COMPLIANCE = "curfewCompliance";
	private static final String DISTRICT_LEVEL_RESTRICTIONS = "districtLevelRestrictions";
	private static final String DISTRICT_LEVEL_RESTRICTIONS_ATTRIBUTE = "districtLevelRestrictionsAttribute";
	private static final String CONTAGIOUS_CONTAINER_OPTIMIZATION = "contagiousContainerOptimization";
	private static final String REPORT_TIME_USE = "reportTimeUse";
	private static final String SINGLE_EVENT_FILE = "singleEventFile";
	private static final String END_EARLY = "endEarly";

	private static final Logger log = LogManager.getLogger(EpisimConfigGroup.class);
	private static final String GROUPNAME = "episim";

	private final Trie<String, InfectionParams> paramsTrie = Tries.forStrings();

	/**
	 * Number of initial infections per day.
	 * Default is 1 infection per day for {@link VirusStrain#SARS_CoV_2}.
	 */
	private final Map<VirusStrain, NavigableMap<LocalDate, Integer>> infectionsPerDay = new EnumMap<>(Map.of(VirusStrain.SARS_CoV_2, new TreeMap<>()));

	/**
	 * Leisure outdoor fractions per day.
	 */
	private final Map<LocalDate, Double> leisureOutdoorFraction = new TreeMap<>(Map.of(
			LocalDate.parse("2020-01-15"), 0.1,
			LocalDate.parse("2020-04-15"), 0.8,
			LocalDate.parse("2020-09-15"), 0.8,
			LocalDate.parse("2020-11-15"), 0.1,
			LocalDate.parse("2021-02-15"), 0.1,
			LocalDate.parse("2021-04-15"), 0.8,
			LocalDate.parse("2021-09-15"), 0.8)
	);

	/**
	 * Re-mapping of specific dates to different week days events.
	 */
	private final Map<LocalDate, DayOfWeek> inputDays = new HashMap<>();

	/**
	 * Which events to write in the output.
	 */
	private WriteEvents writeEvents = WriteEvents.episim;
	// this is current default for 25% scenarios
	private double calibrationParameter = 0.000002;
	private double hospitalFactor = 1.;
	private double sampleSize = 0.1;
	private int initialInfections = 10;
	private int lowerAgeBoundaryForInitInfections = -1;
	private int upperAgeBoundaryForInitInfections = -1;
	/**
	 * If not null, filter persons for initial infection by district.
	 */
	private String initialInfectionDistrict = null;
	/**
	 * Start date of the simulation (Day 1).
	 */
	private LocalDate startDate = LocalDate.of(1970, 1, 1);
	/**
	 * Offset of start date in unix epoch seconds.
	 */
	private long startOffset = 0;
	/**
	 * Write snapshot every x days.
	 */
	private int snapshotInterval = 0;
	/**
	 * Path to snapshot file.
	 */
	private String startFromSnapshot = null;
	private String startFromImmunization = null;

	/**
	 * Filename prefix for snapshot.
	 */
	private String snapshotPrefix = "episim-snapshot";

	/**
	 * How the internal rng state should be handled.
	 */
	private SnapshotSeed snapshotSeed = SnapshotSeed.restore;
	private FacilitiesHandling facilitiesHandling = FacilitiesHandling.snz;
	private ActivityHandling activityHandling = ActivityHandling.duringContact;
	private Config policyConfig = ConfigFactory.empty();
	private Config progressionConfig = ConfigFactory.empty();
	private String overwritePolicyLocation = null;
	private double maxContacts = 3.;
	private int daysInfectious = 4;
	private DistrictLevelRestrictions districtLevelRestrictions = DistrictLevelRestrictions.no;
	private String districtLevelRestrictionsAttribute = "";
	private ContagiousOptimization contagiousContainerOptimization = ContagiousOptimization.no;
	private ReportTimeUse reportTimeUse = ReportTimeUse.no;
	private SingleEventFile singleEventFile = SingleEventFile.yes;
	private boolean endEarly = false;
	private int threads = 2;


	/**
	 * Compliance if a curfew is set.
	 */
	private NavigableMap<LocalDate, Double> curfewCompliance = new TreeMap<>();

	/**
	 * Default constructor.
	 */
	public EpisimConfigGroup() {
		super(GROUPNAME);
	}

	public String getInputEventsFile() {
		List<EventFileParams> list = Lists.newArrayList(getInputEventsFiles());

		if (list.size() != 1) {
			throw new IllegalStateException("There is not exactly one input event file. Use .getEventFileParams() instead.");
		}

		return list.get(0).path;
	}

	/**
	 * Adds one single input event file for all days of the week.
	 */
	public void setInputEventsFile(String inputEventsFile) {

		clearParameterSetsForType(EventFileParams.SET_TYPE);
		addInputEventsFile(inputEventsFile)
				.addDays(DayOfWeek.values());
	}

	@StringSetter(THREADS)
	public void setThreads(int threads) {
		this.threads = threads;
	}

	@StringGetter(THREADS)
	public int getThreads() {
		return threads;
	}

	@StringGetter(WRITE_EVENTS)
	public WriteEvents getWriteEvents() {
		return writeEvents;
	}

	@StringSetter(WRITE_EVENTS)
	public void setWriteEvents(WriteEvents writeEvents) {
		this.writeEvents = writeEvents;
	}

	@StringGetter(CALIBRATION_PARAMETER)
	public double getCalibrationParameter() {
		return this.calibrationParameter;
	}

	@StringSetter(CALIBRATION_PARAMETER)
	public void setCalibrationParameter(double calibrationParameter) {
		this.calibrationParameter = calibrationParameter;
	}

	/**
	 * Is multiplied with probability to transition to seriously sick in age dependent progression model
	 */
	@StringGetter(HOSPITAL_FACTOR)
	public double getHospitalFactor() {
		return this.hospitalFactor;
	}

	@StringSetter(HOSPITAL_FACTOR)
	public void setHospitalFactor(double hospitalFactor) {
		this.hospitalFactor = hospitalFactor;
	}

	@StringGetter(INITIAL_INFECTIONS)
	public int getInitialInfections() {
		return this.initialInfections;
	}

	/**
	 * @param initialInfections -- number of initial infections to start the dynamics.  These will be distributed over several days.
	 * @see #setInfections_pers_per_day(Map)
	 */
	@StringSetter(INITIAL_INFECTIONS)
	public void setInitialInfections(int initialInfections) {
		this.initialInfections = initialInfections;
	}

	@StringGetter(LOWER_AGE_BOUNDARY_FOR_INIT_INFECTIONS)
	public int getLowerAgeBoundaryForInitInfections() {
		return this.lowerAgeBoundaryForInitInfections;
	}

	@StringSetter(LOWER_AGE_BOUNDARY_FOR_INIT_INFECTIONS)
	public void setLowerAgeBoundaryForInitInfections(int lowerAgeBoundaryForInitInfections) {
		this.lowerAgeBoundaryForInitInfections = lowerAgeBoundaryForInitInfections;
	}

	@StringGetter(UPPER_AGE_BOUNDARY_FOR_INIT_INFECTIONS)
	public int getUpperAgeBoundaryForInitInfections() {
		return this.upperAgeBoundaryForInitInfections;
	}

	@StringSetter(UPPER_AGE_BOUNDARY_FOR_INIT_INFECTIONS)
	public void setUpperAgeBoundaryForInitInfections(int upperAgeBoundaryForInitInfections) {
		this.upperAgeBoundaryForInitInfections = upperAgeBoundaryForInitInfections;
	}

	public Map<VirusStrain, NavigableMap<LocalDate, Integer>> getInfections_pers_per_day() {
		return infectionsPerDay;
	}

	/**
	 * @param infectionsPerDay -- From each given date, this will be the number of infections.  Until {@link #setInitialInfections(int)} are used up.
	 */
	public void setInfections_pers_per_day(Map<LocalDate, Integer> infectionsPerDay) {
		this.setInfections_pers_per_day(VirusStrain.SARS_CoV_2, infectionsPerDay);
	}

	public void setInfections_pers_per_day(VirusStrain strain, Map<LocalDate, Integer> infectionsPerDay) {

		Map<LocalDate, Integer> perDay = this.infectionsPerDay.computeIfAbsent(strain, (k) -> new TreeMap<>());

		// yyyy Is it really so plausible to have this here _and_ the plain integer initial infections?  kai, oct'20
		// yyyyyy Is it correct that the default of this is empty, so even if someone sets the initial infections to some number, this will not have any effect?  kai, nov'20
		// No, If no entry is present, 1 will be assumed (because this was default at some point).
		// This logic of handling no entries is not part of the config, but the initial infection handler  - cr, nov'20
		perDay.clear();
		perDay.putAll(infectionsPerDay);
	}

	@StringGetter(INFECTIONS_PER_DAY)
	String getInfectionsPerDay() {
		Map<VirusStrain, String> collect =
				infectionsPerDay.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> JOINER.join(e.getValue())));

		return Joiner.on("|").withKeyValueSeparator(">").join(collect);
	}

	@StringSetter(INFECTIONS_PER_DAY)
	void setInfectionsPerDay(String capacity) {

		Map<String, String> cap = Splitter.on("|").withKeyValueSeparator(">").split(capacity);

		for (Map.Entry<String, String> v : cap.entrySet()) {

			if (v.getValue().isBlank()) {
				setInfections_pers_per_day(VirusStrain.valueOf(v.getKey()), new TreeMap<>());
				continue;
			}

			Map<String, String> map = SPLITTER.split(v.getValue());
			setInfections_pers_per_day(VirusStrain.valueOf(v.getKey()), map.entrySet().stream().collect(Collectors.toMap(
					e -> LocalDate.parse(e.getKey()), e -> Integer.parseInt(e.getValue())
			)));
		}
	}

	@StringGetter(INITIAL_INFECTION_DISTRICT)
	public String getInitialInfectionDistrict() {
		return initialInfectionDistrict;
	}

	@StringSetter(INITIAL_INFECTION_DISTRICT)
	public void setInitialInfectionDistrict(String initialInfectionDistrict) {
		this.initialInfectionDistrict = initialInfectionDistrict;
	}

	@StringGetter(START_DATE)
	public LocalDate getStartDate() {
		return startDate;
	}

	@StringSetter(START_DATE)
	public void setStartDate(String startDate) {
		setStartDate(LocalDate.parse(startDate));
	}

	public void setStartDate(LocalDate startDate) {
		this.startDate = startDate;
		this.startOffset = EpisimUtils.getStartOffset(startDate);
	}

	@StringGetter(SNAPSHOT_INTERVAL)
	public int getSnapshotInterval() {
		return snapshotInterval;
	}

	@StringSetter(SNAPSHOT_INTERVAL)
	public void setSnapshotInterval(int snapshotInterval) {
		this.snapshotInterval = snapshotInterval;
	}

	@StringGetter(START_FROM_SNAPSHOT)
	public String getStartFromSnapshot() {
		return startFromSnapshot;
	}

	@StringSetter(START_FROM_SNAPSHOT)
	public void setStartFromSnapshot(String startFromSnapshot) {
		this.startFromSnapshot = startFromSnapshot;
	}

	@StringGetter(START_FROM_IMMUNIZATION)
	public String getStartFromImmunization() {
		return startFromImmunization;
	}

	@StringSetter(START_FROM_IMMUNIZATION)
	public void setStartFromImmunization(String startFromImmunization) {
		this.startFromImmunization = startFromImmunization;
	}

	@StringGetter(SNAPSHOT_PREFIX)
	public String getSnapshotPrefix() {
		return snapshotPrefix;
	}

	@StringSetter(SNAPSHOT_PREFIX)
	public void setSnapshotPrefix(String snapshotPrefix) {
		this.snapshotPrefix = snapshotPrefix;
	}

	@StringGetter(SNAPSHOT_SEED)
	public SnapshotSeed getSnapshotSeed() {
		return snapshotSeed;
	}

	@StringSetter(SNAPSHOT_SEED)
	public void setSnapshotSeed(SnapshotSeed snapshotSeed) {
		this.snapshotSeed = snapshotSeed;
	}

	public long getStartOffset() {
		return startOffset;
	}

	/**
	 * Sample size in relation to whole population, between (0, 1].
	 */
	@StringGetter(SAMPLE_SIZE)
	public double getSampleSize() {
		return sampleSize;
	}

	@StringSetter(SAMPLE_SIZE)
	public void setSampleSize(double sampleSize) {
		this.sampleSize = sampleSize;
	}

	@StringGetter("policyConfig")
	public String getPolicyConfig() {
		if (overwritePolicyLocation != null)
			return overwritePolicyLocation;

		return policyConfig.origin().filename();
	}

	/**
	 * Set the policy config instance.
	 */
	public void setPolicyConfig(Config policyConfig) {
		this.policyConfig = policyConfig;
	}

	/**
	 * Sets policy config by loading it from a file first.
	 *
	 * @param policyConfig resource of filename to policy
	 */
	@StringSetter("policyConfig")
	public void setPolicyConfig(String policyConfig) {
		if (policyConfig == null)
			this.policyConfig = ConfigFactory.empty();
		else {
			File file = new File(policyConfig);
			if (!policyConfig.equals("null") && !file.exists())
				throw new IllegalArgumentException("Policy config does not exist: " + policyConfig);
			this.policyConfig = ConfigFactory.parseFileAnySyntax(file);
		}
	}

	/**
	 * Overwrite the policy location, which will be returned by {@link #getPolicyConfig()}.
	 */
	public void setOverwritePolicyLocation(String overwritePolicyLocation) {
		this.overwritePolicyLocation = overwritePolicyLocation;
	}

	/**
	 * Gets the actual policy configuration.
	 */
	public Config getPolicy() {
		return policyConfig;
	}

	/**
	 * Sets policy class and desired config.
	 * @deprecated set policy class via guice.
	 * @see #setPolicy(Config)
	 */
	@Deprecated
	public void setPolicy(Class<? extends ShutdownPolicy> policy, Config config) {
		this.policyConfig = config;
	}

	/**
	 * Sets policy config.
	 */
	public void setPolicy(Config config) {
		this.policyConfig = config;
	}

	@StringGetter("progressionConfig")
	public String getProgressionConfigName() {
		if (progressionConfig.origin().filename() != null)
			return progressionConfig.origin().filename();

		return progressionConfig.origin().description();
	}

	/**
	 * Gets the progression config configuration.
	 */
	public Config getProgressionConfig() {
		return progressionConfig;
	}

	public void setProgressionConfig(Config progressionConfig) {
		this.progressionConfig = progressionConfig;
	}

	/**
	 * Sets the progression config location as file name.
	 */
	@StringSetter("progressionConfig")
	public void setProgressionConfig(String progressionConfig) {
		if (progressionConfig == null)
			this.progressionConfig = ConfigFactory.empty();
		else {
			File file = new File(progressionConfig);
			if (!progressionConfig.equals("null") && !file.exists())
				throw new IllegalArgumentException("Progression config does not exist: " + progressionConfig);
			this.progressionConfig = ConfigFactory.parseFileAnySyntax(file);
		}
	}

	@StringGetter(MAX_CONTACTS)
	public double getMaxContacts() {
		return maxContacts;
	}

	@StringSetter(MAX_CONTACTS)
	public void setMaxContacts(double maxContacts) {
		this.maxContacts = maxContacts;
	}

	@StringGetter(DAYS_INFECTIOUS)
	public int getDaysInfectious() {
		return daysInfectious;
	}

	@StringSetter(DAYS_INFECTIOUS)
	public void setDaysInfectious(int daysInfectious) {
		this.daysInfectious = daysInfectious;
	}

	/**
	 * Sets the leisure outdoor fraction for the whole simulation period.
	 */
	public void setLeisureOutdoorFraction(double fraction) {
		setLeisureOutdoorFraction(Map.of(LocalDate.of(1970, 1, 1), fraction));
	}

	/**
	 * Sets the leisure outdoor fraction for individual days. If a day has no entry the values will be interpolated.
	 */
	public void setLeisureOutdoorFraction(Map<LocalDate, Double> fraction) {
		leisureOutdoorFraction.clear();
		leisureOutdoorFraction.putAll(fraction);
	}

	public Map<LocalDate, Double> getLeisureOutdoorFraction() {
		return leisureOutdoorFraction;
	}

	@StringSetter(LEISUREOUTDOORFRACTION)
	void setLeisureOutdoorFraction(String capacity) {

		Map<String, String> map = SPLITTER.split(capacity);
		setLeisureOutdoorFraction(map.entrySet().stream().collect(Collectors.toMap(
				e -> LocalDate.parse(e.getKey()), e -> Double.parseDouble(e.getValue())
		)));
	}

	@StringGetter(LEISUREOUTDOORFRACTION)
	String getLeisureOutdoorFractionString() {
		return JOINER.join(leisureOutdoorFraction);
	}


	public NavigableMap<LocalDate, Double> getCurfewCompliance() {
		return curfewCompliance;
	}

	public void setCurfewCompliance(Map<LocalDate, Double> curfewCompliance) {
		this.curfewCompliance.clear();
		this.curfewCompliance.putAll(curfewCompliance);
	}

	@StringGetter(CURFEW_COMPLIANCE)
	String getCurfewComplianceString() {
		return JOINER.join(curfewCompliance);
	}

	@StringSetter(CURFEW_COMPLIANCE)
	void setCurfewCompliance(String config) {
		Map<String, String> map = SPLITTER.split(config);
		setCurfewCompliance(map.entrySet().stream().collect(Collectors.toMap(
				e -> LocalDate.parse(e.getKey()), e -> Double.parseDouble(e.getValue())
		)));
	}

	/**
	 * Get remapping of input days.
	 */
	public Map<LocalDate, DayOfWeek> getInputDays() {
		return inputDays;
	}

	public void setInputDays(Map<LocalDate, DayOfWeek> days) {
		this.inputDays.clear();
		this.inputDays.putAll(days);
	}

	@StringSetter(INPUT_DAYS)
	void setInputDays(String days) {
		Map<String, String> map = SPLITTER.split(days);
		setInputDays(map.entrySet().stream().collect(Collectors.toMap(
				e -> LocalDate.parse(e.getKey()), e -> DayOfWeek.valueOf(e.getValue())
		)));
	}

	@StringGetter(INPUT_DAYS)
	String getInputDaysString() {
		return JOINER.join(inputDays);
	}


	/**
	 * Create restriction for each {@link InfectionParams}.
	 */
	public Map<String, Restriction> createInitialRestrictions() {
		Map<String, Restriction> r = new LinkedHashMap<>();
		getContainerParams().forEach((s, p) -> r.put(s, Restriction.none()));
		return r;
	}

	@StringGetter("facilitiesHandling")
	public FacilitiesHandling getFacilitiesHandling() {
		return facilitiesHandling;
	}

	@StringSetter("facilitiesHandling")
	public void setFacilitiesHandling(FacilitiesHandling facilitiesHandling) {
		this.facilitiesHandling = facilitiesHandling;
	}


	@StringGetter(ACTIVITY_HANDLING)
	public ActivityHandling getActivityHandling() {
		return activityHandling;
	}

	@StringSetter(ACTIVITY_HANDLING)
	public void setActivityHandling(ActivityHandling activityHandling) {
		this.activityHandling = activityHandling;
	}

	/**
	 * District level restrictions for location based restrictions;
	 */
	@StringGetter(DISTRICT_LEVEL_RESTRICTIONS)
	public DistrictLevelRestrictions getDistrictLevelRestrictions() {
		return this.districtLevelRestrictions;
	}

	@StringSetter(DISTRICT_LEVEL_RESTRICTIONS)
	public void setDistrictLevelRestrictions(DistrictLevelRestrictions districtLevelRestrictions) {
		this.districtLevelRestrictions = districtLevelRestrictions;
	}

	@StringGetter(DISTRICT_LEVEL_RESTRICTIONS_ATTRIBUTE)
	public String getDistrictLevelRestrictionsAttribute() {
		return this.districtLevelRestrictionsAttribute;
	}

	@StringSetter(DISTRICT_LEVEL_RESTRICTIONS_ATTRIBUTE)
	public void setDistrictLevelRestrictionsAttribute(String districtLevelRestrictionsAttribute) {
		this.districtLevelRestrictionsAttribute = districtLevelRestrictionsAttribute;
	}

	@StringGetter(CONTAGIOUS_CONTAINER_OPTIMIZATION)
	public ContagiousOptimization getContagiousOptimization() {
		return this.contagiousContainerOptimization;
	}

	@StringSetter(CONTAGIOUS_CONTAINER_OPTIMIZATION)
	public void setContagiousOptimization(ContagiousOptimization contagiousOptimization) {
		this.contagiousContainerOptimization = contagiousOptimization;
	}

	@StringGetter(SINGLE_EVENT_FILE)
	public SingleEventFile getSingleEventFile() {
		return singleEventFile;
	}

	@StringSetter(SINGLE_EVENT_FILE)
	public void setSingleEventFile(SingleEventFile singleEventFile) {
		this.singleEventFile = singleEventFile;
	}

	@StringGetter(REPORT_TIME_USE)
	public ReportTimeUse getReportTimeUse() {
		return reportTimeUse;
	}

	@StringSetter(REPORT_TIME_USE)
	public void setReportTimeUse(ReportTimeUse reportTimeUse) {
		this.reportTimeUse = reportTimeUse;
	}


	@Override
	public void addParameterSet(final ConfigGroup set) {
		// this is, I think, necessary for the automatic reading from file, and possibly for the commandline stuff.
		switch (set.getName()) {
			case InfectionParams.SET_TYPE:
				addContainerParams((InfectionParams) set);
				break;
			case EventFileParams.SET_TYPE:
				super.addParameterSet(set);
				break;
			default:
				throw new IllegalArgumentException(set.getName());
		}
	}

	@Override
	public ConfigGroup createParameterSet(final String type) {
		switch (type) {
			case InfectionParams.SET_TYPE:
				return new InfectionParams();
			case EventFileParams.SET_TYPE:
				return new EventFileParams();
			default:
				throw new IllegalArgumentException(type);
		}
	}

	@Override
	protected void checkParameterSet(final ConfigGroup module) {
		switch (module.getName()) {
			case InfectionParams.SET_TYPE:
				if (!(module instanceof InfectionParams)) {
					throw new IllegalArgumentException("unexpected class for module " + module);
				}
				break;
			case EventFileParams.SET_TYPE:
				if (!(module instanceof EventFileParams)) {
					throw new IllegalArgumentException("unexpected class for module " + module);
				}
				break;
			default:
				throw new IllegalArgumentException(module.getName());
		}
	}

	/**
	 * Adds given params to the parameter set, replacing existing ones.
	 */
	public void addContainerParams(final InfectionParams params) {
		final InfectionParams previous = this.getContainerParams().get(params.getContainerName());

		Optional<String> match = params.mappedNames.stream().filter(s -> paramsTrie.get(s, TrieMatch.STARTS_WITH) != null).findAny();
		if (match.isPresent()) {
			throw new IllegalArgumentException("New param for " + match.get() + " matches one of the already present params. Try to define the params in different order.");
		}

		params.mappedNames.forEach(name -> paramsTrie.put(name, params));

		if (previous != null) {
			log.info("scoring parameters for activityType=" + previous.getContainerName() + " were just replaced.");

			params.mappedNames.forEach(paramsTrie::remove);

			final boolean removed = removeParameterSet(previous);
			if (!removed)
				throw new IllegalStateException("problem replacing params");
		}

		super.addParameterSet(params);
	}

	/**
	 * Returns a container from the parameter set if it exists or creates a new one.
	 */
	public InfectionParams getOrAddContainerParams(final String containerName, String... mappedNames) {
		InfectionParams params = this.getContainerParams().get(containerName);

		if (params != null)
			return params;

		params = new InfectionParams(containerName, mappedNames);

		addParameterSet(params);
		return params;
	}

	/**
	 * Adds an event file to the config.
	 */
	public EventFileParams addInputEventsFile(final String path) {

		for (EventFileParams f : getInputEventsFiles()) {
			if (f.path.equals(path)) throw new IllegalArgumentException("Input file already defined: " + path);
		}

		EventFileParams params = new EventFileParams(path);
		addParameterSet(params);
		return params;
	}

	/**
	 * Removes all defined input files.
	 */
	public void clearInputEventsFiles() {
		clearParameterSetsForType(EventFileParams.SET_TYPE);
	}

	/**
	 * Get a copy of container params. Don't use this heavily, it is slow because a new map is created every time.
	 */
	Map<String, InfectionParams> getContainerParams() {
		@SuppressWarnings("unchecked") final Collection<InfectionParams> parameters = (Collection<InfectionParams>) getParameterSets(InfectionParams.SET_TYPE);
		final Map<String, InfectionParams> map = new LinkedHashMap<>();

		for (InfectionParams pars : parameters) {
			if (this.isLocked()) {
				pars.setLocked();
			}
			map.put(pars.getContainerName(), pars);
		}

		return Collections.unmodifiableMap(map);
	}

	/**
	 * Lookup which infection param is relevant for an activity. Throws exception when none was found.
	 *
	 * @param activity full activity identifier (including id etc.)
	 * @return matched infection param
	 * @throws NoSuchElementException when no param could be matched
	 */
	@NotNull
	public InfectionParams selectInfectionParams(String activity) {

		InfectionParams params = paramsTrie.get(activity, TrieMatch.STARTS_WITH);
		if (params != null)
			return params;

		throw new NoSuchElementException(String.format("No params known for activity %s. Please add prefix to one infection parameter.", activity));
	}

	/**
	 * Get the {@link InfectionParams} of a container by its name.
	 */
	public InfectionParams getInfectionParam(String containerName) {
		return this.getContainerParams().get(containerName);
	}

	/**
	 * All defined infection parameter.
	 */
	public Collection<InfectionParams> getInfectionParams() {
		return (Collection<InfectionParams>) getParameterSets(InfectionParams.SET_TYPE);
	}

	/**
	 * All defined input event files.
	 */
	public Collection<EventFileParams> getInputEventsFiles() {
		return (Collection<EventFileParams>) getParameterSets(EventFileParams.SET_TYPE);
	}

	/**
	 * Whether simulation ends when there are no further infected persons.
	 */
	@StringGetter(END_EARLY)
	public boolean isEndEarly() {
		return endEarly;
	}

	@StringSetter(END_EARLY)
	public void setEndEarly(boolean endEarly) {
		this.endEarly = endEarly;
	}

	/**
	 * Defines how facilities should be handled.
	 */
	public enum FacilitiesHandling {
		/**
		 * A facility id will be constructed using the link id where the activity is performed.
		 */
		bln,
		/**
		 * Facilities ids of activities will be used directly.
		 */
		snz
	}

	/**
	 * Defines which events will be written.
	 */
	public enum WriteEvents {
		/**
		 * Disable event writing completely.
		 */
		none,
		/**
		 * Write basic events like infections or disease status change.
		 */
		episim,

		/**
		 * Write the input event each day.
		 */
		input,

		/**
		 * Write additional contact tracing events.
		 */
		tracing,
		/**
		 * Write all, including input events.
		 */
		all
	}

	/**
	 * Defines how the snapshot seed should be processed.
	 */
	public enum SnapshotSeed {
		/**
		 * Restore rng state from the snapshot and continue as before.
		 */
		restore,

		/**
		 * Overwrite the rng state with a new seed taken from config.
		 */
		reseed,
	}

	/**
	 * Defines how activity participation is handled.
	 */
	public enum ActivityHandling {

		/**
		 * Activity participation is randdom during each contact.
		 */
		duringContact,

		/**
		 * Activity participation is fixed at start of the day.
		 */
		startOfDay

	}


	/**
	 * Decides whether location based restrictions should be implemented
	 */
	public enum DistrictLevelRestrictions {
		yes,
		no
	}


    /**
     * In the case that this optimization is enabled, the infectionDynamics
     * methods are only called, if a contagious person is in the container
     */
	public enum ContagiousOptimization {
		yes,
		no
	}

	/**
	 * The used time tracking costs a lot of CPU cycles, so this
     * can be disabled with
     */
	public enum ReportTimeUse {
		yes,
		no
	}


	/**
	 * Whether to write all events into a single file.
	 */
	public enum SingleEventFile {
		yes,
		no
	}

	/**
	 * Parameter set for one activity type.
	 */
	public static final class InfectionParams extends ReflectiveConfigGroup {
		public static final String ACTIVITY_TYPE = "activityType";
		public static final String CONTACT_INTENSITY = "contactIntensity";
		public static final String SPACES_PER_FACILITY = "nSpacesPerFacility";
		public static final String SEASONALITY = "seasonal";
		public static final String MAPPED_NAMES = "mappedNames";

		static final String SET_TYPE = "infectionParams";
		/**
		 * Name of the container as reference by {@link ShutdownPolicy}.
		 */
		private String containerName;

		/**
		 * Prefixes of activity names that will be associated with this container type.
		 */
		private Set<String> mappedNames;
		private double contactIntensity = 1.;

		/**
		 * Typical number of distinct spaces per facility.
		 */
		private double spacesPerFacility = 20.;

		/**
		 * If 1.0, activity type is seasonal; 0.0 means it is not.
		 */
		private double seasonality = 0.0;

		/**
		 * See {@link #InfectionParams(String, String...)}. Name itself will also be used as prefix.
		 */
		InfectionParams(final String containerName) {
			this();
			this.containerName = containerName;
			this.mappedNames = Sets.newHashSet(containerName);
		}

		/**
		 * Constructor.
		 *
		 * @param containerName name of this activity type
		 * @param mappedNames   activity prefixes that will also be mapped to this container
		 */
		InfectionParams(final String containerName, String... mappedNames) {
			this();
			this.containerName = containerName;
			this.mappedNames = mappedNames.length == 0 ?
					Sets.newHashSet(containerName) : Sets.newHashSet(mappedNames);
		}

		private InfectionParams() {
			super(SET_TYPE);
		}

		@StringGetter(MAPPED_NAMES)
		public String getMappedNames() {
			return Joiner.on(",").join(mappedNames);
		}

		@StringSetter(MAPPED_NAMES)
		void setMappedNames(String mappedNames) {
			this.mappedNames = Sets.newHashSet(mappedNames.split(","));
		}

		@StringGetter(ACTIVITY_TYPE)
		public String getContainerName() {
			return containerName;
		}

		@StringSetter(ACTIVITY_TYPE)
		void setContainerName(String actType) {
			this.containerName = actType;
		}

		/**
		 * this is from iteration 0!
		 */
		@StringGetter(CONTACT_INTENSITY)
		public double getContactIntensity() {
			return contactIntensity;
		}

		/**
		 * this is from iteration 0!
		 **/
		@StringSetter(CONTACT_INTENSITY)
		public InfectionParams setContactIntensity(double contactIntensity) {
			this.contactIntensity = contactIntensity;
			return this;
		}

		/**
		 * Returns the spaces for facilities.
		 *
		 * @implNote Don't use this yet, may be removed or renamed.
		 */
		@Beta
		@StringGetter(SPACES_PER_FACILITY)
		public double getSpacesPerFacility() {
			return spacesPerFacility;
		}

		@Beta
		@StringSetter(SPACES_PER_FACILITY)
		public InfectionParams setSpacesPerFacility(double nSpacesPerFacility) {
			this.spacesPerFacility = nSpacesPerFacility;
			return this;
		}

		@StringSetter(SEASONALITY)
		public InfectionParams setSeasonality(double seasonality) {
			this.seasonality = seasonality;
			return this;
		}

		/**
		 * The extent of an activity's seasonal effects
		 */
		@StringGetter(SEASONALITY)
		public double getSeasonality() {
			return seasonality;
		}

		/**
		 * Check whether an activity belong to this container group.
		 */
		public boolean includesActivity(String actType) {
			for (String mapped : mappedNames)
				if (actType.startsWith(mapped))
					return true;

			return false;
		}

	}

	/**
	 * Event file configuration for certain weekdays.
	 */
	public static final class EventFileParams extends ReflectiveConfigGroup {

		public static final String DAYS = "days";
		public static final String PATH = "path";
		static final String SET_TYPE = "eventFiles";
		private final Set<DayOfWeek> days = EnumSet.noneOf(DayOfWeek.class);
		private String path;

		EventFileParams(String path) {
			this();
			this.path = path;
		}

		private EventFileParams() {
			super(SET_TYPE);
		}

		@StringGetter(PATH)
		public String getPath() {
			return path;
		}

		@StringSetter(PATH)
		void setPath(String path) {
			this.path = path;
		}

		/**
		 * Adds week days when this event file should be used.
		 */
		public void addDays(DayOfWeek... days) {
			this.days.addAll(Arrays.asList(days));
		}

		@StringGetter(DAYS)
		public Set<DayOfWeek> getDays() {
			return days;
		}

		@StringSetter(DAYS)
		public void setDays(String days) {
			String str = days.replace("[", "").replace(" ", "").replace("]", "");

			this.days.addAll(
					Arrays.stream(str.split(",")).map(DayOfWeek::valueOf).collect(Collectors.toSet())
			);
		}

		@Override
		public boolean equals(Object o) {
			if (this == o) return true;
			if (o == null || getClass() != o.getClass()) return false;
			EventFileParams that = (EventFileParams) o;
			return path.equals(that.path) &&
					days.equals(that.days);
		}

		@Override
		public int hashCode() {
			return Objects.hash(path, days);
		}
	}
}