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    *      https://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  package org.apache.commons.statistics.distribution;
18  
19  import java.math.BigDecimal;
20  import java.math.MathContext;
21  import org.junit.jupiter.api.Assertions;
22  import org.junit.jupiter.api.MethodOrderer;
23  import org.junit.jupiter.api.Order;
24  import org.junit.jupiter.api.Test;
25  import org.junit.jupiter.api.TestMethodOrder;
26  import org.junit.jupiter.params.ParameterizedTest;
27  import org.junit.jupiter.params.provider.CsvFileSource;
28  import org.junit.jupiter.params.provider.ValueSource;
29  
30  /**
31   * Test for {@link ExtendedPrecision}.
32   */
33  @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
34  class ExtendedPrecisionTest {
35      /** sqrt(2). */
36      private static final double ROOT2 = Math.sqrt(2.0);
37      /** sqrt(2 pi) as a String. Computed to 64-digits. https://oeis.org/A019727. */
38      private static final String SQRT_TWO_PI = "2.506628274631000502415765284811045253006986740609938316629923576";
39      /** sqrt(2 pi) as a double. Note: This is 1 ULP different from Math.sqrt(2 * Math.PI). */
40      private static final double ROOT2PI = Double.parseDouble(SQRT_TWO_PI);
41      /** The sum of the squared ULP error for the first standard computation for sqrt(2 * x * x). */
42      private static final RMS SQRT2XX_RMS1 = new RMS();
43      /** The sum of the squared ULP error for the second standard computation for sqrt(2 * x * x). */
44      private static final RMS SQRT2XX_RMS2 = new RMS();
45      /** The sum of the squared ULP error for the first computation for x * sqrt(2 pi). */
46      private static final RMS XSQRT2PI_RMS = new RMS();
47      /** The sum of the squared ULP error for the first computation for exp(-0.5*x*x). */
48      private static final RMS EXPMHXX_RMS1 = new RMS();
49      /** The sum of the squared ULP error for the second computation for exp(-0.5*x*x). */
50      private static final RMS EXPMHXX_RMS2 = new RMS();
51  
52      /**
53       * Class to compute the root mean squared error (RMS).
54       * @see <a href="https://en.wikipedia.org/wiki/Root_mean_square">Wikipedia: RMS</a>
55       */
56      private static final class RMS {
57          private double ss;
58          private double max;
59          private int n;
60  
61          /**
62           * @param x Value (assumed to be positive)
63           */
64          void add(double x) {
65              // Overflow is not supported.
66              // Assume the expected and actual are quite close when measuring the RMS.
67              ss += x * x;
68              n++;
69              // Absolute error when detecting the maximum
70              x = Math.abs(x);
71              max = max < x ? x : max;
72          }
73  
74          /**
75           * Gets the maximum error.
76           *
77           * <p>This is not used for assertions. It can be used to set maximum ULP thresholds
78           * for test data if the TestUtils.assertEquals method is used with a large maxUlps
79           * to measure the ulp (and effectively ignore failures) and the maximum reported
80           * as the end of testing.
81           *
82           * @return maximum error
83           */
84          double getMax() {
85              return max;
86          }
87  
88          /**
89           * Gets the root mean squared error (RMS).
90           *
91           * <p> Note: If no data has been added this will return 0/0 = nan.
92           * This prevents using in assertions without adding data.
93           *
94           * @return root mean squared error (RMS)
95           */
96          double getRMS() {
97              return Math.sqrt(ss / n);
98          }
99      }
100 
101     @Test
102     void testSqrt2PiConstants() {
103         final BigDecimal sqrt2pi = new BigDecimal(SQRT_TWO_PI);
104 
105         // Use a 106-bit number as:
106         // (value, roundOff)
107         final double value = sqrt2pi.doubleValue();
108         final double roundOff = sqrt2pi.subtract(new BigDecimal(value)).doubleValue();
109         // Adding the round-off does not change the value
110         Assertions.assertEquals(value, value + roundOff, "value + round-off");
111         // Check constants
112         Assertions.assertEquals(value, ExtendedPrecision.SQRT2PI.hi(), "sqrt(2 pi)");
113         Assertions.assertEquals(roundOff, ExtendedPrecision.SQRT2PI.lo(), "sqrt(2 pi) round-off");
114         // Sanity check against JDK Math
115         Assertions.assertEquals(value, Math.sqrt(2 * Math.PI), Math.ulp(value), "Math.sqrt(2 pi)");
116     }
117 
118     @Test
119     void testSqrt2xxUnderAndOverflow() {
120         final double x = 1.5;
121         final double e = 2.12132034355964257320253308631;
122         Assertions.assertEquals(e, ExtendedPrecision.sqrt2xx(x));
123         for (final int i : new int[] {-1000, -600, -200, 200, 600, 1000}) {
124             final double scale = Math.scalb(1.0, i);
125             final double x1 = x * scale;
126             final double e1 = e * scale;
127             Assertions.assertEquals(e1, ExtendedPrecision.sqrt2xx(x1), () -> Double.toString(x1));
128         }
129     }
130 
131     @Test
132     void testSqrt2xxExtremes() {
133         // Handle big numbers
134         Assertions.assertEquals(Double.POSITIVE_INFINITY, ExtendedPrecision.sqrt2xx(Double.MAX_VALUE));
135         Assertions.assertEquals(Double.POSITIVE_INFINITY, ExtendedPrecision.sqrt2xx(Double.POSITIVE_INFINITY));
136         Assertions.assertEquals(0.0, ExtendedPrecision.sqrt2xx(0));
137         Assertions.assertEquals(ROOT2, ExtendedPrecision.sqrt2xx(1));
138         Assertions.assertEquals(Math.sqrt(8), ExtendedPrecision.sqrt2xx(2));
139         // Handle sub-normal numbers
140         for (int i = 2; i <= 10; i++) {
141             Assertions.assertEquals(i * Double.MIN_VALUE * Math.sqrt(2), ExtendedPrecision.sqrt2xx(i * Double.MIN_VALUE));
142         }
143         // Currently the argument is assumed to be positive.
144         Assertions.assertEquals(Double.NaN, ExtendedPrecision.sqrt2xx(Double.NaN));
145         // Big negative numbers overflow the square and the extended precision computation generates the overflow result.
146         Assertions.assertEquals(Double.POSITIVE_INFINITY, ExtendedPrecision.sqrt2xx(-1e300));
147     }
148 
149     /**
150      * Test the extended precision {@code sqrt(2 * x * x)}. The expected result
151      * is an extended precision computation. For comparison ulp errors are collected for
152      * two standard precision computations.
153      *
154      * @param x Value x
155      * @param expected Expected result of {@code sqrt(2 * x * x)}.
156      */
157     @ParameterizedTest
158     @Order(1)
159     @CsvFileSource(resources = "sqrt2xx.csv")
160     void testSqrt2xx(double x, BigDecimal expected) {
161         final double e = expected.doubleValue();
162         Assertions.assertEquals(e, ExtendedPrecision.sqrt2xx(x));
163         // Compute error for the standard computations
164         addError(Math.sqrt(2 * x * x), expected, e, SQRT2XX_RMS1);
165         addError(x * ROOT2, expected, e, SQRT2XX_RMS2);
166     }
167 
168     @Test
169     void testSqrt2xxStandardPrecision1() {
170         // Typical result:   max   0.7780  rms   0.2144
171         assertPrecision(SQRT2XX_RMS1, 0.9, 0.3);
172     }
173 
174     @Test
175     void testSqrt2xxStandardPrecision2() {
176         // Typical result:   max   1.0598  rms   0.4781
177         assertPrecision(SQRT2XX_RMS2, 1.3, 0.6);
178     }
179 
180     @ParameterizedTest
181     @ValueSource(doubles = {0, 1, Double.MAX_VALUE, Double.POSITIVE_INFINITY, Double.NaN})
182     void testXsqrt2piEdgeCases(double x) {
183         final double expected = x * ROOT2PI;
184         final double actual = ExtendedPrecision.xsqrt2pi(x);
185         Assertions.assertEquals(expected, actual, 1e-15);
186     }
187 
188     /**
189      * Test the extended precision {@code x * sqrt(2 * pi)}. The expected result
190      * is an extended precision computation. For comparison ulp errors are collected for
191      * a standard computation.
192      *
193      * @param x Value x
194      * @param expected Expected result of {@code x * sqrt(2 * pi)}.
195      */
196     @ParameterizedTest
197     @Order(1)
198     @CsvFileSource(resources = "xsqrt2pi.csv")
199     void testXsqrt2pi(double x, BigDecimal expected) {
200         final double e = expected.doubleValue();
201         Assertions.assertEquals(e, ExtendedPrecision.xsqrt2pi(x));
202         // Compute error for the standard computation
203         addError(x * ROOT2PI, expected, e, XSQRT2PI_RMS);
204     }
205 
206     @Test
207     void testXsqrt2piPrecision() {
208         // Typical result:   max   1.1397  rms   0.5368
209         assertPrecision(XSQRT2PI_RMS, 1.2, 0.6);
210     }
211 
212     @ParameterizedTest
213     @ValueSource(doubles = {0, 0.5, 1, 2, 3, 4, 5, 38.5, Double.MAX_VALUE, Double.POSITIVE_INFINITY, Double.NaN})
214     void testExpmhxxEdgeCases(double x) {
215         final double expected = Math.exp(-0.5 * x * x);
216         Assertions.assertEquals(expected, ExtendedPrecision.expmhxx(x));
217         Assertions.assertEquals(expected, ExtendedPrecision.expmhxx(-x));
218     }
219 
220     /**
221      * Test the extended precision {@code exp(-0.5 * x * x)}. The expected result
222      * is an extended precision computation. For comparison ulp errors are collected for
223      * the standard precision computation.
224      *
225      * @param x Value x
226      * @param expected Expected result of {@code exp(-0.5 * x * x)}.
227      */
228     @ParameterizedTest
229     @Order(1)
230     @CsvFileSource(resources = "expmhxx.csv")
231     void testExpmhxx(double x, BigDecimal expected) {
232         final double e = expected.doubleValue();
233         final double actual = ExtendedPrecision.expmhxx(x);
234         Assertions.assertEquals(e, actual, Math.ulp(e) * 2);
235         // Compute errors
236         addError(actual, expected, e, EXPMHXX_RMS1);
237         addError(Math.exp(-0.5 * x * x), expected, e, EXPMHXX_RMS2);
238     }
239 
240     @Test
241     void testExpmhxxHighPrecision() {
242         // Typical result:   max    0.9727  rms   0.3481
243         assertPrecision(EXPMHXX_RMS1, 1.5, 0.5);
244     }
245 
246     @Test
247     void testExpmhxxStandardPrecision() {
248         // Typical result:   max   385.7193  rms   50.7769
249         assertPrecision(EXPMHXX_RMS2, 400, 60);
250     }
251 
252     private static void assertPrecision(RMS rms, double maxError, double rmsError) {
253         Assertions.assertTrue(rms.getMax() < maxError, () -> "max error: " + rms.getMax());
254         Assertions.assertTrue(rms.getRMS() < rmsError, () -> "rms error: " + rms.getRMS());
255     }
256 
257     private static void addError(double z, BigDecimal expected, double e, RMS rms) {
258         double error;
259         if (z == e) {
260             error = 0;
261         } else {
262             // Compute ULP error
263             error = expected.subtract(new BigDecimal(z))
264                 .divide(new BigDecimal(Math.ulp(e)), MathContext.DECIMAL64).doubleValue();
265         }
266         rms.add(error);
267     }
268 }