This is an automated email from the ASF dual-hosted git repository.

aherbert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-numbers.git

commit c283420af7165e09729002978ce195ec48fbab25
Author: Alex Herbert <aherb...@apache.org>
AuthorDate: Tue Dec 24 14:44:44 2019 +0000

    Update log() to use method from Hull et al.
    
    This requires changes to the edge case test as small values are computed
    more accurately.
---
 .../apache/commons/numbers/complex/Complex.java    | 163 ++++++++++++++++-----
 .../numbers/complex/ComplexEdgeCaseTest.java       |  86 ++++++++++-
 2 files changed, 212 insertions(+), 37 deletions(-)

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 68dc607..a223f2a 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
@@ -73,8 +73,16 @@ public final class Complex implements Serializable  {
     private static final int MASK_INT_TO_EVEN = ~0x1;
     /** Natural logarithm of 2 (ln(2)). */
     private static final double LN_2 = Math.log(2);
+    /** Base 10 logarithm of 10 divided by 2 (log10(e)/2). */
+    private static final double LOG_10E_O_2 = Math.log10(Math.E) / 2;
     /** Base 10 logarithm of 2 (log10(2)). */
     private static final double LOG10_2 = Math.log10(2);
+    /** {@code 1/2}. */
+    private static final double HALF = 0.5;
+    /** {@code sqrt(2)}. */
+    private static final double ROOT2 = Math.sqrt(2);
+    /** The number of bits of precision of the mantissa of a {@code double} + 
1: {@code 54}. */
+    private static final double PRECISION_1 = 54;
 
     /**
      * Crossover point to switch computation for asin/acos factor A.
@@ -2076,6 +2084,13 @@ public final class Complex implements Serializable  {
      *   ln(a + b i) = ln(|a + b i|) + i arg(a + b i)
      * </pre>
      *
+     * <p>The implementation is based on the method described in:</p>
+     * <blockquote>
+     * T E Hull, Thomas F Fairgrieve and Ping Tak Peter Tang (1994)
+     * Implementing complex elementary functions using exception handling.
+     * ACM Transactions on Mathematical Software, Vol 20, No 2, pp 215-244.
+     * </blockquote>
+     *
      * @return the natural logarithm of {@code this}.
      * @see Math#log(double)
      * @see #abs()
@@ -2083,7 +2098,7 @@ public final class Complex implements Serializable  {
      * @see <a 
href="http://functions.wolfram.com/ElementaryFunctions/Log/";>Log</a>
      */
     public Complex log() {
-        return log(Math::log, LN_2, Complex::ofCartesian);
+        return log(Math::log, HALF, LN_2, Complex::ofCartesian);
     }
 
     /**
@@ -2101,7 +2116,7 @@ public final class Complex implements Serializable  {
      * @see #arg()
      */
     public Complex log10() {
-        return log(Math::log10, LOG10_2, Complex::ofCartesian);
+        return log(Math::log10, LOG_10E_O_2, LOG10_2, Complex::ofCartesian);
     }
 
     /**
@@ -2116,51 +2131,131 @@ public final class Complex implements Serializable  {
      * will be incorrect. This is provided as an internal optimisation.
      *
      * @param log Log function.
+     * @param logOfeOver2 The log function applied to e, then divided by 2.
      * @param logOf2 The log function applied to 2.
      * @param constructor Constructor for the returned complex.
      * @return the logarithm of {@code this}.
      * @see #abs()
      * @see #arg()
      */
-    private Complex log(UnaryOperation log, double logOf2, ComplexConstructor 
constructor) {
-        // TODO - Add edge cases with values for abs() close to 1
-        // These should be handled correctly.
+    private Complex log(UnaryOperation log, double logOfeOver2, double logOf2, 
ComplexConstructor constructor) {
+        // Handle NaN
+        if (Double.isNaN(real) || Double.isNaN(imaginary)) {
+            // Return NaN unless infinite
+            if (isInfinite()) {
+                return constructor.create(Double.POSITIVE_INFINITY, 
Double.NaN);
+            }
+            // No-use of the input constructor
+            return NAN;
+        }
 
-        // Change this to use a method based on glibc with detection of values 
of
-        // abs() close to 1 and switch to using log1p(abs - 1).
+        // Compute the real part:
+        // log(sqrt(x^2 + y^2))
+        // log(x^2 + y^2) / 2
 
-        // All ISO C99 edge cases satisfied by the Math library.
-        // Make computation overflow safe.
+        // Compute with positive values
+        double x = Math.abs(real);
+        double y = Math.abs(imaginary);
 
-        // Note:
-        // log(|a + b i|) = log(sqrt(a^2 + b^2)) = 0.5 * log(a^2 + b^2)
-        // If real and imaginary are with a safe region then omit the sqrt().
-        final double x = Math.abs(real);
-        final double y = Math.abs(imaginary);
+        // Find the larger magnitude.
+        if (x < y) {
+            double tmp = x;
+            x = y;
+            y = tmp;
+        }
 
-        // Use the safe region defined for atanh to avoid over/underflow for 
x^2
-        if (inRegion(x, y, SAFE_LOWER, SAFE_UPPER)) {
-            //return constructor.create(log.apply(abs()), arg());
-            return constructor.create(0.5 * log.apply(x * x + y * y), arg());
+        if (x == 0) {
+            // Handle zero: raises the ‘‘divide-by-zero’’ floating-point 
exception.
+            return constructor.create(Double.NEGATIVE_INFINITY,
+                                      negative(real) ? Math.copySign(Math.PI, 
imaginary) : imaginary);
         }
 
-        final double abs = abs();
-        if (abs == Double.POSITIVE_INFINITY && isFinite()) {
-            // Edge-case where the |a + b i| overflows.
-            // |a + b i| = sqrt(a^2 + b^2)
-            // This can be scaled linearly.
-            // Scale the absolute and exploit:
-            // ln(abs / scale) = ln(abs) - ln(scale)
-            // ln(abs) = ln(abs / scale) + ln(scale)
-            // Use precise scaling with:
-            // scale ~ 2^exponent
-            final int exponent = getMaxExponent(real, imaginary);
-            // Implement scaling using 2^-exponent
-            final double absOs = Math.hypot(Math.scalb(real, -exponent), 
Math.scalb(imaginary, -exponent));
-            // log(2^exponent) = ln2(2^exponent) * log(2)
-            return constructor.create(log.apply(absOs) + exponent * logOf2, 
arg());
+        double re;
+
+        if (x > HALF && x < ROOT2) {
+            // x^2+y^2 close to 1. Use log1p(x^2+y^2 - 1) / 2.
+            re = Math.log1p(x2y2m1(x, y)) * logOfeOver2;
+        } else if (y == 0) {
+            // Handle real only number
+            re = log.apply(x);
+        } else if (x > SAFE_MAX || x < SAFE_MIN || y < SAFE_MIN) {
+            // Over/underflow of sqrt(x^2+y^2)
+            if (isPosInfinite(x)) {
+                // Handle infinity
+                re = x;
+            } else {
+                // Do scaling
+                int expx = Math.getExponent(x);
+                int expy = Math.getExponent(y);
+                if (2 * (expx - expy) > PRECISION_1) {
+                    // y can be ignored
+                    re = log.apply(x);
+                } else {
+                    // Hull et al:
+                    // "It is important that the scaling be chosen so
+                    // that there is no possibility of cancellation in this 
addition"
+                    // i.e. sx^2 + sy^2 should not be close to 1.
+                    // Their paper uses expx + 2 for underflow but expx for 
overflow.
+                    // It has been modified here to use expx - 2.
+                    int scale;
+                    if (x > SAFE_MAX) {
+                        // overflow
+                        scale = expx - 2;
+                    } else {
+                        // underflow
+                        scale = expx + 2;
+                    }
+                    double sx = Math.scalb(x, -scale);
+                    double sy = Math.scalb(y, -scale);
+                    re = scale * logOf2 + 0.5 * log.apply(sx * sx + sy * sy);
+                }
+            }
+        } else {
+            // Safe region that avoids under/overflow
+            re = 0.5 * log.apply(x * x + y * y);
+        }
+
+        // All ISO C99 edge cases for the imaginary are satisfied by the Math 
library.
+        return constructor.create(re, arg());
+    }
+
+    /**
+     * Compute {@code x^2 + y^2 - 1} in high precision.
+     * Assumes that the values x and y can be multiplied without overflow; that
+     * {@code x >= y}; and both values are positive.
+     *
+     * @param x the x value
+     * @param y the y value
+     * @return {@code x^2 + y^2 - 1}
+     */
+    private static double x2y2m1(double x, double y) {
+        // Hull et al used (x-1)*(x+1)+y*y.
+        // From the paper on page 236:
+
+        // If x == 1 there is no cancellation.
+
+        // If x > 1, there is also no cancellation, but the argument is now 
accurate
+        // only to within a factor of 1 + 3 EPSILSON (note that x – 1 is 
exact),
+        // so that error = 3 EPSILON.
+
+        // If x < 1, there can be serious cancellation:
+
+        // If 4 y^2 < |x^2 – 1| the cancellation is not serious ... the 
argument is accurate
+        // only to within a factor of 1 + 4 EPSILSON so that error = 4 EPSILON.
+
+        // Otherwise there can be serious cancellation and the relative error 
in the real part
+        // could be enormous.
+
+        // TODO - investigate the computation in high precision using
+        // LinearCombination#value(double, double, double, double, double, 
double)
+        // from o.a.c.numbers.arrays.
+        final double xm1xp1 = (x - 1) * (x + 1);
+        final double yy = y * y;
+        if (x < 1 && 4 * yy > Math.abs(xm1xp1)) {
+            // Large relative error...
+            return xm1xp1 + yy;
         }
-        return constructor.create(log.apply(abs), arg());
+        return xm1xp1 + yy;
     }
 
     /**
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 86d8e00..df20dc9 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
@@ -20,6 +20,7 @@ package org.apache.commons.numbers.complex;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigDecimal;
 import java.util.function.BiFunction;
 import java.util.function.UnaryOperator;
 
@@ -344,9 +345,88 @@ public class ComplexEdgeCaseTest {
         assertComplex(-Double.MAX_VALUE, Double.MAX_VALUE / 4, name, 
operation, 7.098130252042921e2, 2.896613990462929);
         assertComplex(Double.MAX_VALUE, Double.MAX_VALUE / 4, name, operation, 
7.098130252042921e2, 2.449786631268641e-1, 2);
 
-        // Underflow if sqrt(a^2 + b^2) == 0
-        assertComplex(-Double.MIN_VALUE, Double.MIN_VALUE, name, operation, 
-744.44007192138122, 2.3561944901923448);
-        assertComplex(Double.MIN_VALUE, Double.MIN_VALUE, name, operation, 
-744.44007192138122, 0.78539816339744828);
+        // Underflow if sqrt(a^2 + b^2) -> 0
+        assertComplex(-Double.MIN_NORMAL, Double.MIN_NORMAL, name, operation, 
-708.04984494198413, 2.3561944901923448);
+        assertComplex(Double.MIN_NORMAL, Double.MIN_NORMAL, name, operation, 
-708.04984494198413, 0.78539816339744828);
+        // Math.hypot(min, min) = min.
+        // To compute the expected result do scaling of the actual hypot = 
sqrt(2).
+        // log(a/n) = log(a) - log(n)
+        // n = 2^1074 => log(a) - log(2) * 1074
+        double expected = Math.log(Math.sqrt(2)) - Math.log(2) * 1074;
+        assertComplex(-Double.MIN_VALUE, Double.MIN_VALUE, name, operation, 
expected, Math.atan2(1, -1));
+        expected = Math.log(Math.sqrt(5)) - Math.log(2) * 1074;
+        assertComplex(-Double.MIN_VALUE, 2 * Double.MIN_VALUE, name, 
operation, expected, Math.atan2(2, -1));
+
+        // Imprecision if sqrt(a^2 + b^2) == 1 as log(1) is 0.
+        // Method should switch to using log1p(x^2 + x^2 - 1) * 0.5.
+
+        // In the following:
+        // max = max(real, imaginary)
+        // min = min(real, imaginary)
+
+        // No cancellation error when max > 1
+
+        assertLog(1.0001, Math.sqrt(1.2 - 1.0001 * 1.0001), 1);
+        assertLog(1.0001, Math.sqrt(1.1 - 1.0001 * 1.0001), 2);
+        assertLog(1.0001, Math.sqrt(1.02 - 1.0001 * 1.0001), 6);
+
+        // Cancellation error when max < 1.
+        // The ULPs may need to be revised if the log() method is improved.
+
+        // Hard: 4 * min^2 < |max^2 - 1|
+        assertLog(0.9, 0.00001, 3);
+        assertLog(0.95, 0.00001, 8);
+
+        // Very hard: 4 * min^2 > |max^2 - 1|
+
+        // Radius 0.99
+        assertLog(0.97, Math.sqrt(0.99 - 0.97 * 0.97), 30);
+        // Radius 1.01
+        assertLog(0.97, Math.sqrt(1.01 - 0.97 * 0.97), 34);
+
+        // Massive relative error
+        // Radius 0.9999
+        assertLog(0.97, Math.sqrt(0.9999 - 0.97 * 0.97), 3639);
+
+        // This code is for demonstration purposes.
+
+//        // Demonstrate relative error using
+//        // cis numbers on a 1/8 circle with a set radius.
+//        int steps = 10;
+//        for (double radius : new double[] {0.99, 1.0, 1.01}) {
+//            for (int i = 1; i < steps; i++) {
+//                double theta = i * Math.PI / (8 * steps);
+//                assertLog(radius * Math.sin(theta), radius * 
Math.cos(theta), -1);
+//            }
+//        }
+//
+//        // Extreme. Here for documentation of the relative error.
+//        double up1 = Math.nextUp(1.0);
+//        double down1 = Math.nextDown(1.0);
+//        assertLog(up1, Double.MIN_NORMAL, -1);
+//        assertLog(up1, Double.MIN_VALUE, -1);
+//        assertLog(down1, Double.MIN_NORMAL, -1);
+//        assertLog(down1, Double.MIN_VALUE, -1);
+    }
+
+    /**
+     * Assert the Complex log function using BigDecimal to compute the field 
norm
+     * {@code x*x + y*y} and then {@link Math#log1p(double)} to compute the 
log of
+     * the modulus \ using {@code 0.5 * log1p(x*x + y*y - 1)}. This test is 
for the
+     * extreme case for performance around {@code sqrt(x*x + y*y) = 1} where 
using
+     * {@link Math#log(double)} will fail dramatically.
+     *
+     * @param x the real value of the complex
+     * @param y the imaginary value of the complex
+     * @param maxUlps the maximum units of least precision between the two 
values
+     */
+    private static void assertLog(double x, double y, long maxUlps) {
+        // Compute the best value we can
+        BigDecimal bx = BigDecimal.valueOf(x);
+        BigDecimal by = BigDecimal.valueOf(y);
+        double real = 0.5 * 
Math.log1p(bx.multiply(bx).add(by.multiply(by)).subtract(BigDecimal.ONE).doubleValue());
+        double imag = Math.atan2(y, x);
+        assertComplex(x, y, "log", Complex::log, real, imag, maxUlps);
     }
 
     @Test

Reply via email to