/*
 * Copyright 1997-2022 Optimatika
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.ojalgo.random;

import static org.ojalgo.function.constant.PrimitiveMath.*;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.ojalgo.TestUtils;
import org.ojalgo.array.Primitive64Array;
import org.ojalgo.function.special.ErrorFunction;
import org.ojalgo.netio.BasicLogger;
import org.ojalgo.scalar.PrimitiveScalar;
import org.ojalgo.series.CalendarDateSeries;
import org.ojalgo.structure.Access1D;
import org.ojalgo.type.context.NumberContext;

/**
 * RandomNumberTest
 *
 * @author apete
 * @author Chris Lucas
 */
public class RandomNumberTest extends RandomTests {

    // A wrapper for two-parameter random numbers to make it easier to generalize tests. Easy to extend to single-parameter random numbers,
    // or just apply as-is by having one throw-away parameter.
    private abstract class Dist2 {

        public abstract RandomNumber getDist(double p0, double p1);
    }

    // Erlang's first argument is an int. For convenience, this takes the int value of the corresponding double's floor, which
    // should be the nearest lower int, according to the javadoc.
    private class Dist2Erlang extends Dist2 {

        @Override
        public RandomNumber getDist(final double p0, final double p1) {
            if (p0 < 1.0) {
                throw new IllegalArgumentException("The first argument must be at least 1");
            }
            return new Erlang((int) FLOOR.invoke(p0), p1);
        }
    }

    private class Dist2Gamma extends Dist2 {

        @Override
        public RandomNumber getDist(final double p0, final double p1) {
            return new Gamma(p0, p1);
        }
    }

    private class Dist2Norm extends Dist2 {

        @Override
        public RandomNumber getDist(final double p0, final double p1) {
            return new Normal(p0, p1);
        }
    }

    private class Dist2Weibull extends Dist2 {

        @Override
        public RandomNumber getDist(final double p0, final double p1) {
            return new Weibull(p0, p1);
        }
    }

    public static void compareDensity(final ContinuousDistribution expected, final ContinuousDistribution actual) {
        for (int d = -20; d < 21; d++) { // -2 .. 2
            double value = d / 10.0;
            double e = expected.getDensity(value);
            double a = actual.getDensity(value);
            TestUtils.assertEquals(e, a, 0.000001);
        }
    }

    public static void compareDistribution(final ContinuousDistribution expected, final ContinuousDistribution actual) {
        for (int d = -20; d < 21; d++) { // -2 .. 2
            double value = d / 10.0;
            double e = expected.getDistribution(-value);
            double a = actual.getDistribution(-value);
            TestUtils.assertEquals(e, a, 0.0001);
        }
    }

    public static void compareQuantile(final ContinuousDistribution expected, final ContinuousDistribution actual) {
        for (int t = 1; t < 10; t++) { // 0.1 .. 0.9
            double probability = t / 10.0;
            double e = expected.getQuantile(probability);
            double a = actual.getQuantile(probability);
            TestUtils.assertEquals(e, a, 0.0000001);
        }
    }

