This is an automated email from the ASF dual-hosted git repository. aherbert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-statistics.git
commit 780f790c5936e00aeff28c637c7b794497a8bbb2 Author: aherbert <aherb...@apache.org> AuthorDate: Fri Jul 30 15:55:41 2021 +0100 Consistent ordering of abstract tests Ensure ContinuousDistributionAbstractTest and DiscreteDistributionAbstractTest are as close as possible given the interfaces that they must test. Updated the names for the probability precondition tests. --- .../ConstantContinuousDistributionTest.java | 2 +- .../ContinuousDistributionAbstractTest.java | 399 ++++++++++++--------- .../DiscreteDistributionAbstractTest.java | 337 +++++++++-------- .../TruncatedNormalDistributionTest.java | 8 +- 4 files changed, 429 insertions(+), 317 deletions(-) diff --git a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ConstantContinuousDistributionTest.java b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ConstantContinuousDistributionTest.java index 7552c76..be882c0 100644 --- a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ConstantContinuousDistributionTest.java +++ b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ConstantContinuousDistributionTest.java @@ -77,7 +77,7 @@ class ConstantContinuousDistributionTest extends ContinuousDistributionAbstractT @Test @Override - void testSampler() { + void testSampling() { final double value = 12.345; final ContinuousDistribution.Sampler sampler = new ConstantContinuousDistribution(value).createSampler(null); for (int i = 0; i < 10; i++) { diff --git a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ContinuousDistributionAbstractTest.java b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ContinuousDistributionAbstractTest.java index 6c5efff..0180328 100644 --- a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ContinuousDistributionAbstractTest.java +++ b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ContinuousDistributionAbstractTest.java @@ -17,6 +17,7 @@ package org.apache.commons.statistics.distribution; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import org.apache.commons.math3.analysis.UnivariateFunction; @@ -39,12 +40,13 @@ import org.junit.jupiter.api.BeforeEach; * distribution returned by makeDistribution(). Default implementations * are provided for the makeInverseXxx methods that just invert the mapping * defined by the arrays returned by the makeCumulativeXxx methods. - * <p> - * makeCumulativeTestPoints() -- arguments used to test cumulative probabilities - * makeCumulativeTestValues() -- expected cumulative probabilities - * makeDensityTestValues() -- expected density values at cumulativeTestPoints - * makeInverseCumulativeTestPoints() -- arguments used to test inverse cdf - * makeInverseCumulativeTestValues() -- expected inverse cdf values + * <ul> + * <li>makeCumulativeTestPoints() -- arguments used to test cumulative probabilities + * <li>makeCumulativeTestValues() -- expected cumulative probabilities + * <li>makeDensityTestValues() -- expected density values at cumulativeTestPoints + * <li>makeInverseCumulativeTestPoints() -- arguments used to test inverse cdf evaluation + * <li>makeInverseCumulativeTestValues() -- expected inverse cdf values + * </ul> * <p> * If the continuous distribution provides higher precision implementations of cumulativeProbability * and/or survivalProbability, the following methods should be implemented to provide testing. @@ -53,12 +55,12 @@ import org.junit.jupiter.api.BeforeEach; * arithmetic. * * NOTE: The default high-precision threshold is 1e-22. - * <pre> - * makeCumulativePrecisionTestPoints() -- high precision test inputs - * makeCumulativePrecisionTestValues() -- high precision expected results - * makeSurvivalPrecisionTestPoints() -- high precision test inputs - * makeSurvivalPrecisionTestValues() -- high precision expected results - * </pre> + * <ul> + * <li>makeCumulativePrecisionTestPoints() -- high precision test inputs + * <li>makeCumulativePrecisionTestValues() -- high precision expected results + * <li>makeSurvivalPrecisionTestPoints() -- high precision test inputs + * <li>makeSurvivalPrecisionTestValues() -- high precision expected results + * </ul> * <p> * To implement additional test cases with different distribution instances and * test data, use the setXxx methods for the instance data in test cases and @@ -76,7 +78,8 @@ import org.junit.jupiter.api.BeforeEach; */ abstract class ContinuousDistributionAbstractTest { -//-------------------- Private test instance data ------------------------- + //-------------------- Private test instance data ------------------------- + /** Distribution instance used to perform tests. */ private ContinuousDistribution distribution; @@ -84,7 +87,17 @@ abstract class ContinuousDistributionAbstractTest { private double tolerance = 1e-4; /** Tolerance used in high precision tests. */ - private final double highPrecisionTolerance = 1e-22; + private double highPrecisionTolerance = 1e-22; + + // Note: + // The ContinuousDistribution interface defines the density as the gradient of the CDF. + // It is evaluated using the cumulativeTestPoints. + + /** Values used to test density calculations. */ + private double[] densityTestValues; + + /** Values used to test logarithmic density calculations. */ + private double[] logDensityTestValues; /** Arguments used to test cumulative probability density calculations. */ private double[] cumulativeTestPoints; @@ -110,17 +123,25 @@ abstract class ContinuousDistributionAbstractTest { /** Values used to test inverse cumulative probability density calculations. */ private double[] inverseCumulativeTestValues; - /** Values used to test density calculations. */ - private double[] densityTestValues; - - /** Values used to test logarithmic density calculations. */ - private double[] logDensityTestValues; - //-------------------- Abstract methods ----------------------------------- /** Creates the default continuous distribution instance to use in tests. */ public abstract ContinuousDistribution makeDistribution(); + /** Creates the default density test expected values. */ + public abstract double[] makeDensityTestValues(); + + /** Creates the default logarithmic probability density test expected values. + * + * <p>The default implementation simply computes the logarithm of all the values in + * {@link #makeDensityTestValues()}. + * + * @return the default logarithmic probability density test expected values. + */ + public double[] makeLogDensityTestValues() { + return Arrays.stream(makeDensityTestValues()).map(Math::log).toArray(); + } + /** Creates the default cumulative probability test input values. */ public abstract double[] makeCumulativeTestPoints(); @@ -155,21 +176,6 @@ abstract class ContinuousDistributionAbstractTest { return new double[0]; } - /** Creates the default density test expected values. */ - public abstract double[] makeDensityTestValues(); - - /** Creates the default logarithmic density test expected values. - * The default implementation simply computes the logarithm - * of each value returned by {@link #makeDensityTestValues()}.*/ - public double[] makeLogDensityTestValues() { - final double[] density = makeDensityTestValues(); - final double[] logDensity = new double[density.length]; - for (int i = 0; i < density.length; i++) { - logDensity[i] = Math.log(density[i]); - } - return logDensity; - } - //---- Default implementations of inverse test data generation methods ---- /** Creates the default inverse cumulative probability test input values. */ @@ -193,6 +199,8 @@ abstract class ContinuousDistributionAbstractTest { @BeforeEach void setUp() { distribution = makeDistribution(); + densityTestValues = makeDensityTestValues(); + logDensityTestValues = makeLogDensityTestValues(); cumulativeTestPoints = makeCumulativeTestPoints(); cumulativeTestValues = makeCumulativeTestValues(); cumulativePrecisionTestPoints = makeCumulativePrecisionTestPoints(); @@ -201,8 +209,6 @@ abstract class ContinuousDistributionAbstractTest { survivalPrecisionTestValues = makeSurvivalPrecisionTestValues(); inverseCumulativeTestPoints = makeInverseCumulativeTestPoints(); inverseCumulativeTestValues = makeInverseCumulativeTestValues(); - densityTestValues = makeDensityTestValues(); - logDensityTestValues = makeLogDensityTestValues(); } /** @@ -211,17 +217,47 @@ abstract class ContinuousDistributionAbstractTest { @AfterEach void tearDown() { distribution = null; + densityTestValues = null; + logDensityTestValues = null; cumulativeTestPoints = null; cumulativeTestValues = null; + cumulativePrecisionTestPoints = null; + cumulativePrecisionTestValues = null; + survivalPrecisionTestPoints = null; + survivalPrecisionTestValues = null; inverseCumulativeTestPoints = null; inverseCumulativeTestValues = null; - densityTestValues = null; - logDensityTestValues = null; } //-------------------- Verification methods ------------------------------- /** + * Verifies that density calculations match expected values + * using current test instance data. + */ + protected void verifyDensities() { + for (int i = 0; i < cumulativeTestPoints.length; i++) { + final double x = cumulativeTestPoints[i]; + Assertions.assertEquals(densityTestValues[i], + distribution.density(x), getTolerance(), + () -> "Incorrect probability density value returned for " + x); + } + } + + /** + * Verifies that logarithmic density calculations match expected values + * using current test instance data. + */ + protected void verifyLogDensities() { + for (int i = 0; i < cumulativeTestPoints.length; i++) { + final double x = cumulativeTestPoints[i]; + Assertions.assertEquals(logDensityTestValues[i], + distribution.logDensity(x), getTolerance(), + () -> "Incorrect probability density value returned for " + x); + } + } + + /** * Verifies that cumulative probability density calculations match expected values * using current test instance data. */ @@ -229,9 +265,8 @@ abstract class ContinuousDistributionAbstractTest { // verify cumulativeProbability(double) for (int i = 0; i < cumulativeTestPoints.length; i++) { final double x = cumulativeTestPoints[i]; - Assertions.assertEquals( - cumulativeTestValues[i], - distribution.cumulativeProbability(cumulativeTestPoints[i]), + Assertions.assertEquals(cumulativeTestValues[i], + distribution.cumulativeProbability(x), getTolerance(), () -> "Incorrect cumulative probability value returned for " + x); } @@ -272,7 +307,7 @@ abstract class ContinuousDistributionAbstractTest { 1.0, distribution.survivalProbability(x) + distribution.cumulativeProbability(x), getTolerance(), - () -> "survival + cumulative probability were not close to 1.0" + x); + () -> "survival + cumulative probability were not close to 1.0 for " + x); } } @@ -321,40 +356,18 @@ abstract class ContinuousDistributionAbstractTest { } } - /** - * Verifies that density calculations match expected values. - */ - protected void verifyDensities() { - for (int i = 0; i < cumulativeTestPoints.length; i++) { - final double x = cumulativeTestPoints[i]; - Assertions.assertEquals( - densityTestValues[i], - distribution.density(cumulativeTestPoints[i]), - getTolerance(), - () -> "Incorrect probability density value returned for " + x); - } - } + //------------------------ Default test cases ----------------------------- - /** - * Verifies that logarithmic density calculations match expected values. - */ - protected void verifyLogDensities() { - for (int i = 0; i < cumulativeTestPoints.length; i++) { - final double x = cumulativeTestPoints[i]; - Assertions.assertEquals( - logDensityTestValues[i], - distribution.logDensity(cumulativeTestPoints[i]), - getTolerance(), - () -> "Incorrect probability density value returned for " + x); - } + @Test + void testDensities() { + verifyDensities(); } - //------------------------ Default test cases ----------------------------- + @Test + void testLogDensities() { + verifyLogDensities(); + } - /** - * Verifies that cumulative probability density calculations match expected values - * using default test instance data. - */ @Test void testCumulativeProbabilities() { verifyCumulativeProbabilities(); @@ -380,34 +393,12 @@ abstract class ContinuousDistributionAbstractTest { verifySurvivalProbabilityPrecision(); } - /** - * Verifies that inverse cumulative probability density calculations match expected values - * using default test instance data. - */ @Test void testInverseCumulativeProbabilities() { verifyInverseCumulativeProbabilities(); } /** - * Verifies that density calculations return expected values - * for default test instance data. - */ - @Test - void testDensities() { - verifyDensities(); - } - - /** - * Verifies that logarithmic density calculations return expected values - * for default test instance data. - */ - @Test - void testLogDensities() { - verifyLogDensities(); - } - - /** * Verifies that probability computations are consistent. */ @Test @@ -416,9 +407,9 @@ abstract class ContinuousDistributionAbstractTest { // check that cdf(x, x) = 0 Assertions.assertEquals( - 0d, + 0.0, distribution.probability(cumulativeTestPoints[i], cumulativeTestPoints[i]), - tolerance); + getTolerance()); // check that P(a < X <= b) = P(X <= b) - P(X <= a) final double upper = Math.max(cumulativeTestPoints[i], cumulativeTestPoints[i - 1]); @@ -426,61 +417,65 @@ abstract class ContinuousDistributionAbstractTest { final double diff = distribution.cumulativeProbability(upper) - distribution.cumulativeProbability(lower); final double direct = distribution.probability(lower, upper); - Assertions.assertEquals(diff, direct, tolerance, + Assertions.assertEquals(diff, direct, getTolerance(), () -> "Inconsistent probability for (" + lower + "," + upper + ")"); } } - /** - * Verifies that illegal arguments are correctly handled - */ @Test - void testPrecondition1() { - Assertions.assertThrows(DistributionException.class, () -> distribution.probability(1, 0)); + void testOutsideSupport() { + // Test various quantities when the variable is outside the support. + final double lo = distribution.getSupportLowerBound(); + Assertions.assertEquals(lo, distribution.inverseCumulativeProbability(0.0)); + + final double below = Math.nextDown(lo); + Assertions.assertEquals(0.0, distribution.probability(below)); + Assertions.assertEquals(Double.NEGATIVE_INFINITY, distribution.logDensity(below)); + Assertions.assertEquals(0.0, distribution.cumulativeProbability(below)); + Assertions.assertEquals(1.0, distribution.survivalProbability(below)); + + final double hi = distribution.getSupportUpperBound(); + Assertions.assertEquals(0.0, distribution.survivalProbability(hi)); + Assertions.assertEquals(hi, distribution.inverseCumulativeProbability(1.0)); + + final double above = Math.nextUp(hi); + Assertions.assertEquals(0.0, distribution.probability(above)); + Assertions.assertEquals(Double.NEGATIVE_INFINITY, distribution.logDensity(above)); + Assertions.assertEquals(1.0, distribution.cumulativeProbability(above)); + Assertions.assertEquals(0.0, distribution.survivalProbability(above)); } + @Test - void testPrecondition2() { - Assertions.assertThrows(DistributionException.class, () -> distribution.inverseCumulativeProbability(-1)); + void testProbabilityWithLowerBoundAboveUpperBound() { + Assertions.assertThrows(DistributionException.class, () -> distribution.probability(1, 0)); } + @Test - void testPrecondition3() { - Assertions.assertThrows(DistributionException.class, () -> distribution.inverseCumulativeProbability(2)); + void testInverseCumulativeProbabilityWithProbabilityBelowZero() { + Assertions.assertThrows(DistributionException.class, () -> distribution.inverseCumulativeProbability(-1)); } @Test - void testOutsideSupport() { - // Test various quantities when the variable is outside the support. - final double lo = distribution.getSupportLowerBound(); - final double hi = distribution.getSupportUpperBound(); - final double below = lo - Math.ulp(lo); - final double above = hi + Math.ulp(hi); - - Assertions.assertEquals(0d, distribution.density(below)); - Assertions.assertEquals(0d, distribution.density(above)); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, distribution.logDensity(below)); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, distribution.logDensity(above)); - Assertions.assertEquals(0d, distribution.cumulativeProbability(below)); - Assertions.assertEquals(1d, distribution.cumulativeProbability(above)); - Assertions.assertEquals(1d, distribution.survivalProbability(below)); - Assertions.assertEquals(0d, distribution.survivalProbability(above)); + void testInverseCumulativeProbabilityWithProbabilityAboveOne() { + Assertions.assertThrows(DistributionException.class, () -> distribution.inverseCumulativeProbability(2)); } - /** - * Test sampling. - */ @Test - void testSampler() { + void testSampling() { + // Use fixed seed. final int sampleSize = 1000; final ContinuousDistribution.Sampler sampler = - distribution.createSampler(RandomSource.create(RandomSource.WELL_19937_C, 123456789L)); + getDistribution().createSampler(RandomSource.create(RandomSource.WELL_19937_C, 123456789L)); final double[] sample = TestUtils.sample(sampleSize, sampler); - final double[] quartiles = TestUtils.getDistributionQuartiles(distribution); + + final double[] quartiles = TestUtils.getDistributionQuartiles(getDistribution()); final double[] expected = {250, 250, 250, 250}; - final long[] counts = new long[4]; + final long[] counts = new long[4]; for (int i = 0; i < sampleSize; i++) { TestUtils.updateCounts(sample[i], counts, quartiles); } + TestUtils.assertChiSquareAccept(expected, counts, 0.001); } @@ -537,32 +532,47 @@ abstract class ContinuousDistributionAbstractTest { } //------------------ Getters / Setters for test instance data ----------- + /** - * @return Returns the cumulativeTestPoints. + * @return Returns the distribution. */ - protected double[] getCumulativeTestPoints() { - return cumulativeTestPoints; + protected ContinuousDistribution getDistribution() { + return distribution; } /** - * @param cumulativeTestPoints The cumulativeTestPoints to set. + * @param distribution The distribution to set. */ - protected void setCumulativeTestPoints(double[] cumulativeTestPoints) { - this.cumulativeTestPoints = cumulativeTestPoints; + protected void setDistribution(ContinuousDistribution distribution) { + this.distribution = distribution; } /** - * @return Returns the cumulativeTestValues. + * @return Returns the tolerance. */ - protected double[] getCumulativeTestValues() { - return cumulativeTestValues; + protected double getTolerance() { + return tolerance; } /** - * @param cumulativeTestValues The cumulativeTestValues to set. + * @param tolerance The tolerance to set. */ - protected void setCumulativeTestValues(double[] cumulativeTestValues) { - this.cumulativeTestValues = cumulativeTestValues; + protected void setTolerance(double tolerance) { + this.tolerance = tolerance; + } + + /** + * @return Returns the high precision tolerance. + */ + protected double getHighPrecisionTolerance() { + return highPrecisionTolerance; + } + + /** + * @param highPrecisionTolerance The high precision highPrecisionTolerance to set. + */ + protected void setHighPrecisionTolerance(double highPrecisionTolerance) { + this.highPrecisionTolerance = highPrecisionTolerance; } /** @@ -573,10 +583,14 @@ abstract class ContinuousDistributionAbstractTest { } /** + * Set the density test values. + * For convenience this recomputes the log density test values using {@link Math#log(double)}. + * * @param densityTestValues The densityTestValues to set. */ protected void setDensityTestValues(double[] densityTestValues) { this.densityTestValues = densityTestValues; + logDensityTestValues = Arrays.stream(densityTestValues).map(Math::log).toArray(); } /** @@ -594,66 +608,115 @@ abstract class ContinuousDistributionAbstractTest { } /** - * @return Returns the distribution. + * @return Returns the cumulativeTestPoints. */ - protected ContinuousDistribution getDistribution() { - return distribution; + protected double[] getCumulativeTestPoints() { + return cumulativeTestPoints; } /** - * @param distribution The distribution to set. + * @param cumulativeTestPoints The cumulativeTestPoints to set. */ - protected void setDistribution(ContinuousDistribution distribution) { - this.distribution = distribution; + protected void setCumulativeTestPoints(double[] cumulativeTestPoints) { + this.cumulativeTestPoints = cumulativeTestPoints; } /** - * @return Returns the inverseCumulativeTestPoints. + * @return Returns the cumulativeTestValues. */ - protected double[] getInverseCumulativeTestPoints() { - return inverseCumulativeTestPoints; + protected double[] getCumulativeTestValues() { + return cumulativeTestValues; } /** - * @param inverseCumulativeTestPoints The inverseCumulativeTestPoints to set. + * @param cumulativeTestValues The cumulativeTestValues to set. */ - protected void setInverseCumulativeTestPoints(double[] inverseCumulativeTestPoints) { - this.inverseCumulativeTestPoints = inverseCumulativeTestPoints; + protected void setCumulativeTestValues(double[] cumulativeTestValues) { + this.cumulativeTestValues = cumulativeTestValues; } /** - * @return Returns the inverseCumulativeTestValues. + * @return Returns the cumulativePrecisionTestPoints. */ - protected double[] getInverseCumulativeTestValues() { - return inverseCumulativeTestValues; + protected double[] getCumulativePrecisionTestPoints() { + return cumulativePrecisionTestPoints; } /** - * @param inverseCumulativeTestValues The inverseCumulativeTestValues to set. + * @param cumulativePrecisionTestPoints The cumulativePrecisionTestPoints to set. */ - protected void setInverseCumulativeTestValues(double[] inverseCumulativeTestValues) { - this.inverseCumulativeTestValues = inverseCumulativeTestValues; + protected void setCumulativePrecisionTestPoints(double[] cumulativePrecisionTestPoints) { + this.cumulativePrecisionTestPoints = cumulativePrecisionTestPoints; } /** - * @return Returns the tolerance. + * @return Returns the cumulativePrecisionTestValues. */ - protected double getTolerance() { - return tolerance; + protected double[] getCumulativePrecisionTestValues() { + return cumulativePrecisionTestValues; } /** - * @return Returns the high precision tolerance. + * @param cumulativePrecisionTestValues The cumulativePrecisionTestValues to set. */ - protected double getHighPrecisionTolerance() { - return highPrecisionTolerance; + protected void setCumulativePrecisionTestValues(double[] cumulativePrecisionTestValues) { + this.cumulativePrecisionTestValues = cumulativePrecisionTestValues; } /** - * @param tolerance The tolerance to set. + * @return Returns the survivalPrecisionTestPoints. */ - protected void setTolerance(double tolerance) { - this.tolerance = tolerance; + protected double[] getSurvivalPrecisionTestPoints() { + return survivalPrecisionTestPoints; + } + + /** + * @param survivalPrecisionTestPoints The survivalPrecisionTestPoints to set. + */ + protected void setSurvivalPrecisionTestPoints(double[] survivalPrecisionTestPoints) { + this.survivalPrecisionTestPoints = survivalPrecisionTestPoints; + } + + /** + * @return Returns the survivalPrecisionTestValues. + */ + protected double[] getSurvivalPrecisionTestValues() { + return survivalPrecisionTestValues; + } + + /** + * @param survivalPrecisionTestValues The survivalPrecisionTestValues to set. + */ + protected void setSurvivalPrecisionTestValues(double[] survivalPrecisionTestValues) { + this.survivalPrecisionTestValues = survivalPrecisionTestValues; + } + + /** + * @return Returns the inverseCumulativeTestPoints. + */ + protected double[] getInverseCumulativeTestPoints() { + return inverseCumulativeTestPoints; + } + + /** + * @param inverseCumulativeTestPoints The inverseCumulativeTestPoints to set. + */ + protected void setInverseCumulativeTestPoints(double[] inverseCumulativeTestPoints) { + this.inverseCumulativeTestPoints = inverseCumulativeTestPoints; + } + + /** + * @return Returns the inverseCumulativeTestValues. + */ + protected double[] getInverseCumulativeTestValues() { + return inverseCumulativeTestValues; + } + + /** + * @param inverseCumulativeTestValues The inverseCumulativeTestValues to set. + */ + protected void setInverseCumulativeTestValues(double[] inverseCumulativeTestValues) { + this.inverseCumulativeTestValues = inverseCumulativeTestValues; } /** diff --git a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/DiscreteDistributionAbstractTest.java b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/DiscreteDistributionAbstractTest.java index 0df01dd..f96c3e2 100644 --- a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/DiscreteDistributionAbstractTest.java +++ b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/DiscreteDistributionAbstractTest.java @@ -26,18 +26,22 @@ import org.junit.jupiter.api.Test; /** * Abstract base class for {@link DiscreteDistribution} tests. * <p> - * To create a concrete test class for an integer distribution implementation, - * implement makeDistribution() to return a distribution instance to use in - * tests and each of the test data generation methods below. In each case, the - * test points and test values arrays returned represent parallel arrays of - * inputs and expected values for the distribution returned by makeDistribution(). - * <p> - * makeDensityTestPoints() -- arguments used to test probability density calculation - * makeDensityTestValues() -- expected probability densities - * makeCumulativeTestPoints() -- arguments used to test cumulative probabilities - * makeCumulativeTestValues() -- expected cumulative probabilites - * makeInverseCumulativeTestPoints() -- arguments used to test inverse cdf evaluation - * makeInverseCumulativeTestValues() -- expected inverse cdf values + * To create a concrete test class for a continuous distribution + * implementation, first implement makeDistribution() to return a distribution + * instance to use in tests. Then implement each of the test data generation + * methods below. In each case, the test points and test values arrays + * returned represent parallel arrays of inputs and expected values for the + * distribution returned by makeDistribution(). Default implementations + * are provided for the makeInverseXxx methods that just invert the mapping + * defined by the arrays returned by the makeCumulativeXxx methods. + * <ul> + * <li>makeDensityTestPoints() -- arguments used to test probability density calculation + * <li>makeDensityTestValues() -- expected probability densities + * <li>makeCumulativeTestPoints() -- arguments used to test cumulative probabilities + * <li>makeCumulativeTestValues() -- expected cumulative probabilities + * <li>makeInverseCumulativeTestPoints() -- arguments used to test inverse cdf evaluation + * <li>makeInverseCumulativeTestValues() -- expected inverse cdf values + * </ul> * <p> * If the discrete distribution provides higher precision implementations of cumulativeProbability * and/or survivalProbability, the following methods should be implemented to provide testing. @@ -46,20 +50,31 @@ import org.junit.jupiter.api.Test; * arithmetic. * * NOTE: The default high-precision threshold is 1e-22. - * <pre> - * makeCumulativePrecisionTestPoints() -- high precision test inputs - * makeCumulativePrecisionTestValues() -- high precision expected results - * makeSurvivalPrecisionTestPoints() -- high precision test inputs - * makeSurvivalPrecisionTestValues() -- high precision expected results - * </pre> + * <ul> + * <li>makeCumulativePrecisionTestPoints() -- high precision test inputs + * <li>makeCumulativePrecisionTestValues() -- high precision expected results + * <li>makeSurvivalPrecisionTestPoints() -- high precision test inputs + * <li>makeSurvivalPrecisionTestValues() -- high precision expected results + * </ul> * <p> - * To implement additional test cases with different distribution instances and test data, - * use the setXxx methods for the instance data in test cases and call the verifyXxx methods - * to verify results. + * To implement additional test cases with different distribution instances and + * test data, use the setXxx methods for the instance data in test cases and + * call the verifyXxx methods to verify results. + * <p> + * Error tolerance can be overridden by implementing getTolerance(). + * <p> + * Test data should be validated against reference tables or other packages + * where possible, and the source of the reference data and/or validation + * should be documented in the test cases. A framework for validating + * distribution data against R is included in the /src/test/R source tree. + * <p> + * See {@link PoissonDistributionTest} and {@link PascalDistributionTest} + * for examples. */ abstract class DiscreteDistributionAbstractTest { -//-------------------- Private test instance data ------------------------- + //-------------------- Private test instance data ------------------------- + /** Discrete distribution instance used to perform tests. */ private DiscreteDistribution distribution; @@ -115,19 +130,19 @@ abstract class DiscreteDistributionAbstractTest { /** Creates the default logarithmic probability density test expected values. * - * The default implementation simply computes the logarithm of all the values in + * <p>The default implementation simply computes the logarithm of all the values in * {@link #makeDensityTestValues()}. * - * @return double[] the default logarithmic probability density test expected values. + * @return the default logarithmic probability density test expected values. */ public double[] makeLogDensityTestValues() { return Arrays.stream(makeDensityTestValues()).map(Math::log).toArray(); } - /** Creates the default cumulative probability density test input values. */ + /** Creates the default cumulative probability test input values. */ public abstract int[] makeCumulativeTestPoints(); - /** Creates the default cumulative probability density test expected values. */ + /** Creates the default cumulative probability test expected values. */ public abstract double[] makeCumulativeTestValues(); /** Creates the default cumulative probability precision test input values. */ @@ -158,6 +173,8 @@ abstract class DiscreteDistributionAbstractTest { return new double[0]; } + //---- Default implementations of inverse test data generation methods ---- + /** Creates the default inverse cumulative probability test input values. */ public abstract double[] makeInverseCumulativeTestPoints(); @@ -212,10 +229,10 @@ abstract class DiscreteDistributionAbstractTest { */ protected void verifyDensities() { for (int i = 0; i < densityTestPoints.length; i++) { - final int testPoint = densityTestPoints[i]; + final int x = densityTestPoints[i]; Assertions.assertEquals(densityTestValues[i], - distribution.probability(testPoint), getTolerance(), - () -> "Incorrect density value returned for " + testPoint); + distribution.probability(x), getTolerance(), + () -> "Incorrect probability value returned for " + x); } } @@ -225,10 +242,10 @@ abstract class DiscreteDistributionAbstractTest { */ protected void verifyLogDensities() { for (int i = 0; i < densityTestPoints.length; i++) { - final int testPoint = densityTestPoints[i]; + final int x = densityTestPoints[i]; Assertions.assertEquals(logDensityTestValues[i], - distribution.logProbability(testPoint), tolerance, - () -> "Incorrect log density value returned for " + testPoint); + distribution.logProbability(x), getTolerance(), + () -> "Incorrect log probability value returned for " + x); } } @@ -238,10 +255,28 @@ abstract class DiscreteDistributionAbstractTest { */ protected void verifyCumulativeProbabilities() { for (int i = 0; i < cumulativeTestPoints.length; i++) { - final int testPoint = cumulativeTestPoints[i]; + final int x = cumulativeTestPoints[i]; Assertions.assertEquals(cumulativeTestValues[i], - distribution.cumulativeProbability(testPoint), getTolerance(), - () -> "Incorrect cumulative probability value returned for " + testPoint); + distribution.cumulativeProbability(x), getTolerance(), + () -> "Incorrect cumulative probability value returned for " + x); + } + // verify probability(double, double) + for (int i = 0; i < cumulativeTestPoints.length; i++) { + for (int j = 0; j < cumulativeTestPoints.length; j++) { + if (cumulativeTestPoints[i] <= cumulativeTestPoints[j]) { + Assertions.assertEquals( + cumulativeTestValues[j] - cumulativeTestValues[i], + distribution.probability(cumulativeTestPoints[i], cumulativeTestPoints[j]), + getTolerance()); + } else { + try { + distribution.probability(cumulativeTestPoints[i], cumulativeTestPoints[j]); + } catch (final IllegalArgumentException e) { + continue; + } + Assertions.fail("distribution.probability(double, double) should have thrown an exception that second argument is too large"); + } + } } } @@ -311,28 +346,16 @@ abstract class DiscreteDistributionAbstractTest { //------------------------ Default test cases ----------------------------- - /** - * Verifies that probability density calculations match expected values - * using default test instance data. - */ @Test void testDensities() { verifyDensities(); } - /** - * Verifies that logarithmic probability density calculations match expected values - * using default test instance data. - */ @Test void testLogDensities() { verifyLogDensities(); } - /** - * Verifies that cumulative probability density calculations match expected values - * using default test instance data. - */ @Test void testCumulativeProbabilities() { verifyCumulativeProbabilities(); @@ -358,78 +381,103 @@ abstract class DiscreteDistributionAbstractTest { verifySurvivalProbabilityPrecision(); } - /** - * Verifies that inverse cumulative probability density calculations match expected values - * using default test instance data. - */ @Test void testInverseCumulativeProbabilities() { verifyInverseCumulativeProbabilities(); } + /** + * Verifies that probability computations are consistent. + */ + @Test + void testConsistency() { + for (int i = 1; i < cumulativeTestPoints.length; i++) { + + // check that cdf(x, x) = 0 + Assertions.assertEquals( + 0.0, + distribution.probability(cumulativeTestPoints[i], cumulativeTestPoints[i]), + getTolerance()); + + // check that P(a < X <= b) = P(X <= b) - P(X <= a) + final int upper = Math.max(cumulativeTestPoints[i], cumulativeTestPoints[i - 1]); + final int lower = Math.min(cumulativeTestPoints[i], cumulativeTestPoints[i - 1]); + final double diff = distribution.cumulativeProbability(upper) - + distribution.cumulativeProbability(lower); + final double direct = distribution.probability(lower, upper); + Assertions.assertEquals(diff, direct, getTolerance(), + () -> "Inconsistent probability for (" + lower + "," + upper + ")"); + } + } + @Test - void testConsistencyAtSupportBounds() { - final int lower = distribution.getSupportLowerBound(); - Assertions.assertEquals(0.0, distribution.cumulativeProbability(lower - 1), 0.0, - "Cumulative probability must be 0 below support lower bound."); - Assertions.assertEquals(distribution.probability(lower), distribution.cumulativeProbability(lower), getTolerance(), - "Cumulative probability of support lower bound must be equal to probability mass at this point."); - Assertions.assertEquals(1.0, distribution.survivalProbability(lower - 1), 0.0, - "Survival probability must be 1.0 below support lower bound."); - Assertions.assertEquals(lower, distribution.inverseCumulativeProbability(0.0), - "Inverse cumulative probability of 0 must be equal to support lower bound."); - - final int upper = distribution.getSupportUpperBound(); - if (upper != Integer.MAX_VALUE) { - Assertions.assertEquals(1.0, distribution.cumulativeProbability(upper), 0.0, - "Cumulative probability of support upper bound must be equal to 1."); - Assertions.assertEquals(0.0, distribution.survivalProbability(upper), 0.0, - "Survival probability of support upper bound must be equal to 0."); + void testOutsideSupport() { + // Test various quantities when the variable is outside the support. + final int lo = distribution.getSupportLowerBound(); + Assertions.assertEquals(distribution.probability(lo), distribution.cumulativeProbability(lo), getTolerance()); + Assertions.assertEquals(lo, distribution.inverseCumulativeProbability(0.0)); + + if (lo != Integer.MIN_VALUE) { + final int below = lo - 1; + Assertions.assertEquals(0.0, distribution.probability(below)); + Assertions.assertEquals(Double.NEGATIVE_INFINITY, distribution.logProbability(below)); + Assertions.assertEquals(0.0, distribution.cumulativeProbability(below)); + Assertions.assertEquals(1.0, distribution.survivalProbability(below)); + } + + final int hi = distribution.getSupportUpperBound(); + Assertions.assertEquals(0.0, distribution.survivalProbability(hi)); + Assertions.assertEquals(distribution.probability(hi), distribution.survivalProbability(hi - 1), getTolerance()); + Assertions.assertEquals(hi, distribution.inverseCumulativeProbability(1.0)); + if (hi != Integer.MAX_VALUE) { + final int above = hi + 1; + Assertions.assertEquals(0.0, distribution.probability(above)); + Assertions.assertEquals(Double.NEGATIVE_INFINITY, distribution.logProbability(above)); + Assertions.assertEquals(1.0, distribution.cumulativeProbability(above)); + Assertions.assertEquals(0.0, distribution.survivalProbability(above)); } - Assertions.assertEquals(upper, distribution.inverseCumulativeProbability(1.0), - "Inverse cumulative probability of 1 must be equal to support upper bound."); } @Test - void testPrecondition1() { + void testProbabilityWithLowerBoundAboveUpperBound() { Assertions.assertThrows(DistributionException.class, () -> distribution.probability(1, 0)); } + @Test - void testPrecondition2() { + void testInverseCumulativeProbabilityWithProbabilityBelowZero() { Assertions.assertThrows(DistributionException.class, () -> distribution.inverseCumulativeProbability(-1)); } + @Test - void testPrecondition3() { + void testInverseCumulativeProbabilityWithProbabilityAboveOne() { Assertions.assertThrows(DistributionException.class, () -> distribution.inverseCumulativeProbability(2)); } - /** - * Test sampling. - */ @Test void testSampling() { - final int[] densityPoints = makeDensityTestPoints(); - final double[] densityValues = makeDensityTestValues(); - final int sampleSize = 1000; - final int length = TestUtils.eliminateZeroMassPoints(densityPoints, densityValues); - final AbstractDiscreteDistribution dist = (AbstractDiscreteDistribution) makeDistribution(); - final double[] expectedCounts = new double[length]; - final long[] observedCounts = new long[length]; - for (int i = 0; i < length; i++) { - expectedCounts[i] = sampleSize * densityValues[i]; - } // Use fixed seed. + final int sampleSize = 1000; final DiscreteDistribution.Sampler sampler = - dist.createSampler(RandomSource.create(RandomSource.WELL_512_A, 1000)); + getDistribution().createSampler(RandomSource.create(RandomSource.WELL_512_A, 1000)); final int[] sample = TestUtils.sample(sampleSize, sampler); + + final int[] densityPoints = makeDensityTestPoints(); + final double[] densityValues = makeDensityTestValues(); + final int length = TestUtils.eliminateZeroMassPoints(densityPoints, densityValues); + final double[] expected = Arrays.copyOf(densityValues, length); + + final long[] counts = new long[length]; for (int i = 0; i < sampleSize; i++) { + final int x = sample[i]; for (int j = 0; j < length; j++) { - if (sample[i] == densityPoints[j]) { - observedCounts[j]++; + if (x == densityPoints[j]) { + counts[j]++; + break; } } } - TestUtils.assertChiSquareAccept(densityPoints, expectedCounts, observedCounts, 0.001); + + TestUtils.assertChiSquareAccept(densityPoints, expected, counts, 0.001); } /** @@ -442,32 +490,47 @@ abstract class DiscreteDistributionAbstractTest { } //------------------ Getters / Setters for test instance data ----------- + /** - * @return Returns the cumulativeTestPoints. + * @return Returns the distribution. */ - protected int[] getCumulativeTestPoints() { - return cumulativeTestPoints; + protected DiscreteDistribution getDistribution() { + return distribution; } /** - * @param cumulativeTestPoints The cumulativeTestPoints to set. + * @param distribution The distribution to set. */ - protected void setCumulativeTestPoints(int[] cumulativeTestPoints) { - this.cumulativeTestPoints = cumulativeTestPoints; + protected void setDistribution(DiscreteDistribution distribution) { + this.distribution = distribution; } /** - * @return Returns the cumulativeTestValues. + * @return Returns the tolerance. */ - protected double[] getCumulativeTestValues() { - return cumulativeTestValues; + protected double getTolerance() { + return tolerance; } /** - * @param cumulativeTestValues The cumulativeTestValues to set. + * @param tolerance The tolerance to set. */ - protected void setCumulativeTestValues(double[] cumulativeTestValues) { - this.cumulativeTestValues = cumulativeTestValues; + protected void setTolerance(double tolerance) { + this.tolerance = tolerance; + } + + /** + * @return Returns the high precision tolerance. + */ + protected double getHighPrecisionTolerance() { + return highPrecisionTolerance; + } + + /** + * @param highPrecisionTolerance The high precision highPrecisionTolerance to set. + */ + protected void setHighPrecisionTolerance(double highPrecisionTolerance) { + this.highPrecisionTolerance = highPrecisionTolerance; } /** @@ -517,6 +580,34 @@ abstract class DiscreteDistributionAbstractTest { } /** + * @return Returns the cumulativeTestPoints. + */ + protected int[] getCumulativeTestPoints() { + return cumulativeTestPoints; + } + + /** + * @param cumulativeTestPoints The cumulativeTestPoints to set. + */ + protected void setCumulativeTestPoints(int[] cumulativeTestPoints) { + this.cumulativeTestPoints = cumulativeTestPoints; + } + + /** + * @return Returns the cumulativeTestValues. + */ + protected double[] getCumulativeTestValues() { + return cumulativeTestValues; + } + + /** + * @param cumulativeTestValues The cumulativeTestValues to set. + */ + protected void setCumulativeTestValues(double[] cumulativeTestValues) { + this.cumulativeTestValues = cumulativeTestValues; + } + + /** * @return Returns the cumulativePrecisionTestPoints. */ protected int[] getCumulativePrecisionTestPoints() { @@ -573,20 +664,6 @@ abstract class DiscreteDistributionAbstractTest { } /** - * @return Returns the distribution. - */ - protected DiscreteDistribution getDistribution() { - return distribution; - } - - /** - * @param distribution The distribution to set. - */ - protected void setDistribution(DiscreteDistribution distribution) { - this.distribution = distribution; - } - - /** * @return Returns the inverseCumulativeTestPoints. */ protected double[] getInverseCumulativeTestPoints() { @@ -615,34 +692,6 @@ abstract class DiscreteDistributionAbstractTest { } /** - * @return Returns the tolerance. - */ - protected double getTolerance() { - return tolerance; - } - - /** - * @param tolerance The tolerance to set. - */ - protected void setTolerance(double tolerance) { - this.tolerance = tolerance; - } - - /** - * @return Returns the high precision tolerance. - */ - protected double getHighPrecisionTolerance() { - return highPrecisionTolerance; - } - - /** - * @param highPrecisionTolerance The high precision highPrecisionTolerance to set. - */ - protected void setHighPrecisionTolerance(double highPrecisionTolerance) { - this.highPrecisionTolerance = highPrecisionTolerance; - } - - /** * The expected value for {@link DiscreteDistribution#isSupportConnected()}. * The default is {@code true}. Test class should override this when the distribution * is not support connected. diff --git a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/TruncatedNormalDistributionTest.java b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/TruncatedNormalDistributionTest.java index c837a84..bee103c 100644 --- a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/TruncatedNormalDistributionTest.java +++ b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/TruncatedNormalDistributionTest.java @@ -98,7 +98,7 @@ class TruncatedNormalDistributionTest extends ContinuousDistributionAbstractTest testMoments(distribution, mean, variance); testConsistency(); - testSampler(); + testSampling(); testOutsideSupport(); testDensities(); testLogDensities(); @@ -107,9 +107,9 @@ class TruncatedNormalDistributionTest extends ContinuousDistributionAbstractTest testDensityIntegrals(); testCumulativeProbabilities(); testIsSupportConnected(); - testPrecondition1(); - testPrecondition2(); - testPrecondition3(); + testProbabilityWithLowerBoundAboveUpperBound(); + testInverseCumulativeProbabilityWithProbabilityBelowZero(); + testInverseCumulativeProbabilityWithProbabilityAboveOne(); // Bound test Assertions.assertTrue(distribution.getSupportLowerBound() <= distribution.inverseCumulativeProbability(Double.MIN_VALUE));