blob: 521dcf5ae889f1d79dec007f69a521b4060aaa20 [file] [log] [blame]
/*
* 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.
*/
package org.apache.commons.statistics.distribution;
import org.apache.commons.rng.simple.RandomSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
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
* <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.
*/
abstract class DiscreteDistributionAbstractTest {
//-------------------- Private test instance data -------------------------
/** Discrete distribution instance used to perform tests. */
private DiscreteDistribution distribution;
/** Tolerance used in comparing expected and returned values. */
private double tolerance = 1e-12;
/** Arguments used to test probability density calculations. */
private int[] densityTestPoints;
/** Values used to test probability density calculations. */
private double[] densityTestValues;
/** Values used to test logarithmic probability density calculations. */
private double[] logDensityTestValues;
/** Arguments used to test cumulative probability density calculations. */
private int[] cumulativeTestPoints;
/** Values used to test cumulative probability density calculations. */
private double[] cumulativeTestValues;
/** Arguments used to test inverse cumulative probability density calculations. */
private double[] inverseCumulativeTestPoints;
/** Values used to test inverse cumulative probability density calculations. */
private int[] inverseCumulativeTestValues;
//-------------------- Abstract methods -----------------------------------
/** Creates the default discrete distribution instance to use in tests. */
public abstract DiscreteDistribution makeDistribution();
/** Creates the default probability density test input values. */
public abstract int[] makeDensityTestPoints();
/** Creates the default probability density test expected values. */
public abstract double[] makeDensityTestValues();
/** Creates the default logarithmic probability density test expected values.
*
* The default implementation simply computes the logarithm of all the values in
* {@link #makeDensityTestValues()}.
*
* @return double[] the default logarithmic probability density test expected values.
*/
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;
}
/** Creates the default cumulative probability density test input values. */
public abstract int[] makeCumulativeTestPoints();
/** Creates the default cumulative probability density test expected values. */
public abstract double[] makeCumulativeTestValues();
/** Creates the default inverse cumulative probability test input values. */
public abstract double[] makeInverseCumulativeTestPoints();
/** Creates the default inverse cumulative probability density test expected values. */
public abstract int[] makeInverseCumulativeTestValues();
//-------------------- Setup / tear down ----------------------------------
/**
* Setup sets all test instance data to default values.
*/
@BeforeEach
void setUp() {
distribution = makeDistribution();
densityTestPoints = makeDensityTestPoints();
densityTestValues = makeDensityTestValues();
logDensityTestValues = makeLogDensityTestValues();
cumulativeTestPoints = makeCumulativeTestPoints();
cumulativeTestValues = makeCumulativeTestValues();
inverseCumulativeTestPoints = makeInverseCumulativeTestPoints();
inverseCumulativeTestValues = makeInverseCumulativeTestValues();
}
/**
* Cleans up test instance data
*/
@AfterEach
void tearDown() {
distribution = null;
densityTestPoints = null;
densityTestValues = null;
logDensityTestValues = null;
cumulativeTestPoints = null;
cumulativeTestValues = null;
inverseCumulativeTestPoints = null;
inverseCumulativeTestValues = null;
}
//-------------------- Verification methods -------------------------------
/**
* Verifies that probability density calculations match expected values
* using current test instance data.
*/
protected void verifyDensities() {
for (int i = 0; i < densityTestPoints.length; i++) {
final int testPoint = densityTestPoints[i];
Assertions.assertEquals(densityTestValues[i],
distribution.probability(testPoint), getTolerance(),
() -> "Incorrect density value returned for " + testPoint);
}
}
/**
* Verifies that logarithmic probability density calculations match expected values
* using current test instance data.
*/
protected void verifyLogDensities() {
for (int i = 0; i < densityTestPoints.length; i++) {
// FIXME: when logProbability methods are added to DiscreteDistribution in 4.0, remove cast below
final int testPoint = densityTestPoints[i];
Assertions.assertEquals(logDensityTestValues[i],
((AbstractDiscreteDistribution) distribution).logProbability(testPoint), tolerance,
() -> "Incorrect log density value returned for " + testPoint);
}
}
/**
* Verifies that cumulative probability density calculations match expected values
* using current test instance data.
*/
protected void verifyCumulativeProbabilities() {
for (int i = 0; i < cumulativeTestPoints.length; i++) {
final int testPoint = cumulativeTestPoints[i];
Assertions.assertEquals(cumulativeTestValues[i],
distribution.cumulativeProbability(testPoint), getTolerance(),
() -> "Incorrect cumulative probability value returned for " + testPoint);
}
}
/**
* Verifies that inverse cumulative probability density calculations match expected values
* using current test instance data.
*/
protected void verifyInverseCumulativeProbabilities() {
for (int i = 0; i < inverseCumulativeTestPoints.length; i++) {
final double testPoint = inverseCumulativeTestPoints[i];
Assertions.assertEquals(inverseCumulativeTestValues[i],
distribution.inverseCumulativeProbability(testPoint),
() -> "Incorrect inverse cumulative probability value returned for " + testPoint);
}
}
//------------------------ 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();
}
/**
* Verifies that inverse cumulative probability density calculations match expected values
* using default test instance data.
*/
@Test
void testInverseCumulativeProbabilities() {
verifyInverseCumulativeProbabilities();
}
@Test
void testConsistencyAtSupportBounds() {
final int lower = distribution.getSupportLowerBound();
Assertions.assertEquals(0.0, distribution.cumulativeProbability(lower - 1), 0.0,
"Cumulative probability mmust 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(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(upper, distribution.inverseCumulativeProbability(1.0),
"Inverse cumulative probability of 1 must be equal to support upper bound.");
}
@Test
void testPrecondition1() {
Assertions.assertThrows(DistributionException.class, () -> distribution.probability(1, 0));
}
@Test
void testPrecondition2() {
Assertions.assertThrows(DistributionException.class, () -> distribution.inverseCumulativeProbability(-1));
}
@Test
void testPrecondition3() {
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 DiscreteDistribution.Sampler sampler =
dist.createSampler(RandomSource.create(RandomSource.WELL_512_A, 1000));
final int[] sample = TestUtils.sample(sampleSize, sampler);
for (int i = 0; i < sampleSize; i++) {
for (int j = 0; j < length; j++) {
if (sample[i] == densityPoints[j]) {
observedCounts[j]++;
}
}
}
TestUtils.assertChiSquareAccept(densityPoints, expectedCounts, observedCounts, 0.001);
}
/**
* Test if the distribution is support connected. This test exists to ensure the support
* connected property is tested.
*/
@Test
void testIsSupportConnected() {
Assertions.assertEquals(isSupportConnected(), distribution.isSupportConnected());
}
//------------------ Getters / Setters for test instance data -----------
/**
* @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 densityTestPoints.
*/
protected int[] getDensityTestPoints() {
return densityTestPoints;
}
/**
* @param densityTestPoints The densityTestPoints to set.
*/
protected void setDensityTestPoints(int[] densityTestPoints) {
this.densityTestPoints = densityTestPoints;
}
/**
* @return Returns the densityTestValues.
*/
protected double[] getDensityTestValues() {
return densityTestValues;
}
/**
* @param densityTestValues The densityTestValues to set.
*/
protected void setDensityTestValues(double[] densityTestValues) {
this.densityTestValues = densityTestValues;
}
/**
* @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() {
return inverseCumulativeTestPoints;
}
/**
* @param inverseCumulativeTestPoints The inverseCumulativeTestPoints to set.
*/
protected void setInverseCumulativeTestPoints(double[] inverseCumulativeTestPoints) {
this.inverseCumulativeTestPoints = inverseCumulativeTestPoints;
}
/**
* @return Returns the inverseCumulativeTestValues.
*/
protected int[] getInverseCumulativeTestValues() {
return inverseCumulativeTestValues;
}
/**
* @param inverseCumulativeTestValues The inverseCumulativeTestValues to set.
*/
protected void setInverseCumulativeTestValues(int[] inverseCumulativeTestValues) {
this.inverseCumulativeTestValues = inverseCumulativeTestValues;
}
/**
* @return Returns the tolerance.
*/
protected double getTolerance() {
return tolerance;
}
/**
* @param tolerance The tolerance to set.
*/
protected void setTolerance(double tolerance) {
this.tolerance = tolerance;
}
/**
* The expected value for {@link DiscreteDistribution#isSupportConnected()}.
* The default is {@code true}. Test class should override this when the distribution
* is not support connected.
*
* @return Returns true if the distribution is support connected.
*/
protected boolean isSupportConnected() {
return true;
}
}