    /**
     * Tests that the error function implementation returns correct confidence intervals for +/- 6 standard
     * deviations. They are all correct to at least 10 decimal places.
     */
    @Test
    public void testERF() {

        double tmpStdDevCount;
        double tmpConfidence;
        double tmpError = NumberContext.of(7, 12).epsilon();

        tmpStdDevCount = ZERO;
        tmpConfidence = ZERO;
        TestUtils.assertEquals(tmpConfidence, ErrorFunction.erf(tmpStdDevCount / SQRT_TWO), tmpError);
        TestUtils.assertEquals(-tmpConfidence, ErrorFunction.erf(-tmpStdDevCount / SQRT_TWO), tmpError);

        tmpStdDevCount = ONE;
        tmpConfidence = 0.682689492137;
        TestUtils.assertEquals(tmpConfidence, ErrorFunction.erf(tmpStdDevCount / SQRT_TWO), tmpError);
        TestUtils.assertEquals(-tmpConfidence, ErrorFunction.erf(-tmpStdDevCount / SQRT_TWO), tmpError);

        tmpStdDevCount = TWO;
        tmpConfidence = 0.954499736104;
        TestUtils.assertEquals(tmpConfidence, ErrorFunction.erf(tmpStdDevCount / SQRT_TWO), tmpError);
        TestUtils.assertEquals(-tmpConfidence, ErrorFunction.erf(-tmpStdDevCount / SQRT_TWO), tmpError);

        tmpStdDevCount = THREE;
        tmpConfidence = 0.997300203937;
        TestUtils.assertEquals(tmpConfidence, ErrorFunction.erf(tmpStdDevCount / SQRT_TWO), tmpError);
        TestUtils.assertEquals(-tmpConfidence, ErrorFunction.erf(-tmpStdDevCount / SQRT_TWO), tmpError);

        tmpStdDevCount = FOUR;
        tmpConfidence = 0.999936657516;
        TestUtils.assertEquals(tmpConfidence, ErrorFunction.erf(tmpStdDevCount / SQRT_TWO), tmpError);
        TestUtils.assertEquals(-tmpConfidence, ErrorFunction.erf(-tmpStdDevCount / SQRT_TWO), tmpError);

        tmpStdDevCount = FIVE;
        tmpConfidence = 0.999999426697;
        TestUtils.assertEquals(tmpConfidence, ErrorFunction.erf(tmpStdDevCount / SQRT_TWO), tmpError);
        TestUtils.assertEquals(-tmpConfidence, ErrorFunction.erf(-tmpStdDevCount / SQRT_TWO), tmpError);

        tmpError = NumberContext.of(7, 8).epsilon();

        tmpStdDevCount = SIX;
        tmpConfidence = 0.999999998027;
        TestUtils.assertEquals(tmpConfidence, ErrorFunction.erf(tmpStdDevCount / SQRT_TWO), tmpError);
        TestUtils.assertEquals(-tmpConfidence, ErrorFunction.erf(-tmpStdDevCount / SQRT_TWO), tmpError);
    }

    @Test
    public void testERFandERFI() {

        final double precision = 1E-14;
        double expected = -1.5;
        double actual;

        while (expected <= 1.5) {
            actual = ErrorFunction.erfi(ErrorFunction.erf(expected));
            TestUtils.assertEquals(expected, actual, precision);
            expected += 0.1;
        }

        if (DEBUG) {
            for (int i = -10; i <= 10; i++) {
                double d = i / 2.0;
                double erf = ErrorFunction.erf(d);
                double erfi = ErrorFunction.erfi(erf);
                double err = Math.abs(erfi - d);
                double mag = Math.abs(erfi);
                BasicLogger.debug("{} => {} => {} : {} : {}", d, erf, erfi, err, err / mag);
            }
        }
    }

    @Test
    public void testERFI() {

        double tmpConfidenceLevel;
        double tmpExpected;
        final NumberContext tmpNewScale = NumberContext.of(2, 5);

        tmpConfidenceLevel = 0.80;
        tmpExpected = 1.28155;
        TestUtils.assertEquals(tmpExpected, SQRT_TWO * ErrorFunction.erfi(tmpConfidenceLevel), tmpNewScale);

        tmpConfidenceLevel = 0.90;
        tmpExpected = 1.64485;
        TestUtils.assertEquals(tmpExpected, SQRT_TWO * ErrorFunction.erfi(tmpConfidenceLevel), tmpNewScale);

        tmpConfidenceLevel = 0.95;
        tmpExpected = 1.95996;
        TestUtils.assertEquals(tmpExpected, SQRT_TWO * ErrorFunction.erfi(tmpConfidenceLevel), tmpNewScale);

        tmpConfidenceLevel = 0.98;
        tmpExpected = 2.32635;
        TestUtils.assertEquals(tmpExpected, SQRT_TWO * ErrorFunction.erfi(tmpConfidenceLevel), tmpNewScale);

        tmpConfidenceLevel = 0.99;
        tmpExpected = 2.57583;
        TestUtils.assertEquals(tmpExpected, SQRT_TWO * ErrorFunction.erfi(tmpConfidenceLevel), tmpNewScale);

        tmpConfidenceLevel = 0.995;
        tmpExpected = 2.80703;
        TestUtils.assertEquals(tmpExpected, SQRT_TWO * ErrorFunction.erfi(tmpConfidenceLevel), tmpNewScale);

        tmpConfidenceLevel = 0.998;
        tmpExpected = 3.09023;
        TestUtils.assertEquals(tmpExpected, SQRT_TWO * ErrorFunction.erfi(tmpConfidenceLevel), tmpNewScale);

        tmpConfidenceLevel = 0.999;
        tmpExpected = 3.29052;
        TestUtils.assertEquals(tmpExpected, SQRT_TWO * ErrorFunction.erfi(tmpConfidenceLevel), tmpNewScale);
    }

