package org.apache.cassandra.stress.settings; /* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.base.Function; import org.apache.commons.math3.distribution.ExponentialDistribution; import org.apache.commons.math3.distribution.NormalDistribution; import org.apache.commons.math3.distribution.UniformRealDistribution; import org.apache.commons.math3.distribution.WeibullDistribution; import org.apache.commons.math3.random.JDKRandomGenerator; import org.apache.cassandra.stress.generate.*; /** * For selecting a mathematical distribution */ public class OptionDistribution extends Option { public static final Function<String, DistributionFactory> BUILDER = new Function<String, DistributionFactory>() { public DistributionFactory apply(String s) { return get(s); } }; private static final Pattern FULL = Pattern.compile("(~?)([A-Z]+)\\((.+)\\)", Pattern.CASE_INSENSITIVE); private static final Pattern ARGS = Pattern.compile("[^,]+"); final String prefix; private String spec; private final String defaultSpec; private final String description; private final boolean required; public OptionDistribution(String prefix, String defaultSpec, String description) { this(prefix, defaultSpec, description, defaultSpec == null); } public OptionDistribution(String prefix, String defaultSpec, String description, boolean required) { this.prefix = prefix; this.defaultSpec = defaultSpec; this.description = description; this.required = required; } @Override public boolean accept(String param) { if (!param.toLowerCase().startsWith(prefix)) return false; spec = param.substring(prefix.length()); return true; } public static DistributionFactory get(String spec) { Matcher m = FULL.matcher(spec); if (!m.matches()) throw new IllegalArgumentException("Illegal distribution specification: " + spec); boolean inverse = m.group(1).equals("~"); String name = m.group(2); Impl impl = LOOKUP.get(name.toLowerCase()); if (impl == null) throw new IllegalArgumentException("Illegal distribution type: " + name); List<String> params = new ArrayList<>(); m = ARGS.matcher(m.group(3)); while (m.find()) params.add(m.group()); DistributionFactory factory = impl.getFactory(params); return inverse ? new InverseFactory(factory) : factory; } public DistributionFactory get() { return spec != null ? get(spec) : defaultSpec != null ? get(defaultSpec) : null; } @Override public boolean happy() { return !required || spec != null; } public String longDisplay() { return shortDisplay() + ": " + description; } @Override public List<String> multiLineDisplay() { return Arrays.asList( GroupedOptions.formatMultiLine("EXP(min..max)", "An exponential distribution over the range [min..max]"), GroupedOptions.formatMultiLine("EXTREME(min..max,shape)", "An extreme value (Weibull) distribution over the range [min..max]"), GroupedOptions.formatMultiLine("QEXTREME(min..max,shape,quantas)", "An extreme value, split into quantas, within which the chance of selection is uniform"), GroupedOptions.formatMultiLine("GAUSSIAN(min..max,stdvrng)", "A gaussian/normal distribution, where mean=(min+max)/2, and stdev is (mean-min)/stdvrng"), GroupedOptions.formatMultiLine("GAUSSIAN(min..max,mean,stdev)", "A gaussian/normal distribution, with explicitly defined mean and stdev"), GroupedOptions.formatMultiLine("UNIFORM(min..max)", "A uniform distribution over the range [min, max]"), GroupedOptions.formatMultiLine("FIXED(val)", "A fixed distribution, always returning the same value"), "Preceding the name with ~ will invert the distribution, e.g. ~exp(1..10) will yield 10 most, instead of least, often", "Aliases: extr, qextr, gauss, normal, norm, weibull" ); } boolean setByUser() { return spec != null; } boolean present() { return setByUser() || defaultSpec != null; } @Override public String shortDisplay() { return (defaultSpec != null ? "[" : "") + prefix + "DIST(?)" + (defaultSpec != null ? "]" : ""); } private static final Map<String, Impl> LOOKUP; static { final Map<String, Impl> lookup = new HashMap<>(); lookup.put("exp", new ExponentialImpl()); lookup.put("extr", new ExtremeImpl()); lookup.put("qextr", new QuantizedExtremeImpl()); lookup.put("extreme", lookup.get("extr")); lookup.put("qextreme", lookup.get("qextr")); lookup.put("weibull", lookup.get("weibull")); lookup.put("gaussian", new GaussianImpl()); lookup.put("normal", lookup.get("gaussian")); lookup.put("gauss", lookup.get("gaussian")); lookup.put("norm", lookup.get("gaussian")); lookup.put("uniform", new UniformImpl()); lookup.put("fixed", new FixedImpl()); LOOKUP = lookup; } // factory builders private static interface Impl { public DistributionFactory getFactory(List<String> params); } public static long parseLong(String value) { long multiplier = 1; value = value.trim().toLowerCase(); switch (value.charAt(value.length() - 1)) { case 'b': multiplier *= 1000; case 'm': multiplier *= 1000; case 'k': multiplier *= 1000; value = value.substring(0, value.length() - 1); } return Long.parseLong(value) * multiplier; } private static final class GaussianImpl implements Impl { @Override public DistributionFactory getFactory(List<String> params) { if (params.size() > 3 || params.size() < 1) throw new IllegalArgumentException("Invalid parameter list for gaussian distribution: " + params); try { String[] bounds = params.get(0).split("\\.\\.+"); final long min = parseLong(bounds[0]); final long max = parseLong(bounds[1]); final double mean, stdev; if (params.size() == 3) { mean = Double.parseDouble(params.get(1)); stdev = Double.parseDouble(params.get(2)); } else { final double stdevsToEdge = params.size() == 1 ? 3d : Double.parseDouble(params.get(1)); mean = (min + max) / 2d; stdev = ((max - min) / 2d) / stdevsToEdge; } if (min == max) return new FixedFactory(min); return new GaussianFactory(min, max, mean, stdev); } catch (Exception ignore) { throw new IllegalArgumentException("Invalid parameter list for uniform distribution: " + params); } } } private static final class ExponentialImpl implements Impl { @Override public DistributionFactory getFactory(List<String> params) { if (params.size() != 1) throw new IllegalArgumentException("Invalid parameter list for gaussian distribution: " + params); try { String[] bounds = params.get(0).split("\\.\\.+"); final long min = parseLong(bounds[0]); final long max = parseLong(bounds[1]); if (min == max) return new FixedFactory(min); ExponentialDistribution findBounds = new ExponentialDistribution(1d); // max probability should be roughly equal to accuracy of (max-min) to ensure all values are visitable, // over entire range, but this results in overly skewed distribution, so take sqrt final double mean = (max - min) / findBounds.inverseCumulativeProbability(1d - Math.sqrt(1d/(max-min))); return new ExpFactory(min, max, mean); } catch (Exception ignore) { throw new IllegalArgumentException("Invalid parameter list for uniform distribution: " + params); } } } private static final class ExtremeImpl implements Impl { @Override public DistributionFactory getFactory(List<String> params) { if (params.size() != 2) throw new IllegalArgumentException("Invalid parameter list for extreme (Weibull) distribution: " + params); try { String[] bounds = params.get(0).split("\\.\\.+"); final long min = parseLong(bounds[0]); final long max = parseLong(bounds[1]); if (min == max) return new FixedFactory(min); final double shape = Double.parseDouble(params.get(1)); WeibullDistribution findBounds = new WeibullDistribution(shape, 1d); // max probability should be roughly equal to accuracy of (max-min) to ensure all values are visitable, // over entire range, but this results in overly skewed distribution, so take sqrt final double scale = (max - min) / findBounds.inverseCumulativeProbability(1d - Math.sqrt(1d/(max-min))); return new ExtremeFactory(min, max, shape, scale); } catch (Exception ignore) { throw new IllegalArgumentException("Invalid parameter list for extreme (Weibull) distribution: " + params); } } } private static final class QuantizedExtremeImpl implements Impl { @Override public DistributionFactory getFactory(List<String> params) { if (params.size() != 3) throw new IllegalArgumentException("Invalid parameter list for quantized extreme (Weibull) distribution: " + params); try { String[] bounds = params.get(0).split("\\.\\.+"); final long min = parseLong(bounds[0]); final long max = parseLong(bounds[1]); final double shape = Double.parseDouble(params.get(1)); final int quantas = Integer.parseInt(params.get(2)); WeibullDistribution findBounds = new WeibullDistribution(shape, 1d); // max probability should be roughly equal to accuracy of (max-min) to ensure all values are visitable, // over entire range, but this results in overly skewed distribution, so take sqrt final double scale = (max - min) / findBounds.inverseCumulativeProbability(1d - Math.sqrt(1d/(max-min))); if (min == max) return new FixedFactory(min); return new QuantizedExtremeFactory(min, max, shape, scale, quantas); } catch (Exception ignore) { throw new IllegalArgumentException("Invalid parameter list for quantized extreme (Weibull) distribution: " + params); } } } private static final class UniformImpl implements Impl { @Override public DistributionFactory getFactory(List<String> params) { if (params.size() != 1) throw new IllegalArgumentException("Invalid parameter list for uniform distribution: " + params); try { String[] bounds = params.get(0).split("\\.\\.+"); final long min = parseLong(bounds[0]); final long max = parseLong(bounds[1]); if (min == max) return new FixedFactory(min); return new UniformFactory(min, max); } catch (Exception ignore) { throw new IllegalArgumentException("Invalid parameter list for uniform distribution: " + params); } } } private static final class FixedImpl implements Impl { @Override public DistributionFactory getFactory(List<String> params) { if (params.size() != 1) throw new IllegalArgumentException("Invalid parameter list for uniform distribution: " + params); try { final long key = parseLong(params.get(0)); return new FixedFactory(key); } catch (Exception ignore) { throw new IllegalArgumentException("Invalid parameter list for uniform distribution: " + params); } } } private static final class InverseFactory implements DistributionFactory { final DistributionFactory wrapped; private InverseFactory(DistributionFactory wrapped) { this.wrapped = wrapped; } public Distribution get() { return new DistributionInverted(wrapped.get()); } } // factories private static final class ExpFactory implements DistributionFactory { final long min, max; final double mean; private ExpFactory(long min, long max, double mean) { this.min = min; this.max = max; this.mean = mean; } @Override public Distribution get() { return new DistributionOffsetApache(new ExponentialDistribution(new JDKRandomGenerator(), mean, ExponentialDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max); } } private static class ExtremeFactory implements DistributionFactory { final long min, max; final double shape, scale; private ExtremeFactory(long min, long max, double shape, double scale) { this.min = min; this.max = max; this.shape = shape; this.scale = scale; } @Override public Distribution get() { return new DistributionOffsetApache(new WeibullDistribution(new JDKRandomGenerator(), shape, scale, WeibullDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max); } } private static final class QuantizedExtremeFactory extends ExtremeFactory { final int quantas; private QuantizedExtremeFactory(long min, long max, double shape, double scale, int quantas) { super(min, max, shape, scale); this.quantas = quantas; } @Override public Distribution get() { return new DistributionQuantized(new DistributionOffsetApache(new WeibullDistribution(new JDKRandomGenerator(), shape, scale, WeibullDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max), quantas); } } private static final class GaussianFactory implements DistributionFactory { final long min, max; final double mean, stdev; private GaussianFactory(long min, long max, double mean, double stdev) { this.min = min; this.max = max; this.stdev = stdev; this.mean = mean; } @Override public Distribution get() { return new DistributionBoundApache(new NormalDistribution(new JDKRandomGenerator(), mean, stdev, NormalDistribution.DEFAULT_INVERSE_ABSOLUTE_ACCURACY), min, max); } } private static final class UniformFactory implements DistributionFactory { final long min, max; private UniformFactory(long min, long max) { this.min = min; this.max = max; } @Override public Distribution get() { return new DistributionBoundApache(new UniformRealDistribution(new JDKRandomGenerator(), min, max + 1), min, max); } } private static final class FixedFactory implements DistributionFactory { final long key; private FixedFactory(long key) { this.key = key; } @Override public Distribution get() { return new DistributionFixed(key); } } @Override public int hashCode() { return prefix.hashCode(); } @Override public boolean equals(Object that) { return super.equals(that) && ((OptionDistribution) that).prefix.equals(this.prefix); } }