This is an automated email from the ASF dual-hosted git repository. erans pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-numbers.git
commit 06a613d29c950acff30feb06ac95d54c0d5151ea Author: Matt Juntunen <mattjuntu...@apache.org> AuthorDate: Sat Jun 19 08:33:35 2021 -0400 NUMBERS-163: replacing LinearCombination and Summation with Sum class --- .../org/apache/commons/numbers/angle/CosAngle.java | 5 +- .../commons/numbers/core/LinearCombination.java | 222 -------- .../java/org/apache/commons/numbers/core/Norm.java | 16 +- .../java/org/apache/commons/numbers/core/Sum.java | 258 ++++++++++ .../org/apache/commons/numbers/core/Summation.java | 179 ------- .../numbers/core/LinearCombinationTest.java | 416 --------------- .../org/apache/commons/numbers/core/SumTest.java | 568 +++++++++++++++++++++ .../apache/commons/numbers/core/SummationTest.java | 283 ---------- .../examples/jmh/core/EuclideanNormAlgorithms.java | 4 +- .../jmh/core/LinearCombinationPerformance.java | 15 +- .../numbers/examples/jmh/core/NormPerformance.java | 6 +- .../numbers/examples/jmh/core/SumPerformance.java | 183 +++++++ 12 files changed, 1033 insertions(+), 1122 deletions(-) diff --git a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/CosAngle.java b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/CosAngle.java index d9d7dd4..fcbabba 100644 --- a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/CosAngle.java +++ b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/CosAngle.java @@ -16,8 +16,8 @@ */ package org.apache.commons.numbers.angle; -import org.apache.commons.numbers.core.LinearCombination; import org.apache.commons.numbers.core.Norm; +import org.apache.commons.numbers.core.Sum; /** * Computes the cosine of the angle between two vectors. @@ -39,7 +39,6 @@ public final class CosAngle { */ public static double value(double[] v1, double[] v2) { - return LinearCombination.value(v1, v2) / Norm.L2.of(v1) / Norm.L2.of(v2); + return Sum.ofProducts(v1, v2).getAsDouble() / Norm.L2.of(v1) / Norm.L2.of(v2); } } - diff --git a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/LinearCombination.java b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/LinearCombination.java deleted file mode 100644 index 09cc821..0000000 --- a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/LinearCombination.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * 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.core; - -/** - * Computes linear combinations accurately. - * This method computes the sum of the products - * <code>a<sub>i</sub> b<sub>i</sub></code> to high accuracy. - * It does so by using specific multiplication and addition algorithms to - * preserve accuracy and reduce cancellation effects. - * - * <p>It is based on the 2005 paper - * <a href="https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.2.1547"> - * Accurate Sum and Dot Product</a> by Takeshi Ogita, Siegfried M. Rump, - * and Shin'ichi Oishi published in <em>SIAM J. Sci. Comput</em>. - */ -public final class LinearCombination { - /* - * Caveat: - * - * The code below uses many additions/subtractions that may - * appear redundant. However, they should NOT be simplified, as they - * do use IEEE754 floating point arithmetic rounding properties. - * - * Algorithms are based on computing the product or sum of two values x and y in - * extended precision. The standard result is stored using a double (high part z) and - * the round-off error (or low part zz) is stored in a second double, e.g: - * x * y = (z, zz); z + zz = x * y - * x + y = (z, zz); z + zz = x + y - * - * To sum multiple (z, zz) results ideally the parts are sorted in order of - * non-decreasing magnitude and summed. This is exact if each number's most significant - * bit is below the least significant bit of the next (i.e. does not - * overlap). Creating non-overlapping parts requires a rebalancing - * of adjacent pairs using a summation z + zz = (z1, zz1) iteratively through the parts - * (see Shewchuk (1997) Grow-Expansion and Expansion-Sum [1]). - * - * In this class the sum of the low parts in computed separately from the sum of the - * high parts for an approximate 2-fold increase in precision in the event of cancellation - * (sum positives and negatives to a result of much smaller magnitude than the parts). - * Uses the dot2s algorithm of Ogita to avoid allocation of an array to store intermediates. - * - * [1] Shewchuk (1997): Arbitrary Precision Floating-Point Arithmetic - * http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps - */ - - /** Private constructor. */ - private LinearCombination() { - // intentionally empty. - } - - /** - * @param a Factors. - * @param b Factors. - * @return \( \sum_i a_i b_i \). - * @throws IllegalArgumentException if the sizes of the arrays are different. - */ - public static double value(double[] a, - double[] b) { - if (a.length != b.length) { - throw new IllegalArgumentException("Dimension mismatch: " + a.length + " != " + b.length); - } - - // Implement dot2s (Algorithm 5.4) from Ogita et al (2005). - final int len = a.length; - - // p is the standard scalar product sum. - // s is the sum of round-off parts. - double p = a[0] * b[0]; - double s = ExtendedPrecision.productLow(a[0], b[0], p); - - // Remaining split products added to the current sum and round-off sum. - for (int i = 1; i < len; i++) { - final double h = a[i] * b[i]; - final double r = ExtendedPrecision.productLow(a[i], b[i], h); - - final double x = p + h; - // s_i = s_(i-1) + (q_i + r_i) - s += ExtendedPrecision.twoSumLow(p, h, x) + r; - p = x; - } - - return getSum(p, p + s); - } - - /** - * @param a1 First factor of the first term. - * @param b1 Second factor of the first term. - * @param a2 First factor of the second term. - * @param b2 Second factor of the second term. - * @return \( a_1 b_1 + a_2 b_2 \) - * - * @see #value(double, double, double, double, double, double) - * @see #value(double, double, double, double, double, double, double, double) - * @see #value(double[], double[]) - */ - public static double value(double a1, double b1, - double a2, double b2) { - // p/pn are the standard scalar product old/new sum. - // s is the sum of round-off parts. - final double p = a1 * b1; - double s = ExtendedPrecision.productLow(a1, b1, p); - final double h = a2 * b2; - final double r = ExtendedPrecision.productLow(a2, b2, h); - final double pn = p + h; - s += ExtendedPrecision.twoSumLow(p, h, pn) + r; - - // Final summation - return getSum(pn, pn + s); - } - - /** - * @param a1 First factor of the first term. - * @param b1 Second factor of the first term. - * @param a2 First factor of the second term. - * @param b2 Second factor of the second term. - * @param a3 First factor of the third term. - * @param b3 Second factor of the third term. - * @return \( a_1 b_1 + a_2 b_2 + a_3 b_3 \) - * - * @see #value(double, double, double, double) - * @see #value(double, double, double, double, double, double, double, double) - * @see #value(double[], double[]) - */ - public static double value(double a1, double b1, - double a2, double b2, - double a3, double b3) { - // p/q are the standard scalar product old/new sum (alternating). - // s is the sum of round-off parts. - // pn is the final scalar product sum. - final double p = a1 * b1; - double s = ExtendedPrecision.productLow(a1, b1, p); - double h = a2 * b2; - double r = ExtendedPrecision.productLow(a2, b2, h); - final double q = p + h; - s += r + ExtendedPrecision.twoSumLow(p, h, q); - h = a3 * b3; - r = ExtendedPrecision.productLow(a3, b3, h); - final double pn = q + h; - s += r + ExtendedPrecision.twoSumLow(q, h, pn); - - // Final summation - return getSum(pn, pn + s); - } - - /** - * @param a1 First factor of the first term. - * @param b1 Second factor of the first term. - * @param a2 First factor of the second term. - * @param b2 Second factor of the second term. - * @param a3 First factor of the third term. - * @param b3 Second factor of the third term. - * @param a4 First factor of the fourth term. - * @param b4 Second factor of the fourth term. - * @return \( a_1 b_1 + a_2 b_2 + a_3 b_3 + a_4 b_4 \) - * - * @see #value(double, double, double, double) - * @see #value(double, double, double, double, double, double) - * @see #value(double[], double[]) - */ - public static double value(double a1, double b1, - double a2, double b2, - double a3, double b3, - double a4, double b4) { - // p/q are the standard scalar product old/new sum (alternating). - // s is the sum of round-off parts. - // pn is the final scalar product sum. - double p = a1 * b1; - double s = ExtendedPrecision.productLow(a1, b1, p); - double h = a2 * b2; - double r = ExtendedPrecision.productLow(a2, b2, h); - final double q = p + h; - s += ExtendedPrecision.twoSumLow(p, h, q) + r; - h = a3 * b3; - r = ExtendedPrecision.productLow(a3, b3, h); - p = q + h; - s += ExtendedPrecision.twoSumLow(q, h, p) + r; - h = a4 * b4; - r = ExtendedPrecision.productLow(a4, b4, h); - final double pn = p + h; - s += ExtendedPrecision.twoSumLow(p, h, pn) + r; - - // Final summation - return getSum(pn, pn + s); - } - - /** - * Gets the final sum. This checks the high precision sum is finite, otherwise - * returns the standard precision sum for the IEEE754 result. - * - * <p>The high precision sum may be non-finite due to input infinite - * or NaN numbers or overflow in the summation. In all cases returning the - * standard sum ensures the IEEE754 result. - * - * @param sum Standard sum. - * @param hpSum High precision sum. - * @return the sum - */ - private static double getSum(double sum, double hpSum) { - if (!Double.isFinite(hpSum)) { - // Either we have split infinite numbers, some coefficients were NaNs, - // or the sum overflowed. - // Return the naive implementation for the IEEE754 result. - return sum; - } - return hpSum; - } -} diff --git a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Norm.java b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Norm.java index 65859b9..255215a 100644 --- a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Norm.java +++ b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Norm.java @@ -219,9 +219,9 @@ public enum Norm { private static double manhattan(final double x, final double y, final double z) { - return Summation.value(Math.abs(x), - Math.abs(y), - Math.abs(z)); + return Sum.of(Math.abs(x), + Math.abs(y), + Math.abs(z)).getAsDouble(); } /** Computes the Manhattan norm. @@ -234,17 +234,13 @@ public enum Norm { * @see #of(double[]) */ private static double manhattan(final double[] v) { - double sum = 0d; - double comp = 0d; + final Sum sum = Sum.create(); for (int i = 0; i < v.length; ++i) { - final double x = Math.abs(v[i]); - final double sx = sum + x; - comp += ExtendedPrecision.twoSumLow(sum, x, sx); - sum = sx; + sum.add(Math.abs(v[i])); } - return Summation.summationResult(sum, comp); + return sum.getAsDouble(); } /** Computes the Euclidean norm. diff --git a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Sum.java b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Sum.java new file mode 100644 index 0000000..9939e62 --- /dev/null +++ b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Sum.java @@ -0,0 +1,258 @@ +/* + * 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.core; + +import java.util.function.DoubleConsumer; +import java.util.function.DoubleSupplier; + +/** Class providing accurate floating-point sums and linear combinations. The methods + * provided use compensated summation and multiplication techniques to reduce numerical errors. + * The approach is based on the <em>Sum2S</em> and <em>Dot2S</em> algorithms described in the + * 2005 paper <a href="https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.2.1547"> + * Accurate Sum and Dot Product</a> by Takeshi Ogita, Siegfried M. Rump, + * and Shin'ichi Oishi published in <em>SIAM J. Sci. Comput</em>. + * + * <p>Method results follow the standard rules for IEEE 754 addition. For example, + * if any input value is NaN, the result is NaN. + */ +public final class Sum implements DoubleSupplier, DoubleConsumer { + + /** Standard sum. */ + private double sum; + + /** Compensation value. */ + private double comp; + + /** Construct a new instance with an initial value of zero. + */ + private Sum() { + this(0d); + } + + /** Construct a new instance with the given initial value. + * @param initialValue initial value + */ + private Sum(final double initialValue) { + this.sum = initialValue; + } + + /** Add a single term to this sum. + * @param t value to add + * @return this instance + */ + public Sum add(final double t) { + final double newSum = sum + t; + comp += ExtendedPrecision.twoSumLow(sum, t, newSum); + sum = newSum; + + return this; + } + + /** Add an array of value to the sum. + * @param terms terms to add + * @return this instance + */ + public Sum add(final double[] terms) { + for (double t : terms) { + add(t); + } + + return this; + } + + /** Add the high-accuracy product \(a b\) to this sum. + * @param a first factor + * @param b second factor + * @return this instance + */ + public Sum addProduct(final double a, final double b) { + final double ab = a * b; + final double pLow = ExtendedPrecision.productLow(a, b, ab); + + final double newSum = sum + ab; + comp += ExtendedPrecision.twoSumLow(sum, ab, newSum) + pLow; + sum = newSum; + + return this; + } + + /** Add \( \sum_i a_i b_i \). In other words, multiply each element + * in {@code a} with its corresponding element in {@code b} and add the product + * to the sum. + * @param a factors + * @param b factors + * @return this instance + * @throws IllegalArgumentException if the arrays do not have the same length + */ + public Sum addProducts(final double[] a, final double[] b) { + final int len = a.length; + if (len != b.length) { + throw new IllegalArgumentException("Dimension mismatch: " + a.length + " != " + b.length); + } + + for (int i = 0; i < len; ++i) { + addProduct(a[i], b[i]); + } + + return this; + } + + /** Add another sum to this sum. + * @param other sum to add + * @return this instance + */ + public Sum add(final Sum other) { + // pull both values first to ensure there are + // no issues when adding a sum to itself + final double s = other.sum; + final double c = other.comp; + + return add(s) + .add(c); + } + + /** Add a single term to this sum. This is equivalent to {@link #add(double)}. + * @param value value to add + * @see #add(double) + */ + @Override + public void accept(final double value) { + add(value); + } + + /** Get the sum value. + * @return sum value as a double + */ + @Override + public double getAsDouble() { + // compute and return the high precision sum if it is finite; otherwise, + // return the standard IEEE754 result + final double hpsum = sum + comp; + return Double.isFinite(hpsum) ? + hpsum : + sum; + } + + /** Create a new sum instance with an initial value of zero. + * @return new sum instance + */ + public static Sum create() { + return new Sum(); + } + + /** Return a new sum instance containing a single value. + * @param a value + * @return new sum instance + * @see #add(double) + */ + public static Sum of(final double a) { + return new Sum(a); + } + + /** Return a new sum instance containing the value \(a + b\). + * @param a first term + * @param b second term + * @return new sum instance + * @see #add(double) + */ + public static Sum of(final double a, final double b) { + return new Sum(a).add(b); + } + + /** Return a new sum instance containing the value \(a + b + c\). + * @param a first term + * @param b second term + * @param c third term + * @return new sum instance + * @see #add(double) + */ + public static Sum of(final double a, final double b, final double c) { + return new Sum(a) + .add(b) + .add(c); + } + + /** Return a new sum instance containing the value \(a + b + c + d\). + * @param a first term + * @param b second term + * @param c third term + * @param d fourth term + * @return new sum instance + * @see #add(double) + */ + public static Sum of(final double a, final double b, final double c, final double d) { + return new Sum(a) + .add(b) + .add(c) + .add(d); + } + + /** Return a new sum instance containing the sum of the given values. + * @param values input values + * @return new sum instance + * @see #add(double[]) + */ + public static Sum of(final double[] values) { + return new Sum().add(values); + } + + /** Return a new sum instance containing the linear combination + * \(a_1 b_1 + a_2 b_2\). + * @param a1 first factor of first term + * @param b1 second factor of first term + * @param a2 first factor of second term + * @param b2 second factor of second term + * @return new sum instance + * @see #addProduct(double, double) + */ + public static Sum ofProducts(final double a1, final double b1, + final double a2, final double b2) { + return new Sum() + .addProduct(a1, b1) + .addProduct(a2, b2); + } + + /** Return a new sum instance containing the linear combination + * \(a_1 b_1 + a_2 b_2 + a_3 b_3\). + * @param a1 first factor of first term + * @param b1 second factor of first term + * @param a2 first factor of second term + * @param b2 second factor of second term + * @param a3 first factor of third term + * @param b3 second factor of third term + * @return new sum instance + * @see #addProduct(double, double) + */ + public static Sum ofProducts(final double a1, final double b1, + final double a2, final double b2, + final double a3, final double b3) { + return new Sum() + .addProduct(a1, b1) + .addProduct(a2, b2) + .addProduct(a3, b3); + } + + /** Return a new sum instance containing \( \sum_i a_i b_i \). + * @param a first set of factors + * @param b second set of factors + * @return new sum instance + * @see #addProducts(double[], double[]) + */ + public static Sum ofProducts(final double[] a, final double[] b) { + return new Sum().addProducts(a, b); + } +} diff --git a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Summation.java b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Summation.java deleted file mode 100644 index 3c26ab9..0000000 --- a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Summation.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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.core; - -/** Class providing accurate floating-point summations. The methods provided - * use a compensated summation technique to reduce numerical errors. - * The approach is based on the <em>Sum2S</em> algorithm described in the - * 2005 paper <a href="https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.2.1547"> - * Accurate Sum and Dot Product</a> by Takeshi Ogita, Siegfried M. Rump, - * and Shin'ichi Oishi published in <em>SIAM J. Sci. Comput</em>. - * - * <p>Method results follow the standard rules for IEEE 754 addition. For example, - * if any input value is NaN, the result is NaN. - */ -public final class Summation { - - /** Utility class; no instantiation. */ - private Summation() {} - - /** Compute the sum of the input values. - * @param a first value - * @param b second value - * @param c third value - * @return sum of the input values - */ - public static double value(final double a, final double b, final double c) { - double sum = a; - double comp = 0d; - - final double sb = sum + b; - comp += ExtendedPrecision.twoSumLow(sum, b, sb); - sum = sb; - - final double sc = sum + c; - comp += ExtendedPrecision.twoSumLow(sum, c, sc); - sum = sc; - - return summationResult(sum, comp); - } - - /** Compute the sum of the input values. - * @param a first value - * @param b second value - * @param c third value - * @param d fourth value - * @return sum of the input values - */ - public static double value(final double a, final double b, final double c, final double d) { - double sum = a; - double comp = 0d; - - final double sb = sum + b; - comp += ExtendedPrecision.twoSumLow(sum, b, sb); - sum = sb; - - final double sc = sum + c; - comp += ExtendedPrecision.twoSumLow(sum, c, sc); - sum = sc; - - final double sd = sum + d; - comp += ExtendedPrecision.twoSumLow(sum, d, sd); - sum = sd; - - return summationResult(sum, comp); - } - - /** Compute the sum of the input values. - * @param a first value - * @param b second value - * @param c third value - * @param d fourth value - * @param e fifth value - * @return sum of the input values - */ - public static double value(final double a, final double b, final double c, final double d, - final double e) { - double sum = a; - double comp = 0d; - - final double sb = sum + b; - comp += ExtendedPrecision.twoSumLow(sum, b, sb); - sum = sb; - - final double sc = sum + c; - comp += ExtendedPrecision.twoSumLow(sum, c, sc); - sum = sc; - - final double sd = sum + d; - comp += ExtendedPrecision.twoSumLow(sum, d, sd); - sum = sd; - - final double se = sum + e; - comp += ExtendedPrecision.twoSumLow(sum, e, se); - sum = se; - - return summationResult(sum, comp); - } - - /** Compute the sum of the input values. - * @param a first value - * @param b second value - * @param c third value - * @param d fourth value - * @param e fifth value - * @param f sixth value - * @return sum of the input values - */ - public static double value(final double a, final double b, final double c, final double d, - final double e, final double f) { - double sum = a; - double comp = 0d; - - final double sb = sum + b; - comp += ExtendedPrecision.twoSumLow(sum, b, sb); - sum = sb; - - final double sc = sum + c; - comp += ExtendedPrecision.twoSumLow(sum, c, sc); - sum = sc; - - final double sd = sum + d; - comp += ExtendedPrecision.twoSumLow(sum, d, sd); - sum = sd; - - final double se = sum + e; - comp += ExtendedPrecision.twoSumLow(sum, e, se); - sum = se; - - final double sf = sum + f; - comp += ExtendedPrecision.twoSumLow(sum, f, sf); - sum = sf; - - return summationResult(sum, comp); - } - - /** Compute the sum of the input values. - * @param a array containing values to sum - * @return sum of the input values - */ - public static double value(final double[] a) { - double sum = 0d; - double comp = 0d; - - for (final double x : a) { - final double s = sum + x; - comp += ExtendedPrecision.twoSumLow(sum, x, s); - sum = s; - } - - return summationResult(sum, comp); - } - - /** Return the final result from a summation operation. - * @param sum standard sum value - * @param comp compensation value - * @return final summation result - */ - static double summationResult(final double sum, final double comp) { - // only add comp if finite; otherwise, return the raw sum - // to comply with standard double addition rules - return Double.isFinite(comp) ? - sum + comp : - sum; - } -} diff --git a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/LinearCombinationTest.java b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/LinearCombinationTest.java deleted file mode 100644 index 99c9876..0000000 --- a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/LinearCombinationTest.java +++ /dev/null @@ -1,416 +0,0 @@ -/* - * 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.core; - -import java.math.BigDecimal; - -import org.apache.commons.rng.UniformRandomProvider; -import org.apache.commons.rng.simple.RandomSource; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -/** - * Test cases for the {@link LinearCombination} class. - */ -class LinearCombinationTest { - @Test - void testDimensionMismatch() { - Assertions.assertThrows(IllegalArgumentException.class, - () -> LinearCombination.value(new double[1], new double[2])); - } - - // MATH-1005 - @Test - void testSingleElementArray() { - final double[] a = {1.23456789}; - final double[] b = {98765432.1}; - - Assertions.assertEquals(a[0] * b[0], LinearCombination.value(a, b)); - } - - @Test - void testTwoSums() { - final BigDecimal[] aFN = new BigDecimal[] { - BigDecimal.valueOf(-1321008684645961L), - BigDecimal.valueOf(-5774608829631843L), - BigDecimal.valueOf(-7645843051051357L), - }; - final BigDecimal[] aFD = new BigDecimal[] { - BigDecimal.valueOf(268435456L), - BigDecimal.valueOf(268435456L), - BigDecimal.valueOf(8589934592L) - }; - final BigDecimal[] bFN = new BigDecimal[] { - BigDecimal.valueOf(-5712344449280879L), - BigDecimal.valueOf(-4550117129121957L), - BigDecimal.valueOf(8846951984510141L) - }; - final BigDecimal[] bFD = new BigDecimal[] { - BigDecimal.valueOf(2097152L), - BigDecimal.valueOf(2097152L), - BigDecimal.valueOf(131072L) - }; - - final int len = aFN.length; - final double[] a = new double[len]; - final double[] b = new double[len]; - for (int i = 0; i < len; i++) { - a[i] = aFN[i].doubleValue() / aFD[i].doubleValue(); - b[i] = bFN[i].doubleValue() / bFD[i].doubleValue(); - } - - // Ensure "array" and "inline" implementations give the same result. - final double abSumInline = LinearCombination.value(a[0], b[0], - a[1], b[1], - a[2], b[2]); - final double abSumArray = LinearCombination.value(a, b); - Assertions.assertEquals(abSumInline, abSumArray); - - // Compare with arbitrary precision computation. - BigDecimal result = BigDecimal.ZERO; - for (int i = 0; i < a.length; i++) { - result = result.add(aFN[i].divide(aFD[i]).multiply(bFN[i].divide(bFD[i]))); - } - final double expected = result.doubleValue(); - Assertions.assertEquals(expected, abSumInline, 1e-15); - - final double naive = a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; - Assertions.assertTrue(Math.abs(naive - abSumInline) > 1.5); - } - - @Test - void testArrayVsInline() { - final UniformRandomProvider rng = RandomSource.create(RandomSource.XO_SHI_RO_256_PP); - - double sInline; - double sArray; - final double scale = 1e17; - for (int i = 0; i < 10000; ++i) { - final double u1 = scale * rng.nextDouble(); - final double u2 = scale * rng.nextDouble(); - final double u3 = scale * rng.nextDouble(); - final double u4 = scale * rng.nextDouble(); - final double v1 = scale * rng.nextDouble(); - final double v2 = scale * rng.nextDouble(); - final double v3 = scale * rng.nextDouble(); - final double v4 = scale * rng.nextDouble(); - - // One sum. - sInline = LinearCombination.value(u1, v1, u2, v2); - sArray = LinearCombination.value(new double[] {u1, u2}, - new double[] {v1, v2}); - Assertions.assertEquals(sInline, sArray); - - // Two sums. - sInline = LinearCombination.value(u1, v1, u2, v2, u3, v3); - sArray = LinearCombination.value(new double[] {u1, u2, u3}, - new double[] {v1, v2, v3}); - Assertions.assertEquals(sInline, sArray); - - // Three sums. - sInline = LinearCombination.value(u1, v1, u2, v2, u3, v3, u4, v4); - sArray = LinearCombination.value(new double[] {u1, u2, u3, u4}, - new double[] {v1, v2, v3, v4}); - Assertions.assertEquals(sInline, sArray); - } - } - - @Test - void testHuge() { - int scale = 971; - final double[] a = new double[] { - -1321008684645961.0 / 268435456.0, - -5774608829631843.0 / 268435456.0, - -7645843051051357.0 / 8589934592.0 - }; - final double[] b = new double[] { - -5712344449280879.0 / 2097152.0, - -4550117129121957.0 / 2097152.0, - 8846951984510141.0 / 131072.0 - }; - - final int len = a.length; - final double[] scaledA = new double[len]; - final double[] scaledB = new double[len]; - for (int i = 0; i < len; ++i) { - scaledA[i] = Math.scalb(a[i], -scale); - scaledB[i] = Math.scalb(b[i], scale); - } - final double abSumInline = LinearCombination.value(scaledA[0], scaledB[0], - scaledA[1], scaledB[1], - scaledA[2], scaledB[2]); - final double abSumArray = LinearCombination.value(scaledA, scaledB); - - Assertions.assertEquals(abSumInline, abSumArray); - Assertions.assertEquals(-1.8551294182586248737720779899, abSumInline, 1e-15); - - final double naive = scaledA[0] * scaledB[0] + scaledA[1] * scaledB[1] + scaledA[2] * scaledB[2]; - Assertions.assertTrue(Math.abs(naive - abSumInline) > 1.5); - } - - @Test - void testNonFinite() { - final double[][] a = new double[][] { - {1, 2, 3, 4}, - {1, Double.POSITIVE_INFINITY, 3, 4}, - {1, 2, Double.POSITIVE_INFINITY, 4}, - {1, Double.POSITIVE_INFINITY, 3, Double.NEGATIVE_INFINITY}, - {1, 2, 3, 4}, - {1, 2, 3, 4}, - {1, 2, 3, 4}, - {1, 2, 3, 4}, - {1, Double.MAX_VALUE, 3, 4}, - {1, 2, Double.MAX_VALUE, 4}, - {1, Double.MAX_VALUE / 2, 3, -Double.MAX_VALUE / 4}, - }; - final double[][] b = new double[][] { - {1, -2, 3, 4}, - {1, -2, 3, 4}, - {1, -2, 3, 4}, - {1, -2, 3, 4}, - {1, Double.POSITIVE_INFINITY, 3, 4}, - {1, -2, Double.POSITIVE_INFINITY, 4}, - {1, Double.POSITIVE_INFINITY, 3, Double.NEGATIVE_INFINITY}, - {Double.NaN, -2, 3, 4}, - {1, -2, 3, 4}, - {1, -2, 3, 4}, - {1, -2, 3, 4}, - }; - - Assertions.assertEquals(-3, - LinearCombination.value(a[0][0], b[0][0], - a[0][1], b[0][1])); - Assertions.assertEquals(6, - LinearCombination.value(a[0][0], b[0][0], - a[0][1], b[0][1], - a[0][2], b[0][2])); - Assertions.assertEquals(22, - LinearCombination.value(a[0][0], b[0][0], - a[0][1], b[0][1], - a[0][2], b[0][2], - a[0][3], b[0][3])); - Assertions.assertEquals(22, LinearCombination.value(a[0], b[0])); - - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[1][0], b[1][0], - a[1][1], b[1][1])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[1][0], b[1][0], - a[1][1], b[1][1], - a[1][2], b[1][2])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[1][0], b[1][0], - a[1][1], b[1][1], - a[1][2], b[1][2], - a[1][3], b[1][3])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, LinearCombination.value(a[1], b[1])); - - Assertions.assertEquals(-3, - LinearCombination.value(a[2][0], b[2][0], - a[2][1], b[2][1])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[2][0], b[2][0], - a[2][1], b[2][1], - a[2][2], b[2][2])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[2][0], b[2][0], - a[2][1], b[2][1], - a[2][2], b[2][2], - a[2][3], b[2][3])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, LinearCombination.value(a[2], b[2])); - - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[3][0], b[3][0], - a[3][1], b[3][1])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[3][0], b[3][0], - a[3][1], b[3][1], - a[3][2], b[3][2])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[3][0], b[3][0], - a[3][1], b[3][1], - a[3][2], b[3][2], - a[3][3], b[3][3])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, LinearCombination.value(a[3], b[3])); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[4][0], b[4][0], - a[4][1], b[4][1])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[4][0], b[4][0], - a[4][1], b[4][1], - a[4][2], b[4][2])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[4][0], b[4][0], - a[4][1], b[4][1], - a[4][2], b[4][2], - a[4][3], b[4][3])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, LinearCombination.value(a[4], b[4])); - - Assertions.assertEquals(-3, - LinearCombination.value(a[5][0], b[5][0], - a[5][1], b[5][1])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[5][0], b[5][0], - a[5][1], b[5][1], - a[5][2], b[5][2])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[5][0], b[5][0], - a[5][1], b[5][1], - a[5][2], b[5][2], - a[5][3], b[5][3])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, LinearCombination.value(a[5], b[5])); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[6][0], b[6][0], - a[6][1], b[6][1])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[6][0], b[6][0], - a[6][1], b[6][1], - a[6][2], b[6][2])); - Assertions.assertEquals(Double.NaN, - LinearCombination.value(a[6][0], b[6][0], - a[6][1], b[6][1], - a[6][2], b[6][2], - a[6][3], b[6][3])); - Assertions.assertEquals(Double.NaN, LinearCombination.value(a[6], b[6])); - - Assertions.assertEquals(Double.NaN, - LinearCombination.value(a[7][0], b[7][0], - a[7][1], b[7][1])); - Assertions.assertEquals(Double.NaN, - LinearCombination.value(a[7][0], b[7][0], - a[7][1], b[7][1], - a[7][2], b[7][2])); - Assertions.assertEquals(Double.NaN, - LinearCombination.value(a[7][0], b[7][0], - a[7][1], b[7][1], - a[7][2], b[7][2], - a[7][3], b[7][3])); - Assertions.assertEquals(Double.NaN, LinearCombination.value(a[7], b[7])); - - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[8][0], b[8][0], - a[8][1], b[8][1])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[8][0], b[8][0], - a[8][1], b[8][1], - a[8][2], b[8][2])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[8][0], b[8][0], - a[8][1], b[8][1], - a[8][2], b[8][2], - a[8][3], b[8][3])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, LinearCombination.value(a[8], b[8])); - - Assertions.assertEquals(-3, - LinearCombination.value(a[9][0], b[9][0], - a[9][1], b[9][1])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[9][0], b[9][0], - a[9][1], b[9][1], - a[9][2], b[9][2])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - LinearCombination.value(a[9][0], b[9][0], - a[9][1], b[9][1], - a[9][2], b[9][2], - a[9][3], b[9][3])); - Assertions.assertEquals(Double.POSITIVE_INFINITY, LinearCombination.value(a[9], b[9])); - - Assertions.assertEquals(-Double.MAX_VALUE, - LinearCombination.value(a[10][0], b[10][0], - a[10][1], b[10][1])); - Assertions.assertEquals(-Double.MAX_VALUE, - LinearCombination.value(a[10][0], b[10][0], - a[10][1], b[10][1], - a[10][2], b[10][2])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - LinearCombination.value(a[10][0], b[10][0], - a[10][1], b[10][1], - a[10][2], b[10][2], - a[10][3], b[10][3])); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, LinearCombination.value(a[10], b[10])); - } - - /** - * This creates a scenario where the split product will overflow but the standard - * precision computation will not. The result is expected to be in extended precision, - * i.e. the method correctly detects and handles intermediate overflow. - * - * <p>Note: This test assumes that LinearCombination computes a split number - * using Dekker's method. This can result in the high part of the number being - * greater in magnitude than the the original number due to round-off in the split. - */ - @Test - void testOverflow() { - // Create a simple dot product that is different in high precision and has - // values that create a high part above the original number. This can be done using - // a mantissa with almost all bits set to 1. - final double x = Math.nextDown(2.0); - final double y = -Math.nextDown(x); - final double xxMxy = x * x + x * y; - final double xxMxyHighPrecision = LinearCombination.value(x, x, x, y); - Assertions.assertNotEquals(xxMxy, xxMxyHighPrecision, "High precision result should be different"); - - // Scale it close to max value. - // The current exponent is 0 so the combined scale must be 1023-1 as the - // partial product x*x and x*y have an exponent 1 higher - Assertions.assertEquals(0, Math.getExponent(x)); - Assertions.assertEquals(0, Math.getExponent(y)); - - final double a1 = Math.scalb(x, 1022 - 30); - final double b1 = Math.scalb(x, 30); - final double a2 = a1; - final double b2 = Math.scalb(y, 30); - // Verify low precision result is scaled and finite - final double sxxMxy = Math.scalb(xxMxy, 1022); - Assertions.assertEquals(sxxMxy, a1 * b1 + a2 * b2); - Assertions.assertTrue(Double.isFinite(sxxMxy)); - - // High precision result using Dekker's multiplier. - final double m = (1 << 27) + 1; - // First demonstrate that Dekker's split will create overflow in the high part. - double c; - c = a1 * m; - final double ha1 = c - (c - a1); - c = b1 * m; - final double hb1 = c - (c - b1); - c = a2 * m; - final double ha2 = c - (c - a2); - c = b2 * m; - final double hb2 = c - (c - b2); - Assertions.assertTrue(Double.isFinite(ha1)); - Assertions.assertTrue(Double.isFinite(hb1)); - Assertions.assertTrue(Double.isFinite(ha2)); - Assertions.assertTrue(Double.isFinite(hb2)); - // High part should be bigger in magnitude - Assertions.assertTrue(Math.abs(ha1) > Math.abs(a1)); - Assertions.assertTrue(Math.abs(hb1) > Math.abs(b1)); - Assertions.assertTrue(Math.abs(ha2) > Math.abs(a2)); - Assertions.assertTrue(Math.abs(hb2) > Math.abs(b2)); - Assertions.assertEquals(Double.POSITIVE_INFINITY, ha1 * hb1, "Expected split high part to overflow"); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, ha2 * hb2, "Expected split high part to overflow"); - - // LinearCombination should detect and handle intermediate overflow and return the - // high precision result. - final double expected = Math.scalb(xxMxyHighPrecision, 1022); - Assertions.assertEquals(expected, LinearCombination.value(a1, b1, a2, b2)); - Assertions.assertEquals(expected, LinearCombination.value(a1, b1, a2, b2, 0, 0)); - Assertions.assertEquals(expected, LinearCombination.value(a1, b1, a2, b2, 0, 0, 0, 0)); - Assertions.assertEquals(expected, LinearCombination.value(new double[] {a1, a2}, new double[] {b1, b2})); - } -} diff --git a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SumTest.java b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SumTest.java new file mode 100644 index 0000000..70afcd5 --- /dev/null +++ b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SumTest.java @@ -0,0 +1,568 @@ +/* + * 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.core; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Arrays; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SumTest { + + @Test + void testSum_simple() { + // act/assert + Assertions.assertEquals(0d, Sum.create().getAsDouble()); + + assertSum(Math.PI, Math.PI); + assertSum(Math.PI + Math.E, Math.PI, Math.E); + + assertSum(0, 0, 0, 0); + assertSum(6, 1, 2, 3); + assertSum(2, 1, -2, 3); + + assertSum(Double.NaN, Double.NaN, 0, 0); + assertSum(Double.NaN, 0, Double.NaN, 0); + assertSum(Double.NaN, 0, 0, Double.NaN); + + assertSum(Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0); + + assertSum(Double.POSITIVE_INFINITY, Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); + + assertSum(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, 1, 1); + assertSum(Double.NEGATIVE_INFINITY, 1, Double.NEGATIVE_INFINITY, 1); + } + + @Test + void testSumAccuracy() { + // arrange + final double a = 9.999999999; + final double b = Math.scalb(a, -53); + final double c = Math.scalb(a, -53); + final double d = Math.scalb(a, -27); + final double e = Math.scalb(a, -27); + final double f = Math.scalb(a, -50); + + // act/assert + assertSumExact(a); + + assertSumExact(a, b); + assertSumExact(b, a); + + assertSumExact(a, b, c); + assertSumExact(c, b, a); + + assertSumExact(a, b, c, d); + assertSumExact(d, c, b, a); + + assertSumExact(a, -b, c, -d); + assertSumExact(d, -c, b, -a); + + assertSumExact(a, b, c, d, e, f); + assertSumExact(f, e, d, c, b, a); + + assertSumExact(a, -b, c, -d, e, f); + assertSumExact(f, -e, d, -c, b, -a); + } + + @Test + void testAdd_sumInstance() { + // arrange + final double a = Math.PI; + final double b = Math.scalb(a, -53); + final double c = Math.scalb(a, -53); + final double d = Math.scalb(a, -27); + final double e = Math.scalb(a, -27); + final double f = Math.scalb(a, -50); + + // act/assert + Assertions.assertEquals(exactSum(a, b, c, d), Sum.of(a, b, c, d).add(Sum.create()).getAsDouble()); + Assertions.assertEquals(exactSum(a, a, b, c, d, e, f), + Sum.of(a, b) + .add(Sum.of(a, c)) + .add(Sum.of(d, e, f)).getAsDouble()); + + final Sum s = Sum.of(a, b); + Assertions.assertEquals(exactSum(a, b, a, b), s.add(s).getAsDouble()); + } + + @Test + void testSumOfProducts_dimensionMismatch() { + // act/assert + final Sum sum = Sum.create(); + Assertions.assertThrows(IllegalArgumentException.class, + () -> sum.addProducts(new double[1], new double[2])); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> Sum.ofProducts(new double[1], new double[2])); + } + + @Test + void testSumOfProducts_singleElement() { + final double[] a = {1.23456789}; + final double[] b = {98765432.1}; + + Assertions.assertEquals(a[0] * b[0], Sum.ofProducts(a, b).getAsDouble()); + } + + @Test + void testSumOfProducts() { + // arrange + final BigDecimal[] aFN = new BigDecimal[] { + BigDecimal.valueOf(-1321008684645961L), + BigDecimal.valueOf(-5774608829631843L), + BigDecimal.valueOf(-7645843051051357L), + }; + final BigDecimal[] aFD = new BigDecimal[] { + BigDecimal.valueOf(268435456L), + BigDecimal.valueOf(268435456L), + BigDecimal.valueOf(8589934592L) + }; + final BigDecimal[] bFN = new BigDecimal[] { + BigDecimal.valueOf(-5712344449280879L), + BigDecimal.valueOf(-4550117129121957L), + BigDecimal.valueOf(8846951984510141L) + }; + final BigDecimal[] bFD = new BigDecimal[] { + BigDecimal.valueOf(2097152L), + BigDecimal.valueOf(2097152L), + BigDecimal.valueOf(131072L) + }; + + final int len = aFN.length; + final double[] a = new double[len]; + final double[] b = new double[len]; + for (int i = 0; i < len; i++) { + a[i] = aFN[i].doubleValue() / aFD[i].doubleValue(); + b[i] = bFN[i].doubleValue() / bFD[i].doubleValue(); + } + + // act + final double sum = Sum.ofProducts(a, b).getAsDouble(); + + // assert + // Compare with arbitrary precision computation. + BigDecimal result = BigDecimal.ZERO; + for (int i = 0; i < a.length; i++) { + result = result.add(aFN[i].divide(aFD[i]).multiply(bFN[i].divide(bFD[i]))); + } + final double expected = result.doubleValue(); + Assertions.assertEquals(expected, sum, 1e-15); + + final double naive = a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + Assertions.assertTrue(Math.abs(naive - sum) > 1.5); + } + + @Test + void testSumOfProducts_huge() { + // arrange + int scale = 971; + final double[] a = new double[] { + -1321008684645961.0 / 268435456.0, + -5774608829631843.0 / 268435456.0, + -7645843051051357.0 / 8589934592.0 + }; + final double[] b = new double[] { + -5712344449280879.0 / 2097152.0, + -4550117129121957.0 / 2097152.0, + 8846951984510141.0 / 131072.0 + }; + + final int len = a.length; + final double[] scaledA = new double[len]; + final double[] scaledB = new double[len]; + for (int i = 0; i < len; ++i) { + scaledA[i] = Math.scalb(a[i], -scale); + scaledB[i] = Math.scalb(b[i], scale); + } + + // act + final double sum = Sum.ofProducts(scaledA[0], scaledB[0], + scaledA[1], scaledB[1], + scaledA[2], scaledB[2]).getAsDouble(); + + // assert + Assertions.assertEquals(-1.8551294182586248737720779899, sum, 1e-15); + + final double naive = scaledA[0] * scaledB[0] + scaledA[1] * scaledB[1] + scaledA[2] * scaledB[2]; + Assertions.assertTrue(Math.abs(naive - sum) > 1.5); + } + + @Test + void testSumOfProducts_nonFinite() { + // arrange + final double[][] a = new double[][] { + {1, 2, 3, 4}, + {1, Double.POSITIVE_INFINITY, 3, 4}, + {1, 2, Double.POSITIVE_INFINITY, 4}, + {1, Double.POSITIVE_INFINITY, 3, Double.NEGATIVE_INFINITY}, + {1, 2, 3, 4}, + {1, 2, 3, 4}, + {1, 2, 3, 4}, + {1, 2, 3, 4}, + {1, Double.MAX_VALUE, 3, 4}, + {1, 2, Double.MAX_VALUE, 4}, + {1, Double.MAX_VALUE / 2, 3, -Double.MAX_VALUE / 4}, + }; + final double[][] b = new double[][] { + {1, -2, 3, 4}, + {1, -2, 3, 4}, + {1, -2, 3, 4}, + {1, -2, 3, 4}, + {1, Double.POSITIVE_INFINITY, 3, 4}, + {1, -2, Double.POSITIVE_INFINITY, 4}, + {1, Double.POSITIVE_INFINITY, 3, Double.NEGATIVE_INFINITY}, + {Double.NaN, -2, 3, 4}, + {1, -2, 3, 4}, + {1, -2, 3, 4}, + {1, -2, 3, 4}, + }; + + // act/assert + assertSumOfProducts(-3, + a[0][0], b[0][0], + a[0][1], b[0][1]); + assertSumOfProducts(6, + a[0][0], b[0][0], + a[0][1], b[0][1], + a[0][2], b[0][2]); + assertSumOfProducts(22, a[0], b[0]); + + assertSumOfProducts(Double.NEGATIVE_INFINITY, + a[1][0], b[1][0], + a[1][1], b[1][1]); + assertSumOfProducts(Double.NEGATIVE_INFINITY, + a[1][0], b[1][0], + a[1][1], b[1][1], + a[1][2], b[1][2]); + assertSumOfProducts(Double.NEGATIVE_INFINITY, a[1], b[1]); + + assertSumOfProducts(-3, + a[2][0], b[2][0], + a[2][1], b[2][1]); + assertSumOfProducts(Double.POSITIVE_INFINITY, + a[2][0], b[2][0], + a[2][1], b[2][1], + a[2][2], b[2][2]); + assertSumOfProducts(Double.POSITIVE_INFINITY, a[2], b[2]); + + assertSumOfProducts(Double.NEGATIVE_INFINITY, + a[3][0], b[3][0], + a[3][1], b[3][1]); + assertSumOfProducts(Double.NEGATIVE_INFINITY, + a[3][0], b[3][0], + a[3][1], b[3][1], + a[3][2], b[3][2]); + assertSumOfProducts(Double.NEGATIVE_INFINITY, a[3], b[3]); + + assertSumOfProducts(Double.POSITIVE_INFINITY, + a[4][0], b[4][0], + a[4][1], b[4][1]); + assertSumOfProducts(Double.POSITIVE_INFINITY, + a[4][0], b[4][0], + a[4][1], b[4][1], + a[4][2], b[4][2]); + assertSumOfProducts(Double.POSITIVE_INFINITY, a[4], b[4]); + + assertSumOfProducts(-3, + a[5][0], b[5][0], + a[5][1], b[5][1]); + assertSumOfProducts(Double.POSITIVE_INFINITY, + a[5][0], b[5][0], + a[5][1], b[5][1], + a[5][2], b[5][2]); + assertSumOfProducts(Double.POSITIVE_INFINITY, a[5], b[5]); + + assertSumOfProducts(Double.POSITIVE_INFINITY, + a[6][0], b[6][0], + a[6][1], b[6][1]); + assertSumOfProducts(Double.POSITIVE_INFINITY, + a[6][0], b[6][0], + a[6][1], b[6][1], + a[6][2], b[6][2]); + assertSumOfProducts(Double.NaN, a[6], b[6]); + + assertSumOfProducts(Double.NaN, + a[7][0], b[7][0], + a[7][1], b[7][1]); + assertSumOfProducts(Double.NaN, + a[7][0], b[7][0], + a[7][1], b[7][1], + a[7][2], b[7][2]); + assertSumOfProducts(Double.NaN, a[7], b[7]); + + assertSumOfProducts(Double.NEGATIVE_INFINITY, + a[8][0], b[8][0], + a[8][1], b[8][1]); + assertSumOfProducts(Double.NEGATIVE_INFINITY, + a[8][0], b[8][0], + a[8][1], b[8][1], + a[8][2], b[8][2]); + assertSumOfProducts(Double.NEGATIVE_INFINITY, a[8], b[8]); + + assertSumOfProducts(-3, + a[9][0], b[9][0], + a[9][1], b[9][1]); + assertSumOfProducts(Double.POSITIVE_INFINITY, + a[9][0], b[9][0], + a[9][1], b[9][1], + a[9][2], b[9][2]); + assertSumOfProducts(Double.POSITIVE_INFINITY, a[9], b[9]); + + assertSumOfProducts(-Double.MAX_VALUE, + a[10][0], b[10][0], + a[10][1], b[10][1]); + assertSumOfProducts(-Double.MAX_VALUE, + a[10][0], b[10][0], + a[10][1], b[10][1], + a[10][2], b[10][2]); + assertSumOfProducts(Double.NEGATIVE_INFINITY, a[10], b[10]); + } + + /** + * This creates a scenario where the split product will overflow but the standard + * precision computation will not. The result is expected to be in extended precision, + * i.e. the method correctly detects and handles intermediate overflow. + * + * <p>Note: This test assumes that LinearCombination computes a split number + * using Dekker's method. This can result in the high part of the number being + * greater in magnitude than the the original number due to round-off in the split. + */ + @Test + void testSumOfProducts_overflow() { + // Create a simple dot product that is different in high precision and has + // values that create a high part above the original number. This can be done using + // a mantissa with almost all bits set to 1. + final double x = Math.nextDown(2.0); + final double y = -Math.nextDown(x); + final double xxMxy = x * x + x * y; + final double xxMxyHighPrecision = Sum.ofProducts(x, x, x, y).getAsDouble(); + Assertions.assertNotEquals(xxMxy, xxMxyHighPrecision, "High precision result should be different"); + + // Scale it close to max value. + // The current exponent is 0 so the combined scale must be 1023-1 as the + // partial product x*x and x*y have an exponent 1 higher + Assertions.assertEquals(0, Math.getExponent(x)); + Assertions.assertEquals(0, Math.getExponent(y)); + + final double a1 = Math.scalb(x, 1022 - 30); + final double b1 = Math.scalb(x, 30); + final double a2 = a1; + final double b2 = Math.scalb(y, 30); + // Verify low precision result is scaled and finite + final double sxxMxy = Math.scalb(xxMxy, 1022); + Assertions.assertEquals(sxxMxy, a1 * b1 + a2 * b2); + Assertions.assertTrue(Double.isFinite(sxxMxy)); + + // High precision result using Dekker's multiplier. + final double m = (1 << 27) + 1; + // First demonstrate that Dekker's split will create overflow in the high part. + double c; + c = a1 * m; + final double ha1 = c - (c - a1); + c = b1 * m; + final double hb1 = c - (c - b1); + c = a2 * m; + final double ha2 = c - (c - a2); + c = b2 * m; + final double hb2 = c - (c - b2); + Assertions.assertTrue(Double.isFinite(ha1)); + Assertions.assertTrue(Double.isFinite(hb1)); + Assertions.assertTrue(Double.isFinite(ha2)); + Assertions.assertTrue(Double.isFinite(hb2)); + // High part should be bigger in magnitude + Assertions.assertTrue(Math.abs(ha1) > Math.abs(a1)); + Assertions.assertTrue(Math.abs(hb1) > Math.abs(b1)); + Assertions.assertTrue(Math.abs(ha2) > Math.abs(a2)); + Assertions.assertTrue(Math.abs(hb2) > Math.abs(b2)); + Assertions.assertEquals(Double.POSITIVE_INFINITY, ha1 * hb1, "Expected split high part to overflow"); + Assertions.assertEquals(Double.NEGATIVE_INFINITY, ha2 * hb2, "Expected split high part to overflow"); + + // LinearCombination should detect and handle intermediate overflow and return the + // high precision result. + final double expected = Math.scalb(xxMxyHighPrecision, 1022); + assertSumOfProducts(expected, a1, b1, a2, b2); + assertSumOfProducts(expected, a1, b1, a2, b2, 0, 0); + assertSumOfProducts(expected, a1, b1, a2, b2, 0, 0, 0, 0); + } + + @Test + void testMixedSingleTermAndProduct() { + // arrange + final double a = 9.999999999; + final double b = Math.scalb(a, -53); + final double c = Math.scalb(a, -53); + final double d = Math.scalb(a, -27); + + // act/assert + Assertions.assertEquals(exactLinearCombination(1, a, -1, b, 2, c, 4, d), + Sum.create() + .add(a) + .add(-b) + .addProduct(2, c) + .addProduct(d, 4).getAsDouble()); + + Assertions.assertEquals(exactLinearCombination(1, a, -1, b, 2, c, 4, d), + Sum.create() + .addProduct(d, 4) + .add(a) + .addProduct(2, c) + .add(-b).getAsDouble()); + } + + @Test + public void testUnityValuesInProduct() { + // arrange + final double a = 9.999999999; + final double b = Math.scalb(a, -53); + final double c = Math.scalb(a, -53); + final double d = Math.scalb(a, -27); + + // act/assert + Assertions.assertEquals(exactLinearCombination(1, a, -1, b, 2, c, 4, d), + Sum.create() + .addProduct(1, a) + .addProduct(-1, b) + .addProduct(2, c) + .addProduct(d, 4).getAsDouble()); + + Assertions.assertEquals(exactLinearCombination(1, a, -1, b, 2, c, 4, d), + Sum.create() + .addProduct(a, 1) + .addProduct(b, -1) + .addProduct(2, c) + .addProduct(d, 4).getAsDouble()); + } + + private static void assertSumExact(final double... values) { + final double exact = exactSum(values); + assertSum(exact, values); + } + + private static void assertSum(final double expected, final double... values) { + // check non-array method variants + final int len = values.length; + if (len == 1) { + Assertions.assertEquals(expected, Sum.of(values[0]).getAsDouble()); + } else if (len == 2) { + Assertions.assertEquals(expected, Sum.of(values[0], values[1]).getAsDouble()); + } else if (len == 3) { + Assertions.assertEquals(expected, Sum.of(values[0], values[1], values[2]).getAsDouble()); + } else if (len == 4) { + Assertions.assertEquals(expected, Sum.of(values[0], values[1], values[2], values[3]).getAsDouble()); + } + + // check use with add() + final Sum addAccumulator = Sum.create(); + for (int i = 0; i < len; ++i) { + addAccumulator.add(values[i]); + } + Assertions.assertEquals(expected, addAccumulator.getAsDouble()); + + // check with accept() + final Sum acceptAccumulator = Sum.create(); + for (int i = 0; i < len; ++i) { + acceptAccumulator.accept(values[i]); + } + Assertions.assertEquals(expected, acceptAccumulator.getAsDouble()); + + // check using stream + final Sum streamAccumulator = Sum.create(); + Arrays.stream(values).forEach(streamAccumulator); + Assertions.assertEquals(expected, streamAccumulator.getAsDouble()); + + // check array instance method + Assertions.assertEquals(expected, Sum.create().add(values).getAsDouble()); + + // check array factory method + Assertions.assertEquals(expected, Sum.of(values).getAsDouble()); + } + + private static void assertSumOfProducts(final double expected, final double... args) { + final int halfLen = args.length / 2; + + final double[] a = new double[halfLen]; + final double[] b = new double[halfLen]; + for (int i = 0; i < halfLen; ++i) { + a[i] = args[2 * i]; + b[i] = args[(2 * i) + 1]; + } + + assertSumOfProducts(expected, a, b); + } + + private static void assertSumOfProducts(final double expected, final double[] a, final double[] b) { + final int len = a.length; + + // check non-array method variants + if (len == 2) { + Assertions.assertEquals(expected, Sum.ofProducts(a[0], b[0], + a[1], b[1]).getAsDouble()); + } else if (len == 3) { + Assertions.assertEquals(expected, Sum.ofProducts(a[0], b[0], + a[1], b[1], + a[2], b[2]).getAsDouble()); + } + + // check use of addProduct() + final Sum accumulator = Sum.create(); + for (int i = 0; i < len; ++i) { + accumulator.addProduct(a[i], b[i]); + } + Assertions.assertEquals(expected, accumulator.getAsDouble()); + + // check use of array instance method + Assertions.assertEquals(expected, Sum.create().addProducts(a, b).getAsDouble()); + + // check use of array factory method + Assertions.assertEquals(expected, Sum.ofProducts(a, b).getAsDouble()); + } + + /** Return the double estimation of the exact summation result computed with unlimited precision. + * @param values values to add + * @return double value closest to the exact result + */ + private static double exactSum(final double... values) { + BigDecimal sum = BigDecimal.ZERO; + for (double value : values) { + sum = sum.add(new BigDecimal(value), MathContext.UNLIMITED); + } + + return sum.doubleValue(); + } + + /** Return the double estimation of the exact linear combination result. Factors are + * listed sequentially in the argument array, e.g., {@code a1, b1, a2, b2, ...}. + * @param values linear combination input + * @return double value closest to the exact result + */ + private static double exactLinearCombination(final double... values) { + final int halfLen = values.length / 2; + + BigDecimal sum = BigDecimal.ZERO; + for (int i = 0; i < halfLen; ++i) { + final BigDecimal a = new BigDecimal(values[2 * i]); + final BigDecimal b = new BigDecimal(values[(2 * i) + 1]); + + sum = sum.add(a.multiply(b, MathContext.UNLIMITED)); + } + + return sum.doubleValue(); + } +} diff --git a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SummationTest.java b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SummationTest.java deleted file mode 100644 index fd67160..0000000 --- a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SummationTest.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * 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.core; - -import java.math.BigDecimal; -import java.math.MathContext; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class SummationTest { - - @Test - void test3n_simple() { - // act/assert - Assertions.assertEquals(0, Summation.value(0, 0, 0)); - Assertions.assertEquals(6, Summation.value(1, 2, 3)); - Assertions.assertEquals(2, Summation.value(1, -2, 3)); - - Assertions.assertEquals(Double.NaN, Summation.value(Double.NaN, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, Double.NaN, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, Double.NaN)); - - Assertions.assertEquals(Double.NaN, Summation.value(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0)); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, - Summation.value(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE)); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, Summation.value(Double.POSITIVE_INFINITY, 1, 1)); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, Summation.value(1, Double.NEGATIVE_INFINITY, 1)); - } - - @Test - void test3n_accuracy() { - // arrange - final double a = 9.999999999; - final double b = Math.scalb(a, -53); - final double c = Math.scalb(a, -53); - - // act/assert - assertValue(a, b, c); - assertValue(c, b, a); - - assertValue(a, -b, c); - assertValue(-c, b, -a); - } - - @Test - void test4n_simple() { - // act/assert - Assertions.assertEquals(0, Summation.value(0, 0, 0, 0)); - Assertions.assertEquals(10, Summation.value(1, 2, 3, 4)); - Assertions.assertEquals(-2, Summation.value(1, -2, 3, -4)); - - Assertions.assertEquals(Double.NaN, Summation.value(Double.NaN, 0, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, Double.NaN, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, Double.NaN, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, 0, Double.NaN)); - - Assertions.assertEquals(Double.NaN, Summation.value(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0, 0)); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, - Summation.value(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE)); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, Summation.value(Double.POSITIVE_INFINITY, 1, 1, 1)); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, Summation.value(1, Double.NEGATIVE_INFINITY, 1, 1)); - } - - @Test - void test4n_accuracy() { - // arrange - final double a = 9.999999999; - final double b = Math.scalb(a, -53); - final double c = Math.scalb(a, -53); - final double d = Math.scalb(a, -27); - - // act/assert - assertValue(a, b, c, d); - assertValue(d, c, b, a); - - assertValue(a, -b, c, -d); - assertValue(d, -c, b, -a); - } - - @Test - void test5n_simple() { - // act/assert - Assertions.assertEquals(0, Summation.value(0, 0, 0, 0, 0)); - Assertions.assertEquals(15, Summation.value(1, 2, 3, 4, 5)); - Assertions.assertEquals(3, Summation.value(1, -2, 3, -4, 5)); - - Assertions.assertEquals(Double.NaN, Summation.value(Double.NaN, 0, 0, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, Double.NaN, 0, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, Double.NaN, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, 0, Double.NaN, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, 0, 0, Double.NaN)); - - Assertions.assertEquals(Double.NaN, Summation.value( - Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0, 0, 0)); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, Summation.value( - Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE)); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, Summation.value(Double.POSITIVE_INFINITY, 1, 1, 1, 1)); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, Summation.value(1, Double.NEGATIVE_INFINITY, 1, 1, 1)); - } - - @Test - void test5n_accuracy() { - // arrange - final double a = 9.999999999; - final double b = Math.scalb(a, -53); - final double c = Math.scalb(a, -53); - final double d = Math.scalb(a, -27); - final double e = Math.scalb(a, -27); - - // act/assert - assertValue(a, b, c, d, e); - assertValue(e, d, c, b, a); - - assertValue(a, -b, c, -d, e); - assertValue(-e, d, -c, b, -a); - } - - @Test - void test6n_simple() { - // act/assert - Assertions.assertEquals(0, Summation.value(0, 0, 0, 0, 0, 0)); - Assertions.assertEquals(21, Summation.value(1, 2, 3, 4, 5, 6)); - Assertions.assertEquals(-3, Summation.value(1, -2, 3, -4, 5, -6)); - - Assertions.assertEquals(Double.NaN, Summation.value(Double.NaN, 0, 0, 0, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, Double.NaN, 0, 0, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, Double.NaN, 0, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, 0, Double.NaN, 0, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, 0, 0, Double.NaN, 0)); - Assertions.assertEquals(Double.NaN, Summation.value(0, 0, 0, 0, 0, Double.NaN)); - - Assertions.assertEquals(Double.NaN, Summation.value( - Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0, 0, 0, 0)); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, Summation.value( - Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE, - Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE)); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, Summation.value(Double.POSITIVE_INFINITY, 1, 1, 1, 1, 1)); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, Summation.value(1, Double.NEGATIVE_INFINITY, 1, 1, 1, 1)); - } - - @Test - void test6n_accuracy() { - // arrange - final double a = 9.999999999; - final double b = Math.scalb(a, -53); - final double c = Math.scalb(a, -53); - final double d = Math.scalb(a, -27); - final double e = Math.scalb(a, -27); - final double f = Math.scalb(a, -50); - - // act/assert - assertValue(a, b, c, d, e, f); - assertValue(f, e, d, c, b, a); - - assertValue(a, -b, c, -d, e, f); - assertValue(f, -e, d, -c, b, -a); - } - - @Test - void testArray_simple() { - // act/assert - Assertions.assertEquals(0, Summation.value(new double[0])); - Assertions.assertEquals(-1, Summation.value(new double[] {-1})); - Assertions.assertEquals(0, Summation.value(new double[] {0, 0, 0})); - Assertions.assertEquals(6, Summation.value(new double[] {1, 2, 3})); - Assertions.assertEquals(2, Summation.value(new double[] {1, -2, 3})); - - Assertions.assertEquals(Double.MAX_VALUE, Summation.value(new double[] {Double.MAX_VALUE})); - Assertions.assertEquals(Double.MIN_VALUE, Summation.value(new double[] {Double.MIN_VALUE})); - - Assertions.assertEquals(Double.NaN, Summation.value(new double[] {0d, Double.NaN})); - Assertions.assertEquals(Double.NaN, Summation.value(new double[] {Double.POSITIVE_INFINITY, Double.NaN})); - Assertions.assertEquals(Double.NaN, - Summation.value(new double[] {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY})); - - Assertions.assertEquals(Double.POSITIVE_INFINITY, - Summation.value(new double[] {Double.MAX_VALUE, Double.MAX_VALUE})); - Assertions.assertEquals(Double.POSITIVE_INFINITY, - Summation.value(new double[] {Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY})); - Assertions.assertEquals(Double.NEGATIVE_INFINITY, - Summation.value(new double[] {Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY})); - } - - @Test - void testArray_accuracy() { - // arrange - final double a = 9.999999999; - final double b = Math.scalb(a, -53); - final double c = Math.scalb(a, -53); - final double d = Math.scalb(a, -27); - final double e = Math.scalb(a, -27); - final double f = Math.scalb(a, -50); - - // act/assert - assertArrayValue(a); - - assertArrayValue(a, b); - assertArrayValue(b, a); - - assertArrayValue(a, b, c); - assertArrayValue(c, b, a); - - assertArrayValue(a, b, c, d); - assertArrayValue(d, c, b, a); - - assertArrayValue(a, -b, c, -d); - assertArrayValue(d, -c, b, -a); - - assertArrayValue(a, b, c, d, e, f); - assertArrayValue(f, e, d, c, b, a); - - assertArrayValue(a, -b, c, -d, e, f); - assertArrayValue(f, -e, d, -c, b, -a); - } - - private static void assertValue(final double a, final double b, final double c) { - final double computed = Summation.value(a, b, c); - assertComputedValue(computed, a, b, c); - } - - private static void assertValue(final double a, final double b, final double c, final double d) { - final double computed = Summation.value(a, b, c, d); - assertComputedValue(computed, a, b, c, d); - } - - private static void assertValue(final double a, final double b, final double c, final double d, - final double e) { - final double computed = Summation.value(a, b, c, d, e); - assertComputedValue(computed, a, b, c, d, e); - } - - private static void assertValue(final double a, final double b, final double c, final double d, - final double e, final double f) { - final double computed = Summation.value(a, b, c, d, e, f); - assertComputedValue(computed, a, b, c, d, e, f); - } - - private static void assertArrayValue(final double... values) { - final double computed = Summation.value(values); - assertComputedValue(computed, values); - } - - private static void assertComputedValue(final double computed, final double... values) { - final double exact = computeExact(values); - Assertions.assertEquals(exact, computed); - } - - /** Return the double estimation of the exact summation result computed with unlimited precision. - * @param values values to add - * @return double value closest to the exact result - */ - private static double computeExact(final double... values) { - BigDecimal sum = BigDecimal.ZERO; - for (double value : values) { - sum = sum.add(new BigDecimal(value), MathContext.UNLIMITED); - } - - return sum.doubleValue(); - } -} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/EuclideanNormAlgorithms.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/EuclideanNormAlgorithms.java index ec8f689..5f65f84 100644 --- a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/EuclideanNormAlgorithms.java +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/EuclideanNormAlgorithms.java @@ -20,6 +20,8 @@ import java.math.BigDecimal; import java.math.MathContext; import java.util.function.ToDoubleFunction; +import org.apache.commons.numbers.core.Sum; + /** Class containing various Euclidean norm computation methods for comparison. */ public final class EuclideanNormAlgorithms { @@ -370,7 +372,7 @@ public final class EuclideanNormAlgorithms { } rescale = 0x1.0p-600; } - return Math.sqrt(org.apache.commons.numbers.core.LinearCombination.value(x, x)) * rescale; + return Math.sqrt(Sum.ofProducts(x, x).getAsDouble()) * rescale; } } diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/LinearCombinationPerformance.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/LinearCombinationPerformance.java index 0590842..9873e22 100644 --- a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/LinearCombinationPerformance.java +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/LinearCombinationPerformance.java @@ -22,7 +22,7 @@ import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.function.IntFunction; -import org.apache.commons.numbers.core.LinearCombination; +import org.apache.commons.numbers.core.Sum; import org.apache.commons.numbers.examples.jmh.core.LinearCombination.FourD; import org.apache.commons.numbers.examples.jmh.core.LinearCombination.ND; import org.apache.commons.numbers.examples.jmh.core.LinearCombination.ThreeD; @@ -230,10 +230,15 @@ public class LinearCombinationPerformance { @Setup public void setup() { if ("current".endsWith(name)) { - twod = LinearCombination::value; - threed = LinearCombination::value; - fourd = LinearCombination::value; - nd = LinearCombination::value; + twod = (a1, b1, a2, b2) -> Sum.ofProducts(a1, b1, a2, b2).getAsDouble(); + threed = (a1, b1, a2, b2, a3, b3) -> Sum.ofProducts(a1, b1, a2, b2, a3, b3).getAsDouble(); + fourd = (a1, b1, a2, b2, a3, b3, a4, b4) -> + Sum.create() + .addProduct(a1, b1) + .addProduct(a2, b2) + .addProduct(a3, b3) + .addProduct(a4, b4).getAsDouble(); + nd = (a, b) -> Sum.ofProducts(a, b).getAsDouble(); return; } // All implementations below are expected to implement all the interfaces. diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/NormPerformance.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/NormPerformance.java index 1792bb3..c6475ca 100644 --- a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/NormPerformance.java +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/NormPerformance.java @@ -184,7 +184,7 @@ public class NormPerformance { eval(v -> Math.hypot(v[0], v[1]), input, bh); } - /** Compute the performance of the {@link Norm#of(double, double)} method. + /** Compute the performance of the {@link Norm#L2} 2D method. * @param input benchmark input * @param bh blackhole */ @@ -193,7 +193,7 @@ public class NormPerformance { eval(v -> Norm.L2.of(v[0], v[1]), input, bh); } - /** Compute the performance of the {@link Norm#of(double, double, double)} method. + /** Compute the performance of the {@link Norm#L2} 3D norm computation. * @param input benchmark input * @param bh blackhole */ @@ -202,7 +202,7 @@ public class NormPerformance { eval(v -> Norm.L2.of(v[0], v[1], v[2]), input, bh); } - /** Compute the performance of the {@link Norm#of(double[])} method. + /** Compute the performance of the {@link Norm#L2} array norm method. * @param input benchmark input * @param bh blackhole */ diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/SumPerformance.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/SumPerformance.java new file mode 100644 index 0000000..0dd6732 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/core/SumPerformance.java @@ -0,0 +1,183 @@ +/* + * 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.examples.jmh.core; + +import java.util.concurrent.TimeUnit; +import java.util.function.ToDoubleBiFunction; +import java.util.function.ToDoubleFunction; + +import org.apache.commons.numbers.core.Sum; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Executes a benchmark to measure the speed of operations in the {@link Sum} class. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@State(Scope.Benchmark) +@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"}) +public class SumPerformance { + /** + * The seed to use to create the random benchmark input. + * Using a fixed seed ensures the same values are created across benchmarks. + */ + private static final long SEED = System.currentTimeMillis(); + + /** Class providing double arrays for benchmarks. + */ + @State(Scope.Benchmark) + public static class ArrayInput { + + /** Number of array samples. */ + @Param("100000") + private int samples; + + /** Number of values in each input array. */ + @Param("50") + private int len; + + /** Minimum possible double exponent. */ + @Param("-550") + private int minExp; + + /** Maximum possible double exponent. */ + @Param("+550") + private int maxExp; + + /** Range of exponents within a single array. */ + @Param("26") + private int expRange; + + /** First set of input arrays. */ + private double[][] a; + + /** Second set of input arrays. */ + private double[][] b; + + /** Get the first set of input arrays. + * @return first set of input arrays + */ + public double[][] getA() { + return a; + } + + /** Get the second set of input arrays. + * @return second set of input arrays + */ + public double[][] getB() { + return b; + } + + /** Create the input arrays for the instance. */ + @Setup + public void createArrays() { + final UniformRandomProvider rng = RandomSource.create(RandomSource.XO_RO_SHI_RO_1024_PP, SEED); + + a = new double[samples][]; + b = new double[samples][]; + for (int i = 0; i < samples; ++i) { + // pick a general range for the array element exponents and then + // create values within that range + final int vMidExp = rng.nextInt(maxExp - minExp + 1) + minExp; + final int vExpRadius = expRange / 2; + final int vMinExp = vMidExp - vExpRadius; + final int vMaxExp = vMidExp + vExpRadius; + + a[i] = DoubleUtils.randomArray(len, vMinExp, vMaxExp, rng); + b[i] = DoubleUtils.randomArray(len, vMinExp, vMaxExp, rng); + } + } + } + + /** Run a benchmark for a function that accepts a single array and produces a double result. + * @param input benchmark input + * @param bh data sink + * @param fn function to benchmark + */ + private static void runSingle(final ArrayInput input, final Blackhole bh, + final ToDoubleFunction<double[]> fn) { + final double[][] a = input.getA(); + for (int i = 0; i < a.length; ++i) { + bh.consume(fn.applyAsDouble(a[i])); + } + } + + /** Run a benchmark for a function that accepts a two arrays and produces a double result. + * @param input benchmark input + * @param bh data sink + * @param fn function to benchmark + */ + private static void runDouble(final ArrayInput input, final Blackhole bh, + final ToDoubleBiFunction<double[], double[]> fn) { + final double[][] a = input.getA(); + final double[][] b = input.getB(); + for (int i = 0; i < a.length; ++i) { + bh.consume(fn.applyAsDouble(a[i], b[i])); + } + } + + /** Benchmark baseline for functions that use a single input array. + * @param input benchmark input + * @param bh data sink + */ + @Benchmark + public void baselineSingle(final ArrayInput input, final Blackhole bh) { + runSingle(input, bh, a -> 0d); + } + + /** Benchmark baseline for functions that use two input arrays. + * @param input benchmark input + * @param bh data sink + */ + @Benchmark + public void baselineDouble(final ArrayInput input, final Blackhole bh) { + runDouble(input, bh, (a, b) -> 0d); + } + + /** Benchmark testing {@link Sum} addition performance. + * @param input benchmark input + * @param bh data sink + */ + @Benchmark + public void sum(final ArrayInput input, final Blackhole bh) { + runSingle(input, bh, a -> Sum.of(a).getAsDouble()); + } + + /** Benchmark testing {@link Sum} linear combination performance. + * @param input benchmark input + * @param bh data sink + */ + @Benchmark + public void sumOfProducts(final ArrayInput input, final Blackhole bh) { + runDouble(input, bh, (a, b) -> Sum.ofProducts(a, b).getAsDouble()); + } +}