    @Test
    public void testErlang() {
        // Erlang is a special case of the gamma where the count is an integer -- verify that this is true for current implementation.
        for (double theta = .01; theta <= 10.0; theta = theta * 10.0) {
            for (int i = 1; i < 10; i++) {
                final Erlang erl = new Erlang(i, theta);
                final Gamma gam = new Gamma(i, theta);
                TestUtils.assertEquals("Gamma should match erlang for integer k", erl.getVariance(), gam.getVariance(), MACHINE_SMALLEST);
                TestUtils.assertEquals("Gamma should match erlang for integer k", erl.getExpected(), gam.getExpected(), MACHINE_SMALLEST);
            }
        }
        // param 1 is cast to integers by Dist2Erlang
        this.testDist2(new Dist2Erlang(), new double[] { 1, .01 }, new double[] { 5, 10 }, new double[] { 5, 100 }, 2500000, .05);
    }

    @Test
    @Tag("unstable")
    public void testGamma() {
        // TODO 15% error seems a little high
        this.testDist2(new Dist2Gamma(), new double[] { .01, .01 }, new double[] { 10, 10 }, new double[] { 100, 100 }, 200000, .15);
    }

    @Test
    @Tag("unstable")
    public void testGeometricMeanAndStandardDeviation() {

        final int tmpSize = 1000;

        final double tmpFactoryExpected = 1.05;
        final double tmpFactoryStdDev = ABS.invoke(new Normal(0.0, tmpFactoryExpected - ONE).doubleValue());
        final Normal tmpFactoryDistr = new Normal(tmpFactoryExpected, tmpFactoryStdDev);
        TestUtils.assertEquals("Factory Expected", tmpFactoryExpected, tmpFactoryDistr.getExpected(), 1E-14 / THREE);
        TestUtils.assertEquals("Factory Std Dev", tmpFactoryStdDev, tmpFactoryDistr.getStandardDeviation(), 1E-14 / THREE);

        final Primitive64Array tmpRawValues = Primitive64Array.make(tmpSize);
        final Primitive64Array tmpLogValues = Primitive64Array.make(tmpSize);
        for (int i = 0; i < tmpSize; i++) {
            tmpRawValues.data[i] = tmpFactoryDistr.doubleValue();
            tmpLogValues.data[i] = LOG.invoke(tmpRawValues.data[i]);
        }
        final SampleSet tmpLogValuesSet = SampleSet.wrap(tmpLogValues);
        final LogNormal tmpLogDistribut = new LogNormal(tmpLogValuesSet.getMean(), tmpLogValuesSet.getStandardDeviation());

        final double tmpGeometricMean = tmpLogDistribut.getGeometricMean();
        final double tmpGeometricStandardDeviation = tmpLogDistribut.getGeometricStandardDeviation();

        double tmpRawProduct = ONE;
        for (int i = 0; i < tmpSize; i++) {
            tmpRawProduct *= tmpRawValues.data[i];
        }
        TestUtils.assertEquals(tmpGeometricMean, POW.invoke(tmpRawProduct, ONE / tmpSize), 1E-14 / THREE);

        double tmpLogSum = ZERO;
        for (int i = 0; i < tmpSize; i++) {
            tmpLogSum += tmpLogValues.data[i];
        }
        TestUtils.assertEquals(tmpGeometricMean, EXP.invoke(tmpLogSum / tmpSize), 1E-14 / THREE);

        final double tmpLogGeoMean = LOG.invoke(tmpGeometricMean);

        double tmpVal;
        double tmpSumSqrDiff = ZERO;
        for (int i = 0; i < tmpSize; i++) {
            tmpVal = tmpLogValues.data[i] - tmpLogGeoMean;
            tmpSumSqrDiff += tmpVal * tmpVal;
        }
        TestUtils.assertEquals(tmpGeometricStandardDeviation / tmpGeometricStandardDeviation,
                EXP.invoke(SQRT.invoke(tmpSumSqrDiff / tmpSize)) / tmpGeometricStandardDeviation, 0.00005);
        // Check that the geometric standard deviation is within ±0.005% of what it should be.
    }

