Transition.java
package org.matsim.episim.model;
import com.google.common.base.Objects;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
import org.apache.commons.math3.util.FastMath;
import org.matsim.episim.EpisimPerson.DiseaseStatus;
import org.matsim.episim.EpisimUtils;
import java.util.*;
/**
* Describes how long a person stays in a certain state.
* Also provides factory methods for all available transitions.
* <p>
* Please note that it is not possible nor intended to inherit from this class outside of this package,
* as this would break serialization.
*/
public abstract class Transition {
/**
* Inheritance is prohibited for external classes.
*/
Transition() {
}
/**
* Parse Transition builder from a config file.
*/
public static Builder parse(Config config) {
return new Builder(config);
}
/**
* Create a new transition config builder.
*/
public static Builder config() {
return new Builder((String) null);
}
/**
* Create a new transition config builder with a filename, that will be used if the config is persisted.
*/
public static Builder config(String filename) {
return new Builder(filename);
}
/**
* Creates a to transition, to be used in conjunction with the {@link Builder}.
*
* @param status target state
* @param t desired transition
*/
public static ToHolder to(DiseaseStatus status, Transition t) {
return new ToHolder(status, t);
}
/**
* Deterministic transition at day {@code day}.
*/
public static Transition fixed(int day) {
return new FixedTransition(day);
}
/**
* Probabilistic transition with log normal distribution.
* Parameter of the distribution will be calculated from given mean und standard deviation.
*
* @param mean desired mean of the distribution
* @param std desired standard deviation
* @see LogNormalTransition
*/
public static Transition logNormalWithMean(double mean, double std) {
// mean==median if std=0
if (std == 0) return logNormalWithMedianAndSigma(mean, 0);
double mu = Math.log((mean * mean) / Math.sqrt(mean * mean + std * std));
double sigma = Math.log(1 + (std * std) / (mean * mean));
return new LogNormalTransition(mu, Math.sqrt(sigma));
}
/**
* Same as {@link #logNormalWithMean(double, double)}.
*/
public static Transition logNormalWithMeanAndStd(double mean, double std) {
return logNormalWithMean(mean, std);
}
/**
* Probabilistic state transition with log normal distribution.
*
* @param median desired median, i.e. exp(mu)
* @param sigma sigma parameter
* @see LogNormalTransition
*/
public static Transition logNormalWithMedianAndSigma(double median, double sigma) {
double mu = Math.log(median);
return new LogNormalTransition(mu, sigma);
}
/**
* Probabilistic state transition with log normal distribution.
*
* @param median desired median, i.e. exp(mu)
* @param std desired standard deviation
* @see LogNormalTransition
*/
public static Transition logNormalWithMedianAndStd(double median, double std) {
// equation below is numerical unstable for std near zero
if (std == 0) return logNormalWithMedianAndSigma(median, 0);
double mu = Math.log(median);
// Given the formula for std:
// \sqrt{\operatorname{Var}(X)}= \sqrt{\mathrm{e}^{2\mu+\sigma^{2}}(\mathrm{e}^{\sigma^{2}}-1)}=\mathrm{e}^{\mu+\frac{\sigma^{2}}{2}}\cdot\sqrt{\mathrm{e}^{\sigma^{2}}-1}
// solve for sigma
// https://www.wolframalpha.com/input/?i=solve+e%5E%28mu+%2B+s+%2F+2%29+*+sqrt%28e%5Es+-+1%29+%3D+x+for+s
double ssigma = Math.log(0.5 * Math.exp(-2 * mu) * (Math.exp(2 * mu) + Math.sqrt(Math.exp(4 * mu) + 4 * Math.exp(2 * mu) * std * std)));
return new LogNormalTransition(mu, Math.sqrt(ssigma));
}
/**
* Returns the day when the transition should occur.
*/
public abstract int getTransitionDay(SplittableRandom rnd);
/**
* Implementation for a fixed transition.
*/
private static final class FixedTransition extends Transition {
private final int day;
private FixedTransition(int day) {
this.day = day;
}
@Override
public int getTransitionDay(SplittableRandom rnd) {
return day;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FixedTransition that = (FixedTransition) o;
return day == that.day;
}
@Override
public int hashCode() {
return Objects.hashCode(day);
}
}
/**
* Implementation for log normal distributed transition.
*
* @see EpisimUtils#nextLogNormal(SplittableRandom, double, double)
*/
private static final class LogNormalTransition extends Transition {
private final double mu;
private final double sigma;
private LogNormalTransition(double mu, double sigma) {
this.mu = mu;
this.sigma = sigma;
if (sigma < 0 || Double.isNaN(sigma))
throw new IllegalArgumentException("Sigma must be >= 0");
}
@Override
public int getTransitionDay(SplittableRandom rnd) {
return (int) FastMath.round(EpisimUtils.nextLogNormal(rnd, mu, sigma));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LogNormalTransition that = (LogNormalTransition) o;
return Double.compare(that.mu, mu) == 0 &&
Double.compare(that.sigma, sigma) == 0;
}
@Override
public int hashCode() {
return Objects.hashCode(mu, sigma);
}
}
/**
* Builder for a transition config.
*/
public static final class Builder {
private final String origin;
private final Map<DiseaseStatus, Map<DiseaseStatus, Transition>> transitions = new EnumMap<>(DiseaseStatus.class);
private Builder(String origin) {
this.origin = origin;
}
/**
* Initialize from config.
*/
@SuppressWarnings("unchecked")
private Builder(Config config) {
for (Map.Entry<String, ConfigValue> e : config.root().entrySet()) {
DiseaseStatus status = DiseaseStatus.valueOf(e.getKey());
Config toConfig = config.getConfig(e.getKey());
List<ToHolder> tos = new ArrayList<>();
for (Map.Entry<String, ConfigValue> to : toConfig.root().entrySet()) {
Map<String, String> params = (Map<String, String>) to.getValue().unwrapped();
DiseaseStatus toStatus = DiseaseStatus.valueOf(to.getKey());
Transition t;
if (params.get("type").equals("FixedTransition"))
t = new FixedTransition(Integer.parseInt(params.get("day")));
else if (params.get("type").equals("LogNormalTransition"))
t = new LogNormalTransition(Double.parseDouble(params.get("mu")), Double.parseDouble(params.get("sigma")));
else
throw new IllegalArgumentException("Could not parse transition: " + params);
tos.add(to(toStatus, t));
}
from(status, tos.toArray(new ToHolder[0]));
}
this.origin = config.origin().description();
}
/**
* Defines which transitions should be taken from the state {@code} status to the states defined in {@code to}.
*
* @param status the current disease status
* @param to collection of target states and their transitions.
* @see #to(DiseaseStatus, Transition)
*/
public Builder from(DiseaseStatus status, ToHolder... to) {
if (to.length == 0) throw new IllegalArgumentException("No target states specified");
for (ToHolder t : to) {
transitions.computeIfAbsent(status, (k) -> new EnumMap<>(DiseaseStatus.class))
.put(t.status, t.t);
}
return this;
}
/**
* Creates a config representation.
*/
public Config build() {
Map<String, Object> config = new LinkedHashMap<>();
for (Map.Entry<DiseaseStatus, Map<DiseaseStatus, Transition>> e : transitions.entrySet()) {
Map<String, Object> toConfig = new LinkedHashMap<>();
for (Map.Entry<DiseaseStatus, Transition> to : e.getValue().entrySet()) {
// params of the transition
Map<String, String> params = new LinkedHashMap<>();
Transition t = to.getValue();
params.put("type", t.getClass().getSimpleName());
if (t instanceof FixedTransition) {
params.put("day", String.valueOf(((FixedTransition) t).day));
} else if (t instanceof LogNormalTransition) {
params.put("mu", String.valueOf(((LogNormalTransition) t).mu));
params.put("sigma", String.valueOf(((LogNormalTransition) t).sigma));
} else
throw new IllegalArgumentException("Can not serialize unknown transition " + t);
toConfig.put(to.getKey().name(), params);
}
config.put(e.getKey().name(), toConfig);
}
return ConfigFactory.parseMap(config, origin);
}
/**
* Returns the config as matrix with entries as transition from -> to, according to {@link DiseaseStatus#ordinal()}.
* Not defined transitions will be null.
*/
public Transition[] asArray() {
Transition[] array = new Transition[DiseaseStatus.values().length * DiseaseStatus.values().length];
for (Map.Entry<DiseaseStatus, Map<DiseaseStatus, Transition>> e : transitions.entrySet()) {
for (Map.Entry<DiseaseStatus, Transition> to : e.getValue().entrySet()) {
array[e.getKey().ordinal() * DiseaseStatus.values().length + to.getKey().ordinal()] = to.getValue();
}
}
return array;
}
@Override
public String toString() {
return build().root().render(ConfigRenderOptions.concise().setJson(false));
}
}
/**
* Holder class that saves the target status and desired transition.
*/
public static final class ToHolder {
public final DiseaseStatus status;
public final Transition t;
private ToHolder(DiseaseStatus status, Transition t) {
this.status = status;
this.t = t;
}
@Override
public String toString() {
return "ToHolder{" +
"status=" + status +
", t=" + t +
'}';
}
}
}