BoostTools.java
/*
* 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.
*/
// (C) Copyright John Maddock 2006.
// Use, modification and distribution are subject to the
// Boost Software License, Version 1.0. (See accompanying file
// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
package org.apache.commons.numbers.gamma;
import java.util.function.DoubleSupplier;
/**
* Utility tools used by the Boost functions.
*
* <p>This code has been adapted from the <a href="https://www.boost.org/">Boost</a>
* {@code c++} implementations in {@code <boost/math/tools/>}.
* All work is copyright John Maddock 2006 and subject to the Boost Software License.
*/
final class BoostTools {
/**
* The minimum epsilon value for relative error in the summation.
* Equal to Math.ulp(1.0) or 2^-52.
*
* <h2>Note</h2>
*
* <p>The summation will terminate when any additional terms are too small to
* change the sum. Assuming additional terms are reducing in magnitude this
* occurs when the term is 1 ULP from the sum:
* <pre>{@code
* ulp(sum) >= term
* }</pre>
*
* <p>The epsilon is used to set a configurable threshold using:
* <pre>{@code
* sum * eps >= term
* }</pre>
* <p>The minimum epsilon is the smallest value that will create a value
* {@code >= 1} ULP and {@code < 2} ULP of the sum. For any normal number the ULP
* of all values with the same exponent b is scalb(1.0, b - 52). This can
* be achieved by multiplication by 2^-52.
*/
private static final double EPSILON = 0x1.0p-52;
/**
* The minimum epsilon value for relative error in the Kahan summation.
* This can be lower than {@link #EPSILON}. Set to 2^-62.
*
* <h2>Note</h2>
*
* <p>The Kahan summation uses a carry term to extend the precision of the sum.
* This extends the 53-bit mantissa by adding more bits to hold round-off error.
* Thus the term may effect the sum when it has a magnitude smaller than 1 ULP
* of the current sum. The epsilon can be lowered from 2^-52 to include the
* extra bits in the convergence criteria. The lower limit for the epsilon is
* 2^-104. Boost uses an epsilon specified as a number of bits of accuracy. Each
* lowering of the epsilon by a factor of 2 adds a guard digit to the sum.
* Slower converging series will benefit from a lower epsilon. This uses 2^-62
* to add 10 guard digits and allow testing of different thresholds when the
* Kahan summation is used, for example in the gamma function. Lowering the
* epsilon further is only of benefit if the terms can be computed exactly.
* Otherwise the rounding errors of the early terms affect the final result as
* much as the inclusion of extra guard digits.
*/
private static final double KAHAN_EPSILON = 0x1.0p-62;
/** Message for failure to converge. */
private static final String MSG_FAILED_TO_CONVERGE = "Failed to converge within %d iterations";
/** Private constructor. */
private BoostTools() {
// intentionally empty.
}
/**
* Sum the series.
*
* <p>Adapted from {@code boost/math/tools/series.hpp}.
*
* @param func Series generator
* @param epsilon Maximum relative error allowed
* @param maxTerms Maximum number of terms
* @return result
*/
static double sumSeries(DoubleSupplier func, double epsilon, int maxTerms) {
return sumSeries(func, epsilon, maxTerms, 0);
}
/**
* Sum the series.
*
* <p>Adapted from {@code boost/math/tools/series.hpp}.
*
* @param func Series generator
* @param epsilon Maximum relative error allowed
* @param maxTerms Maximum number of terms
* @param initValue Initial value
* @return result
*/
static double sumSeries(DoubleSupplier func, double epsilon, int maxTerms, double initValue) {
// Note:
// The Boost code requires eps to be non-zero. It is created in the
// <boost/math/policies/policy.hpp> as a non-zero relative error term.
// An alternative termination condition with a divide is:
// (eps < Math.abs(nextTerm / result))
//
// Here the argument is checked against the minimum epsilon for a double
// to provide functional equivalence with the Boost policy.
// In the min eps case the loop terminates if the most recently added term is
// 0 or 1 ulp of the result. This condition is acceptable if the next
// computed term will be at most half of the most recent term (thus
// cannot be added to the current result).
final double eps = getEpsilon(epsilon, EPSILON);
int counter = maxTerms;
double result = initValue;
double nextTerm;
do {
nextTerm = func.getAsDouble();
result += nextTerm;
} while (Math.abs(eps * result) < Math.abs(nextTerm) && --counter > 0);
if (counter <= 0) {
throw new ArithmeticException(
String.format(MSG_FAILED_TO_CONVERGE, maxTerms));
}
return result;
}
/**
* Sum the series using Kahan summation.
*
* <p>Adapted from {@code boost/math/tools/series.hpp}.
*
* @param func Series generator
* @param epsilon Maximum relative error allowed
* @param maxTerms Maximum number of terms
* @return result
*/
static double kahanSumSeries(DoubleSupplier func, double epsilon, int maxTerms) {
return kahanSumSeries(func, epsilon, maxTerms, 0);
}
/**
* Sum the series using Kahan summation.
*
* <p>Adapted from {@code boost/math/tools/series.hpp}.
*
* @param func Series generator
* @param epsilon Maximum relative error allowed
* @param maxTerms Maximum number of terms
* @param initValue Initial value
* @return result
*/
static double kahanSumSeries(DoubleSupplier func, double epsilon, int maxTerms, double initValue) {
final double eps = getEpsilon(epsilon, KAHAN_EPSILON);
int counter = maxTerms;
// Kahan summation:
// https://en.wikipedia.org/wiki/Kahan_summation_algorithm
// This summation is accurate if the term is smaller in magnitude
// than the current sum. This is a condition required for the
// series termination thus the extended precision sum need not
// check magnitudes of terms to compute the carry.
double result = initValue;
double carry = 0;
double nextTerm;
do {
nextTerm = func.getAsDouble();
final double y = nextTerm - carry;
final double t = result + y;
carry = t - result;
carry -= y;
result = t;
} while (Math.abs(eps * result) < Math.abs(nextTerm) && --counter > 0);
if (counter <= 0) {
throw new ArithmeticException(
String.format(MSG_FAILED_TO_CONVERGE, maxTerms));
}
return result;
}
/**
* Gets the epsilon ensuring it satisfies the minimum allowed value.
*
* <p>This is returning the maximum of the two arguments.
* Do not use Math.max as it returns NaN if either value is NaN.
* In this case the desired result in the default minEpsilon, not NaN.
* Math.max will also check ordering when terms are equal to support
* -0.0 and 0.0. This does not apply here and a single conditional
* returns the desired result.
*
* @param epsilon Configured epsilon
* @param minEpsilon Minimum allowed epsilon
* @return the epsilon
*/
private static double getEpsilon(double epsilon, double minEpsilon) {
return epsilon > minEpsilon ? epsilon : minEpsilon;
}
/**
* Evaluate the polynomial using Horner's method.
* The coefficients are used in descending order, for example a polynomial of order
* 3 requires 4 coefficients:
* <pre>
* f(x) = c[3] * x^3 + c[2] * x^2 + c[1] * x + c[0]
* </pre>
*
* @param c Polynomial coefficients (must have {@code length > 0})
* @param x Argument x
* @return polynomial value
*/
static double evaluatePolynomial(double[] c, double x) {
final int count = c.length;
double sum = c[count - 1];
for (int i = count - 2; i >= 0; --i) {
sum *= x;
sum += c[i];
}
return sum;
}
}