This is an automated email from the ASF dual-hosted git repository. aherbert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-numbers.git
commit 44298409892b82031cef4540274f913e52d583f0 Author: Alex Herbert <[email protected]> AuthorDate: Tue Dec 10 23:56:01 2019 +0000 Added a ComplexEdgeCaseTest. Overflow and underflow are tested for log, exp and sqrt. The test is incomplete. Placeholders are left for the other C99 functions to test boundary conditions. --- .../commons/numbers/complex/CReferenceTest.java | 6 +- .../numbers/complex/ComplexEdgeCaseTest.java | 300 +++++++++++++++++++++ 2 files changed, 303 insertions(+), 3 deletions(-) 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 e11a4e3..57ad59f 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 @@ -96,7 +96,7 @@ public class CReferenceTest { * @param actual the actual * @param maxUlps the maximum units of least precision between the two values */ - private static void assertEquals(Supplier<String> msg, double expected, double actual, long maxUlps) { + static void assertEquals(Supplier<String> msg, double expected, double actual, long maxUlps) { final long e = Double.doubleToLongBits(expected); final long a = Double.doubleToLongBits(actual); @@ -160,7 +160,7 @@ public class CReferenceTest { * @param expected Expected result. * @param maxUlps the maximum units of least precision between the two values */ - private static void assertComplex(Complex c, + static void assertComplex(Complex c, String name, UnaryOperator<Complex> operation, Complex expected, long maxUlps) { final Complex z = operation.apply(c); @@ -184,7 +184,7 @@ public class CReferenceTest { * @param expected Expected real part. * @param maxUlps the maximum units of least precision between the two values */ - private static void assertComplex(Complex c1, Complex c2, + static void assertComplex(Complex c1, Complex c2, String name, BiFunction<Complex, Complex, Complex> operation, Complex expected, long maxUlps) { final Complex z = operation.apply(c1, c2); 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 new file mode 100644 index 0000000..84d856f --- /dev/null +++ b/commons-numbers-complex/src/test/java/org/apache/commons/numbers/complex/ComplexEdgeCaseTest.java @@ -0,0 +1,300 @@ +/* + * 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 org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.function.BiFunction; +import java.util.function.UnaryOperator; + +/** + * Edge case tests for the functions defined by the C.99 standard for complex numbers + * defined in ISO/IEC 9899, Annex G. + * + * <p>The test contained here are specifically written to target edge cases of finite valued + * input values that cause overflow/underflow during the computation. + * + * <p>The test data is generated from a known implementation of the standard. + * + * @see <a href="http://www.open-std.org/JTC1/SC22/WG14/www/standards"> + * ISO/IEC 9899 - Programming languages - C</a> + */ +public class ComplexEdgeCaseTest { + private static final double inf = Double.POSITIVE_INFINITY; + private static final double nan = Double.NaN; + + /** + * Assert the operation on the complex number is equal to the expected value. + * + * <p>The results are are considered equal if there are no floating-point values between them. + * + * @param a Real part. + * @param b Imaginary part. + * @param name The operation name. + * @param operation The operation. + * @param x Expected real part. + * @param y Expected imaginary part. + */ + private static void assertComplex(double a, double b, + String name, UnaryOperator<Complex> operation, + double x, double y) { + assertComplex(a, b, name, operation, 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}. + * + * @param a Real part. + * @param b Imaginary part. + * @param name The operation name. + * @param operation The operation. + * @param x Expected real part. + * @param y Expected imaginary part. + * @param maxUlps the maximum units of least precision between the two values + */ + private static void assertComplex(double a, double b, + String name, UnaryOperator<Complex> operation, + double x, double y, long maxUlps) { + final Complex c = Complex.ofCartesian(a, b); + final Complex z = operation.apply(c); + CReferenceTest.assertComplex(c, name, operation, z, maxUlps); + } + + /** + * Assert the operation on the complex numbers is equal to the expected value. + * + * <p>The results are considered equal if there are no floating-point values between them. + * + * @param a Real part of first number. + * @param b Imaginary part of first number. + * @param c Real part of second number. + * @param d Imaginary part of second number. + * @param name The operation name. + * @param operation The operation. + * @param x Expected real part. + * @param y Expected imaginary part. + */ + // CHECKSTYLE: stop ParameterNumberCheck + private static void assertComplex(double a, double b, double c, double d, + String name, BiFunction<Complex, Complex, Complex> operation, + double x, double y) { + assertComplex(a, b, c, d, name, operation, x, y, 1); + } + + /** + * Assert the operation on the complex numbers 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}. + * + * @param a Real part of first number. + * @param b Imaginary part of first number. + * @param c Real part of second number. + * @param d Imaginary part of second number. + * @param name The operation name + * @param operation the operation + * @param x Expected real part. + * @param y Expected imaginary part. + * @param maxUlps the maximum units of least precision between the two values + */ + private static void assertComplex(double a, double b, double c, double d, + String name, BiFunction<Complex, Complex, Complex> operation, + double x, double y, long maxUlps) { + final Complex c1 = Complex.ofCartesian(a, b); + final Complex c2 = Complex.ofCartesian(c, d); + final Complex z = operation.apply(c1, c2); + CReferenceTest.assertComplex(c1, c2, name, operation, z, maxUlps); + } + + @Test + public void testAcos() { + // acos(z) = (pi / 2) + i ln(iz + sqrt(1 - z^2)) + // TODO + } + + @Test + public void testAcosh() { + // Defined by acos() so edge cases are the same + // TODO + } + + @Test + public void testAsinh() { + // asinh(z) = ln(z + sqrt(1 + z^2)) + // Odd function: negative real cases defined by positive real cases + // TODO + } + + @Test + public void testAtanh() { + // atanh(z) = (1/2) ln((1 + z) / (1 - z)) + // Odd function: negative real cases defined by positive real cases + // TODO + } + + @Test + public void testCosh() { + // cosh(a + b i) = cosh(a)cos(b) + i sinh(a)sin(b) + // Even function: negative real cases defined by positive real cases + // TODO + } + + @Test + public void testSinh() { + // sinh(a + b i) = sinh(a)cos(b)) + i cosh(a)sin(b) + // Odd function: negative real cases defined by positive real cases + // TODO + } + + @Test + public void testTanh() { + // tan(a + b i) = sinh(2a)/(cosh(2a)+cos(2b)) + i [sin(2b)/(cosh(2a)+cos(2b))] + // Odd function: negative real cases defined by positive real cases + // TODO + } + + @Test + public void testExp() { + final String name = "exp"; + final UnaryOperator<Complex> operation = Complex::exp; + + // exp(a + b i) = exp(a) (cos(b) + i sin(b)) + + // Overflow if exp(a) == inf + assertComplex(1000, 0, name, operation, inf, 0.0); + assertComplex(1000, 1, name, operation, inf, inf); + assertComplex(1000, 2, name, operation, -inf, inf); + assertComplex(1000, 3, name, operation, -inf, inf); + assertComplex(1000, 4, name, operation, inf, inf); + + // Underflow if exp(a) == 0 + assertComplex(-1000, 0, name, operation, 0.0, 0.0); + assertComplex(-1000, 1, name, operation, 0.0, 0.0); + assertComplex(-1000, 2, name, operation, -0.0, 0.0); + assertComplex(-1000, 3, name, operation, -0.0, 0.0); + assertComplex(-1000, 4, name, operation, 0.0, 0.0); + } + + @Test + public void testLog() { + final String name = "log"; + final UnaryOperator<Complex> operation = Complex::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); + + // Underflow if sqrt(a^2 + b^2) == 0 + assertComplex(-Double.MIN_VALUE, Double.MIN_VALUE, name, operation, -744.44007192138122, 2.3561944901923448); + assertComplex(Double.MIN_VALUE, Double.MIN_VALUE, name, operation, -744.44007192138122, 0.78539816339744828); + } + + @Test + public void testSqrt() { + final String name = "sqrt"; + final UnaryOperator<Complex> operation = Complex::sqrt; + + // Computed in polar coordinates: + // real = (x^2 + y^2)^0.25 * cos(0.5 * atan2(y, x)) + // imag = (x^2 + y^2)^0.25 * sin(0.5 * atan2(y, x)) + // --- + // Note: + // If x is positive and y is +/-0.0 atan2 returns +/-0. + // If x is negative and y is +/-0.0 atan2 returns +/-PI. + // This causes problems as + // cos(0.5 * PI) = 6.123233995736766e-17 + // assert: Math.cos(Math.acos(0)) != 0.0 + // Thus an unchecked polar computation will produce incorrect output when + // there is no imaginary component and real is negative. + // The computation should be done for real only numbers separately. + // This condition is tested in the reference test against known data + // so not repeated here. + // --- + + // Check overflow safe. + double a = Double.MAX_VALUE; + final double b = a / 4; + Assertions.assertEquals(inf, Complex.ofCartesian(a, b).abs(), "Expected overflow"); + // Compute the expected new magnitude by expressing b as a scale factor of a: + // (x^2 + y^2)^0.25 + // = sqrt(sqrt(a^2 + (b/a)^2 * a^2)) + // = sqrt(sqrt((1+(b/a)^2) * a^2)) + // = sqrt(sqrt((1+(b/a)^2))) * sqrt(a) + final double newAbs = Math.sqrt(Math.sqrt(1 + (b / a) * (b / a))) * Math.sqrt(a); + assertComplex(a, b, name, operation, newAbs * Math.cos(0.5 * Math.atan2(b, a)), + newAbs * Math.sin(0.5 * Math.atan2(b, a)), 3); + assertComplex(b, a, name, operation, newAbs * Math.cos(0.5 * Math.atan2(a, b)), + newAbs * Math.sin(0.5 * Math.atan2(a, b)), 2); + + // In polar coords: + // real = sqrt(abs()) * Math.cos(arg() / 2) + // imag = sqrt(abs()) * Math.sin(arg() / 2) + // This is possible if abs() does not overflow. + a = Double.MAX_VALUE / 2; + assertComplex(-a, a, name, operation, 4.3145940638864765e+153, 1.0416351505169177e+154, 2); + assertComplex(a, a, name, operation, 1.0416351505169177e+154, 4.3145940638864758e+153); + assertComplex(-a, -a, name, operation, 4.3145940638864765e+153, -1.0416351505169177e+154, 2); + assertComplex(a, -a, name, operation, 1.0416351505169177e+154, -4.3145940638864758e+153); + + // Check minimum normal value conditions + // Computing in Polar coords produces a very different result with + // MIN_VALUE so use MIN_NORMAL + a = Double.MIN_NORMAL; + assertComplex(-a, a, name, operation, 6.7884304867749663e-155, 1.6388720948399111e-154); + assertComplex(a, a, name, operation, 1.6388720948399111e-154, 6.7884304867749655e-155); + assertComplex(-a, -a, name, operation, 6.7884304867749663e-155, -1.6388720948399111e-154); + assertComplex(a, -a, name, operation, 1.6388720948399111e-154, -6.7884304867749655e-155); + } + + // Note: + // multiply is tested in CStandardTest + // divide is tested in CStandardTest + + @Test + public void testPow() { + final String name = "pow"; + final BiFunction<Complex, Complex, Complex> operation = Complex::pow; + + // pow(Complex) is log().multiply(Complex).exp() + // All are overflow safe and handle infinities as defined in the C99 standard. + // TODO: Test edge cases with: + // Double.MAX_VALUE, Double.MIN_NORMAL, Inf + // using other library implementations. + + // Test NaN + assertComplex(1, 1, nan, nan, name, operation, nan, nan); + assertComplex(nan, nan, 1, 1, name, operation, nan, nan); + assertComplex(nan, 1, 1, 1, name, operation, nan, nan); + assertComplex(1, nan, 1, 1, name, operation, nan, nan); + assertComplex(1, 1, nan, 1, name, operation, nan, nan); + assertComplex(1, 1, 1, nan, name, operation, nan, nan); + } +}
