Approximation of sinh/cosh using exp can be overflow/underflow safe.
Updated the sinh(), cosh() and tanh() methods for large absolute real to
avoid overflow of exp(x) to infinite.
sinh()/cosh() now use a common function to compute the result as only
the sign of the result components differs. The computation of e^x can be
made in stages to allow computation of results even when e^x / 2
overflows.
tanh() now computes e^x in stages.
Added edge cases where exp(x) overflows but the result can be computed.
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 3b7eff0..81c3ac5 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
@@ -147,17 +147,37 @@
*/
private static final double SAFE_LOWER = Math.sqrt(Double.MIN_NORMAL) * 2;
/**
- * A safe maximum double value {@code x} where {@code e^x} is not infinite.
+ * A safe maximum double value {@code m} where {@code e^m} is not infinite.
* This can be used when functions require approximations of sinh(x) or cosh(x)
- * when x is large:
+ * when x is large using exp(x):
* <pre>
* sinh(x) = (e^x - e^-x) / 2 = sign(x) * e^|x| / 2
* cosh(x) = (e^x + e^-x) / 2 = e^|x| / 2 </pre>
*
- * <p>The value should be ln(max_value) ~ 709.783. However it is set to an integer (709)
- * to provide headroom.
+ * <p>This value can be used to approximate e^x using a product:
+ *
+ * <pre>
+ * e^x = product_n (e^m) * e^(x-nm)
+ * n = (int) x/m
+ * e.g. e^2000 = e^m * e^m * e^(2000 - 2m) </pre>
+ *
+ * <p>The value should be below ln(max_value) ~ 709.783.
+ * The value m is set to an integer for less error when subtracting m and chosen as
+ * even (m=708) as it is used as a threshold in tanh with m/2.
+ *
+ * <p>The value is used to compute e^x multiplied by a small number avoiding
+ * overflow (sinh/cosh) or a small number divided by e^x without underflow due to
+ * infinite e^x (tanh). The following conditions are used:
+ * <pre>
+ * 0.5 * e^m * Double.MIN_VALUE * e^m * e^m = Infinity
+ * 2.0 / e^m / e^m = 0.0 </pre>
*/
- private static final double SAFE_EXP_MAX = 709;
+ private static final double SAFE_EXP = 708;
+ /**
+ * The value of Math.exp(SAFE_EXP): e^708.
+ * To be used in overflow/underflow safe products of e^m to approximate e^x where x > m.
+ */
+ private static final double EXP_M = Math.exp(SAFE_EXP);
/** Serializable version identifier. */
private static final long serialVersionUID = 20180201L;
@@ -2022,16 +2042,76 @@
return constructor.create(Math.cosh(real), changeSign(imaginary, real));
}
final double x = Math.abs(real);
- if (x > SAFE_EXP_MAX) {
- final double exp2 = Math.exp(x) / 2;
- return constructor.create(exp2 * Math.cos(imaginary),
- Math.copySign(exp2, real) * Math.sin(imaginary));
+ if (x > SAFE_EXP) {
+ // Approximate sinh/cosh(x) using exp^|x| / 2
+ return coshsinh(x, real, imaginary, false, constructor);
}
+ // No overflow of sinh/cosh
return constructor.create(Math.cosh(real) * Math.cos(imaginary),
Math.sinh(real) * Math.sin(imaginary));
}
/**
+ * Compute cosh or sinh when the absolute real component |x| is large. In this case
+ * cosh(x) and sinh(x) can be approximated by exp(|x|) / 2:
+ *
+ * <pre>
+ * cosh(x+iy) real = (e^|x| / 2) * cos(y)
+ * cosh(x+iy) imag = (e^|x| / 2) * sin(y) * sign(x)
+ * sinh(x+iy) real = (e^|x| / 2) * cos(y) * sign(x)
+ * sinh(x+iy) imag = (e^|x| / 2) * sin(y)
+ * </pre>
+ *
+ * @param x Absolute real component |x|.
+ * @param real Real part (x).
+ * @param imaginary Imaginary part (y).
+ * @param sinh Set to true to compute sinh, otherwise cosh.
+ * @param constructor Constructor.
+ * @return The hyperbolic sine/cosine of the complex number.
+ */
+ private static Complex coshsinh(double x, double real, double imaginary, boolean sinh,
+ ComplexConstructor constructor) {
+ // Always require the cos and sin.
+ double re = Math.cos(imaginary);
+ double im = Math.sin(imaginary);
+ // Compute the correct function
+ if (sinh) {
+ re = changeSign(re, real);
+ } else {
+ im = changeSign(im, real);
+ }
+ // Multiply by (e^|x| / 2).
+ // Overflow safe computation since sin/cos can be very small allowing a result
+ // when e^x overflows: e^x / 2 = (e^m / 2) * e^m * e^(x-2m)
+ if (x > SAFE_EXP * 3) {
+ // e^x > e^m * e^m * e^m
+ // y * (e^m / 2) * e^m * e^m will overflow when starting with Double.MIN_VALUE.
+ // Note: Do not multiply by +inf to safeguard against sin(y)=0.0 which
+ // will create 0 * inf = nan.
+ re *= Double.MAX_VALUE * Double.MAX_VALUE * Double.MAX_VALUE;
+ im *= Double.MAX_VALUE * Double.MAX_VALUE * Double.MAX_VALUE;
+ } else {
+ // Initial part of (e^x / 2) using (e^m / 2)
+ re *= EXP_M / 2;
+ im *= EXP_M / 2;
+ double xm;
+ if (x > SAFE_EXP * 2) {
+ // e^x = e^m * e^m * e^(x-2m)
+ re *= EXP_M;
+ im *= EXP_M;
+ xm = x - SAFE_EXP * 2;
+ } else {
+ // e^x = e^m * e^(x-m)
+ xm = x - SAFE_EXP;
+ }
+ final double exp = Math.exp(xm);
+ re *= exp;
+ im *= exp;
+ }
+ return constructor.create(re, im);
+ }
+
+ /**
* Returns the
* <a href="http://mathworld.wolfram.com/ExponentialFunction.html">
* exponential function</a> of this complex number.
@@ -2095,7 +2175,7 @@
return new Complex(zeroOrInf * Math.cos(imaginary),
zeroOrInf * Math.sin(imaginary));
} else if (Double.isNaN(real)) {
- // (NaN + i0) returns (NaN + i0);
+ // (NaN + i0) returns (NaN + i0)
// (NaN + iy) returns (NaN + iNaN) and optionally raises the invalid floating-point exception
// (NaN + iNaN) returns (NaN + iNaN)
return imaginary == 0 ? this : NAN;
@@ -2617,11 +2697,11 @@
return constructor.create(Math.sinh(real), imaginary);
}
final double x = Math.abs(real);
- if (x > SAFE_EXP_MAX) {
- final double exp2 = Math.exp(x) / 2;
- return constructor.create(Math.copySign(exp2, real) * Math.cos(imaginary),
- exp2 * Math.sin(imaginary));
+ if (x > SAFE_EXP) {
+ // Approximate sinh/cosh(x) using exp^|x| / 2
+ return coshsinh(x, real, imaginary, true, constructor);
}
+ // No overflow of sinh/cosh
return constructor.create(Math.sinh(real) * Math.cos(imaginary),
Math.cosh(real) * Math.sin(imaginary));
}
@@ -2837,7 +2917,8 @@
// Cache the absolute real value
final double x = Math.abs(real);
- // Handle inf or nan
+ // Handle inf or nan.
+ // Deliberate logic inversion using x to match !Double.isFinite(x) knowing x is absolute.
if (!(x <= Double.MAX_VALUE) || !Double.isFinite(imaginary)) {
if (isPosInfinite(x)) {
if (Double.isFinite(imaginary)) {
@@ -2878,19 +2959,30 @@
// cosh(2x) = 2 sinh^2(x) + 1
// cos(2y) = 2 cos^2(y) - 1
// tanh(x+iy) = (sinh(x)cosh(x) + i sin(y)cos(y)) / (sinh^2(x) + cos^2(y))
+ // tanh(x+iy) = (sinh(x)cosh(x) + i 0.5 sin(2y)) / (sinh^2(x) + cos^2(y))
- // Check for overflow in sinh/cosh:
- // sinh = (e^x - e^-x) / 2
- // cosh = (e^x + e^-x) / 2
- // sinh(x) -> sign(x) * e^|x| / 2 when x is large.
- // cosh(x) -> e^|x| / 2 when x is large.
- // sinh^2(x) -> e^2|x| / 4 when x is large.
- if (x > SAFE_EXP_MAX / 2) {
- // Ignore cos^2(y) in the divisor.
+ if (x > SAFE_EXP / 2) {
+ // Potential overflow in sinh/cosh(2x).
+ // Approximate sinh/cosh using exp^x.
+ // Ignore cos^2(y) in the divisor as it is insignificant.
// real = sinh(x)cosh(x) / sinh^2(x) = +/-1
- // imag = sin(2y) / (e^2|x| / 4) = 2 sin(2y) / e^2|x|
- return constructor.create(Math.copySign(1, real),
- 2 * sin2(imaginary) / Math.exp(2 * x));
+ final double re = Math.copySign(1, real);
+ // imag = sin(2y) / 2 sinh^2(x)
+ // sinh(x) -> sign(x) * e^|x| / 2 when x is large.
+ // sinh^2(x) -> e^2|x| / 4 when x is large.
+ // imag = sin(2y) / 2 (e^2|x| / 4) = 2 sin(2y) / e^2|x|
+ // Underflow safe divide as e^2|x| may overflow:
+ // imag = 2 sin(2y) / e^m / e^(2|x| - m)
+ double im = sin2(imaginary);
+ if (x > SAFE_EXP) {
+ // e^2|x| > e^m * e^m
+ // This will underflow 2.0 / e^m / e^m
+ im = Math.copySign(0.0, im);
+ } else {
+ // e^2|x| = e^m * e^(2|x| - m)
+ im = 2 * im / EXP_M / Math.exp(2 * x - SAFE_EXP);
+ }
+ return constructor.create(re, im);
}
// No overflow of sinh(2x) and cosh(2x)
diff --git a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexEdgeCaseTest.java b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexEdgeCaseTest.java
index 606a9b1..400b843 100644
--- a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexEdgeCaseTest.java
+++ b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexEdgeCaseTest.java
@@ -247,11 +247,53 @@
assertComplex(small, big, name, operation, -0.99998768942655991, 1.1040715888508271e-310);
assertComplex(small, medium, name, operation, -0.41614683654714241, 2.0232539340376892e-308);
assertComplex(small, small, name, operation, 1, 0);
+
+ // Overflow test.
+ // Based on MATH-901 discussion of FastMath functionality.
+ // https://issues.apache.org/jira/browse/MATH-901#comment-13500669
+ // sinh(x)/cosh(x) can be approximated by exp(x) but must be overflow safe.
+
+ // sinh(x) = sign(x) * e^|x| / 2 when x is large.
+ // cosh(x) = e^|x| / 2 when x is large.
+ // Thus e^|x| can overflow but e^|x| / 2 may not.
+ // (e^|x| / 2) * sin/cos will always be smaller.
+ final double tiny = Double.MIN_VALUE;
+ final double x = 709.783;
+ Assertions.assertEquals(inf, Math.exp(x));
+ // As computed by GNU g++
+ assertComplex(x, 0, name, operation, 8.9910466927705402e+307, 0.0);
+ assertComplex(-x, 0, name, operation, 8.9910466927705402e+307, -0.0);
+ // sub-normal number x:
+ // cos(x) = 1 => real = (e^|x| / 2)
+ // sin(x) = x => imaginary = x * (e^|x| / 2)
+ assertComplex(x, small, name, operation, 8.9910466927705402e+307, 2.0005742956701358);
+ assertComplex(-x, small, name, operation, 8.9910466927705402e+307, -2.0005742956701358);
+ assertComplex(x, tiny, name, operation, 8.9910466927705402e+307, 4.4421672910524807e-16);
+ assertComplex(-x, tiny, name, operation, 8.9910466927705402e+307, -4.4421672910524807e-16);
+ // Should not overflow imaginary.
+ assertComplex(2 * x, tiny, name, operation, inf, 7.9879467061901743e+292);
+ assertComplex(-2 * x, tiny, name, operation, inf, -7.9879467061901743e+292);
+ // Test when large enough to overflow any non-zero value to infinity. Result should be
+ // as if x was infinite and y was finite.
+ assertComplex(3 * x, tiny, name, operation, inf, inf);
+ assertComplex(-3 * x, tiny, name, operation, inf, -inf);
+ // pi / 2 x:
+ // cos(x) = ~0 => real = x * (e^|x| / 2)
+ // sin(x) = ~1 => imaginary = (e^|x| / 2)
+ final double pi2 = Math.PI / 2;
+ assertComplex(x, pi2, name, operation, 5.5054282766429199e+291, 8.9910466927705402e+307);
+ assertComplex(-x, pi2, name, operation, 5.5054282766429199e+291, -8.9910466927705402e+307);
+ assertComplex(2 * x, pi2, name, operation, inf, inf);
+ assertComplex(-2 * x, pi2, name, operation, inf, -inf);
+ // Test when large enough to overflow any non-zero value to infinity. Result should be
+ // as if x was infinite and y was finite.
+ assertComplex(3 * x, pi2, name, operation, inf, inf);
+ assertComplex(-3 * x, pi2, name, operation, inf, -inf);
}
@Test
public void testSinh() {
- // sinh(a + b i) = sinh(a)cos(b)) + i cosh(a)sin(b)
+ // sinh(a + b i) = sinh(a)cos(b) + i cosh(a)sin(b)
// Odd function: negative real cases defined by positive real cases
final String name = "sinh";
final UnaryOperator<Complex> operation = Complex::sinh;
@@ -270,6 +312,46 @@
assertComplex(small, big, name, operation, -2.2250464665720564e-308, 0.004961954789184062);
assertComplex(small, medium, name, operation, -9.2595744730151568e-309, 0.90929742682568171);
assertComplex(small, small, name, operation, 2.2250738585072014e-308, 2.2250738585072014e-308);
+
+ // Overflow test.
+ // As per cosh with sign changes to real and imaginary
+
+ // sinh(x) = sign(x) * e^|x| / 2 when x is large.
+ // cosh(x) = e^|x| / 2 when x is large.
+ // Thus e^|x| can overflow but e^|x| / 2 may not.
+ // sinh(x) * sin/cos will always be smaller.
+ final double tiny = Double.MIN_VALUE;
+ final double x = 709.783;
+ Assertions.assertEquals(inf, Math.exp(x));
+ // As computed by GNU g++
+ assertComplex(x, 0, name, operation, 8.9910466927705402e+307, 0.0);
+ assertComplex(-x, 0, name, operation, -8.9910466927705402e+307, 0.0);
+ // sub-normal number:
+ // cos(x) = 1 => real = (e^|x| / 2)
+ // sin(x) = x => imaginary = x * (e^|x| / 2)
+ assertComplex(x, small, name, operation, 8.9910466927705402e+307, 2.0005742956701358);
+ assertComplex(-x, small, name, operation, -8.9910466927705402e+307, 2.0005742956701358);
+ assertComplex(x, tiny, name, operation, 8.9910466927705402e+307, 4.4421672910524807e-16);
+ assertComplex(-x, tiny, name, operation, -8.9910466927705402e+307, 4.4421672910524807e-16);
+ // Should not overflow imaginary.
+ assertComplex(2 * x, tiny, name, operation, inf, 7.9879467061901743e+292);
+ assertComplex(-2 * x, tiny, name, operation, -inf, 7.9879467061901743e+292);
+ // Test when large enough to overflow any non-zero value to infinity. Result should be
+ // as if x was infinite and y was finite.
+ assertComplex(3 * x, tiny, name, operation, inf, inf);
+ assertComplex(-3 * x, tiny, name, operation, -inf, inf);
+ // pi / 2 x:
+ // cos(x) = ~0 => real = x * (e^|x| / 2)
+ // sin(x) = ~1 => imaginary = (e^|x| / 2)
+ final double pi2 = Math.PI / 2;
+ assertComplex(x, pi2, name, operation, 5.5054282766429199e+291, 8.9910466927705402e+307);
+ assertComplex(-x, pi2, name, operation, -5.5054282766429199e+291, 8.9910466927705402e+307);
+ assertComplex(2 * x, pi2, name, operation, inf, inf);
+ assertComplex(-2 * x, pi2, name, operation, -inf, inf);
+ // Test when large enough to overflow any non-zero value to infinity. Result should be
+ // as if x was infinite and y was finite.
+ assertComplex(3 * x, pi2, name, operation, inf, inf);
+ assertComplex(-3 * x, pi2, name, operation, -inf, inf);
}
@Test
@@ -305,6 +387,18 @@
// cosh(2a) -> 0
assertComplex(Double.MIN_NORMAL, 1, name, operation, 7.6220323800193346e-308, 1.5574077246549021);
assertComplex(Double.MIN_VALUE, 1, name, operation, 1.4821969375237396e-323, 1.5574077246549021);
+
+ // Underflow test.
+ // sinh(x) can be approximated by exp(x) but must be overflow safe.
+ // im = 2 sin(2y) / e^2|x|
+ // This can be computed when e^2|x| only just overflows.
+ // Set a case where e^2|x| overflows but the imaginary can be computed
+ double x = 709.783 / 2;
+ double y = Math.PI / 4;
+ Assertions.assertEquals(1.0, Math.sin(2 * y), 1e-16);
+ Assertions.assertEquals(Double.POSITIVE_INFINITY, Math.exp(2 * x));
+ // As computed by GNU g++
+ assertComplex(x, y, name, operation, 1, 1.1122175583895849e-308);
}
@Test
diff --git a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexTest.java b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexTest.java
index 6a910f4..a98b65d 100644
--- a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexTest.java
+++ b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexTest.java
@@ -1967,16 +1967,16 @@
}
@Test
- public void testTanhAssumptions() {
- // Use the same constants used by tanh
- final double safeExpMax = 709;
+ public void testCoshSinhTanhAssumptions() {
+ // Use the same constants used to approximate cosh/sinh with e^|x| / 2
+ final double safeExpMax = 708;
final double big = Math.exp(safeExpMax);
final double small = Math.exp(-safeExpMax);
// Overflow assumptions
Assertions.assertTrue(Double.isFinite(big));
- Assertions.assertTrue(Double.isInfinite(Math.exp(safeExpMax + 1)));
+ Assertions.assertTrue(Double.isInfinite(Math.exp(safeExpMax + 2)));
// Can we assume cosh(x) = (e^x + e^-x) / 2 = e^|x| / 2
Assertions.assertEquals(big + small, big);
@@ -1992,6 +1992,15 @@
// Can we assume sinh(x/2) * cosh(x/2) is finite
Assertions.assertTrue(Double.isFinite(Math.sinh(safeExpMax / 2) * Math.cosh(safeExpMax / 2)));
+ // Will 2.0 / (2 * (e^|x|)) underflow
+ Assertions.assertNotEquals(0.0, 2.0 / big);
+ Assertions.assertEquals(0.0, 2.0 / big / big);
+
+ // This is an assumption used in sinh/cosh.
+ // Will 3 * (e^|x|/2) * y overflow for any positive y
+ Assertions.assertTrue(Double.isFinite(0.5 * big * Double.MIN_VALUE * big));
+ Assertions.assertTrue(Double.isInfinite(0.5 * big * Double.MIN_VALUE * big * big));
+
// Is overflow point monotonic:
// The values at the switch are exact. Previous values are close.
final double x = Double.MAX_VALUE / 2;
@@ -2006,6 +2015,10 @@
Assertions.assertEquals(Math.cos(2 * x), 2 * Math.cos(x) * Math.cos(x) - 1);
Assertions.assertEquals(Math.cos(2 * x1), 2 * Math.cos(x1) * Math.cos(x1) - 1, 4 * Math.ulp(Math.cos(2 * x1)));
Assertions.assertEquals(Math.cos(2 * x2), 2 * Math.cos(x2) * Math.cos(x2) - 1, 2 * Math.ulp(Math.cos(2 * x2)));
+
+ // tanh: 2.0 / Double.MAX_VALUE does not underflow.
+ // Thus 2 sin(2y) / e^2|x| can be computed when e^2|x| only just overflows
+ Assertions.assertTrue(2.0 / Double.MAX_VALUE > 0);
}
/**