This is an automated email from the ASF dual-hosted git repository. aherbert pushed a commit to branch complex-gsoc-2022 in repository https://gitbox.apache.org/repos/asf/commons-numbers.git
commit ae7deb75a30c0e682448251210cff292592aed5d Author: Sumanth Rajkumar <rajkumar.suma...@gmail.com> AuthorDate: Wed Jul 20 10:36:41 2022 +0100 NUMBERS-188: Refactor complex log functions to static methods Added functional interface for a unary operator on a complex number. --- .../apache/commons/numbers/complex/Complex.java | 597 +-------------- .../commons/numbers/complex/ComplexFunctions.java | 845 +++++++++++++++++++++ .../commons/numbers/complex/ComplexSink.java | 40 + .../numbers/complex/ComplexUnaryOperator.java | 44 ++ .../commons/numbers/complex/CReferenceTest.java | 53 +- .../commons/numbers/complex/CStandardTest.java | 266 ++++++- .../numbers/complex/ComplexEdgeCaseTest.java | 73 +- .../commons/numbers/complex/ComplexNumber.java | 74 ++ .../commons/numbers/complex/ComplexTest.java | 7 +- .../apache/commons/numbers/complex/TestUtils.java | 25 +- .../checkstyle/checkstyle-suppressions.xml | 1 + 11 files changed, 1392 insertions(+), 633 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 6dfa59e8..5bb38cfd 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 @@ -20,7 +20,6 @@ package org.apache.commons.numbers.complex; import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import java.util.function.DoubleUnaryOperator; /** * Cartesian representation of a complex number. The complex number is expressed @@ -89,21 +88,11 @@ public final class Complex implements Serializable { private static final double PI_OVER_4 = 0.25 * Math.PI; /** 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 = 1.4142135623730951; /** {@code 1.0 / sqrt(2)}. * This is pre-computed to the closest double from the exact result. * It is 1 ULP different from 1.0 / Math.sqrt(2) but equal to Math.sqrt(2) / 2. */ private static final double ONE_OVER_ROOT2 = 0.7071067811865476; - /** The bit representation of {@code -0.0}. */ - private static final long NEGATIVE_ZERO_LONG_BITS = Double.doubleToLongBits(-0.0); /** Exponent offset in IEEE754 representation. */ private static final int EXPONENT_OFFSET = 1023; /** @@ -122,10 +111,6 @@ public final class Complex implements Serializable { private static final long UNSIGN_MASK = 0x7fff_ffff_ffff_ffffL; /** Mask to extract the 52-bit mantissa from a long representation of a double. */ private static final long MANTISSA_MASK = 0x000f_ffff_ffff_ffffL; - /** The multiplier used to split the double value into hi and low parts. This must be odd - * and a value of 2^s + 1 in the range {@code p/2 <= s <= p-1} where p is the number of - * bits of precision of the floating point number. Here {@code s = 27}.*/ - private static final double MULTIPLIER = 1.34217729E8; /** * Crossover point to switch computation for asin/acos factor A. @@ -191,20 +176,6 @@ public final class Complex implements Serializable { */ private static final double EXP_M = Math.exp(SAFE_EXP); - /** 54 shifted 20-bits to align with the exponent of the upper 32-bits of a double. */ - private static final int EXP_54 = 0x36_00000; - /** Represents an exponent of 500 in unbiased form shifted 20-bits to align with the upper 32-bits of a double. */ - private static final int EXP_500 = 0x5f3_00000; - /** Represents an exponent of 1024 in unbiased form (infinite or nan) - * shifted 20-bits to align with the upper 32-bits of a double. */ - private static final int EXP_1024 = 0x7ff_00000; - /** Represents an exponent of -500 in unbiased form shifted 20-bits to align with the upper 32-bits of a double. */ - private static final int EXP_NEG_500 = 0x20b_00000; - /** 2^600. */ - private static final double TWO_POW_600 = 0x1.0p+600; - /** 2^-600. */ - private static final double TWO_POW_NEG_600 = 0x1.0p-600; - /** Serializable version identifier. */ private static final long serialVersionUID = 20180201L; @@ -540,7 +511,7 @@ public final class Complex implements Serializable { private static double abs(double real, double imaginary) { // Specialised implementation of hypot. // See NUMBERS-143 - return hypot(real, imaginary); + return ComplexFunctions.abs(real, imaginary); } /** @@ -568,7 +539,7 @@ public final class Complex implements Serializable { */ public double arg() { // Delegate - return Math.atan2(imaginary, real); + return ComplexFunctions.arg(real, imaginary); } /** @@ -633,7 +604,7 @@ public final class Complex implements Serializable { * @see Double#isInfinite(double) */ public boolean isInfinite() { - return Double.isInfinite(real) || Double.isInfinite(imaginary); + return ComplexFunctions.isInfinite(real, imaginary); } /** @@ -1343,7 +1314,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, HALF, LN_2, Complex::ofCartesian); + return ComplexFunctions.log(real, imaginary, Complex::ofCartesian); } /** @@ -1368,107 +1339,7 @@ public final class Complex implements Serializable { * @see #arg() */ public Complex log10() { - return log(Math::log10, LOG_10E_O_2, LOG10_2, Complex::ofCartesian); - } - - /** - * Returns the logarithm of this complex number using the provided function. - * Implements the formula: - * - * <pre> - * log(x + i y) = log(|x + i y|) + i arg(x + i y)</pre> - * - * <p>Warning: The argument {@code logOf2} must be equal to {@code log(2)} using the - * provided log function otherwise scaling using powers of 2 in the case of overflow - * 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 this complex number. - * @see #abs() - * @see #arg() - */ - private Complex log(DoubleUnaryOperator 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; - } - - // Returns the real part: - // log(sqrt(x^2 + y^2)) - // log(x^2 + y^2) / 2 - - // Compute with positive values - double x = Math.abs(real); - double y = Math.abs(imaginary); - - // Find the larger magnitude. - if (x < y) { - final double tmp = x; - x = y; - y = tmp; - } - - 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); - } - - double re; - - // This alters the implementation of Hull et al (1994) which used a standard - // precision representation of |z|: sqrt(x*x + y*y). - // This formula should use the same definition of the magnitude returned - // by Complex.abs() which is a high precision computation with scaling. - // The checks for overflow thus only require ensuring the output of |z| - // will not overflow or underflow. - - 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 { - // Check for over/underflow in |z| - // When scaling: - // log(a / b) = log(a) - log(b) - // So initialize the result with the log of the scale factor. - re = 0; - if (x > Double.MAX_VALUE / 2) { - // Potential overflow. - if (isPosInfinite(x)) { - // Handle infinity - return constructor.create(x, arg()); - } - // Scale down. - x /= 2; - y /= 2; - // log(2) - re = logOf2; - } else if (y < Double.MIN_NORMAL) { - // Potential underflow. - if (y == 0) { - // Handle real only number - return constructor.create(log.applyAsDouble(x), arg()); - } - // Scale up sub-normal numbers to make them normal by scaling by 2^54, - // i.e. more than the mantissa digits. - x *= 0x1.0p54; - y *= 0x1.0p54; - // log(2^-54) = -54 * log(2) - re = -54 * logOf2; - } - re += log.applyAsDouble(abs(x, y)); - } - - // All ISO C99 edge cases for the imaginary are satisfied by the Math library. - return constructor.create(re, arg()); + return ComplexFunctions.log10(real, imaginary, Complex::ofCartesian); } /** @@ -2904,225 +2775,9 @@ public final class Complex implements Serializable { * @param y the y value * @return {@code x^2 + y^2 - 1}. */ + //TODO - make it private in ComplexFunctions in future 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. - - final double xx = x * x; - final double yy = y * y; - // Modify to use high precision before the threshold set by Hull et al. - // This is to preserve the monotonic output of the computation at the switch. - // Set the threshold when x^2 + y^2 is above 0.5 thus subtracting 1 results in a number - // that can be expressed with a higher precision than any number in the range 0.5-1.0 - // due to the variable exponent used below 0.5. - if (x < 1 && xx + yy > 0.5) { - // Large relative error. - // This does not use o.a.c.numbers.LinearCombination.value(x, x, y, y, 1, -1). - // It is optimised knowing that: - // - the products are squares - // - the final term is -1 (which does not require split multiplication and addition) - // - The answer will not be NaN as the terms are not NaN components - // - The order is known to be 1 > |x| >= |y| - // The squares are computed using a split multiply algorithm and - // the summation using an extended precision summation algorithm. - - // Split x and y as one 26 bits number and one 27 bits number - final double xHigh = splitHigh(x); - final double xLow = x - xHigh; - final double yHigh = splitHigh(y); - final double yLow = y - yHigh; - - // Accurate split multiplication x * x and y * y - final double x2Low = squareLow(xLow, xHigh, xx); - final double y2Low = squareLow(yLow, yHigh, yy); - - return sumx2y2m1(xx, x2Low, yy, y2Low); - } - return (x - 1) * (x + 1) + yy; - } - - /** - * Implement Dekker's method to split a value into two parts. Multiplying by (2^s + 1) create - * a big value from which to derive the two split parts. - * <pre> - * c = (2^s + 1) * a - * a_big = c - a - * a_hi = c - a_big - * a_lo = a - a_hi - * a = a_hi + a_lo - * </pre> - * - * <p>The multiplicand must be odd allowing a p-bit value to be split into - * (p-s)-bit value {@code a_hi} and a non-overlapping (s-1)-bit value {@code a_lo}. - * Combined they have (p-1) bits of significand but the sign bit of {@code a_lo} - * contains a bit of information. - * - * @param a Value. - * @return the high part of the value. - * @see <a href="https://doi.org/10.1007/BF01397083"> - * Dekker (1971) A floating-point technique for extending the available precision</a> - */ - private static double splitHigh(double a) { - final double c = MULTIPLIER * a; - return c - (c - a); - } - - /** - * Compute the round-off from the square of a split number with {@code low} and {@code high} - * components. Uses Dekker's algorithm for split multiplication modified for a square product. - * - * <p>Note: This is candidate to be replaced with {@code Math.fma(x, x, -x * x)} to compute - * the round-off from the square product {@code x * x}. This would remove the requirement - * to compute the split number and make this method redundant. {@code Math.fma} requires - * JDK 9 and FMA hardware support. - * - * @param low Low part of number. - * @param high High part of number. - * @param square Square of the number. - * @return <code>low * low - (((product - high * high) - low * high) - high * low)</code> - * @see <a href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps"> - * Shewchuk (1997) Theorum 18</a> - */ - private static double squareLow(double low, double high, double square) { - final double lh = low * high; - return low * low - (((square - high * high) - lh) - lh); - } - - /** - * Compute the round-off from the sum of two numbers {@code a} and {@code b} using - * Dekker's two-sum algorithm. The values are required to be ordered by magnitude: - * {@code |a| >= |b|}. - * - * @param a First part of sum. - * @param b Second part of sum. - * @param x Sum. - * @return <code>b - (x - a)</code> - * @see <a href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps"> - * Shewchuk (1997) Theorum 6</a> - */ - private static double fastSumLow(double a, double b, double x) { - // x = a + b - // bVirtual = x - a - // y = b - bVirtual - return b - (x - a); - } - - /** - * Compute the round-off from the sum of two numbers {@code a} and {@code b} using - * Knuth's two-sum algorithm. The values are not required to be ordered by magnitude. - * - * @param a First part of sum. - * @param b Second part of sum. - * @param x Sum. - * @return <code>(a - (x - (x - a))) + (b - (x - a))</code> - * @see <a href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps"> - * Shewchuk (1997) Theorum 7</a> - */ - private static double sumLow(double a, double b, double x) { - // x = a + b - // bVirtual = x - a - // aVirtual = x - bVirtual - // bRoundoff = b - bVirtual - // aRoundoff = a - aVirtual - // y = aRoundoff + bRoundoff - final double bVirtual = x - a; - return (a - (x - bVirtual)) + (b - bVirtual); - } - - /** - * Sum x^2 + y^2 - 1. It is assumed that {@code y <= x < 1}. - * - * <p>Implement Shewchuk's expansion-sum algorithm: [x2Low, x2High] + [-1] + [y2Low, y2High]. - * - * @param x2High High part of x^2. - * @param x2Low Low part of x^2. - * @param y2High High part of y^2. - * @param y2Low Low part of y^2. - * @return x^2 + y^2 - 1 - * @see <a href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps"> - * Shewchuk (1997) Theorum 12</a> - */ - private static double sumx2y2m1(double x2High, double x2Low, double y2High, double y2Low) { - // Let e and f be non-overlapping expansions of components of length m and n. - // The following algorithm will produce a non-overlapping expansion h where the - // sum h_i = e + f and components of h are in increasing order of magnitude. - - // Expansion-sum proceeds by a grow-expansion of the first part from one expansion - // into the other, extending its length by 1. The process repeats for the next part - // but the grow-expansion starts at the previous merge position + 1. - // Thus expansion-sum requires mn two-sum operations to merge length m into length n - // resulting in length m+n-1. - - // Variables numbered from 1 as per Figure 7 (p.12). The output expansion h is placed - // into e increasing its length for each grow expansion. - - // We have two expansions for x^2 and y^2 and the whole number -1. - // Expecting (x^2 + y^2) close to 1 we generate first the intermediate expansion - // (x^2 - 1) moving the result away from 1 where there are sparse floating point - // representations. This is then added to a similar magnitude y^2. Leaving the -1 - // until last suffers from 1 ulp rounding errors more often and the requirement - // for a distillation sum to reduce rounding error frequency. - - // Note: Do not use the alternative fast-expansion-sum of the parts sorted by magnitude. - // The parts can be ordered with a single comparison into: - // [y2Low, (y2High|x2Low), x2High, -1] - // The fast-two-sum saves 1 fast-two-sum and 3 two-sum operations (21 additions) and - // adds a penalty of a single branch condition. - // However the order in not "strongly non-overlapping" and the fast-expansion-sum - // output will not be strongly non-overlapping. The sum of the output has 1 ulp error - // on random cis numbers approximately 1 in 160 events. This can be removed by a - // distillation two-sum pass over the final expansion as a cost of 1 fast-two-sum and - // 3 two-sum operations! So we use the expansion sum with the same operations and - // no branches. - - // q=running sum - double q = x2Low - 1; - double e1 = fastSumLow(-1, x2Low, q); - double e3 = q + x2High; - double e2 = sumLow(q, x2High, e3); - - final double f1 = y2Low; - final double f2 = y2High; - - // Grow expansion of f1 into e - q = f1 + e1; - e1 = sumLow(f1, e1, q); - double p = q + e2; - e2 = sumLow(q, e2, p); - double e4 = p + e3; - e3 = sumLow(p, e3, e4); - - // Grow expansion of f2 into e (only required to start at e2) - q = f2 + e2; - e2 = sumLow(f2, e2, q); - p = q + e3; - e3 = sumLow(q, e3, p); - final double e5 = p + e4; - e4 = sumLow(p, e4, e5); - - // Final summation: - // The sum of the parts is within 1 ulp of the true expansion value e: - // |e - sum| < ulp(sum). - // To achieve the exact result requires iteration of a distillation two-sum through - // the expansion until convergence, i.e. no smaller term changes higher terms. - // This requires (n-1) iterations for length n. Here we neglect this as - // although the method is not ensured to be exact is it robust on random - // cis numbers. - return e1 + e2 + e3 + e4 + e5; + return ComplexFunctions.x2y2m1(x, y); } /** @@ -3296,8 +2951,9 @@ public final class Complex implements Serializable { * @param d Value. * @return {@code true} if {@code d} is negative. */ + //TODO - make private in ComplexFunctions in future private static boolean negative(double d) { - return d < 0 || Double.doubleToLongBits(d) == NEGATIVE_ZERO_LONG_BITS; + return ComplexFunctions.negative(d); } /** @@ -3308,8 +2964,9 @@ public final class Complex implements Serializable { * @param d Value. * @return {@code true} if {@code d} is +inf. */ + //TODO - make private in ComplexFunctions in future private static boolean isPosInfinite(double d) { - return d == Double.POSITIVE_INFINITY; + return ComplexFunctions.isPosInfinite(d); } /** @@ -3462,236 +3119,4 @@ public final class Complex implements Serializable { private static boolean inRegion(double x, double y, double min, double max) { return x < max && x > min && y < max && y > min; } - - /** - * Returns {@code sqrt(x^2 + y^2)} without intermediate overflow or underflow. - * - * <p>Special cases: - * <ul> - * <li>If either argument is infinite, then the result is positive infinity. - * <li>If either argument is NaN and neither argument is infinite, then the result is NaN. - * </ul> - * - * <p>The computed result is expected to be within 1 ulp of the exact result. - * - * <p>This method is a replacement for {@link Math#hypot(double, double)}. There - * will be differences between this method and {@code Math.hypot(double, double)} due - * to the use of a different algorithm to compute the high precision sum of - * {@code x^2 + y^2}. This method has been tested to have a lower maximum error from - * the exact result; any differences are expected to be 1 ULP indicating a rounding - * change in the sum. - * - * <p>JDK9 ported the hypot function to Java for bug JDK-7130085 due to the slow performance - * of the method as a native function. Benchmarks of the Complex class for functions that - * use hypot confirm this is slow pre-Java 9. This implementation outperforms the new faster - * {@code Math.hypot(double, double)} on JDK 11 (LTS). See the Commons numbers examples JMH - * module for benchmarks. Comparisons with alternative implementations indicate - * performance gains are related to edge case handling and elimination of an unpredictable - * branch in the computation of {@code x^2 + y^2}. - * - * <p>This port was adapted from the "Freely Distributable Math Library" hypot function. - * This method only (and not invoked methods within) is distributed under the terms of the - * original notice as shown below: - * <pre> - * ==================================================== - * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. - * - * Developed at SunSoft, a Sun Microsystems, Inc. business. - * Permission to use, copy, modify, and distribute this - * software is freely granted, provided that this notice - * is preserved. - * ==================================================== - * </pre> - * - * <p>Note: The fdlibm c code makes use of the language ability to read and write directly - * to the upper and lower 32-bits of the 64-double. The function performs - * checking on the upper 32-bits for the magnitude of the two numbers by accessing - * the exponent and 20 most significant bits of the mantissa. These upper bits - * are manipulated during scaling and then used to perform extended precision - * computation of the sum {@code x^2 + y^2} where the high part of the number has 20-bit - * precision. Manipulation of direct bits has no equivalent in Java - * other than use of {@link Double#doubleToLongBits(double)} and - * {@link Double#longBitsToDouble(long)}. To avoid conversion to and from long and double - * representations this implementation only scales the double representation. The high - * and low parts of a double for the extended precision computation are extracted - * using the method of Dekker (1971) to create two 26-bit numbers. This works for sub-normal - * numbers and reduces the maximum error in comparison to fdlibm hypot which does not - * use a split number algorithm for sub-normal numbers. - * - * @param x Value x - * @param y Value y - * @return sqrt(x^2 + y^2) - * @see Math#hypot(double, double) - * @see <a href="https://www.netlib.org/fdlibm/e_hypot.c">fdlibm e_hypot.c</a> - * @see <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7130085">JDK-7130085 : Port fdlibm hypot to Java</a> - */ - private static double hypot(double x, double y) { - // Differences to the fdlibm reference: - // - // 1. fdlibm orders the two parts using the magnitude of the upper 32-bits. - // This incorrectly orders numbers which differ only in the lower 32-bits. - // This invalidates hypot(x, y) = hypot(y, x) for small sub-normal numbers and a minority - // of cases of normal numbers. This implementation forces the |x| >= |y| order - // using the entire 63-bits of the unsigned doubles to ensure the function - // is commutative. - // - // 2. fdlibm computed scaling by directly writing changes to the exponent bits - // and maintained the high part (ha) during scaling for use in the high - // precision sum x^2 + y^2. Since exponent scaling cannot be applied to sub-normals - // the original version dropped the split number representation for sub-normals - // and can produce maximum errors above 1 ULP for sub-normal numbers. - // This version uses Dekker's method to split the number. This can be applied to - // sub-normals and allows dropping the condition to check for sub-normal numbers - // since all small numbers are handled with a single scaling factor. - // The effect is increased precision for the majority of sub-normal cases where - // the implementations compute a different result. - // - // 3. An alteration is done here to add an 'else if' instead of a second - // 'if' statement. Thus you cannot scale down and up at the same time. - // - // 4. There is no use of the absolute double value. The magnitude comparison is - // performed using the long bit representation. The computation x^2+y^2 is - // insensitive to the sign bit. Thus use of Math.abs(double) is only in edge-case - // branches. - // - // 5. The exponent different to ignore the smaller component has changed from 60 to 54. - // - // Original comments from fdlibm are in c style: /* */ - // Extra comments added for reference. - // - // Note that the high 32-bits are compared to constants. - // The lowest 20-bits are the upper bits of the 52-bit mantissa. - // The next 11-bits are the biased exponent. The sign bit has been cleared. - // Scaling factors are powers of two for exact scaling. - // For clarity the values have been refactored to named constants. - - // The mask is used to remove the sign bit. - final long xbits = Double.doubleToRawLongBits(x) & UNSIGN_MASK; - final long ybits = Double.doubleToRawLongBits(y) & UNSIGN_MASK; - - // Order by magnitude: |a| >= |b| - double a; - double b; - /* High word of x & y */ - int ha; - int hb; - if (ybits > xbits) { - a = y; - b = x; - ha = (int) (ybits >>> 32); - hb = (int) (xbits >>> 32); - } else { - a = x; - b = y; - ha = (int) (xbits >>> 32); - hb = (int) (ybits >>> 32); - } - - // Check if the smaller part is significant. - // a^2 is computed in extended precision for an effective mantissa of 106-bits. - // An exponent difference of 54 is where b^2 will not overlap a^2. - if ((ha - hb) > EXP_54) { - /* a/b > 2**54 */ - // or a is Inf or NaN. - // No addition of a + b for sNaN. - return Math.abs(a); - } - - double rescale = 1.0; - if (ha > EXP_500) { - /* a > 2^500 */ - if (ha >= EXP_1024) { - /* Inf or NaN */ - // Check b is infinite for the IEEE754 result. - // No addition of a + b for sNaN. - return Math.abs(b) == Double.POSITIVE_INFINITY ? - Double.POSITIVE_INFINITY : - Math.abs(a); - } - /* scale a and b by 2^-600 */ - // Before scaling: a in [2^500, 2^1023]. - // After scaling: a in [2^-100, 2^423]. - // After scaling: b in [2^-154, 2^423]. - a *= TWO_POW_NEG_600; - b *= TWO_POW_NEG_600; - rescale = TWO_POW_600; - } else if (hb < EXP_NEG_500) { - // No special handling of sub-normals. - // These do not matter when we do not manipulate the exponent bits - // for scaling the split representation. - - // Intentional comparison with zero. - if (b == 0) { - return Math.abs(a); - } - - /* scale a and b by 2^600 */ - // Effective min exponent of a sub-normal = -1022 - 52 = -1074. - // Before scaling: b in [2^-1074, 2^-501]. - // After scaling: b in [2^-474, 2^99]. - // After scaling: a in [2^-474, 2^153]. - a *= TWO_POW_600; - b *= TWO_POW_600; - rescale = TWO_POW_NEG_600; - } - - // High precision x^2 + y^2 - return Math.sqrt(x2y2(a, b)) * rescale; - } - - /** - * Return {@code x^2 + y^2} with high accuracy. - * - * <p>It is assumed that {@code 2^500 > |x| >= |y| > 2^-500}. Thus there will be no - * overflow or underflow of the result. The inputs are not assumed to be unsigned. - * - * <p>The computation is performed using Dekker's method for extended precision - * multiplication of x and y and then summation of the extended precision squares. - * - * @param x Value x. - * @param y Value y - * @return x^2 + y^2 - * @see <a href="https://doi.org/10.1007/BF01397083"> - * Dekker (1971) A floating-point technique for extending the available precision</a> - */ - private static double x2y2(double x, double y) { - // Note: - // This method is different from the high-accuracy summation used in fdlibm for hypot. - // The summation could be any valid computation of x^2+y^2. However since this follows - // the re-scaling logic in hypot(x, y) the use of high precision has relatively - // less performance overhead than if used without scaling. - // The Dekker algorithm is branchless for better performance - // than the fdlibm method with a maximum ULP error of approximately 0.86. - // - // See NUMBERS-143 for analysis. - - // Do a Dekker summation of double length products x*x and y*y - // (10 multiply and 20 additions). - final double xx = x * x; - final double yy = y * y; - // Compute the round-off from the products. - // With FMA hardware support in JDK 9+ this can be replaced with the much faster: - // xxLow = Math.fma(x, x, -xx) - // yyLow = Math.fma(y, y, -yy) - // Dekker mul12 - final double xHigh = splitHigh(x); - final double xLow = x - xHigh; - final double xxLow = squareLow(xLow, xHigh, xx); - // Dekker mul12 - final double yHigh = splitHigh(y); - final double yLow = y - yHigh; - final double yyLow = squareLow(yLow, yHigh, yy); - // Dekker add2 - final double r = xx + yy; - // Note: The order is important. Assume xx > yy and drop Dekker's conditional - // check for which is the greater magnitude. - // s = xx - r + yy + yyLow + xxLow - // z = r + s - // zz = r - z + s - // Here we compute z inline and ignore computing the round-off zz. - // Note: The round-off could be used with Dekker's sqrt2 method. - // That adds 7 multiply, 1 division and 19 additions doubling the cost - // and reducing error to < 0.5 ulp for the final sqrt. - return xx - r + yy + yyLow + xxLow + r; - } } diff --git a/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/ComplexFunctions.java b/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/ComplexFunctions.java new file mode 100644 index 00000000..4cecc36e --- /dev/null +++ b/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/ComplexFunctions.java @@ -0,0 +1,845 @@ +/* + * 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. + */ +package org.apache.commons.numbers.complex; + +import java.util.function.DoubleUnaryOperator; + +/** + * Cartesian representation of a complex number. The complex number is expressed + * in the form \( a + ib \) where \( a \) and \( b \) are real numbers and \( i \) + * is the imaginary unit which satisfies the equation \( i^2 = -1 \). For the + * complex number \( a + ib \), \( a \) is called the <em>real part</em> and + * \( b \) is called the <em>imaginary part</em>. + * + * <p>Arithmetic in this class conforms to the C99 standard for complex numbers + * defined in ISO/IEC 9899, Annex G. Methods have been named using the equivalent + * method in ISO C99. The behavior for special cases is listed as defined in C99.</p> + * + * <p>For functions \( f \) which obey the conjugate equality \( conj(f(z)) = f(conj(z)) \), + * the specifications for the upper half-plane imply the specifications for the lower + * half-plane.</p> + * + * <p>For functions that are either odd, \( f(z) = -f(-z) \), or even, \( f(z) = f(-z) \), + * the specifications for the first quadrant imply the specifications for the other three + * quadrants.</p> + * + * <p>Special cases of <a href="http://mathworld.wolfram.com/BranchCut.html">branch cuts</a> + * for multivalued functions adopt the principle value convention from C99. Specials cases + * from C99 that raise the "invalid" or "divide-by-zero" + * <a href="https://en.cppreference.com/w/c/numeric/fenv/FE_exceptions">floating-point + * exceptions</a> return the documented value without an explicit mechanism to notify + * of the exception case, that is no exceptions are thrown during computations in-line with + * the convention of the corresponding single-valued functions in + * {@link java.lang.Math java.lang.Math}. + * These cases are documented in the method special cases as "invalid" or "divide-by-zero" + * floating-point operation. + * Note: Invalid floating-point exception cases will result in a complex number where the + * cardinality of NaN component parts has increased as a real or imaginary part could + * not be computed and is set to NaN. + * + * @see <a href="http://www.open-std.org/JTC1/SC22/WG14/www/standards"> + * ISO/IEC 9899 - Programming languages - C</a> + */ +public final class ComplexFunctions { + + /** Mask to remove the sign bit from a long. */ + static final long UNSIGN_MASK = 0x7fff_ffff_ffff_ffffL; + + /** Natural logarithm of 2 (ln(2)). */ + private static final double LN_2 = Math.log(2); + /** {@code 1/2}. */ + private static final double HALF = 0.5; + /** 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 sqrt(2)}. */ + private static final double ROOT2 = 1.4142135623730951; + /** The bit representation of {@code -0.0}. */ + private static final long NEGATIVE_ZERO_LONG_BITS = Double.doubleToLongBits(-0.0); + /** 54 shifted 20-bits to align with the exponent of the upper 32-bits of a double. */ + private static final int EXP_54 = 0x36_00000; + /** Represents an exponent of 500 in unbiased form shifted 20-bits to align with the upper 32-bits of a double. */ + private static final int EXP_500 = 0x5f3_00000; + /** Represents an exponent of 1024 in unbiased form (infinite or nan) + * shifted 20-bits to align with the upper 32-bits of a double. */ + private static final int EXP_1024 = 0x7ff_00000; + /** Represents an exponent of -500 in unbiased form shifted 20-bits to align with the upper 32-bits of a double. */ + private static final int EXP_NEG_500 = 0x20b_00000; + /** 2^600. */ + private static final double TWO_POW_600 = 0x1.0p+600; + /** 2^-600. */ + private static final double TWO_POW_NEG_600 = 0x1.0p-600; + + /** The multiplier used to split the double value into hi and low parts. This must be odd + * and a value of 2^s + 1 in the range {@code p/2 <= s <= p-1} where p is the number of + * bits of precision of the floating point number. Here {@code s = 27}.*/ + private static final double MULTIPLIER = 1.34217729E8; + + /** + * Private constructor for utility class. + */ + private ComplexFunctions() { + } + + /** + * Returns the absolute value of the complex number. + * <pre>abs(x + i y) = sqrt(x^2 + y^2)</pre> + * + * <p>This should satisfy the special cases of the hypot function in ISO C99 F.9.4.3: + * "The hypot functions compute the square root of the sum of the squares of x and y, + * without undue overflow or underflow." + * + * <ul> + * <li>hypot(x, y), hypot(y, x), and hypot(x, −y) are equivalent. + * <li>hypot(x, ±0) is equivalent to |x|. + * <li>hypot(±∞, y) returns +∞, even if y is a NaN. + * </ul> + * + * <p>This method is called by all methods that require the absolute value of the complex + * number, e.g. abs(), sqrt() and log(). + * + * @param real Real part \( a \) of the complex number \( (a +ib) \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib) \). + * @return The absolute value. + */ + public static double abs(double real, double imaginary) { + // Specialised implementation of hypot. + // See NUMBERS-143 + return hypot(real, imaginary); + } + + /** + * Returns the argument of the complex number. + * + * <p>The argument is the angle phi between the positive real axis and + * the point representing this number in the complex plane. + * The value returned is between \( -\pi \) (not inclusive) + * and \( \pi \) (inclusive), with negative values returned for numbers with + * negative imaginary parts. + * + * <p>If either real or imaginary part (or both) is NaN, then the result is NaN. + * Infinite parts are handled as {@linkplain Math#atan2} handles them, + * essentially treating finite parts as zero in the presence of an + * infinite coordinate and returning a multiple of \( \frac{\pi}{4} \) depending on + * the signs of the infinite parts. + * + * <p>This code follows the + * <a href="http://www.iso-9899.info/wiki/The_Standard">ISO C Standard</a>, Annex G, + * in calculating the returned value using the {@code atan2(y, x)} method for complex + * \( x + iy \). + * + * @param r Real part \( a \) of the complex number \( (a +ib) \). + * @param i Imaginary part \( b \) of the complex number \( (a +ib) \). + * @return The argument of the complex number. + * @see Math#atan2(double, double) + */ + public static double arg(double r, double i) { + // Delegate + return Math.atan2(i, r); + } + + /** + * Returns {@code true} if either real or imaginary component of the complex number is infinite. + * + * <p>Note: A complex number with at least one infinite part is regarded + * as an infinity (even if its other part is a NaN). + * @param real Real part \( a \) of the complex number \( (a +ib) \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib) \). + * @return {@code true} if the complex number contains an infinite value. + * @see Double#isInfinite(double) + */ + public static boolean isInfinite(double real, double imaginary) { + return Double.isInfinite(real) || Double.isInfinite(imaginary); + } + + /** + * Returns the + * <a href="http://mathworld.wolfram.com/NaturalLogarithm.html"> + * natural logarithm</a> of the complex number using its real and imaginary parts. + * + * <p>The natural logarithm of \( z \) is unbounded along the real axis and + * in the range \( [-\pi, \pi] \) along the imaginary axis. The imaginary part of the + * natural logarithm has a branch cut along the negative real axis \( (-infty,0] \). + * Special cases: + * + * <ul> + * <li>{@code z.conj().log() == z.log().conj()}. + * <li>If {@code z} is −0 + i0, returns −∞ + iπ ("divide-by-zero" floating-point operation). + * <li>If {@code z} is +0 + i0, returns −∞ + i0 ("divide-by-zero" floating-point operation). + * <li>If {@code z} is x + i∞ for finite x, returns +∞ + iπ/2. + * <li>If {@code z} is x + iNaN for finite x, returns NaN + iNaN ("invalid" floating-point operation). + * <li>If {@code z} is −∞ + iy for finite positive-signed y, returns +∞ + iπ. + * <li>If {@code z} is +∞ + iy for finite positive-signed y, returns +∞ + i0. + * <li>If {@code z} is −∞ + i∞, returns +∞ + i3π/4. + * <li>If {@code z} is +∞ + i∞, returns +∞ + iπ/4. + * <li>If {@code z} is ±∞ + iNaN, returns +∞ + iNaN. + * <li>If {@code z} is NaN + iy for finite y, returns NaN + iNaN ("invalid" floating-point operation). + * <li>If {@code z} is NaN + i∞, returns +∞ + iNaN. + * <li>If {@code z} is NaN + iNaN, returns NaN + iNaN. + * </ul> + * + * <p>Implements the formula: + * + * <p>\[ \ln(z) = \ln |z| + i \arg(z) \] + * + * <p>where \( |z| \) is the absolute and \( \arg(z) \) is the argument. + * + * <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> + * + * @param real Real part \( a \) of the complex number \( (a +ib \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib \). + * @param action Consumer for the natural logarithm of the complex number. + * @param <R> the return type of the supplied action. + * @return the object returned by the supplied action. + * @see Math#log(double) + * @see <a href="http://functions.wolfram.com/ElementaryFunctions/Log/">Log</a> + */ + public static <R> R log(double real, double imaginary, ComplexSink<R> action) { + return log(Math::log, HALF, LN_2, real, imaginary, action); + } + + /** + * Returns the base 10 + * <a href="http://mathworld.wolfram.com/CommonLogarithm.html"> + * common logarithm</a> of the complex number using its real and imaginary parts. + * + * <p>The common logarithm of \( z \) is unbounded along the real axis and + * in the range \( [-\pi, \pi] \) along the imaginary axis. The imaginary part of the + * common logarithm has a branch cut along the negative real axis \( (-infty,0] \). + * Special cases are as defined in the log: + * + * <p>Implements the formula: + * + * <p>\[ \log_{10}(z) = \log_{10} |z| + i \arg(z) \] + * + * <p>where \( |z| \) is the absolute and \( \arg(z) \) is the argument. + * + * @param real Real part \( a \) of the complex number \( (a +ib \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib \). + * @param action Consumer for the natural logarithm of the complex number. + * @param <R> the return type of the supplied action. + * @return the object returned by the supplied action. + * @see Math#log10(double) + */ + public static <R> R log10(double real, double imaginary, ComplexSink<R> action) { + return log(Math::log10, LOG_10E_O_2, LOG10_2, real, imaginary, action); + } + + /** + * Returns the logarithm of complex number using its real and + * imaginary parts and the provided function. + * Implements the formula: + * + * <pre> + * log(x + i y) = log(|x + i y|) + i arg(x + i y)</pre> + * + * <p>Warning: The argument {@code logOf2} must be equal to {@code log(2)} using the + * provided log function otherwise scaling using powers of 2 in the case of overflow + * 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 real Real part \( a \) of the complex number \( (a +ib \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib \). + * @param action Consumer for the natural logarithm of the complex number. + * @param <R> the return type of the supplied action. + * @return the object returned by the supplied action. + */ + private static <R> R log(DoubleUnaryOperator log, + double logOfeOver2, + double logOf2, + double real, + double imaginary, + ComplexSink<R> action) { + + // Handle NaN + if (Double.isNaN(real) || Double.isNaN(imaginary)) { + // Return NaN unless infinite + if (isInfinite(real, imaginary)) { + return action.apply(Double.POSITIVE_INFINITY, Double.NaN); + } + return action.apply(Double.NaN, Double.NaN); + } + + // Returns the real part: + // log(sqrt(x^2 + y^2)) + // log(x^2 + y^2) / 2 + + // Compute with positive values + double x = Math.abs(real); + double y = Math.abs(imaginary); + + // Find the larger magnitude. + if (x < y) { + final double tmp = x; + x = y; + y = tmp; + } + + if (x == 0) { + // Handle zero: raises the ‘‘divide-by-zero’’ floating-point exception. + return action.apply(Double.NEGATIVE_INFINITY, + negative(real) ? Math.copySign(Math.PI, imaginary) : imaginary); + } + + double re; + + // This alters the implementation of Hull et al (1994) which used a standard + // precision representation of |z|: sqrt(x*x + y*y). + // This formula should use the same definition of the magnitude returned + // by Complex.abs() which is a high precision computation with scaling. + // The checks for overflow thus only require ensuring the output of |z| + // will not overflow or underflow. + + 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 { + // Check for over/underflow in |z| + // When scaling: + // log(a / b) = log(a) - log(b) + // So initialize the result with the log of the scale factor. + re = 0; + if (x > Double.MAX_VALUE / 2) { + // Potential overflow. + if (isPosInfinite(x)) { + // Handle infinity + return action.apply(x, arg(real, imaginary)); + } + // Scale down. + x /= 2; + y /= 2; + // log(2) + re = logOf2; + } else if (y < Double.MIN_NORMAL) { + // Potential underflow. + if (y == 0) { + // Handle real only number + return action.apply(log.applyAsDouble(x), arg(real, imaginary)); + } + // Scale up sub-normal numbers to make them normal by scaling by 2^54, + // i.e. more than the mantissa digits. + x *= 0x1.0p54; + y *= 0x1.0p54; + // log(2^-54) = -54 * log(2) + re = -54 * logOf2; + } + re += log.applyAsDouble(abs(x, y)); + } + + // All ISO C99 edge cases for the imaginary are satisfied by the Math library. + return action.apply(re, arg(real, imaginary)); + } + + /** + * Returns {@code sqrt(x^2 + y^2)} without intermediate overflow or underflow. + * + * <p>Special cases: + * <ul> + * <li>If either argument is infinite, then the result is positive infinity. + * <li>If either argument is NaN and neither argument is infinite, then the result is NaN. + * </ul> + * + * <p>The computed result is expected to be within 1 ulp of the exact result. + * + * <p>This method is a replacement for {@link Math#hypot(double, double)}. There + * will be differences between this method and {@code Math.hypot(double, double)} due + * to the use of a different algorithm to compute the high precision sum of + * {@code x^2 + y^2}. This method has been tested to have a lower maximum error from + * the exact result; any differences are expected to be 1 ULP indicating a rounding + * change in the sum. + * + * <p>JDK9 ported the hypot function to Java for bug JDK-7130085 due to the slow performance + * of the method as a native function. Benchmarks of the Complex class for functions that + * use hypot confirm this is slow pre-Java 9. This implementation outperforms the new faster + * {@code Math.hypot(double, double)} on JDK 11 (LTS). See the Commons numbers examples JMH + * module for benchmarks. Comparisons with alternative implementations indicate + * performance gains are related to edge case handling and elimination of an unpredictable + * branch in the computation of {@code x^2 + y^2}. + * + * <p>This port was adapted from the "Freely Distributable Math Library" hypot function. + * This method only (and not invoked methods within) is distributed under the terms of the + * original notice as shown below: + * <pre> + * ==================================================== + * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. + * + * Developed at SunSoft, a Sun Microsystems, Inc. business. + * Permission to use, copy, modify, and distribute this + * software is freely granted, provided that this notice + * is preserved. + * ==================================================== + * </pre> + * + * <p>Note: The fdlibm c code makes use of the language ability to read and write directly + * to the upper and lower 32-bits of the 64-double. The function performs + * checking on the upper 32-bits for the magnitude of the two numbers by accessing + * the exponent and 20 most significant bits of the mantissa. These upper bits + * are manipulated during scaling and then used to perform extended precision + * computation of the sum {@code x^2 + y^2} where the high part of the number has 20-bit + * precision. Manipulation of direct bits has no equivalent in Java + * other than use of {@link Double#doubleToLongBits(double)} and + * {@link Double#longBitsToDouble(long)}. To avoid conversion to and from long and double + * representations this implementation only scales the double representation. The high + * and low parts of a double for the extended precision computation are extracted + * using the method of Dekker (1971) to create two 26-bit numbers. This works for sub-normal + * numbers and reduces the maximum error in comparison to fdlibm hypot which does not + * use a split number algorithm for sub-normal numbers. + * + * @param x Value x + * @param y Value y + * @return sqrt(x^2 + y^2) + * @see Math#hypot(double, double) + * @see <a href="https://www.netlib.org/fdlibm/e_hypot.c">fdlibm e_hypot.c</a> + * @see <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7130085">JDK-7130085 : Port fdlibm hypot to Java</a> + */ + private static double hypot(double x, double y) { + // Differences to the fdlibm reference: + // + // 1. fdlibm orders the two parts using the magnitude of the upper 32-bits. + // This incorrectly orders numbers which differ only in the lower 32-bits. + // This invalidates hypot(x, y) = hypot(y, x) for small sub-normal numbers and a minority + // of cases of normal numbers. This implementation forces the |x| >= |y| order + // using the entire 63-bits of the unsigned doubles to ensure the function + // is commutative. + // + // 2. fdlibm computed scaling by directly writing changes to the exponent bits + // and maintained the high part (ha) during scaling for use in the high + // precision sum x^2 + y^2. Since exponent scaling cannot be applied to sub-normals + // the original version dropped the split number representation for sub-normals + // and can produce maximum errors above 1 ULP for sub-normal numbers. + // This version uses Dekker's method to split the number. This can be applied to + // sub-normals and allows dropping the condition to check for sub-normal numbers + // since all small numbers are handled with a single scaling factor. + // The effect is increased precision for the majority of sub-normal cases where + // the implementations compute a different result. + // + // 3. An alteration is done here to add an 'else if' instead of a second + // 'if' statement. Thus you cannot scale down and up at the same time. + // + // 4. There is no use of the absolute double value. The magnitude comparison is + // performed using the long bit representation. The computation x^2+y^2 is + // insensitive to the sign bit. Thus use of Math.abs(double) is only in edge-case + // branches. + // + // 5. The exponent difference to ignore the smaller component has changed from 60 to 54. + // + // Original comments from fdlibm are in c style: /* */ + // Extra comments added for reference. + // + // Note that the high 32-bits are compared to constants. + // The lowest 20-bits are the upper bits of the 52-bit mantissa. + // The next 11-bits are the biased exponent. The sign bit has been cleared. + // Scaling factors are powers of two for exact scaling. + // For clarity the values have been refactored to named constants. + + // The mask is used to remove the sign bit. + final long xbits = Double.doubleToRawLongBits(x) & UNSIGN_MASK; + final long ybits = Double.doubleToRawLongBits(y) & UNSIGN_MASK; + + // Order by magnitude: |a| >= |b| + double a; + double b; + /* High word of x & y */ + int ha; + int hb; + if (ybits > xbits) { + a = y; + b = x; + ha = (int) (ybits >>> 32); + hb = (int) (xbits >>> 32); + } else { + a = x; + b = y; + ha = (int) (xbits >>> 32); + hb = (int) (ybits >>> 32); + } + + // Check if the smaller part is significant. + // a^2 is computed in extended precision for an effective mantissa of 106-bits. + // An exponent difference of 54 is where b^2 will not overlap a^2. + if ((ha - hb) > EXP_54) { + /* a/b > 2**54 */ + // or a is Inf or NaN. + // No addition of a + b for sNaN. + return Math.abs(a); + } + + double rescale = 1.0; + if (ha > EXP_500) { + /* a > 2^500 */ + if (ha >= EXP_1024) { + /* Inf or NaN */ + // Check b is infinite for the IEEE754 result. + // No addition of a + b for sNaN. + return Math.abs(b) == Double.POSITIVE_INFINITY ? + Double.POSITIVE_INFINITY : + Math.abs(a); + } + /* scale a and b by 2^-600 */ + // Before scaling: a in [2^500, 2^1023]. + // After scaling: a in [2^-100, 2^423]. + // After scaling: b in [2^-154, 2^423]. + a *= TWO_POW_NEG_600; + b *= TWO_POW_NEG_600; + rescale = TWO_POW_600; + } else if (hb < EXP_NEG_500) { + // No special handling of sub-normals. + // These do not matter when we do not manipulate the exponent bits + // for scaling the split representation. + + // Intentional comparison with zero. + if (b == 0) { + return Math.abs(a); + } + + /* scale a and b by 2^600 */ + // Effective min exponent of a sub-normal = -1022 - 52 = -1074. + // Before scaling: b in [2^-1074, 2^-501]. + // After scaling: b in [2^-474, 2^99]. + // After scaling: a in [2^-474, 2^153]. + a *= TWO_POW_600; + b *= TWO_POW_600; + rescale = TWO_POW_NEG_600; + } + + // High precision x^2 + y^2 + return Math.sqrt(x2y2(a, b)) * rescale; + } + + /** + * Return {@code x^2 + y^2} with high accuracy. + * + * <p>It is assumed that {@code 2^500 > |x| >= |y| > 2^-500}. Thus there will be no + * overflow or underflow of the result. The inputs are not assumed to be unsigned. + * + * <p>The computation is performed using Dekker's method for extended precision + * multiplication of x and y and then summation of the extended precision squares. + * + * @param x Value x. + * @param y Value y + * @return x^2 + y^2 + * @see <a href="https://doi.org/10.1007/BF01397083"> + * Dekker (1971) A floating-point technique for extending the available precision</a> + */ + private static double x2y2(double x, double y) { + // Note: + // This method is different from the high-accuracy summation used in fdlibm for hypot. + // The summation could be any valid computation of x^2+y^2. However since this follows + // the re-scaling logic in hypot(x, y) the use of high precision has relatively + // less performance overhead than if used without scaling. + // The Dekker algorithm is branchless for better performance + // than the fdlibm method with a maximum ULP error of approximately 0.86. + // + // See NUMBERS-143 for analysis. + + // Do a Dekker summation of double length products x*x and y*y + // (10 multiply and 20 additions). + final double xx = x * x; + final double yy = y * y; + // Compute the round-off from the products. + // With FMA hardware support in JDK 9+ this can be replaced with the much faster: + // xxLow = Math.fma(x, x, -xx) + // yyLow = Math.fma(y, y, -yy) + // Dekker mul12 + final double xHigh = splitHigh(x); + final double xLow = x - xHigh; + final double xxLow = squareLow(xLow, xHigh, xx); + // Dekker mul12 + final double yHigh = splitHigh(y); + final double yLow = y - yHigh; + final double yyLow = squareLow(yLow, yHigh, yy); + // Dekker add2 + final double r = xx + yy; + // Note: The order is important. Assume xx > yy and drop Dekker's conditional + // check for which is the greater magnitude. + // s = xx - r + yy + yyLow + xxLow + // z = r + s + // zz = r - z + s + // Here we compute z inline and ignore computing the round-off zz. + // Note: The round-off could be used with Dekker's sqrt2 method. + // That adds 7 multiply, 1 division and 19 additions doubling the cost + // and reducing error to < 0.5 ulp for the final sqrt. + return xx - r + yy + yyLow + xxLow + r; + } + + /** + * 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}. + */ + //TODO - make it private in future + 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. + + final double xx = x * x; + final double yy = y * y; + // Modify to use high precision before the threshold set by Hull et al. + // This is to preserve the monotonic output of the computation at the switch. + // Set the threshold when x^2 + y^2 is above 0.5 thus subtracting 1 results in a number + // that can be expressed with a higher precision than any number in the range 0.5-1.0 + // due to the variable exponent used below 0.5. + if (x < 1 && xx + yy > 0.5) { + // Large relative error. + // This does not use o.a.c.numbers.LinearCombination.value(x, x, y, y, 1, -1). + // It is optimised knowing that: + // - the products are squares + // - the final term is -1 (which does not require split multiplication and addition) + // - The answer will not be NaN as the terms are not NaN components + // - The order is known to be 1 > |x| >= |y| + // The squares are computed using a split multiply algorithm and + // the summation using an extended precision summation algorithm. + + // Split x and y as one 26 bits number and one 27 bits number + final double xHigh = splitHigh(x); + final double xLow = x - xHigh; + final double yHigh = splitHigh(y); + final double yLow = y - yHigh; + + // Accurate split multiplication x * x and y * y + final double x2Low = squareLow(xLow, xHigh, xx); + final double y2Low = squareLow(yLow, yHigh, yy); + + return sumx2y2m1(xx, x2Low, yy, y2Low); + } + return (x - 1) * (x + 1) + yy; + } + + /** + * Implement Dekker's method to split a value into two parts. Multiplying by (2^s + 1) create + * a big value from which to derive the two split parts. + * <pre> + * c = (2^s + 1) * a + * a_big = c - a + * a_hi = c - a_big + * a_lo = a - a_hi + * a = a_hi + a_lo + * </pre> + * + * <p>The multiplicand must be odd allowing a p-bit value to be split into + * (p-s)-bit value {@code a_hi} and a non-overlapping (s-1)-bit value {@code a_lo}. + * Combined they have (p-1) bits of significand but the sign bit of {@code a_lo} + * contains a bit of information. + * + * @param a Value. + * @return the high part of the value. + * @see <a href="https://doi.org/10.1007/BF01397083"> + * Dekker (1971) A floating-point technique for extending the available precision</a> + */ + private static double splitHigh(double a) { + final double c = MULTIPLIER * a; + return c - (c - a); + } + + /** + * Compute the round-off from the square of a split number with {@code low} and {@code high} + * components. Uses Dekker's algorithm for split multiplication modified for a square product. + * + * <p>Note: This is candidate to be replaced with {@code Math.fma(x, x, -x * x)} to compute + * the round-off from the square product {@code x * x}. This would remove the requirement + * to compute the split number and make this method redundant. {@code Math.fma} requires + * JDK 9 and FMA hardware support. + * + * @param low Low part of number. + * @param high High part of number. + * @param square Square of the number. + * @return <code>low * low - (((product - high * high) - low * high) - high * low)</code> + * @see <a href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps"> + * Shewchuk (1997) Theorum 18</a> + */ + private static double squareLow(double low, double high, double square) { + final double lh = low * high; + return low * low - (((square - high * high) - lh) - lh); + } + + /** + * Compute the round-off from the sum of two numbers {@code a} and {@code b} using + * Dekker's two-sum algorithm. The values are required to be ordered by magnitude: + * {@code |a| >= |b|}. + * + * @param a First part of sum. + * @param b Second part of sum. + * @param x Sum. + * @return <code>b - (x - a)</code> + * @see <a href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps"> + * Shewchuk (1997) Theorum 6</a> + */ + private static double fastSumLow(double a, double b, double x) { + // x = a + b + // bVirtual = x - a + // y = b - bVirtual + return b - (x - a); + } + + /** + * Compute the round-off from the sum of two numbers {@code a} and {@code b} using + * Knuth's two-sum algorithm. The values are not required to be ordered by magnitude. + * + * @param a First part of sum. + * @param b Second part of sum. + * @param x Sum. + * @return <code>(a - (x - (x - a))) + (b - (x - a))</code> + * @see <a href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps"> + * Shewchuk (1997) Theorum 7</a> + */ + private static double sumLow(double a, double b, double x) { + // x = a + b + // bVirtual = x - a + // aVirtual = x - bVirtual + // bRoundoff = b - bVirtual + // aRoundoff = a - aVirtual + // y = aRoundoff + bRoundoff + final double bVirtual = x - a; + return (a - (x - bVirtual)) + (b - bVirtual); + } + + /** + * Sum x^2 + y^2 - 1. It is assumed that {@code y <= x < 1}. + * + * <p>Implement Shewchuk's expansion-sum algorithm: [x2Low, x2High] + [-1] + [y2Low, y2High]. + * + * @param x2High High part of x^2. + * @param x2Low Low part of x^2. + * @param y2High High part of y^2. + * @param y2Low Low part of y^2. + * @return x^2 + y^2 - 1 + * @see <a href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps"> + * Shewchuk (1997) Theorum 12</a> + */ + private static double sumx2y2m1(double x2High, double x2Low, double y2High, double y2Low) { + // Let e and f be non-overlapping expansions of components of length m and n. + // The following algorithm will produce a non-overlapping expansion h where the + // sum h_i = e + f and components of h are in increasing order of magnitude. + // Expansion-sum proceeds by a grow-expansion of the first part from one expansion + // into the other, extending its length by 1. The process repeats for the next part + // but the grow-expansion starts at the previous merge position + 1. + // Thus expansion-sum requires mn two-sum operations to merge length m into length n + // resulting in length m+n-1. + + // Variables numbered from 1 as per Figure 7 (p.12). The output expansion h is placed + // into e increasing its length for each grow expansion. + + // We have two expansions for x^2 and y^2 and the whole number -1. + // Expecting (x^2 + y^2) close to 1 we generate first the intermediate expansion + // (x^2 - 1) moving the result away from 1 where there are sparse floating point + // representations. This is then added to a similar magnitude y^2. Leaving the -1 + // until last suffers from 1 ulp rounding errors more often and the requirement + // for a distillation sum to reduce rounding error frequency. + + // Note: Do not use the alternative fast-expansion-sum of the parts sorted by magnitude. + // The parts can be ordered with a single comparison into: + // [y2Low, (y2High|x2Low), x2High, -1] + // The fast-two-sum saves 1 fast-two-sum and 3 two-sum operations (21 additions) and + // adds a penalty of a single branch condition. + // However the order in not "strongly non-overlapping" and the fast-expansion-sum + // output will not be strongly non-overlapping. The sum of the output has 1 ulp error + // on random cis numbers approximately 1 in 160 events. This can be removed by a + // distillation two-sum pass over the final expansion as a cost of 1 fast-two-sum and + // 3 two-sum operations! So we use the expansion sum with the same operations and + // no branches. + + double q = x2Low - 1; + double e1 = fastSumLow(-1, x2Low, q); + double e3 = q + x2High; + double e2 = sumLow(q, x2High, e3); + + final double f1 = y2Low; + final double f2 = y2High; + + // Grow expansion of f1 into e + q = f1 + e1; + e1 = sumLow(f1, e1, q); + double p = q + e2; + e2 = sumLow(q, e2, p); + double e4 = p + e3; + e3 = sumLow(p, e3, e4); + + // Grow expansion of f2 into e (only required to start at e2) + q = f2 + e2; + e2 = sumLow(f2, e2, q); + p = q + e3; + e3 = sumLow(q, e3, p); + final double e5 = p + e4; + e4 = sumLow(p, e4, e5); + + // Final summation: + // The sum of the parts is within 1 ulp of the true expansion value e: + // |e - sum| < ulp(sum). + // To achieve the exact result requires iteration of a distillation two-sum through + // the expansion until convergence, i.e. no smaller term changes higher terms. + // This requires (n-1) iterations for length n. Here we neglect this as + // although the method is not ensured to be exact is it robust on random + // cis numbers. + return e1 + e2 + e3 + e4 + e5; + } + + /** + * Check that a value is negative. It must meet all the following conditions: + * <ul> + * <li>it is not {@code NaN},</li> + * <li>it is negative signed,</li> + * </ul> + * + * <p>Note: This is true for negative zero.</p> + * + * @param d Value. + * @return {@code true} if {@code d} is negative. + */ + //TODO - make private in future + static boolean negative(double d) { + return d < 0 || Double.doubleToLongBits(d) == NEGATIVE_ZERO_LONG_BITS; + } + + /** + * Check that a value is positive infinity. Used to replace {@link Double#isInfinite()} + * when the input value is known to be positive (i.e. in the case where it has been + * set using {@link Math#abs(double)}). + * + * @param d Value. + * @return {@code true} if {@code d} is +inf. + */ + //TODO - make private in future + static boolean isPosInfinite(double d) { + return d == Double.POSITIVE_INFINITY; + } +} diff --git a/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/ComplexSink.java b/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/ComplexSink.java new file mode 100644 index 00000000..1c78d188 --- /dev/null +++ b/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/ComplexSink.java @@ -0,0 +1,40 @@ +/* + * 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. + */ + +package org.apache.commons.numbers.complex; + +/** + * Represents a data sink for a complex number \( (a + i b) \). + * Operations return a result of type {@code R}. + * + * <p>This is a functional interface whose functional method is + * {@link #apply(double, double)}. + * + * @param <R> The type of the result + * @since 1.1 + */ +@FunctionalInterface +public interface ComplexSink<R> { + + /** + * Represents a function that accepts real and imaginary part of complex number and returns an object. + * @param real Real part \( a \) of the complex number \( (a +ib) \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib) \). + * @return R the object encapsulating the complex result + */ + R apply(double real, double imaginary); +} diff --git a/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/ComplexUnaryOperator.java b/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/ComplexUnaryOperator.java new file mode 100644 index 00000000..a9bae229 --- /dev/null +++ b/commons-numbers-complex/src/main/java/org/apache/commons/numbers/complex/ComplexUnaryOperator.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package org.apache.commons.numbers.complex; + +/** + * Represents a unary operation on a Cartesian form of a complex number \( a + ib \) + * where \( a \) and \( b \) are real numbers represented as two {@code double} + * parts. The operation creates a complex number result; the result is supplied + * to a terminating consumer function which may return an object representation + * of the complex result. + * + * <p>This is a functional interface whose functional method is + * {@link #apply(double, double, ComplexSink)}. + * + * @param <R> The type of the complex result + * @since 1.1 + */ +@FunctionalInterface +public interface ComplexUnaryOperator<R> { + + /** + * Represents an operator that accepts real and imaginary parts of a complex number and supplies the complex result to the provided consumer. + * @param real Real part \( a \) of the complex number \( (a +ib) \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib) \). + * @param out Consumer for the complex result. + * @return the object returned by the provided consumer. + */ + R apply(double real, double imaginary, ComplexSink<R> out); +} diff --git a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/CReferenceTest.java b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/CReferenceTest.java index cf724423..dd283af3 100644 --- a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/CReferenceTest.java +++ b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/CReferenceTest.java @@ -145,6 +145,36 @@ class CReferenceTest { } } + /** + * Assert the operation on the complex number is equal to the expected value. + * + * <p>The results are considered equal within the provided units of least + * precision. The maximum count of numbers allowed between the two values is + * {@code maxUlps - 1}. + * + * <p>Numbers must have the same sign. Thus -0.0 and 0.0 are never equal. + * + * Assert the operation on the complex number is <em>exactly</em> equal to the operation on + * complex real and imaginary parts. + * + * @param c Input complex number. + * @param name Operation name. + * @param operation1 Operation on the Complex object. + * @param operation2 Operation on the complex real and imaginary parts. + * @param expected Expected result. + */ + static void assertComplex(Complex c, + String name, + UnaryOperator<Complex> operation1, + ComplexUnaryOperator<ComplexNumber> operation2, + Complex expected, long maxUlps) { + + final Complex z = TestUtils.assertSame(c, operation1, operation2, name); + + assertEquals(() -> "UnaryOperator " + name + "(" + c + "): real", expected.real(), z.real(), maxUlps); + assertEquals(() -> "UnaryOperator " + name + "(" + c + "): imaginary", expected.imag(), z.imag(), maxUlps); + } + /** * Assert the operation on the complex number is equal to the expected value. * @@ -227,7 +257,26 @@ class CReferenceTest { /** * Assert the operation using the data loaded from test resources. * - * @param testData Test data resource name. + * @param name Operation name + * @param operation1 Operation on the Complex object. + * @param operation2 Operation on the complex real and imaginary parts. + * @param maxUlps Maximum units of least precision between the two values. + */ + private static void assertOperation(String name, + UnaryOperator<Complex> operation1, + ComplexUnaryOperator<ComplexNumber> operation2, + long maxUlps) { + final List<Complex[]> data = loadTestData(name); + final long ulps = getTestUlps(maxUlps); + for (final Complex[] pair : data) { + assertComplex(pair[0], name, operation1, operation2, pair[1], ulps); + } + } + + /** + * Assert the operation using the data loaded from test resources. + * + * @param name Test data resource name. * @return the list */ private static List<Complex[]> loadTestData(String name) { @@ -312,7 +361,7 @@ class CReferenceTest { @Test void testLog() { - assertOperation("log", Complex::log, 1); + assertOperation("log", Complex::log, ComplexFunctions::log, 1); } @Test diff --git a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/CStandardTest.java b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/CStandardTest.java index 42e6e9dc..edd3c435 100644 --- a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/CStandardTest.java +++ b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/CStandardTest.java @@ -300,6 +300,105 @@ class CStandardTest { } } + /** + * Assert the operation on the complex number satisfies the conjugate equality. + * + * <pre> + * op(conj(z)) = conj(op(z)) + * </pre> + * + * <p>The results must be binary equal. This includes the sign of zero. + * + * <p>Assert the operation on the complex number is <em>exactly</em> equal to the operation on + * complex real and imaginary parts. + * + * <h2>ISO C99 equalities</h2> + * + * <p>Note that this method currently enforces the conjugate equalities for some cases + * where the sign of the real/imaginary parts are unspecified in ISO C99. This is + * allowed (since they are unspecified). The sign specification is appropriately + * handled during testing of odd/even functions. There are some functions where it + * is not possible to satisfy the conjugate equality and also the odd/even rule. + * The compromise made here is to satisfy only one and the other is allowed to fail + * only on the sign of the output result. Known functions where this applies are: + * + * <ul> + * <li>asinh(NaN, inf) + * <li>atanh(NaN, inf) + * <li>cosh(NaN, 0.0) + * <li>sinh(inf, inf) + * <li>sinh(inf, nan) + * </ul> + * + * @param operation1 Operation on the Complex object. + * @param operation2 Operation on the complex real and imaginary parts. + * @param name Operation name. + */ + private static void assertConjugateEquality(UnaryOperator<Complex> operation1, + ComplexUnaryOperator<ComplexNumber> operation2, + String name) { + // Edge cases. Inf/NaN are specifically handled in the C99 test cases + // but are repeated here to enforce the conjugate equality even when the C99 + // standard does not specify a sign. This may be revised in the future. + final double[] parts = {Double.NEGATIVE_INFINITY, -1, -0.0, 0.0, 1, + Double.POSITIVE_INFINITY, Double.NaN}; + for (final double x : parts) { + for (final double y : parts) { + // No conjugate for imaginary NaN + if (!Double.isNaN(y)) { + assertConjugateEquality(complex(x, y), operation1, operation2, UnspecifiedSign.NONE, name); + } + } + } + // Random numbers + final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(); + for (int i = 0; i < 100; i++) { + final double x = next(rng); + final double y = next(rng); + assertConjugateEquality(complex(x, y), operation1, operation2, UnspecifiedSign.NONE, name); + } + } + + /** + * Assert the operation on the complex number satisfies the conjugate equality. + * + * <pre> + * op(conj(z)) = conj(op(z)) + * </pre> + * + * <p>The results must be binary equal; the sign of the complex number is first processed + * using the provided sign specification. + * + * <p>Assert the operation on the complex number is <em>exactly</em> equal to the operation on + * complex real and imaginary parts. + * + * @param z Input complex number. + * @param operation1 Operation on the Complex object. + * @param operation2 Operation on the complex real and imaginary parts. + * @param sign the sign specification + * @param name Operation name. + */ + private static void assertConjugateEquality(Complex z, + UnaryOperator<Complex> operation1, + ComplexUnaryOperator<ComplexNumber> operation2, + UnspecifiedSign sign, String name) { + + final Complex zConj = z.conj(); + final Complex c1 = TestUtils.assertSame(zConj, operation1, operation2, name); + final Complex c2 = TestUtils.assertSame(z, operation1, operation2, name).conj(); + + final Complex t1 = sign.removeSign(c1); + final Complex t2 = sign.removeSign(c2); + + // Test for binary equality + if (!equals(t1.getReal(), t2.getReal()) || + !equals(t1.getImaginary(), t2.getImaginary())) { + Assertions.fail( + String.format("Conjugate equality failed (z=%s). Expected: %s but was: %s (Unspecified sign = %s)", + z, c1, c2, sign)); + } + } + /** * Assert the operation on the complex number is odd or even. * @@ -508,6 +607,109 @@ class CStandardTest { } } + /** + * Assert the operation on the complex number is equal to the expected value. + * If the imaginary part is not NaN the operation must also satisfy the conjugate equality. + * + * <pre> + * op(conj(z)) = conj(op(z)) + * </pre> + * + * <p>The results must be binary equal. This includes the sign of zero. + * + * <p>Assert the operation on the complex number is <em>exactly</em> equal to the operation on + * complex real and imaginary parts. + * + * @param z Input complex number. + * @param operation1 Operation on the Complex object. + * @param operation2 Operation on the complex real and imaginary parts. + * @param expected Expected complex number. + * @param name Operation name. + */ + private static void assertComplex(Complex z, + UnaryOperator<Complex> operation1, + ComplexUnaryOperator<ComplexNumber> operation2, + Complex expected, String name) { + assertComplex(z, operation1, operation2, expected, FunctionType.NONE, UnspecifiedSign.NONE, name); + } + + /** + * Assert the operation on the complex number is equal to the expected value. + * If the imaginary part is not NaN the operation must also satisfy the conjugate equality. + * + * <pre> + * op(conj(z)) = conj(op(z)) + * </pre> + * + * <p>If the function type is ODD/EVEN the operation must satisfy the function type equality. + * + * <pre> + * Odd : f(z) = -f(-z) + * Even: f(z) = f(-z) + * </pre> + * + * <p>An ODD/EVEN function is also tested that the conjugate equalities hold with {@code -z}. + * This effectively enumerates testing: (re, im); (re, -im); (-re, -im); and (-re, im). + * + * <p>The results must be binary equal; the sign of the complex number is first processed + * using the provided sign specification. + * + * <p>Assert the operation on the complex number is <em>exactly</em> equal to the operation on + * complex real and imaginary parts. + * + * @param z Input complex number. + * @param operation1 Operation on the Complex object. + * @param operation2 Operation on the complex real and imaginary parts. + * @param expected Expected complex number. + * @param type the type + * @param sign the sign specification + * @param name Operation name. + */ + private static void assertComplex(Complex z, + UnaryOperator<Complex> operation1, + ComplexUnaryOperator<ComplexNumber> operation2, + Complex expected, + FunctionType type, + UnspecifiedSign sign, + String name) { + + // Developer note: Set the sign specification to UnspecifiedSign.NONE + // to see which equalities fail. They should be for input defined + // in ISO C99 with an unspecified output sign, e.g. + // sign = UnspecifiedSign.NONE + + // Test the operation + final Complex c = TestUtils.assertSame(z, operation1, operation2, name); + + final Complex t1 = sign.removeSign(c); + final Complex t2 = sign.removeSign(expected); + if (!equals(t1.getReal(), t2.getReal()) || + !equals(t1.getImaginary(), t2.getImaginary())) { + Assertions.fail( + String.format("Operation failed (z=%s). Expected: %s but was: %s (Unspecified sign = %s)", + z, expected, c, sign)); + } + + if (!Double.isNaN(z.getImaginary())) { + assertConjugateEquality(z, operation1, operation2, sign, name); + } + + if (type != FunctionType.NONE) { + assertFunctionType(z, operation1, type, sign); + + // An odd/even function should satisfy the conjugate equality + // on the negated complex. This ensures testing the equalities + // hold for: + // (re, im) = (re, -im) + // (re, im) = (-re, -im) (even) + // = -(-re, -im) (odd) + // (-re, -im) = (-re, im) + if (!Double.isNaN(z.getImaginary())) { + assertConjugateEquality(z.negate(), operation1, operation2, sign, name); + } + } + } + /** * Assert {@link Complex#abs()} is functionally equivalent to using * {@link Math#hypot(double, double)}. If the results differ the true result @@ -1236,31 +1438,33 @@ class CStandardTest { */ @Test void testLog() { - final UnaryOperator<Complex> operation = Complex::log; - assertConjugateEquality(operation); - assertComplex(negZeroZero, operation, negInfPi); - assertComplex(Complex.ZERO, operation, negInfZero); + final UnaryOperator<Complex> operation1 = Complex::log; + final ComplexUnaryOperator<ComplexNumber> operation2 = ComplexFunctions::log; + + assertConjugateEquality(operation1, operation2, "log"); + assertComplex(negZeroZero, operation1, operation2, negInfPi, "log"); + assertComplex(Complex.ZERO, operation1, operation2, negInfZero, "log"); for (double x : finite) { - assertComplex(complex(x, inf), operation, infPiTwo); + assertComplex(complex(x, inf), operation1, operation2, infPiTwo, "log"); } for (double x : finite) { - assertComplex(complex(x, nan), operation, NAN); + assertComplex(complex(x, nan), operation1, operation2, NAN, "log"); } for (double y : positiveFinite) { - assertComplex(complex(-inf, y), operation, infPi); + assertComplex(complex(-inf, y), operation1, operation2, infPi, "log"); } for (double y : positiveFinite) { - assertComplex(complex(inf, y), operation, infZero); + assertComplex(complex(inf, y), operation1, operation2, infZero, "log"); } - assertComplex(negInfInf, operation, infThreePiFour); - assertComplex(infInf, operation, infPiFour); - assertComplex(negInfNaN, operation, infNaN); - assertComplex(infNaN, operation, infNaN); + assertComplex(negInfInf, operation1, operation2, infThreePiFour, "log"); + assertComplex(infInf, operation1, operation2, infPiFour, "log"); + assertComplex(negInfNaN, operation1, operation2, infNaN, "log"); + assertComplex(infNaN, operation1, operation2, infNaN, "log"); for (double y : finite) { - assertComplex(complex(nan, y), operation, NAN); + assertComplex(complex(nan, y), operation1, operation2, NAN, "log"); } - assertComplex(nanInf, operation, infNaN); - assertComplex(NAN, operation, NAN); + assertComplex(nanInf, operation1, operation2, infNaN, "log"); + assertComplex(NAN, operation1, operation2, NAN, "log"); } /** @@ -1269,31 +1473,33 @@ class CStandardTest { */ @Test void testLog10() { - final UnaryOperator<Complex> operation = Complex::log10; - assertConjugateEquality(operation); - assertComplex(negZeroZero, operation, negInfPi); - assertComplex(Complex.ZERO, operation, negInfZero); + final UnaryOperator<Complex> operation1 = Complex::log10; + final ComplexUnaryOperator<ComplexNumber> operation2 = ComplexFunctions::log10; + + assertConjugateEquality(operation1, operation2, "log10"); + assertComplex(negZeroZero, operation1, operation2, negInfPi, "log10"); + assertComplex(Complex.ZERO, operation1, operation2, negInfZero, "log10"); for (double x : finite) { - assertComplex(complex(x, inf), operation, infPiTwo); + assertComplex(complex(x, inf), operation1, operation2, infPiTwo, "log10"); } for (double x : finite) { - assertComplex(complex(x, nan), operation, NAN); + assertComplex(complex(x, nan), operation1, operation2, NAN, "log10"); } for (double y : positiveFinite) { - assertComplex(complex(-inf, y), operation, infPi); + assertComplex(complex(-inf, y), operation1, operation2, infPi, "log10"); } for (double y : positiveFinite) { - assertComplex(complex(inf, y), operation, infZero); + assertComplex(complex(inf, y), operation1, operation2, infZero, "log10"); } - assertComplex(negInfInf, operation, infThreePiFour); - assertComplex(infInf, operation, infPiFour); - assertComplex(negInfNaN, operation, infNaN); - assertComplex(infNaN, operation, infNaN); + assertComplex(negInfInf, operation1, operation2, infThreePiFour, "log10"); + assertComplex(infInf, operation1, operation2, infPiFour, "log10"); + assertComplex(negInfNaN, operation1, operation2, infNaN, "log10"); + assertComplex(infNaN, operation1, operation2, infNaN, "log10"); for (double y : finite) { - assertComplex(complex(nan, y), operation, NAN); + assertComplex(complex(nan, y), operation1, operation2, NAN, "log10"); } - assertComplex(nanInf, operation, infNaN); - assertComplex(NAN, operation, NAN); + assertComplex(nanInf, operation1, operation2, infNaN, "log10"); + assertComplex(NAN, operation1, operation2, NAN, "log10"); } /** 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 95a39520..c0e31008 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 @@ -81,6 +81,57 @@ class ComplexEdgeCaseTest { CReferenceTest.assertComplex(c, name, operation, e, maxUlps); } + /** + * Assert the operation on the complex number is equal to the expected value. + * + * <p>The results are considered equal if there are no floating-point values between them. + * + * <p>Assert the operation on the complex number is <em>exactly</em> equal to the operation on + * complex real and imaginary parts. + * + * @param a Real part. + * @param b Imaginary part. + * @param name Operation name. + * @param operation1 Operation on the Complex object. + * @param operation2 Operation on the complex real and imaginary parts. + * @param x Expected real part. + * @param y Expected imaginary part. + */ + private static void assertComplex(double a, double b, + String name, UnaryOperator<Complex> operation1, + ComplexUnaryOperator<ComplexNumber> operation2, + double x, double y) { + assertComplex(a, b, name, operation1, operation2, x, y, 1); + } + + /** + * Assert the operation on the complex number is equal to the expected value. + * + * <p>The results are considered equal within the provided units of least + * precision. The maximum count of numbers allowed between the two values is + * {@code maxUlps - 1}. + * + * <p>Assert the operation on the complex number is <em>exactly</em> equal to the operation on + * complex real and imaginary parts. + * + * @param a Real part. + * @param b Imaginary part. + * @param name Operation name. + * @param operation1 Operation on the Complex object. + * @param operation2 Operation on the complex real and imaginary parts. + * @param x Expected real part. + * @param y Expected imaginary part. + * @param maxUlps Maximum units of least precision between the two values. + */ + private static void assertComplex(double a, double b, + String name, UnaryOperator<Complex> operation1, + ComplexUnaryOperator<ComplexNumber> operation2, + double x, double y, long maxUlps) { + final Complex c = Complex.ofCartesian(a, b); + final Complex e = Complex.ofCartesian(x, y); + CReferenceTest.assertComplex(c, name, operation1, operation2, e, maxUlps); + } + /** * Assert the operation on the complex numbers is equal to the expected value. * @@ -426,30 +477,30 @@ class ComplexEdgeCaseTest { @Test void testLog() { final String name = "log"; - final UnaryOperator<Complex> operation = Complex::log; - + final UnaryOperator<Complex> operation1 = Complex::log; + final ComplexUnaryOperator<ComplexNumber> operation2 = ComplexFunctions::log; // ln(a + b i) = ln(|a + b i|) + i arg(a + b i) // |a + b i| = sqrt(a^2 + b^2) // arg(a + b i) = Math.atan2(imaginary, real) // Overflow if sqrt(a^2 + b^2) == inf. // Matlab computes this. - assertComplex(-Double.MAX_VALUE, Double.MAX_VALUE, name, operation, 7.101292864836639e2, Math.PI * 3 / 4); - assertComplex(Double.MAX_VALUE, Double.MAX_VALUE, name, operation, 7.101292864836639e2, Math.PI / 4); - 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); + assertComplex(-Double.MAX_VALUE, Double.MAX_VALUE, name, operation1, operation2, 7.101292864836639e2, Math.PI * 3 / 4); + assertComplex(Double.MAX_VALUE, Double.MAX_VALUE, name, operation1, operation2, 7.101292864836639e2, Math.PI / 4); + assertComplex(-Double.MAX_VALUE, Double.MAX_VALUE / 4, name, operation1, operation2, 7.098130252042921e2, 2.896613990462929); + assertComplex(Double.MAX_VALUE, Double.MAX_VALUE / 4, name, operation1, operation2, 7.098130252042921e2, 2.449786631268641e-1, 2); // 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); + assertComplex(-Double.MIN_NORMAL, Double.MIN_NORMAL, name, operation1, operation2, -708.04984494198413, 2.3561944901923448); + assertComplex(Double.MIN_NORMAL, Double.MIN_NORMAL, name, operation1, operation2, -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)); + assertComplex(-Double.MIN_VALUE, Double.MIN_VALUE, name, operation1, operation2, 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)); + assertComplex(-Double.MIN_VALUE, 2 * Double.MIN_VALUE, name, operation1, operation2, 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. @@ -554,7 +605,7 @@ class ComplexEdgeCaseTest { final BigDecimal exact = bx.multiply(bx).add(by.multiply(by)).subtract(BigDecimal.ONE); final double real = 0.5 * Math.log1p(exact.doubleValue()); final double imag = Math.atan2(y, x); - assertComplex(x, y, "log", Complex::log, real, imag, maxUlps); + assertComplex(x, y, "log", Complex::log, ComplexFunctions::log, real, imag, maxUlps); } @Test diff --git a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexNumber.java b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexNumber.java new file mode 100644 index 00000000..4e90c89d --- /dev/null +++ b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexNumber.java @@ -0,0 +1,74 @@ +/* + * 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. + */ + +package org.apache.commons.numbers.complex; + +/** + * Cartesian representation of a complex number. The complex number is expressed + * in the form \( a + ib \) where \( a \) and \( b \) are real numbers and \( i \) + * is the imaginary unit which satisfies the equation \( i^2 = -1 \). For the + * complex number \( a + ib \), \( a \) is called the <em>real part</em> and + * \( b \) is called the <em>imaginary part</em>. + */ +class ComplexNumber { + + /** The real part. */ + private final double real; + /** The imaginary part. */ + private final double imaginary; + + /** + * Constructor representing a complex number by its real and imaginary parts. + * + * @param real Real part \( a \) of the complex number \( (a +ib \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib \). + */ + ComplexNumber(double real, double imaginary) { + this.real = real; + this.imaginary = imaginary; + } + + /** + * Creates a conjugated complex number given the real and imaginary parts, + * that is for the argument (a + ib), returns (a - ib). + * + * @param real Real part \( a \) of the complex number \( (a +ib \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib \). + * @return conjugated complex number. + */ + public static ComplexNumber conj(double real, double imaginary) { + return new ComplexNumber(real, -imaginary); + } + + /** + * Gets the real part \( a \) of complex number \( (a + i b) \). + * + * @return real part + */ + double getReal() { + return real; + } + + /** + * Gets the imaginary part \( b \) of complex number \( (a + i b) \). + * + * @return imaginary part + */ + double getImaginary() { + return imaginary; + } +} 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 dd96b09a..fb358872 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 @@ -31,7 +31,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; /** - * Tests for {@link Complex}. + * Tests for {@link Complex} and {@link ComplexFunctions}. * * <p>Note: The ISO C99 math functions are not fully tested in this class. See also: * @@ -1417,8 +1417,9 @@ class ComplexTest { final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(); for (int i = 0; i < 10; i++) { final Complex z = Complex.ofCartesian(rng.nextDouble() * 2, rng.nextDouble() * 2); - final Complex lnz = z.log(); - final Complex log10z = z.log10(); + final Complex lnz = TestUtils.assertSame(z, Complex::log, ComplexFunctions::log, "log"); + final Complex log10z = TestUtils.assertSame(z, Complex::log10, ComplexFunctions::log10, "log10"); + // This is prone to floating-point error so use a delta Assertions.assertEquals(lnz.getReal() / ln10, log10z.getReal(), 1e-12, "real"); // This test should be exact diff --git a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/TestUtils.java b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/TestUtils.java index 428b7705..6efd2b36 100644 --- a/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/TestUtils.java +++ b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/TestUtils.java @@ -27,6 +27,7 @@ import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import java.util.function.UnaryOperator; import org.apache.commons.numbers.core.Precision; @@ -358,7 +359,7 @@ public final class TestUtils { * Pre-process the next line of data from the input. * Returns null when the line should be ignored. * - * @param input the input + * @param line the input * @param option the option controlling processing of flagged data * @param flaggedDataConsumer the flagged data consumer (can be null) * @return the line of data (or null) @@ -387,4 +388,26 @@ public final class TestUtils { } return line; } + + /** + * Assert the operation on the complex number is <em>exactly</em> equal to the operation on + * complex real and imaginary parts. + * + * @param c Input complex number. + * @param operation1 Operation on the Complex object. + * @param operation2 Operation on the complex real and imaginary parts. + * @param name Operation name. + * @return Resulting complex number from the given operation. + */ + static Complex assertSame(Complex c, + UnaryOperator<Complex> operation1, + ComplexUnaryOperator<ComplexNumber> operation2, + String name) { + final Complex z = operation1.apply(c); + // Test operation2 produces the exact same result + final ComplexNumber z2 = operation2.apply(c.real(), c.imag(), ComplexNumber::new); + Assertions.assertEquals(z.real(), z2.getReal(), () -> name + " real"); + Assertions.assertEquals(z.imag(), z2.getImaginary(), () -> name + " imaginary"); + return z; + } } diff --git a/src/main/resources/checkstyle/checkstyle-suppressions.xml b/src/main/resources/checkstyle/checkstyle-suppressions.xml index 73b2ae14..96e82e5f 100644 --- a/src/main/resources/checkstyle/checkstyle-suppressions.xml +++ b/src/main/resources/checkstyle/checkstyle-suppressions.xml @@ -34,6 +34,7 @@ <suppress checks="LocalVariableName" files=".*[/\\]BoostGamma\.java" /> <suppress checks="LocalFinalVariableName" files=".*[/\\]BoostGamma\.java" /> <suppress checks="ParameterNumber" files=".*[/\\]BoostBeta\.java" /> + <suppress checks="ParameterNumber" files=".*[/\\]ComplexEdgeCaseTest\.java" /> <!-- Method "hashCode()" is defined in the "Angle" parent class; subclasses have no additional fields to include in the hash. --> <suppress checks="EqualsHashCode" files=".*[/\\]Angle\.java" /> <suppress checks="UnnecessaryParentheses" files=".*[/\\]BrentSolver\.java" />