    @Test
    @Disabled("Underscored before JUnit 5")
    public void testLogNormal() {

        final double tmpAccuracy = TENTH / THREE;

        final RandomNumber tmpRandomNumber = new Normal(ONE, TENTH);
        double tmpValue = HUNDRED;
        final CalendarDateSeries<Double> tmpSeries = new CalendarDateSeries<>();
        for (int i = 0; i < 1000; i++) {
            tmpSeries.put(i, tmpValue);
            tmpValue *= tmpRandomNumber.doubleValue();
        }
        final double[] someValues1 = tmpSeries.asPrimitive().toRawCopy1D();
        final int tmpSize1 = someValues1.length - 1;

        final double[] retVal1 = new double[tmpSize1];

        for (int i1 = 0; i1 < tmpSize1; i1++) {
            retVal1[i1] = someValues1[i1 + 1] / someValues1[i1];
        }

        final SampleSet tmpQuotients = SampleSet.wrap(Access1D.wrap(retVal1));
        final double[] someValues = tmpSeries.asPrimitive().toRawCopy1D();
        final int tmpSize = someValues.length - 1;

        final double[] retVal = new double[tmpSize];

        for (int i = 0; i < tmpSize; i++) {
            retVal[i] = LOG.invoke(someValues[i + 1] / someValues[i]);
        }
        final SampleSet tmpLogChanges = SampleSet.wrap(Access1D.wrap(retVal));

        // Quotient distribution parameters within 3% of the generating distribution
        TestUtils.assertEquals(ONE, tmpQuotients.getMean() / tmpRandomNumber.getExpected(), tmpAccuracy);
        TestUtils.assertEquals(ONE, tmpQuotients.getStandardDeviation() / tmpRandomNumber.getStandardDeviation(), tmpAccuracy);

        final Normal tmpNormal = new Normal(tmpQuotients.getMean(), tmpQuotients.getStandardDeviation());
        final LogNormal tmpLogNormal = new LogNormal(tmpLogChanges.getMean(), tmpLogChanges.getStandardDeviation());

        // LogNormal (logarithmic changes) parameters within 3% of the Normal (quotients) parameters
        TestUtils.assertEquals(ONE, tmpLogNormal.getExpected() / tmpNormal.getExpected(), tmpAccuracy);
        TestUtils.assertEquals(ONE, tmpLogNormal.getStandardDeviation() / tmpNormal.getStandardDeviation(), tmpAccuracy);
    }

    // Sample means differ from expectation by an amount greater than anticipated given the large samples. May be due to
    // rounding error. For comparison, matlab's estimates for the mean of the normal for large samples tend to be
    // within 2 percent of the mean.
    @Test
    public void testNorm2() {
        this.testDist2(new Dist2Norm(), new double[] { .01, .01 }, new double[] { 10, 10 }, new double[] { 100, 100 }, 1500000, .05);
    }

