/* * JaamSim Discrete Event Simulation * Copyright (C) 2014 Ausenco Engineering Canada Inc. * Copyright (C) 2016 JaamSim Software Inc. * * Licensed 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. */ package com.jaamsim.ProbabilityDistributions; import com.jaamsim.Samples.SampleConstant; import com.jaamsim.Samples.SampleInput; import com.jaamsim.input.Keyword; import com.jaamsim.math.Gamma; import com.jaamsim.rng.MRG1999a; import com.jaamsim.units.DimensionlessUnit; import com.jaamsim.units.Unit; import com.jaamsim.units.UserSpecifiedUnit; public class BetaDistribution extends Distribution { @Keyword(description = "The alpha tuning parameter.", exampleList = {"5.0", "InputValue1", "'2 * [InputValue1].Value'"}) private final SampleInput alphaInput; @Keyword(description = "The beta tuning parameter.", exampleList = {"5.0", "InputValue1", "'2 * [InputValue1].Value'"}) private final SampleInput betaInput; @Keyword(description = "The scale parameter for the distribution. This scales the " + "value of the distribution so it return values between 0 and scale.", exampleList = {"5.0", "InputValue1", "'2 * [InputValue1].Value'"}) private final SampleInput scaleInput; private final MRG1999a rng = new MRG1999a(); { minValueInput.setDefaultValue(new SampleConstant(0.0d)); alphaInput = new SampleInput("AlphaParam", "Key Inputs", new SampleConstant(1.0d)); alphaInput.setUnitType(DimensionlessUnit.class); alphaInput.setValidRange(0.0d, Double.POSITIVE_INFINITY); alphaInput.setEntity(this); this.addInput(alphaInput); betaInput = new SampleInput("BetaParam", "Key Inputs", new SampleConstant(1.0d)); betaInput.setUnitType(DimensionlessUnit.class); betaInput.setValidRange(0.0d, Double.POSITIVE_INFINITY); betaInput.setEntity(this); this.addInput(betaInput); scaleInput = new SampleInput("Scale", "Key Inputs", new SampleConstant(1.0d)); scaleInput.setValidRange(0.0d, Double.POSITIVE_INFINITY); scaleInput.setUnitType(UserSpecifiedUnit.class); scaleInput.setEntity(this); this.addInput(scaleInput); } public BetaDistribution() {} @Override public void earlyInit() { super.earlyInit(); rng.setSeedStream(getStreamNumber(), getSubstreamNumber()); } @Override protected void setUnitType(Class<? extends Unit> ut) { super.setUnitType(ut); scaleInput.setUnitType(ut); } @Override protected double getSample(double simTime) { // Effectively calculate the inverse CDF double val = rng.nextUniform(); double alpha = alphaInput.getValue().getNextSample(simTime); double beta = betaInput.getValue().getNextSample(simTime); double scale = scaleInput.getValue().getNextSample(simTime); double low = 0; double high = 1; double guess = 0.5; double lowVal = 0; double highVal = 1; while (true) { double attempt = regularizedBeta(guess, alpha, beta, 1E-14, Integer.MAX_VALUE); if (near(val, attempt, 1E-9)) { return guess * scale; } if (val < attempt) { high = guess; highVal = attempt; } else { low = guess; lowVal = attempt; } double ratio = (val - lowVal) / (highVal - lowVal); guess = low + (high - low) * ratio; } } @Override protected double getMean(double simTime) { double alpha = alphaInput.getValue().getNextSample(simTime); double beta = betaInput.getValue().getNextSample(simTime); double scale = scaleInput.getValue().getNextSample(simTime); return (alpha / (alpha + beta)) * scale; } @Override protected double getStandardDev(double simTime) { double alpha = alphaInput.getValue().getNextSample(simTime); double beta = betaInput.getValue().getNextSample(simTime); double scale = scaleInput.getValue().getNextSample(simTime); double apbSqrd = (alpha + beta) * (alpha + beta); return (Math.sqrt(alpha * beta / (apbSqrd * (alpha + beta + 1)))) * scale; } /* * All code below this point is derived from the beta function implementation of the * Apache Commons Math library and can be downloaded from: * http://commons.apache.org/proper/commons-math/download_math.cgi * */ public static double regularizedBeta(double x, final double a, final double b, double epsilon, int maxIterations) { double ret; if (Double.isNaN(x) || Double.isNaN(a) || Double.isNaN(b) || x < 0 || x > 1 || a <= 0 || b <= 0) { ret = Double.NaN; } else if (x > (a + 1) / (2 + b + a) && 1 - x <= (b + 1) / (2 + b + a)) { ret = 1 - regularizedBeta(1 - x, b, a, epsilon, maxIterations); } else { ret = Math.exp((a * Math.log(x)) + (b * Math.log1p(-x)) - Math.log(a) - logBeta(a, b)) * 1.0 / evaluateFraction(a, b, x, epsilon, maxIterations); } return ret; } private static final double HALF_LOG_TWO_PI = .9189385332046727; private static final long SGN_MASK = 0x8000000000000000L; private static boolean equals(double x, double y, int maxUlps) { long xInt = Double.doubleToLongBits(x); long yInt = Double.doubleToLongBits(y); // Make lexicographically ordered as a two's-complement integer. if (xInt < 0) { xInt = SGN_MASK - xInt; } if (yInt < 0) { yInt = SGN_MASK - yInt; } final boolean isEqual = Math.abs(xInt - yInt) <= maxUlps; return isEqual && !Double.isNaN(x) && !Double.isNaN(y); } private static boolean near(double x, double y, double eps) { return equals(x, y, 1) || Math.abs(y - x) <= eps; } private static double getB(double a, double b, int n, double x) { double ret; double m; if (n % 2 == 0) { // even m = n / 2.0; ret = (m * (b - m) * x) / ((a + (2 * m) - 1) * (a + (2 * m))); } else { m = (n - 1.0) / 2.0; ret = -((a + m) * (a + b + m) * x) / ((a + (2 * m)) * (a + (2 * m) + 1.0)); } return ret; } // This evaluates and continued fraction for the beta distribution // I have no idea how the math actually works though... private static double evaluateFraction(double alpha, double beta, double x, double epsilon, int maxIterations) { final double small = 1e-50; double hPrev = 1.0; // use the value of small as epsilon criteria for zero checks if (near(hPrev, 0.0, small)) { hPrev = small; } int n = 1; double dPrev = 0.0; double cPrev = hPrev; while (n < maxIterations) { final double a = 1.0; final double b = getB(alpha, beta, n, x); double dN = a + b * dPrev; if (near(dN, 0.0, small)) { dN = small; } double cN = a + b / cPrev; if (near(cN, 0.0, small)) { cN = small; } dN = 1 / dN; final double deltaN = cN * dN; final double hN = hPrev * deltaN; if (Double.isInfinite(hN)) { throw new RuntimeException("Fraction did not converge"); } if (Double.isNaN(hN)) { throw new RuntimeException("Fraction did not converge"); } if (Math.abs(deltaN - 1.0) < epsilon) { return hN; } dPrev = dN; cPrev = cN; hPrev = hN; n++; } throw new RuntimeException("Fraction did not converge"); } public static double logBeta(final double p, final double q) { if (Double.isNaN(p) || Double.isNaN(q) || (p <= 0.0) || (q <= 0.0)) { return Double.NaN; } final double a = Math.min(p, q); final double b = Math.max(p, q); if (a >= 10.0) { final double w = sumDeltaMinusDeltaSum(a, b); final double h = a / b; final double c = h / (1.0 + h); final double u = -(a - 0.5) * Math.log(c); final double v = b * Math.log1p(h); if (u <= v) { return (((-0.5 * Math.log(b) + HALF_LOG_TWO_PI) + w) - u) - v; } else { return (((-0.5 * Math.log(b) + HALF_LOG_TWO_PI) + w) - v) - u; } } else if (a > 2.0) { if (b > 1000.0) { final int n = (int) Math.floor(a - 1.0); double prod = 1.0; double ared = a; for (int i = 0; i < n; i++) { ared -= 1.0; prod *= ared / (1.0 + ared / b); } return (Math.log(prod) - n * Math.log(b)) + (Gamma.logGamma(ared) + logGammaMinusLogGammaSum( ared, b)); } else { double prod1 = 1.0; double ared = a; while (ared > 2.0) { ared -= 1.0; final double h = ared / b; prod1 *= h / (1.0 + h); } if (b < 10.0) { double prod2 = 1.0; double bred = b; while (bred > 2.0) { bred -= 1.0; prod2 *= bred / (ared + bred); } return Math.log(prod1) + Math.log(prod2) + (Gamma.logGamma(ared) + (Gamma.logGamma(bred) - logGammaSum( ared, bred))); } else { return Math.log(prod1) + Gamma.logGamma(ared) + logGammaMinusLogGammaSum(ared, b); } } } else if (a >= 1.0) { if (b > 2.0) { if (b < 10.0) { double prod = 1.0; double bred = b; while (bred > 2.0) { bred -= 1.0; prod *= bred / (a + bred); } return Math.log(prod) + (Gamma.logGamma(a) + (Gamma.logGamma(bred) - logGammaSum( a, bred))); } else { return Gamma.logGamma(a) + logGammaMinusLogGammaSum(a, b); } } else { return Gamma.logGamma(a) + Gamma.logGamma(b) - logGammaSum(a, b); } } else { if (b >= 10.0) { return Gamma.logGamma(a) + logGammaMinusLogGammaSum(a, b); } else { // The following command is the original NSWC implementation. // return Gamma.logGamma(a) + // (Gamma.logGamma(b) - Gamma.logGamma(a + b)); // The following command turns out to be more accurate. return Math.log(com.jaamsim.math.Gamma.gamma(a) * Gamma.gamma(b) / Gamma.gamma(a + b)); } } } private static double logGammaSum(final double a, final double b) { final double x = (a - 1.0) + (b - 1.0); if (x <= 0.5) { return Gamma.logGamma1p(1.0 + x); } else if (x <= 1.5) { return Gamma.logGamma1p(x) + Math.log1p(x); } else { return Gamma.logGamma1p(x - 1.0) + Math.log(x * (1.0 + x)); } } private static double logGammaMinusLogGammaSum(final double a, final double b) { /* * d = a + b - 0.5 */ final double d; final double w; if (a <= b) { d = b + (a - 0.5); w = deltaMinusDeltaSum(a, b); } else { d = a + (b - 0.5); w = deltaMinusDeltaSum(b, a); } final double u = d * Math.log1p(a / b); final double v = a * (Math.log(b) - 1.0); return u <= v ? (w - u) - v : (w - v) - u; } private static final double[] DELTA = { .833333333333333333333333333333E-01, -.277777777777777777777777752282E-04, .793650793650793650791732130419E-07, -.595238095238095232389839236182E-09, .841750841750832853294451671990E-11, -.191752691751854612334149171243E-12, .641025640510325475730918472625E-14, -.295506514125338232839867823991E-15, .179643716359402238723287696452E-16, -.139228964661627791231203060395E-17, .133802855014020915603275339093E-18, -.154246009867966094273710216533E-19, .197701992980957427278370133333E-20, -.234065664793997056856992426667E-21, .171348014966398575409015466667E-22 }; private static double deltaMinusDeltaSum(final double a, final double b) { final double h = a / b; final double p = h / (1.0 + h); final double q = 1.0 / (1.0 + h); final double q2 = q * q; /* * s[i] = 1 + q + ... - q**(2 * i) */ final double[] s = new double[DELTA.length]; s[0] = 1.0; for (int i = 1; i < s.length; i++) { s[i] = 1.0 + (q + q2 * s[i - 1]); } /* * w = Delta(b) - Delta(a + b) */ final double sqrtT = 10.0 / b; final double t = sqrtT * sqrtT; double w = DELTA[DELTA.length - 1] * s[s.length - 1]; for (int i = DELTA.length - 2; i >= 0; i--) { w = t * w + DELTA[i] * s[i]; } return w * p / b; } private static double sumDeltaMinusDeltaSum(final double p, final double q) { final double a = Math.min(p, q); final double b = Math.max(p, q); final double sqrtT = 10.0 / a; final double t = sqrtT * sqrtT; double z = DELTA[DELTA.length - 1]; for (int i = DELTA.length - 2; i >= 0; i--) { z = t * z + DELTA[i]; } return z / a + deltaMinusDeltaSum(a, b); } }