View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.statistics.distribution;
19  
20  import java.math.BigDecimal;
21  import java.math.MathContext;
22  import java.util.stream.Stream;
23  import org.junit.jupiter.api.Assertions;
24  import org.junit.jupiter.api.Test;
25  import org.junit.jupiter.params.ParameterizedTest;
26  import org.junit.jupiter.params.provider.Arguments;
27  import org.junit.jupiter.params.provider.MethodSource;
28  
29  /**
30   * Test cases for {@link TrapezoidalDistribution}.
31   * Extends {@link BaseContinuousDistributionTest}. See javadoc of that class for details.
32   */
33  class TrapezoidalDistributionTest extends BaseContinuousDistributionTest {
34      @Override
35      ContinuousDistribution makeDistribution(Object... parameters) {
36          final double a = (Double) parameters[0];
37          final double b = (Double) parameters[1];
38          final double c = (Double) parameters[2];
39          final double d = (Double) parameters[3];
40          return TrapezoidalDistribution.of(a, b, c, d);
41      }
42  
43      @Override
44      Object[][] makeInvalidParameters() {
45          return new Object[][] {
46              {0.0, 0.0, 0.0, 0.0},
47              // 1.0, 2.0, 3.0, 4.0 is OK - move points to incorrect locations
48              {5.0, 2.0, 3.0, 4.0}, // a > d
49              {1.0, 5.0, 3.0, 4.0}, // b > d
50              {1.0, 2.0, 5.0, 4.0}, // c > d
51              {3.5, 2.0, 3.0, 4.0}, // a > c
52              {1.0, 3.5, 3.0, 4.0}, // b > c
53              {2.5, 2.0, 3.0, 4.0}, // a > b
54              {1.0, 2.0, 3.0, 0.0}, // d < a
55              {1.0, 2.0, 0.0, 4.0}, // c < a
56              {1.0, 0.0, 3.0, 4.0}, // b < a
57              {1.0, 2.0, 3.0, 1.5}, // d < b
58              {1.0, 2.0, 1.5, 4.0}, // c < b
59              {1.0, 2.0, 3.0, 2.5}, // d < c
60          };
61      }
62  
63      @Override
64      String[] getParameterNames() {
65          return new String[] {"SupportLowerBound", "B", "C", "SupportUpperBound"};
66      }
67  
68      @Override
69      protected double getRelativeTolerance() {
70          // Tolerance is 4.440892098500626E-15.
71          return 20 * RELATIVE_EPS;
72      }
73  
74      //-------------------- Additional test cases -------------------------------
75  
76      @ParameterizedTest
77      @MethodSource
78      void testAdditionalMoments(double a, double b, double c, double d, double mean, double variance, int ulps) {
79          final TrapezoidalDistribution dist = TrapezoidalDistribution.of(a, b, c, d);
80          testMoments(dist, mean, variance, DoubleTolerances.ulps(ulps));
81      }
82  
83      static Stream<Arguments> testAdditionalMoments() {
84          return Stream.of(
85              // Computed using scipy.stats.trapezoid
86              // Up slope, then flat
87              Arguments.of(0, 0.1,   1, 1, 0.5245614035087719, 0.07562480763311791, 8),
88              Arguments.of(0, 1e-3,  1, 1, 0.5002499583124894, 0.08325006249999839, 8),
89              Arguments.of(0, 1e-6,  1, 1, 0.5000002499999582, 0.08333325000006259, 8),
90              Arguments.of(0, 1e-9,  1, 1, 0.50000000025,      0.08333333324999997, 4),
91              Arguments.of(0, 1e-12, 1, 1, 0.50000000000025,   0.08333333333324999, 4),
92              Arguments.of(0, 1e-15, 1, 1, 0.5000000000000003, 0.0833333333333332, 4),
93              Arguments.of(0, 0,     1, 1, 0.5,                0.08333333333333331, 1),
94              // Flat, then down slope
95              Arguments.of(0, 0, 0.9,               1, 0.47543859649122816, 0.07562480763311777, 4),
96              Arguments.of(0, 0, 0.999,             1, 0.49975004168751025, 0.08325006249999842, 4),
97              Arguments.of(0, 0, 0.999999,          1, 0.4999997500000417,  0.08333325000006248, 2),
98              Arguments.of(0, 0, 0.999999999,       1, 0.49999999975000003, 0.08333333325000003, 1),
99              Arguments.of(0, 0, 0.999999999999,    1, 0.49999999999975003, 0.08333333333324999, 1),
100             Arguments.of(0, 0, 0.999999999999999, 1, 0.4999999999999998,  0.08333333333333326, 1),
101             Arguments.of(0, 0, 1,                 1, 0.5,                 0.08333333333333331, 1),
102             // Computed
103             moments(-3, 1, 4, 12, 10),
104             moments(1, 2, 3, 4, 4),
105             moments(11, 15, 22, 30, 4),
106             moments(0, 1, 9, 10, 4),
107             moments(-12, -10, -2, -1, 8)
108         );
109     }
110 
111     /**
112      * Compute the mean and variance and return the arguments: [a, b, c, d, mean, variance, ulps].
113      * The ulps are the expected units of least precision error for the test. This is typically
114      * limited by the variance as it depends on accuracy of the mean squared.
115      */
116     private static Arguments moments(double a, double b, double c, double d, int ulps) {
117         final BigDecimal aa = new BigDecimal(a);
118         final BigDecimal bb = new BigDecimal(b);
119         final BigDecimal cc = new BigDecimal(c);
120         final BigDecimal dd = new BigDecimal(d);
121         final MathContext mc = MathContext.DECIMAL128;
122         // Mean
123         BigDecimal divisor = dd.add(cc).subtract(aa).subtract(bb).multiply(BigDecimal.valueOf(3));
124         BigDecimal dc = dd.pow(3).subtract(cc.pow(3)).divide(dd.subtract(cc), mc);
125         BigDecimal ba = bb.pow(3).subtract(aa.pow(3)).divide(bb.subtract(aa), mc);
126         final BigDecimal mu = dc.subtract(ba).divide(divisor, mc);
127         // Variance
128         divisor = divisor.multiply(BigDecimal.valueOf(2));
129         dc = dd.pow(4).subtract(cc.pow(4)).divide(dd.subtract(cc), mc);
130         ba = bb.pow(4).subtract(aa.pow(4)).divide(bb.subtract(aa), mc);
131         final BigDecimal var = dc.subtract(ba).divide(divisor, mc).subtract(mu.pow(2));
132         return Arguments.of(a, b, c, d, mu.doubleValue(), var.doubleValue(), ulps);
133     }
134 
135     /**
136      * Create a trapezoid with a very long upper tail to explicitly test the survival
137      * probability is high precision.
138      */
139     @Test
140     void testAdditionalSurvivalProbabilityHighPrecision() {
141         final double a = 0;
142         final double b = 0;
143         final double c = 1;
144         final double d = 1 << 14;
145         final double x1 = Math.nextDown(d);
146         final double x2 = Math.nextDown(x1);
147         final double p1 = survivalProbability(a, b, c, d, x1);
148         final double p2 = survivalProbability(a, b, c, d, x2);
149         final TrapezoidalDistribution dist = TrapezoidalDistribution.of(a, b, c, d);
150         final double[] points = {x1, x2};
151         final double[] probabilities = {p1, p2};
152 
153         // This fails if the sf(x) = 1 - cdf(x)
154         testSurvivalProbabilityHighPrecision(
155             dist,
156             points,
157             probabilities,
158             DoubleTolerances.relative(1e-15));
159 
160         // This fails if the isf(p) = icdf(1 - p)
161         testInverseSurvivalProbability(
162             dist,
163             probabilities,
164             points,
165             DoubleTolerances.ulps(0));
166     }
167 
168     /**
169      * Compute the trapezoid distribution survival probability for the value {@code x}
170      * in the region {@code [c, d]}.
171      *
172      * @param a Lower limit of the distribution (inclusive).
173      * @param b Start of the trapezoid constant density.
174      * @param c End of the trapezoid constant density.
175      * @param d Upper limit of the distribution (inclusive).
176      * @param x Value in [c, d].
177      * @return the probability
178      */
179     private static double survivalProbability(double a, double b, double c, double d, double x) {
180         Assertions.assertTrue(c <= x && x <= d, "Domain error");
181         final BigDecimal aa = new BigDecimal(a);
182         final BigDecimal bb = new BigDecimal(b);
183         final BigDecimal cc = new BigDecimal(c);
184         final BigDecimal dd = new BigDecimal(d);
185         final BigDecimal divisor = dd.add(cc).subtract(aa).subtract(bb).multiply(dd.subtract(cc));
186         return dd.subtract(new BigDecimal(x)).pow(2)
187                 .divide(divisor, MathContext.DECIMAL128).doubleValue();
188     }
189 }