    @Test
    public void testNormal() {

        final double[] tmpStdDevCount = new double[] { ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX }; // ± this number of std devs
        final double[] tmpConfidence = new double[] { ZERO, 0.682689492137, 0.954499736104, 0.997300203937, 0.999936657516, 0.999999426697, 0.999999998027 };

        final Normal tmpDistribution = new Normal(TEN, PI);

        for (int c = 0; c < 4; c++) { // Can't handle the more extrem values

            final double tmpHalfSideRemainder = (ONE - tmpConfidence[c]) / TWO;

            final double tmpUpperBound = tmpDistribution.getQuantile(ONE - tmpHalfSideRemainder);
            final double tmpLowerBound = tmpDistribution.getQuantile(tmpHalfSideRemainder);

            final double tmpExpected = tmpStdDevCount[c];
            final double tmpActual = (tmpUpperBound - tmpLowerBound) / TWO_PI; // Std Dev is PI

            TestUtils.assertEquals(tmpExpected, tmpActual, 5.0E-3);
        }
    }

    @Test
    @Tag("unstable")
    public void testSampledMean() {

        final RandomNumber[] tmpRndNmbrs = new RandomNumber[] { new Exponential(), new LogNormal(), new Normal(), new Uniform(), new Binomial(),
                new Geometric(), new Poisson(), new Erlang(), new Gamma(), new Weibull() };

        for (int d = 0; d < tmpRndNmbrs.length; d++) {

            final RandomNumber tmpDistr = tmpRndNmbrs[d];

            final SampleSet tmpSamples = SampleSet.make(tmpDistr, 1000);

            final String tmpDistrName = tmpDistr.getClass().getSimpleName();
            final double tmpDistrValue = tmpDistr.getExpected();
            final double tmpSampledValue = tmpSamples.getMean();
            final double tmpQuotient = tmpSampledValue / tmpDistrValue;

            final double tmpExpected = PrimitiveScalar.isSmall(ONE, tmpDistrValue) ? tmpDistrValue : ONE;
            final double tmpActual = PrimitiveScalar.isSmall(ONE, tmpDistrValue) ? tmpSampledValue : tmpQuotient;

            //            BasicLogger.logDebug("Name={}: Value={} <=> Sampled={} == Quotient={}", tmpDistrName, tmpDistrValue, tmpSampledValue, tmpQuotient);

            TestUtils.assertEquals(tmpDistrName, tmpExpected, tmpActual, TENTH);
        }

    }

    @Test
    public void testTDistributionFreedomCases() {

        RandomNumberTest.compareDensity(new TDistribution.Degree1(), new TDistribution(ONE));
        RandomNumberTest.compareDensity(new TDistribution.Degree2(), new TDistribution(TWO));
        RandomNumberTest.compareDensity(new TDistribution.Degree3(), new TDistribution(THREE));
        RandomNumberTest.compareDensity(new TDistribution.Degree4(), new TDistribution(FOUR));
        RandomNumberTest.compareDensity(new TDistribution.Degree5(), new TDistribution(FIVE));
        //        RandomNumberTest.compareDensity(new TDistribution.DegreeInfinity(), new TDistribution(Double.MAX_VALUE));

        //        RandomNumberTest.compareDistribution(new TDistribution.Degree1(), new TDistribution(ONE));
        //        RandomNumberTest.compareDistribution(new TDistribution.Degree2(), new TDistribution(TWO));
        //        RandomNumberTest.compareDistribution(new TDistribution.Degree3(), new TDistribution(THREE));
        //        RandomNumberTest.compareDistribution(new TDistribution.Degree4(), new TDistribution(FOUR));
        //        RandomNumberTest.compareDistribution(new TDistribution.Degree5(), new TDistribution(FIVE));
        //        RandomNumberTest.compareDistribution(new TDistribution.DegreeInfinity(), new TDistribution(Double.MAX_VALUE));

        //        RandomNumberTest.compareQuantile(new TDistribution.Degree1(), new TDistribution(ONE));
        //        RandomNumberTest.compareQuantile(new TDistribution.Degree2(), new TDistribution(TWO));
        //        RandomNumberTest.compareQuantile(new TDistribution.Degree3(), new TDistribution(THREE));
        //        RandomNumberTest.compareQuantile(new TDistribution.Degree4(), new TDistribution(FOUR));
        //        RandomNumberTest.compareQuantile(new TDistribution.Degree5(), new TDistribution(FIVE));
        //        RandomNumberTest.compareQuantile(new TDistribution.DegreeInfinity(), new TDistribution(Double.MAX_VALUE));
    }

