Updated atanh using an overflow/underflow safe method.
diff --git a/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/Complex.java b/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/Complex.java
index 33aaec2..f1244c7 100644
--- a/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/Complex.java
+++ b/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/Complex.java
@@ -86,15 +86,25 @@
/** Crossover point to switch computation for asin/acos factor B. */
private static final double B_CROSSOVER = 0.6471;
/**
- * The safe maximum double value {@code x} to avoid loss of precision in asin.
+ * The safe maximum double value {@code x} to avoid loss of precision in asin/acos.
* Equal to sqrt(M) / 8 in the Hull, et al with M the largest normalised floating-point value.
*/
private static final double SAFE_MAX = Math.sqrt(Double.MAX_VALUE) / 8;
/**
- * The safe minimum double value {@code x} to avoid loss of precision/underflow in asin.
+ * The safe minimum double value {@code x} to avoid loss of precision/underflow in asin/acos.
* Equal to sqrt(u) * 4 in the Hull, et al with u the smallest normalised floating-point value.
*/
private static final double SAFE_MIN = Math.sqrt(Double.MIN_NORMAL) * 4;
+ /**
+ * The safe maximum double value {@code x} to avoid loss of precision in atanh.
+ * Equal to sqrt(M) / 2 with M the largest normalised floating-point value.
+ */
+ private static final double SAFE_UPPER = Math.sqrt(Double.MAX_VALUE) / 2;
+ /**
+ * The safe minimum double value {@code x} to avoid loss of precision/underflow in atanh.
+ * Equal to sqrt(u) * 2 with u the smallest normalised floating-point value.
+ */
+ private static final double SAFE_LOWER = Math.sqrt(Double.MIN_NORMAL) * 2;
/** Exponent offset in IEEE754 representation. */
private static final long EXPONENT_OFFSET = 1023L;
/**
@@ -1650,7 +1660,20 @@
*
* <p>This is an odd function: {@code f(z) = -f(-z)}.
*
+ * <p>This is implemented using real {@code x} and imaginary {@code y} parts:</p>
+ * <pre>
+ * <code>
+ * atanh(z) = 0.25 ln(1 + 4x/((1-x)<sup>2</sup>+y<sup>2</sup>) + i 0.5 tan<sup>-1</sup>(2y, 1-x<sup>2</sup>-y<sup>2</sup>)
+ * </code>
+ * </pre>
+ *
+ * <p>The code has been adapted from the <a href="https://www.boost.org/">Boost</a>
+ * {@code c++} implementation {@code <boost/math/complex/atanh.hpp>}. The function is well
+ * defined over the entire complex number range, and produces accurate values even at the
+ * extremes due to special handling of overflow and underflow conditions.</p>
+ *
* @return the inverse hyperbolic tangent of this complex number
+ * @see <a href="http://functions.wolfram.com/ElementaryFunctions/ArcTanh/">ArcTanh</a>
*/
public Complex atanh() {
return atanh(real, imaginary, Complex::ofCartesian);
@@ -1667,59 +1690,157 @@
* @param constructor Constructor.
* @return the inverse hyperbolic tangent of the complex number
*/
- private static Complex atanh(double real, double imaginary, ComplexConstructor constructor) {
- if (Double.isFinite(real)) {
- if (Double.isFinite(imaginary)) {
- // ISO C99: Preserve the equality
- // atanh(conj(z)) = conj(atanh(z))
- // and the odd function: f(z) = -f(-z)
- // by always computing on a positive valued Complex number.
- final double a = Math.abs(real);
- final double b = Math.abs(imaginary);
- // Special case for divide-by-zero
- if (a == 1 && b == 0) {
- // raises the ‘‘divide-by-zero’’ floating-point exception.
- return constructor.create(Math.copySign(Double.POSITIVE_INFINITY, real), imaginary);
- }
+ private static Complex atanh(final double real, final double imaginary,
+ final ComplexConstructor constructor) {
+ // Compute with positive values and determine sign at the end
+ double x = Math.abs(real);
+ double y = Math.abs(imaginary);
+ // The result (without sign correction)
+ double re;
+ double im;
+
+ // Handle C99 special cases
+ if (Double.isNaN(x)) {
+ if (isPosInfinite(y)) {
+ // The sign of the real part of the result is unspecified
+ return constructor.create(0, Math.copySign(PI_OVER_2, imaginary));
+ }
+ // No-use of the input constructor.
+ // Optionally raises the ‘‘invalid’’ floating-point exception, for finite y.
+ return NAN;
+ } else if (Double.isNaN(y)) {
+ if (isPosInfinite(x)) {
+ return constructor.create(Math.copySign(0, real), Double.NaN);
+ }
+ if (x == 0) {
+ return constructor.create(real, Double.NaN);
+ }
+ // No-use of the input constructor
+ return NAN;
+ } else {
+ // x && y are finite or infinite.
+ // Cases for very large finite are handled as if infinite.
+
+ // Check the safe region.
+ // The lower and upper bounds have been copied from boost::math::atanh.
+ // They are different from the safe region for asin and acos.
+ // x >= SAFE_UPPER: (1-x) == x && x^2 -> inf
+ // x <= SAFE_LOWER: x^2 -> 0
+
+ if ((x > SAFE_LOWER) && (x < SAFE_UPPER) && (y > SAFE_LOWER) && (y < SAFE_UPPER)) {
+ // Normal computation within a safe region.
+
+ // minus x plus 1: (-x+1)
+ double mxp1 = 1 - x;
+ double yy = y * y;
+ // The definition of real component is:
+ // real = log( ((x+1)^2+y^2) / ((1-x)^2+y^2) ) / 4
+ // This simplifies by adding 1 and subtracting 1 as a fraction:
+ // = log(1 + ((x+1)^2+y^2) / ((1-x)^2+y^2) - ((1-x)^2+y^2)/((1-x)^2+y^2) ) / 4
+ //
+ // real(atanh(z)) == log(1 + 4*x / ((1-x)^2+y^2)) / 4
+ // imag(atanh(z)) == tan^-1 (2y, (1-x)(1+x) - y^2) / 2
+ // The division is done at the end of the function.
+ re = Math.log1p(4 * x / (mxp1 * mxp1 + yy));
+ im = Math.atan2(2 * y, mxp1 * (1 + x) - yy);
+ } else {
+ // This section handles exception cases that would normally cause
+ // underflow or overflow in the main formulas.
+
// C99. G.7: Special case for imaginary only numbers
- if (a == 0) {
+ if (x == 0) {
if (imaginary == 0) {
return constructor.create(real, imaginary);
}
// atanh(iy) = i atan(y)
- final double im = Math.atan(imaginary);
- return constructor.create(real, im);
+ return constructor.create(real, Math.atan(imaginary));
}
- // (1 + (a + b i)) / (1 - (a + b i))
- final Complex result = divide(1 + a, b, 1 - a, -b);
- // Compute the log:
- // (re + im i) = (1/2) * ln((1 + z) / (1 - z))
- return result.log(Math::log, LN_2, (re, im) ->
- // Divide log() by 2 and map back to the correct sign
- constructor.create(0.5 * changeSign(re, real),
- 0.5 * changeSign(im, imaginary))
- );
+
+ // Real part:
+ // real = Math.log1p(4x / ((1-x)^2 + y^2))
+ // real = Math.log1p(4x / (1 - 2x + x^2 + y^2))
+ // real = Math.log1p(4x / (1 + x(x-2) + y^2))
+ // without either overflow or underflow in the squared terms.
+ if (x >= SAFE_UPPER) {
+ // (1-x) = x to machine precision
+ if (isPosInfinite(x) || isPosInfinite(y)) {
+ re = 0;
+ } else if (y >= SAFE_UPPER) {
+ // Big x and y: divide by x*y
+ // This has been modified from the boost version to
+ // include 1/(x*y) and -2/y. These are harmless if
+ // machine precision prevents their addition to have an effect:
+ // 1/(x*y) -> 0
+ // (x-2) -> x
+ re = Math.log1p((4 / y) / (1 / (x * y) + (x - 2) / y + y / x));
+ } else if (y > 1) {
+ // Big x: divide through by x:
+ // This has been modified from the boost version to
+ // include 1/x and -2:
+ re = Math.log1p(4 / (1 / x + x - 2 + y * y / x));
+ } else {
+ // Big x small y, as above but neglect y^2/x:
+ re = Math.log1p(4 / (1 / x + x - 2));
+ }
+ } else if (y >= SAFE_UPPER) {
+ if (x > 1) {
+ // Big y, medium x, divide through by y:
+ double mxp1 = 1 - x;
+ re = Math.log1p((4 * x / y) / (y + mxp1 * mxp1 / y));
+ } else {
+ // Big y, small x, as above but neglect (1-x)^2/y:
+ // Note: The boost version has no log1p here.
+ // This will tend towards 0 and log1p(0) = 0.
+ re = Math.log1p(4 * x / y / y);
+ }
+ } else if (x != 1) {
+ // Modified from boost which checks y > SAFE_LOWER.
+ // if y*y -> 0 it will be ignored so always include it.
+ double mxp1 = 1 - x;
+ re = Math.log1p((4 * x) / (mxp1 * mxp1 + y * y));
+ } else {
+ // x = 1, small y:
+ // Special case when x == 1 as (1-x) is invalid.
+ // Simplify the following formula:
+ // real = log( sqrt((x+1)^2+y^2) ) / 2 - log( sqrt((1-x)^2+y^2) ) / 2
+ // = log( sqrt(4+y^2) ) / 2 - log(y) / 2
+ // if: 4+y^2 -> 4
+ // = log( 2 ) / 2 - log(y) / 2
+ // = (log(2) - log(y)) / 2
+ // Multiply by 2 as it will be divided by 4 at the end.
+ // C99: if y=0 raises the ‘‘divide-by-zero’’ floating-point exception.
+ re = 2 * (LN_2 - Math.log(y));
+ }
+
+ // Imaginary part:
+ // imag = atan2(2y, (1-x)(1+x) - y^2)
+ // if x or y are large, then the formula:
+ // atan2(2y, (1-x)(1+x) - y^2)
+ // evaluates to +(PI - theta) where theta is negligible compared to PI.
+ if ((x >= SAFE_UPPER) || (y >= SAFE_UPPER)) {
+ im = Math.PI;
+ } else if (x <= SAFE_LOWER) {
+ // (1-x)^2 -> 1
+ if (y <= SAFE_LOWER) {
+ // 1 - y^2 -> 1
+ im = Math.atan2(2 * y, 1);
+ } else {
+ im = Math.atan2(2 * y, 1 - y * y);
+ }
+ } else {
+ // Medium x, small y.
+ // Modified from boost which checks (y == 0) && (x == 1) and sets re = 0.
+ // This is same as the result from calling atan2(0, 0) so just do that.
+ // 1 - y^2 = 1 so ignore subtracting y^2
+ im = Math.atan2(2 * y, (1 - x) * (1 + x));
+ }
}
- if (Double.isInfinite(imaginary)) {
- return constructor.create(Math.copySign(0, real), Math.copySign(PI_OVER_2, imaginary));
- }
- // imaginary is NaN
- // Special case for real == 0
- return real == 0 ? constructor.create(real, Double.NaN) : NAN;
}
- if (Double.isInfinite(real)) {
- if (Double.isNaN(imaginary)) {
- return constructor.create(Math.copySign(0, real), Double.NaN);
- }
- // imaginary is finite or infinite
- return constructor.create(Math.copySign(0, real), Math.copySign(PI_OVER_2, imaginary));
- }
- // real is NaN
- if (Double.isInfinite(imaginary)) {
- return constructor.create(0, Math.copySign(PI_OVER_2, imaginary));
- }
- // optionally raises the ‘‘invalid’’ floating-point exception, for finite y.
- return NAN;
+
+ re /= 4;
+ im /= 2;
+ return constructor.create(changeSign(re, real),
+ changeSign(im, imaginary));
}
/**