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  //  (C) Copyright John Maddock 2006.
19  //  Use, modification and distribution are subject to the
20  //  Boost Software License, Version 1.0. (See accompanying file
21  //  LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
22  
23  package org.apache.commons.numbers.gamma;
24  
25  import java.util.function.DoubleSupplier;
26  
27  /**
28   * Utility tools used by the Boost functions.
29   *
30   * <p>This code has been adapted from the <a href="https://www.boost.org/">Boost</a>
31   * {@code c++} implementations in {@code <boost/math/tools/>}.
32   * All work is copyright John Maddock 2006 and subject to the Boost Software License.
33   */
34  final class BoostTools {
35      /**
36       * The minimum epsilon value for relative error in the summation.
37       * Equal to Math.ulp(1.0) or 2^-52.
38       *
39       * <h2>Note</h2>
40       *
41       * <p>The summation will terminate when any additional terms are too small to
42       * change the sum. Assuming additional terms are reducing in magnitude this
43       * occurs when the term is 1 ULP from the sum:
44       * <pre>
45       * ulp(sum) >= term
46       * </pre>
47       *
48       * <p>The epsilon is used to set a configurable threshold using:
49       * <pre>
50       * sum * eps >= term
51       * </pre>
52       * <p>The minimum epsilon is the smallest value that will create a value
53       * {@code >= 1} ULP and {@code < 2} ULP of the sum. For any normal number the ULP
54       * of all values with the same exponent b is scalb(1.0, b - 52). This can
55       * be achieved by multiplication by 2^-52.
56       */
57      private static final double EPSILON = 0x1.0p-52;
58      /**
59       * The minimum epsilon value for relative error in the Kahan summation.
60       * This can be lower than {@link #EPSILON}. Set to 2^-62.
61       *
62       * <h2>Note</h2>
63       *
64       * <p>The Kahan summation uses a carry term to extend the precision of the sum.
65       * This extends the 53-bit mantissa by adding more bits to hold round-off error.
66       * Thus the term may effect the sum when it has a magnitude smaller than 1 ULP
67       * of the current sum. The epsilon can be lowered from 2^-52 to include the
68       * extra bits in the convergence criteria. The lower limit for the epsilon is
69       * 2^-104. Boost uses an epsilon specified as a number of bits of accuracy. Each
70       * lowering of the epsilon by a factor of 2 adds a guard digit to the sum.
71       * Slower converging series will benefit from a lower epsilon. This uses 2^-62
72       * to add 10 guard digits and allow testing of different thresholds when the
73       * Kahan summation is used, for example in the gamma function. Lowering the
74       * epsilon further is only of benefit if the terms can be computed exactly.
75       * Otherwise the rounding errors of the early terms affect the final result as
76       * much as the inclusion of extra guard digits.
77       */
78      private static final double KAHAN_EPSILON = 0x1.0p-62;
79      /** Message for failure to converge. */
80      private static final String MSG_FAILED_TO_CONVERGE = "Failed to converge within %d iterations";
81  
82      /** Private constructor. */
83      private BoostTools() {
84          // intentionally empty.
85      }
86  
87      /**
88       * Sum the series.
89       *
90       * <p>Adapted from {@code boost/math/tools/series.hpp}.
91       *
92       * @param func Series generator
93       * @param epsilon Maximum relative error allowed
94       * @param maxTerms Maximum number of terms
95       * @return result
96       */
97      static double sumSeries(DoubleSupplier func, double epsilon, int maxTerms) {
98          return sumSeries(func, epsilon, maxTerms, 0);
99      }
100 
101     /**
102      * Sum the series.
103      *
104      * <p>Adapted from {@code boost/math/tools/series.hpp}.
105      *
106      * @param func Series generator
107      * @param epsilon Maximum relative error allowed
108      * @param maxTerms Maximum number of terms
109      * @param initValue Initial value
110      * @return result
111      */
112     static double sumSeries(DoubleSupplier func, double epsilon, int maxTerms, double initValue) {
113         // Note:
114         // The Boost code requires eps to be non-zero. It is created in the
115         // <boost/math/policies/policy.hpp> as a non-zero relative error term.
116         // An alternative termination condition with a divide is:
117         // (eps < Math.abs(nextTerm / result))
118         //
119         // Here the argument is checked against the minimum epsilon for a double
120         // to provide functional equivalence with the Boost policy.
121         // In the min eps case the loop terminates if the most recently added term is
122         // 0 or 1 ulp of the result. This condition is acceptable if the next
123         // computed term will be at most half of the most recent term (thus
124         // cannot be added to the current result).
125 
126         final double eps = getEpsilon(epsilon, EPSILON);
127 
128         int counter = maxTerms;
129 
130         double result = initValue;
131         double nextTerm;
132         do {
133             nextTerm = func.getAsDouble();
134             result += nextTerm;
135         } while (Math.abs(eps * result) < Math.abs(nextTerm) && --counter > 0);
136 
137         if (counter <= 0) {
138             throw new ArithmeticException(
139                String.format(MSG_FAILED_TO_CONVERGE, maxTerms));
140         }
141 
142         return result;
143     }
144 
145     /**
146      * Sum the series using Kahan summation.
147      *
148      * <p>Adapted from {@code boost/math/tools/series.hpp}.
149      *
150      * @param func Series generator
151      * @param epsilon Maximum relative error allowed
152      * @param maxTerms Maximum number of terms
153      * @return result
154      */
155     static double kahanSumSeries(DoubleSupplier func, double epsilon, int maxTerms) {
156         return kahanSumSeries(func, epsilon, maxTerms, 0);
157     }
158 
159     /**
160      * Sum the series using Kahan summation.
161      *
162      * <p>Adapted from {@code boost/math/tools/series.hpp}.
163      *
164      * @param func Series generator
165      * @param epsilon Maximum relative error allowed
166      * @param maxTerms Maximum number of terms
167      * @param initValue Initial value
168      * @return result
169      */
170     static double kahanSumSeries(DoubleSupplier func, double epsilon, int maxTerms, double initValue) {
171         final double eps = getEpsilon(epsilon, KAHAN_EPSILON);
172 
173         int counter = maxTerms;
174 
175         // Kahan summation:
176         // https://en.wikipedia.org/wiki/Kahan_summation_algorithm
177         // This summation is accurate if the term is smaller in magnitude
178         // than the current sum. This is a condition required for the
179         // series termination thus the extended precision sum need not
180         // check magnitudes of terms to compute the carry.
181 
182         double result = initValue;
183         double carry = 0;
184         double nextTerm;
185         do {
186             nextTerm = func.getAsDouble();
187             final double y = nextTerm - carry;
188             final double t = result + y;
189             carry = t - result;
190             carry -= y;
191             result = t;
192         } while (Math.abs(eps * result) < Math.abs(nextTerm) && --counter > 0);
193 
194         if (counter <= 0) {
195             throw new ArithmeticException(
196                String.format(MSG_FAILED_TO_CONVERGE, maxTerms));
197         }
198 
199         return result;
200     }
201 
202     /**
203      * Gets the epsilon ensuring it satisfies the minimum allowed value.
204      *
205      * <p>This is returning the maximum of the two arguments.
206      * Do not use Math.max as it returns NaN if either value is NaN.
207      * In this case the desired result in the default minEpsilon, not NaN.
208      * Math.max will also check ordering when terms are equal to support
209      * -0.0 and 0.0. This does not apply here and a single conditional
210      * returns the desired result.
211      *
212      * @param epsilon Configured epsilon
213      * @param minEpsilon Minimum allowed epsilon
214      * @return the epsilon
215      */
216     private static double getEpsilon(double epsilon, double minEpsilon) {
217         return epsilon > minEpsilon ? epsilon : minEpsilon;
218     }
219 
220     /**
221      * Evaluate the polynomial using Horner's method.
222      * The coefficients are used in descending order, for example a polynomial of order
223      * 3 requires 4 coefficients:
224      * <pre>
225      * f(x) = c[3] * x^3 + c[2] * x^2 + c[1] * x + c[0]
226      * </pre>
227      *
228      * @param c Polynomial coefficients (must have length > 0)
229      * @param x Argument x
230      * @return polynomial value
231      */
232     static double evaluatePolynomial(double[] c, double x) {
233         final int count = c.length;
234         double sum = c[count - 1];
235         for (int i = count - 2; i >= 0; --i) {
236             sum *= x;
237             sum += c[i];
238         }
239         return sum;
240     }
241 }