AdaptivePolicy.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.policy;
import com.google.common.collect.ImmutableMap;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigValue;
import it.unimi.dsi.fastutil.objects.*;
import org.matsim.episim.EpisimReporting;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This policy enforces restrictions based on the number of available intensive care beds
* and the number of persons that are in critical health state.
*/
public class AdaptivePolicy extends ShutdownPolicy {
/**
* Amount of days incidence has to stay below the trigger to lift restrictions.
*/
private static final int INTERVAL_DAY = 14;
/**
* Incidences triggers for configured activities.
*/
private final Config incidenceTriggers;
/**
* Policy applied at the start.
*/
private final Config initialPolicy;
/**
* Policy when shutdown is in effect.
*/
private final Config restrictedPolicy;
/**
* Policy when everything is open.
*/
private final Config openPolicy;
/**
* Store incidence for each day.
*/
private final Object2DoubleSortedMap<LocalDate> cumCases = new Object2DoubleAVLTreeMap<>();
/**
* Whether currently in lockdown.
*/
private final Object2BooleanMap<String> inLockdown = new Object2BooleanOpenHashMap<>();
/**
* Constructor from config.
*/
public AdaptivePolicy(Config config) {
super(config);
incidenceTriggers = config.getConfig("incidences");
restrictedPolicy = config.getConfig("restricted-policy");
openPolicy = config.getConfig("open-policy");
initialPolicy = config.hasPath("init-policy") ? config.getConfig("init-policy") : null;
}
/**
* Create a config builder for {@link AdaptivePolicy}.
*/
public static ConfigBuilder config() {
return new ConfigBuilder();
}
@Override
public void init(LocalDate start, ImmutableMap<String, Restriction> restrictions) {
if (initialPolicy != null && !initialPolicy.isEmpty()) {
for (Map.Entry<String, Restriction> e : restrictions.entrySet()) {
updateRestrictions(start, initialPolicy, e.getKey(), e.getValue());
}
}
}
@Override
public void restore(LocalDate start, ImmutableMap<String, Restriction> restrictions) {
init(start, restrictions);
}
@Override
public void updateRestrictions(EpisimReporting.InfectionReport report, ImmutableMap<String, Restriction> restrictions) {
LocalDate date = LocalDate.parse(report.date);
calculateCases(report);
Object2DoubleSortedMap<LocalDate> cases = cumCases.tailMap(date.minus(INTERVAL_DAY + 6, ChronoUnit.DAYS));
Object2DoubleSortedMap<LocalDate> incidence = new Object2DoubleAVLTreeMap<>();
for (Object2DoubleMap.Entry<LocalDate> from : cases.object2DoubleEntrySet()) {
LocalDate until = from.getKey().plusDays(7);
if (cases.containsKey(until)) {
incidence.put(until, cases.getDouble(until) - cases.getDouble(from.getKey()));
} else
// if until was not contained, the next ones will not be either
break;
}
// for first 7 days, restrictions will stay the same
if (incidence.isEmpty())
return;
// TODO: use first incidence to decide whether in lockdown or not
for (Map.Entry<String, ConfigValue> e : incidenceTriggers.entrySet()) {
String act = e.getKey();
List<Double> trigger = (List<Double>) e.getValue().unwrapped();
if (inLockdown.getBoolean(act)) {
if (incidence.values().stream().allMatch(inc -> inc <= trigger.get(0))) {
updateRestrictions(date, openPolicy, act, restrictions.get(act));
inLockdown.put(act, false);
}
} else {
if (incidence.getDouble(incidence.lastKey()) >= trigger.get(1)) {
updateRestrictions(date, restrictedPolicy, act, restrictions.get(act));
inLockdown.put(act, true);
}
}
}
}
/**
* Calculate incidence depending
*/
private void calculateCases(EpisimReporting.InfectionReport report) {
double cases = report.nShowingSymptomsCumulative * (100_000d / report.nTotal());
this.cumCases.put(LocalDate.parse(report.date), cases);
}
private void updateRestrictions(LocalDate start, Config policy, String act, Restriction restriction) {
// activity name
if (!policy.hasPath(act))
return;
Config actConfig = policy.getConfig(act);
for (Map.Entry<String, ConfigValue> days : actConfig.root().entrySet()) {
if (days.getKey().startsWith("day")) continue;
LocalDate date = LocalDate.parse(days.getKey());
if (date.isBefore(start)) {
Restriction r = Restriction.fromConfig(actConfig.getConfig(days.getKey()));
restriction.update(r);
}
}
}
/**
* Config builder for {@link AdaptivePolicy}.
*/
@SuppressWarnings("unchecked")
public static final class ConfigBuilder extends ShutdownPolicy.ConfigBuilder<Object> {
/**
* Use {@link #AdaptivePolicy#config()}.
*/
private ConfigBuilder() {
params.put("start-in-lockdown", false);
}
/**
* Define trigger for weekly incidence and individual activities.
*/
public ConfigBuilder incidenceTrigger(double openAt, double restrictAt, String... activities) {
if (restrictAt < openAt)
throw new IllegalArgumentException("Restrict threshold must be larger than open threshold");
if (activities.length == 0)
throw new IllegalArgumentException("Activities can not be empty");
Map<String, List<Double>> incidences = (Map<String, List<Double>>) params.computeIfAbsent("incidences", (k) -> new HashMap<>());
for (String act : activities) {
incidences.put(act, List.of(openAt, restrictAt));
}
return this;
}
/**
* Set the initial policy that is always applied.
*/
public ConfigBuilder initialPolicy(FixedPolicy.ConfigBuilder policy) {
params.put("init-policy", policy.params);
return this;
}
/**
* See {@link AdaptivePolicy#openPolicy}.
*/
public ConfigBuilder openPolicy(FixedPolicy.ConfigBuilder policy) {
params.put("open-policy", policy.params);
return this;
}
/**
* See {@link AdaptivePolicy#restrictedPolicy}.
*/
public ConfigBuilder restrictedPolicy(FixedPolicy.ConfigBuilder policy) {
params.put("restricted-policy", policy.params);
return this;
}
}
}