    @Test
    @Tag("unstable")
    public void testVariance() {

        final double tmpStdDev = TEN;
        double tmpExpectedVar = tmpStdDev * tmpStdDev;

        final SampleSet tmpSampleSet = SampleSet.make(new Normal(PI, tmpStdDev), 10000);

        double tmpActualVar = tmpSampleSet.getVariance();

        TestUtils.assertEquals(tmpExpectedVar, tmpActualVar, SQRT.invoke(TEN)); // Won't always pass - it's random...

        tmpExpectedVar = tmpSampleSet.getSumOfSquares() / (tmpSampleSet.size() - 1);

        TestUtils.assertEquals(tmpExpectedVar, tmpActualVar, 1E-14 / THREE);

        final double[] tmpValues = tmpSampleSet.getValues();
        double s = ZERO, s2 = ZERO;
        for (final double tmpVal : tmpValues) {
            s += tmpVal;
            s2 += tmpVal * tmpVal;
        }

        tmpActualVar = SampleSet.calculateVariance(s, s2, tmpValues.length);

        TestUtils.assertEquals(tmpExpectedVar, tmpActualVar, THOUSAND * (1E-14 / THREE)); // TODO Large numerical difference, which is better?
    }

    @Test
    public void testWeibull() {
        for (double i = .01; i <= 10.0; i = i * 10) {
            for (double j = .01; j <= 100.0; j = j * 10) {
                final Weibull w0 = new Weibull(i, j);
                final Weibull w1 = new Weibull(i, j);
                // There are analytic solutions available for mean and variance of Weibull, users will probably assume they're being used.
                TestUtils.assertEquals("Weibull distribution's mean should be deterministic and precise.", w0.getExpected(), w1.getExpected());
                TestUtils.assertEquals("Weibull distribution's variance should be deterministic and precise.", w0.getVariance(), w1.getVariance());
            }
        }
        this.testDist2(new Dist2Weibull(), new double[] { .01, .5 }, new double[] { 10, 2 }, new double[] { 100, 4 }, 2500000, .05);
    }

    @Test
    public void testWeibullWithShape1() {

        // Weibull with shape=1.0 shoud be equivalent to Exponential with the same lambda
        final double tmpEpsilon = 1E-14 / THREE * THOUSAND * TEN;

        for (double lambda = HUNDREDTH; lambda <= HUNDRED; lambda *= TEN) {
            final Exponential tmpExpected = new Exponential(lambda);
            final Weibull tmpActual = new Weibull(lambda, ONE);
            TestUtils.assertEquals("Expected/Mean, lambda=" + lambda, tmpExpected.getExpected(), tmpActual.getExpected(), tmpEpsilon);
            TestUtils.assertEquals("Variance, lambda=" + lambda, tmpExpected.getVariance(), tmpActual.getVariance(), tmpEpsilon);
        }
    }

    void testDist2(final Dist2 dist, final double[] min, final double[] mult, final double[] max, final int samples, final double accuracyBound) {

        for (double p0 = min[0]; p0 <= max[0]; p0 *= mult[0]) {
            for (double p1 = min[1]; p1 <= max[1]; p1 *= mult[1]) {
                final RandomNumber tmpDistribution = dist.getDist(p0, p1);
                final SampleSet tmpSamples = SampleSet.make(tmpDistribution, samples);
                // Used to estimate an upper bound on how much the sample should deviate from the analytic expected value.
                final double stErr = SQRT.invoke(tmpDistribution.getVariance() / samples);

                // Within 4 standard errors of the mean. False failures should be rare under this scheme.
                TestUtils.assertEquals("Sample mean was " + tmpSamples.getMean() + ", distribution mean was " + tmpDistribution.getExpected() + ".",
                        tmpSamples.getMean(), tmpDistribution.getExpected(), 4.0 * stErr);
                // Variance of variance is not always available or easy to obtain. accuracyBound is intended as a rough estimate.
                TestUtils.assertEquals(ONE, tmpSamples.getVariance() / tmpDistribution.getVariance(), accuracyBound);
            }
        }
    }

}
