package org.radargun.utils; import java.io.Serializable; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Random; import org.radargun.config.Converter; /** * Represents <a href="https://en.wikipedia.org/wiki/Probability_mass_function">probability mass function</a>, * a random variable providing values from given set with defined (different) probabilities. * <p> * The {@link Builder} allows to configure the values either using fixed probabilities (given value * will be returned with e.g. 10% probability) or using weighting (if one value is added with weight 2 * while others with weight 1, this value will be returned twice as often). * <p> * If fixed probabilities and weights are combined, the probability of weighted values is computed from * 1 - (sum of fixed probabilities). * <p> * Example: * <pre> * {@code new Builder().addFixed(1, 0.2).addFixed(2, 0.3).addWeighted(3, 1).addWeighted(4, 4),build() } * </pre> * * will return a random variable that will return values with following probabilities: * <table> * <tr><td> 1 </td><td> 20% </td></tr> * <tr><td> 2 </td><td> 30% </td></tr> * <tr><td> 3 </td><td> 10% </td></tr> * <tr><td> 4 </td><td> 40% </td></tr> * </table> * * The {@link IntegerConverter} provides a convenient way to use this as a property (specialized for integer values), * the previous example would be configured as: * * <pre> * {@code fuzzy-property="20%: 1, 30%: 2, 1: 3, 4: 4"} * </pre> * <p> * Note that if the weight is not specified, it defaults to one. * * @author Radim Vansa <rvansa@redhat.com> */ public final class Fuzzy<T extends Serializable> implements Serializable { private Serializable[] values; private BigDecimal[] probabilities; private Fuzzy(Serializable[] values, BigDecimal[] probabilities) { this.values = values; this.probabilities = probabilities; } public static <T extends Serializable> Fuzzy<T> uniform(T value) { return new Fuzzy<T>(new Serializable[] {value}, new BigDecimal[] { new BigDecimal(1.0)}); } public T next(Random random) { if (probabilities.length == 1) return (T) values[0]; BigDecimal x = BigDecimal.valueOf(random.nextDouble()); int index = Arrays.binarySearch(probabilities, x); if (index < 0) { index = -index - 1; } else { index = index + 1; } return (T) values[index]; } public Map<T, BigDecimal> getProbabilityMap() { Map<T, BigDecimal> map = new HashMap<T, BigDecimal>(); for (int i = 0; i < values.length; ++i) { map.put((T) values[i], probabilities[i]); } return map; } @Override public String toString() { StringBuilder sb = new StringBuilder("["); for (int i = 0; i < values.length; ++i) { if (i != 0) sb.append(", "); BigDecimal previousProbability = (i == 0 ? BigDecimal.ZERO : probabilities[i - 1]); sb.append(String.format("%.1f%%: ", probabilities[i].subtract(previousProbability).multiply(BigDecimal.valueOf(100)))); sb.append(values[i]); } return sb.append("]").toString(); } public static class Builder<T extends Serializable> { private ArrayList<T> weightedValues = new ArrayList<T>(); private ArrayList<BigDecimal> weights = new ArrayList<BigDecimal>(); private ArrayList<T> fixedValues = new ArrayList<T>(); private ArrayList<BigDecimal> probabilities = new ArrayList<BigDecimal>(); public Builder addWeighted(T value, BigDecimal weight) { //less then 0 if (weight.compareTo(BigDecimal.ZERO) <= 0) throw new IllegalArgumentException(); weightedValues.add(value); weights.add(weight); return this; } public Builder addFixed(T value, BigDecimal probability) { if (!isValid(probability)) throw new IllegalArgumentException(); fixedValues.add(value); probabilities.add(probability); return this; } private boolean isValid(BigDecimal probability) { // greater than 0 but smaller or equal to 1 return probability.compareTo(BigDecimal.ZERO) == 1 && probability.compareTo(BigDecimal.ONE) != 1; } public Fuzzy<T> create() { if (weightedValues.size() + fixedValues.size() == 0) throw new IllegalStateException(); BigDecimal sumWeight = BigDecimal.ZERO; for (BigDecimal w : this.weights) { sumWeight = sumWeight.add(w); } BigDecimal[] cumulativeProbability = new BigDecimal[this.probabilities.size() + this.weights.size()]; int i = 0; BigDecimal cumulatedProbability = BigDecimal.ZERO; for (BigDecimal p : this.probabilities) { cumulatedProbability = cumulatedProbability.add(p); cumulativeProbability[i++] = cumulatedProbability; } if (cumulatedProbability.compareTo(BigDecimal.ONE) == 1 //bigger than 1 || (cumulatedProbability.compareTo(BigDecimal.ONE) == 0 && weights.size() > 0) //equal to one || (cumulatedProbability.compareTo(BigDecimal.ONE) == -1 && weights.size() == 0)) { //less than one throw new IllegalStateException("Probability: " + cumulatedProbability); } BigDecimal cumulatedWeight = BigDecimal.ZERO; MathContext mc = new MathContext(2, RoundingMode.HALF_UP); //required for division for (BigDecimal w : this.weights) { cumulatedWeight = cumulatedWeight.add(w); BigDecimal currentCumulativeProbability = (BigDecimal.ONE.subtract(cumulatedProbability)).multiply(cumulatedWeight).divide(sumWeight, mc); BigDecimal previousCumulativeProbability = (i == 0 ? BigDecimal.ZERO : cumulativeProbability[i - 1]); cumulativeProbability[i++] = previousCumulativeProbability.add(currentCumulativeProbability); } cumulativeProbability[cumulativeProbability.length - 1] = new BigDecimal(1.0); ArrayList<T> values = new ArrayList<T>(fixedValues.size() + weightedValues.size()); values.addAll(fixedValues); values.addAll(weightedValues); return new Fuzzy<T>(values.toArray(new Serializable[values.size()]), cumulativeProbability); } } private abstract static class FuzzyConverter<T extends Serializable> implements Converter<Fuzzy<T>> { @Override public Fuzzy<T> convert(String string, Type type) { string = string.trim(); if (string.startsWith("[") && string.endsWith("]")) { string = string.substring(1, string.length() - 1); } String[] parts = string.split(",", 0); Builder<T> builder = new Builder<T>(); for (String part : parts) { int colon = part.indexOf(':'); if (colon < 0) { builder.addWeighted(parse(part.trim()), BigDecimal.ONE); } else { int percent = part.indexOf('%'); if (percent >= 0 && percent < colon) { BigDecimal probability = new BigDecimal(part.substring(0, percent).trim()).divide(BigDecimal.valueOf(100)); builder.addFixed(parse(part.substring(colon + 1).trim()), probability); } else { BigDecimal weight = new BigDecimal(part.substring(0, colon).trim()); builder.addWeighted(parse(part.substring(colon + 1).trim()), weight); } } } return builder.create(); } @Override public String convertToString(Fuzzy<T> value) { if (value == null) return "null"; else return value.toString(); } @Override public String allowedPattern(Type type) { return "\\s*([0-9.]+(\\.[0-9]*)?\\s*%?\\s*:\\s*)?" + getPattern() + "\\s*(,\\s*([0-9.]+(\\.[0-9]*)?\\s*%?\\s*:\\s*)?" + getPattern() + "\\s*)*"; } protected abstract T parse(String string); protected abstract String getPattern(); } public static class IntegerConverter extends FuzzyConverter<Integer> { @Override protected Integer parse(String string) { return Integer.parseInt(string); } @Override protected String getPattern() { return "[0-9]+"; } } }