BoostTools.java

  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. //  (C) Copyright John Maddock 2006.
  18. //  Use, modification and distribution are subject to the
  19. //  Boost Software License, Version 1.0. (See accompanying file
  20. //  LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

  21. package org.apache.commons.numbers.gamma;

  22. import java.util.function.DoubleSupplier;

  23. /**
  24.  * Utility tools used by the Boost functions.
  25.  *
  26.  * <p>This code has been adapted from the <a href="https://www.boost.org/">Boost</a>
  27.  * {@code c++} implementations in {@code <boost/math/tools/>}.
  28.  * All work is copyright John Maddock 2006 and subject to the Boost Software License.
  29.  */
  30. final class BoostTools {
  31.     /**
  32.      * The minimum epsilon value for relative error in the summation.
  33.      * Equal to Math.ulp(1.0) or 2^-52.
  34.      *
  35.      * <h2>Note</h2>
  36.      *
  37.      * <p>The summation will terminate when any additional terms are too small to
  38.      * change the sum. Assuming additional terms are reducing in magnitude this
  39.      * occurs when the term is 1 ULP from the sum:
  40.      * <pre>{@code
  41.      * ulp(sum) >= term
  42.      * }</pre>
  43.      *
  44.      * <p>The epsilon is used to set a configurable threshold using:
  45.      * <pre>{@code
  46.      * sum * eps >= term
  47.      * }</pre>
  48.      * <p>The minimum epsilon is the smallest value that will create a value
  49.      * {@code >= 1} ULP and {@code < 2} ULP of the sum. For any normal number the ULP
  50.      * of all values with the same exponent b is scalb(1.0, b - 52). This can
  51.      * be achieved by multiplication by 2^-52.
  52.      */
  53.     private static final double EPSILON = 0x1.0p-52;
  54.     /**
  55.      * The minimum epsilon value for relative error in the Kahan summation.
  56.      * This can be lower than {@link #EPSILON}. Set to 2^-62.
  57.      *
  58.      * <h2>Note</h2>
  59.      *
  60.      * <p>The Kahan summation uses a carry term to extend the precision of the sum.
  61.      * This extends the 53-bit mantissa by adding more bits to hold round-off error.
  62.      * Thus the term may effect the sum when it has a magnitude smaller than 1 ULP
  63.      * of the current sum. The epsilon can be lowered from 2^-52 to include the
  64.      * extra bits in the convergence criteria. The lower limit for the epsilon is
  65.      * 2^-104. Boost uses an epsilon specified as a number of bits of accuracy. Each
  66.      * lowering of the epsilon by a factor of 2 adds a guard digit to the sum.
  67.      * Slower converging series will benefit from a lower epsilon. This uses 2^-62
  68.      * to add 10 guard digits and allow testing of different thresholds when the
  69.      * Kahan summation is used, for example in the gamma function. Lowering the
  70.      * epsilon further is only of benefit if the terms can be computed exactly.
  71.      * Otherwise the rounding errors of the early terms affect the final result as
  72.      * much as the inclusion of extra guard digits.
  73.      */
  74.     private static final double KAHAN_EPSILON = 0x1.0p-62;
  75.     /** Message for failure to converge. */
  76.     private static final String MSG_FAILED_TO_CONVERGE = "Failed to converge within %d iterations";

  77.     /** Private constructor. */
  78.     private BoostTools() {
  79.         // intentionally empty.
  80.     }

  81.     /**
  82.      * Sum the series.
  83.      *
  84.      * <p>Adapted from {@code boost/math/tools/series.hpp}.
  85.      *
  86.      * @param func Series generator
  87.      * @param epsilon Maximum relative error allowed
  88.      * @param maxTerms Maximum number of terms
  89.      * @return result
  90.      */
  91.     static double sumSeries(DoubleSupplier func, double epsilon, int maxTerms) {
  92.         return sumSeries(func, epsilon, maxTerms, 0);
  93.     }

  94.     /**
  95.      * Sum the series.
  96.      *
  97.      * <p>Adapted from {@code boost/math/tools/series.hpp}.
  98.      *
  99.      * @param func Series generator
  100.      * @param epsilon Maximum relative error allowed
  101.      * @param maxTerms Maximum number of terms
  102.      * @param initValue Initial value
  103.      * @return result
  104.      */
  105.     static double sumSeries(DoubleSupplier func, double epsilon, int maxTerms, double initValue) {
  106.         // Note:
  107.         // The Boost code requires eps to be non-zero. It is created in the
  108.         // <boost/math/policies/policy.hpp> as a non-zero relative error term.
  109.         // An alternative termination condition with a divide is:
  110.         // (eps < Math.abs(nextTerm / result))
  111.         //
  112.         // Here the argument is checked against the minimum epsilon for a double
  113.         // to provide functional equivalence with the Boost policy.
  114.         // In the min eps case the loop terminates if the most recently added term is
  115.         // 0 or 1 ulp of the result. This condition is acceptable if the next
  116.         // computed term will be at most half of the most recent term (thus
  117.         // cannot be added to the current result).

  118.         final double eps = getEpsilon(epsilon, EPSILON);

  119.         int counter = maxTerms;

  120.         double result = initValue;
  121.         double nextTerm;
  122.         do {
  123.             nextTerm = func.getAsDouble();
  124.             result += nextTerm;
  125.         } while (Math.abs(eps * result) < Math.abs(nextTerm) && --counter > 0);

  126.         if (counter <= 0) {
  127.             throw new ArithmeticException(
  128.                String.format(MSG_FAILED_TO_CONVERGE, maxTerms));
  129.         }

  130.         return result;
  131.     }

  132.     /**
  133.      * Sum the series using Kahan summation.
  134.      *
  135.      * <p>Adapted from {@code boost/math/tools/series.hpp}.
  136.      *
  137.      * @param func Series generator
  138.      * @param epsilon Maximum relative error allowed
  139.      * @param maxTerms Maximum number of terms
  140.      * @return result
  141.      */
  142.     static double kahanSumSeries(DoubleSupplier func, double epsilon, int maxTerms) {
  143.         return kahanSumSeries(func, epsilon, maxTerms, 0);
  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.      * @param initValue Initial value
  154.      * @return result
  155.      */
  156.     static double kahanSumSeries(DoubleSupplier func, double epsilon, int maxTerms, double initValue) {
  157.         final double eps = getEpsilon(epsilon, KAHAN_EPSILON);

  158.         int counter = maxTerms;

  159.         // Kahan summation:
  160.         // https://en.wikipedia.org/wiki/Kahan_summation_algorithm
  161.         // This summation is accurate if the term is smaller in magnitude
  162.         // than the current sum. This is a condition required for the
  163.         // series termination thus the extended precision sum need not
  164.         // check magnitudes of terms to compute the carry.

  165.         double result = initValue;
  166.         double carry = 0;
  167.         double nextTerm;
  168.         do {
  169.             nextTerm = func.getAsDouble();
  170.             final double y = nextTerm - carry;
  171.             final double t = result + y;
  172.             carry = t - result;
  173.             carry -= y;
  174.             result = t;
  175.         } while (Math.abs(eps * result) < Math.abs(nextTerm) && --counter > 0);

  176.         if (counter <= 0) {
  177.             throw new ArithmeticException(
  178.                String.format(MSG_FAILED_TO_CONVERGE, maxTerms));
  179.         }

  180.         return result;
  181.     }

  182.     /**
  183.      * Gets the epsilon ensuring it satisfies the minimum allowed value.
  184.      *
  185.      * <p>This is returning the maximum of the two arguments.
  186.      * Do not use Math.max as it returns NaN if either value is NaN.
  187.      * In this case the desired result in the default minEpsilon, not NaN.
  188.      * Math.max will also check ordering when terms are equal to support
  189.      * -0.0 and 0.0. This does not apply here and a single conditional
  190.      * returns the desired result.
  191.      *
  192.      * @param epsilon Configured epsilon
  193.      * @param minEpsilon Minimum allowed epsilon
  194.      * @return the epsilon
  195.      */
  196.     private static double getEpsilon(double epsilon, double minEpsilon) {
  197.         return epsilon > minEpsilon ? epsilon : minEpsilon;
  198.     }

  199.     /**
  200.      * Evaluate the polynomial using Horner's method.
  201.      * The coefficients are used in descending order, for example a polynomial of order
  202.      * 3 requires 4 coefficients:
  203.      * <pre>
  204.      * f(x) = c[3] * x^3 + c[2] * x^2 + c[1] * x + c[0]
  205.      * </pre>
  206.      *
  207.      * @param c Polynomial coefficients (must have {@code length > 0})
  208.      * @param x Argument x
  209.      * @return polynomial value
  210.      */
  211.     static double evaluatePolynomial(double[] c, double x) {
  212.         final int count = c.length;
  213.         double sum = c[count - 1];
  214.         for (int i = count - 2; i >= 0; --i) {
  215.             sum *= x;
  216.             sum += c[i];
  217.         }
  218.         return sum;
  219.     }
  220. }