TransmissionWeights.java
package org.matsim.episim.model;
import com.google.common.base.Splitter;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* Immutable weights for the transmission routes — currently respiratory and direct contact.
*
* <p>The same value type is used both as a configuration value (parsed from / serialized to the
* {@code route=weight;route=weight} notation) and as the runtime value handed to the infection model.
* Each weight is validated on construction to be finite and non-negative. The weights are relative and
* <b>not</b> normalised: routes are independent, additive exposure channels, so adding weight to one
* route does not take anything away from another. {@code respiratory=1.0} is the conventional anchor
* (see {@link #RESPIRATORY_ONLY}); other weights are read relative to it.</p>
*
* <p>The <b>fomite</b> route is temporarily disabled — there is no within-facility fomite contact
* model yet. Every line needed to bring it back is kept in place, commented with a {@code fomite:}
* marker; restore them together with the {@code FOMITE} constant in {@link ContactTransmissionType}.</p>
*/
public final class TransmissionWeights {
/** All-zero transmission weights. */
public static final TransmissionWeights ZERO = new TransmissionWeights(0, 0 /* fomite: , 0 */);
/** Respiratory-only transmission; the neutral element for both the contact and the pathogen axis. */
public static final TransmissionWeights RESPIRATORY_ONLY = new TransmissionWeights(1, 0 /* fomite: , 0 */);
private static final Splitter.MapSplitter SPLITTER = Splitter.on(";").withKeyValueSeparator("=");
private static final Map<String, ContactTransmissionType> ROUTE_BY_KEY;
static {
Map<String, ContactTransmissionType> routes = new HashMap<>();
routes.put("respiratory", ContactTransmissionType.RESPIRATORY);
routes.put("directcontact", ContactTransmissionType.DIRECT_CONTACT);
// fomite: routes.put("fomite", ContactTransmissionType.FOMITE);
for (ContactTransmissionType type : ContactTransmissionType.values()) {
routes.put(type.name().toLowerCase(Locale.ROOT), type);
}
ROUTE_BY_KEY = Collections.unmodifiableMap(routes);
}
private final double respiratory;
private final double directContact;
// fomite: private final double fomite;
/**
* Creates weights for the transmission routes.
*/
public TransmissionWeights(double respiratory, double directContact /* fomite: , double fomite */) {
this.respiratory = respiratory;
this.directContact = directContact;
// fomite: this.fomite = fomite;
validate();
}
/**
* Creates weights from a route map; omitted routes receive zero.
*/
public TransmissionWeights(Map<ContactTransmissionType, Double> weights) {
this(orZero(weights.get(ContactTransmissionType.RESPIRATORY)),
orZero(weights.get(ContactTransmissionType.DIRECT_CONTACT))
/* fomite: , orZero(weights.get(ContactTransmissionType.FOMITE)) */);
}
/**
* Parses the {@code route=weight;route=weight} notation, e.g. {@code respiratory=0.7;directContact=0.3}.
*/
public static TransmissionWeights parse(String encoded) {
requireNonNull(encoded, "transmissionWeights");
Map<String, String> entries;
try {
entries = SPLITTER.split(encoded);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid transmissionWeights value '" + encoded + "'.", e);
}
EnumMap<ContactTransmissionType, Double> weights = new EnumMap<>(ContactTransmissionType.class);
for (Map.Entry<String, String> entry : entries.entrySet()) {
ContactTransmissionType route = parseRoute(entry.getKey());
if (weights.containsKey(route)) {
throw new IllegalArgumentException("Duplicate transmission route '" + entry.getKey()
+ "' in '" + encoded + "'.");
}
try {
weights.put(route, Double.parseDouble(entry.getValue().trim()));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid transmission weight '" + entry.getValue()
+ "' in '" + encoded + "'.", e);
}
}
return new TransmissionWeights(weights);
}
/**
* Resolves a transmission route from its key (canonical key such as {@code directContact}, or the
* enum name, case-insensitive).
*/
public static ContactTransmissionType parseRoute(String key) {
ContactTransmissionType type = ROUTE_BY_KEY.get(key.trim().toLowerCase(Locale.ROOT));
if (type == null) {
throw new IllegalArgumentException("Unknown transmission route '" + key + "'.");
}
return type;
}
/** Weight of the respiratory route. */
public double getRespiratory() {
return respiratory;
}
/** Weight of the direct-contact route. */
public double getDirectContact() {
return directContact;
}
// fomite: /** Weight of the fomite (contaminated-surface) route. */
// fomite: public double getFomite() {
// fomite: return fomite;
// fomite: }
/**
* Returns the weight of the given route.
*/
public double get(ContactTransmissionType route) {
switch (requireNonNull(route, "transmission route")) {
case RESPIRATORY:
return respiratory;
case DIRECT_CONTACT:
return directContact;
// fomite: case FOMITE:
// fomite: return fomite;
default:
throw new IllegalArgumentException("Unhandled transmission route '" + route + "'.");
}
}
/**
* Returns the sum of all route weights.
*/
public double sum() {
return respiratory + directContact /* fomite: + fomite */;
}
/**
* Returns whether the weight sum is effectively zero.
*/
public boolean isZero() {
return sum() < 1e-12;
}
/**
* Returns a defensive enum-keyed map of all route weights.
*/
public EnumMap<ContactTransmissionType, Double> asMap() {
EnumMap<ContactTransmissionType, Double> result = new EnumMap<>(ContactTransmissionType.class);
result.put(ContactTransmissionType.RESPIRATORY, respiratory);
result.put(ContactTransmissionType.DIRECT_CONTACT, directContact);
// fomite: result.put(ContactTransmissionType.FOMITE, fomite);
return result;
}
/**
* Serializes to the {@code route=weight;route=weight} notation using canonical keys.
*/
public String toToken() {
return "respiratory=" + respiratory + ";directContact=" + directContact
/* fomite: + ";fomite=" + fomite */;
}
private void validate() {
checkFiniteNonNegative("respiratory", respiratory);
checkFiniteNonNegative("directContact", directContact);
// fomite: checkFiniteNonNegative("fomite", fomite);
}
private static void checkFiniteNonNegative(String route, double value) {
if (!Double.isFinite(value) || value < 0) {
throw new IllegalArgumentException("Invalid " + route + " weight '" + value + "'.");
}
}
private static double orZero(Double value) {
return value == null ? 0.0 : value;
}
private static <T> T requireNonNull(T value, String name) {
if (value == null) {
throw new IllegalArgumentException(name + " must not be null.");
}
return value;
}
}