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
The following commit(s) were added to refs/heads/master by this push:
new 1c457b7 NUMBERS-142: Update LinearCombination to use the dot2s
algorithm
1c457b7 is described below
commit 1c457b74cd54ee0b09b274d57543e82633f47bb2
Author: aherbert <[email protected]>
AuthorDate: Wed Apr 21 17:51:19 2021 +0100
NUMBERS-142: Update LinearCombination to use the dot2s algorithm
- Avoids construction of an intermediate array for the dot product of
array input
- Perform splitting using Dekker's multiplication algorithm to retain
all bits of precision
- Handle overflow during the split for large numbers
Splitting now correctly handles sub-normal numbers with no information
in the upper 26-bits as input. The high part will be sub-normal and the
low part will be zero. The previous split created a zero high part and
the input as the low part.
---
.../commons/numbers/arrays/ExtendedPrecision.java | 286 ++++++++++++++++++
.../commons/numbers/arrays/LinearCombination.java | 330 +++++++--------------
.../numbers/arrays/ExtendedPrecisionTest.java | 107 +++++++
.../numbers/arrays/LinearCombinationTest.java | 236 +++++++++++----
src/changes/changes.xml | 5 +
5 files changed, 672 insertions(+), 292 deletions(-)
diff --git
a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/ExtendedPrecision.java
b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/ExtendedPrecision.java
new file mode 100644
index 0000000..da7a72d
--- /dev/null
+++
b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/ExtendedPrecision.java
@@ -0,0 +1,286 @@
+/*
+ * 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.arrays;
+
+/**
+ * Computes extended precision floating-point operations.
+ *
+ * <p>It is based on the 1971 paper
+ * <a href="https://doi.org/10.1007/BF01397083">
+ * Dekker (1971) A floating-point technique for extending the available
precision</a>.
+ */
+final class ExtendedPrecision {
+ /*
+ * 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]).
+ *
+ * [1] Shewchuk (1997): Arbitrary Precision Floating-Point Arithmetic
+ *
http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps
+ */
+
+ /**
+ * The multiplier used to split the double value into high and low parts.
From
+ * Dekker (1971): "The constant should be chosen equal to 2^(p - p/2) + 1,
+ * where p is the number of binary digits in the mantissa". Here p is 53
+ * and the multiplier is {@code 2^27 + 1}.
+ */
+ private static final double MULTIPLIER = 1.0 + 0x1.0p27;
+
+ /**
+ * The upper limit above which a number may overflow during the split into
a high part.
+ * Assuming the multiplier is above 2^27 and the maximum exponent is 1023
then a safe
+ * limit is a value with an exponent of (1023 - 27) = 2^996.
+ * 996 is the value obtained from {@code Math.getExponent(Double.MAX_VALUE
/ MULTIPLIER)}.
+ */
+ private static final double SAFE_UPPER = 0x1.0p996;
+
+ /** The scale to use when down-scaling during a split into a high part.
+ * This must be smaller than the inverse of the multiplier and a power of
2 for exact scaling. */
+ private static final double DOWN_SCALE = 0x1.0p-30;
+
+ /** The scale to use when re-scaling during a split into a high part.
+ * This is the inverse of {@link #DOWN_SCALE}. */
+ private static final double UP_SCALE = 0x1.0p30;
+
+ /** The mask to extract the raw 11-bit exponent.
+ * The value must be shifted 52-bits to remove the mantissa bits. */
+ private static final int EXP_MASK = 0x7ff;
+
+ /** The value 2046 converted for use if using {@link
Integer#compareUnsigned(int, int)}.
+ * This requires adding {@link Integer#MIN_VALUE} to 2046. */
+ private static final int CMP_UNSIGNED_2046 = Integer.MIN_VALUE + 2046;
+
+ /** The value -1 converted for use if using {@link
Integer#compareUnsigned(int, int)}.
+ * This requires adding {@link Integer#MIN_VALUE} to -1. */
+ private static final int CMP_UNSIGNED_MINUS_1 = Integer.MIN_VALUE - 1;
+
+ /** Private constructor. */
+ private ExtendedPrecision() {
+ // intentionally empty.
+ }
+
+ /**
+ * Compute the low part of the double length number {@code (z,zz)} for the
exact
+ * product of {@code x} and {@code y}. This is equivalent to computing a
{@code double}
+ * containing the magnitude of the rounding error when converting the
exact 106-bit
+ * significand of the multiplication result to a 53-bit significand.
+ *
+ * <p>The method is written to be functionally similar to using a fused
multiply add (FMA)
+ * operation to compute the low part, for example JDK 9's Math.fma
function (note the sign
+ * change in the input argument for the product):
+ * <pre>
+ * double x = ...;
+ * double y = ...;
+ * double xy = x * y;
+ * double low1 = Math.fma(x, y, -xy);
+ * double low2 = productLow(x, y, xy);
+ * </pre>
+ *
+ * <p>Special cases:
+ *
+ * <ul>
+ * <li>If {@code x * y} is sub-normal or zero then the result is 0.0.
+ * <li>If {@code x * y} is infinite or NaN then the result is NaN.
+ * </ul>
+ *
+ * @param x First factor.
+ * @param y Second factor.
+ * @param xy Product of the factors (x * y).
+ * @return the low part of the product double length number
+ * @see #highPartUnscaled(double)
+ */
+ static double productLow(double x, double y, double xy) {
+ // Verify the input. This must be NaN safe.
+ //assert Double.compare(x * y, xy) == 0
+
+ // If the number is sub-normal, inf or nan there is no round-off.
+ if (isNotNormal(xy)) {
+ // Returns 0.0 for sub-normal xy, otherwise NaN for inf/nan:
+ return xy - xy;
+ }
+
+ // The result xy is finite and normal.
+ // Use Dekker's mul12 algorithm that splits the values into high and
low parts.
+ // Dekker's split using multiplication will overflow if the value is
within 2^27
+ // of double max value. It can also produce 26-bit approximations that
are larger
+ // than the input numbers for the high part causing overflow in hx *
hy when
+ // x * y does not overflow. So we must scale down big numbers.
+ // We only have to scale the largest number as we know the product
does not overflow
+ // (if one is too big then the other cannot be).
+ // We also scale if the product is close to overflow to avoid
intermediate overflow.
+ // This could be done at a higher limit (e.g. Math.abs(xy) >
Double.MAX_VALUE / 4)
+ // but is included here to have a single low probability branch
condition.
+
+ // Add the absolute inputs for a single comparison. The sum will not
be more than
+ // 3-fold higher than any component.
+ final double a = Math.abs(x);
+ final double b = Math.abs(y);
+ if (a + b + Math.abs(xy) >= SAFE_UPPER) {
+ // Only required to scale the largest number as x*y does not
overflow.
+ if (a > b) {
+ return productLowUnscaled(x * DOWN_SCALE, y, xy * DOWN_SCALE)
* UP_SCALE;
+ }
+ return productLowUnscaled(x, y * DOWN_SCALE, xy * DOWN_SCALE) *
UP_SCALE;
+ }
+
+ // No scaling required
+ return productLowUnscaled(x, y, xy);
+ }
+
+ /**
+ * Checks if the number is not normal. This is functionally equivalent to:
+ * <pre>
+ * final double abs = Math.abs(a);
+ * return (abs <= Double.MIN_NORMAL || !(abs <= Double.MAX_VALUE));
+ * </pre>
+ *
+ * @param a The value.
+ * @return true if the value is not normal
+ */
+ static boolean isNotNormal(double a) {
+ // Sub-normal numbers have a biased exponent of 0.
+ // Inf/NaN numbers have a biased exponent of 2047.
+ // Catch both cases by extracting the raw exponent, subtracting 1
+ // and compare unsigned (so 0 underflows to a unsigned large value).
+ final int baisedExponent = ((int) (Double.doubleToRawLongBits(a) >>>
52)) & EXP_MASK;
+ // Pre-compute the additions used by Integer.compareUnsigned
+ return baisedExponent + CMP_UNSIGNED_MINUS_1 >= CMP_UNSIGNED_2046;
+ }
+
+ /**
+ * Compute the low part of the double length number {@code (z,zz)} for the
exact
+ * product of {@code x} and {@code y} using Dekker's mult12 algorithm. The
standard
+ * precision product {@code x*y} must be provided. The numbers {@code x}
and {@code y}
+ * are split into high and low parts using Dekker's algorithm.
+ *
+ * <p>Warning: This method does not perform scaling in Dekker's split and
large
+ * finite numbers can create NaN results.
+ *
+ * @param x First factor.
+ * @param y Second factor.
+ * @param xy Product of the factors (x * y).
+ * @return the low part of the product double length number
+ * @see #highPartUnscaled(double)
+ * @see #productLow(double, double, double, double, double)
+ */
+ private static double productLowUnscaled(double x, double y, double xy) {
+ // Split the numbers using Dekker's algorithm without scaling
+ final double hx = highPartUnscaled(x);
+ final double lx = x - hx;
+
+ final double hy = highPartUnscaled(y);
+ final double ly = y - hy;
+
+ return productLow(hx, lx, hy, ly, xy);
+ }
+
+ /**
+ * Compute the low part of the double length number {@code (z,zz)} for the
exact
+ * product of {@code x} and {@code y} using Dekker's mult12 algorithm. The
standard
+ * precision product {@code x*y} must be provided. The numbers {@code x}
and {@code y}
+ * should already be split into low and high parts.
+ *
+ * <p>Note: This uses the high part of the result {@code (z,zz)} as {@code
x * y} and not
+ * {@code hx * hy + hx * ty + tx * hy} as specified in Dekker's original
paper.
+ * See Shewchuk (1997) for working examples.
+ *
+ * @param hx High part of first factor.
+ * @param lx Low part of first factor.
+ * @param hy High part of second factor.
+ * @param ly Low part of second factor.
+ * @param xy Product of the factors.
+ * @return <code>lx * ly - (((xy - hx * hy) - lx * hy) - hx * ly)</code>
+ * @see <a
href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps">
+ * Shewchuk (1997) Theorum 18</a>
+ */
+ private static double productLow(double hx, double lx, double hy, double
ly, double xy) {
+ // Compute the multiply low part:
+ // err1 = xy - hx * hy
+ // err2 = err1 - lx * hy
+ // err3 = err2 - hx * ly
+ // low = lx * ly - err3
+ return lx * ly - (((xy - hx * hy) - lx * hy) - hx * ly);
+ }
+
+ /**
+ * Implement Dekker's method to split a value into two parts. Multiplying
by (2^s + 1) creates
+ * a big value from which to derive the two split parts.
+ * <pre>
+ * c = (2^s + 1) * a
+ * a_big = c - a
+ * a_hi = c - a_big
+ * a_lo = a - a_hi
+ * a = a_hi + a_lo
+ * </pre>
+ *
+ * <p>The multiplicand allows a p-bit value to be split into
+ * (p-s)-bit value {@code a_hi} and a non-overlapping (s-1)-bit value
{@code a_lo}.
+ * Combined they have (p-1) bits of significand but the sign bit of {@code
a_lo}
+ * contains a bit of information. The constant is chosen so that s is
ceil(p/2) where
+ * the precision p for a double is 53-bits (1-bit of the mantissa is
assumed to be
+ * 1 for a non sub-normal number) and s is 27.
+ *
+ * <p>This conversion does not use scaling and the result of overflow is
NaN. Overflow
+ * may occur when the exponent of the input value is above 996.
+ *
+ * <p>Splitting a NaN or infinite value will return NaN.
+ *
+ * @param value Value.
+ * @return the high part of the value.
+ * @see Math#getExponent(double)
+ */
+ static double highPartUnscaled(double value) {
+ final double c = MULTIPLIER * value;
+ return c - (c - value);
+ }
+
+ /**
+ * Compute the round-off from the sum of two numbers {@code a} and {@code
b} using
+ * Knuth's two-sum algorithm. The values are not required to be ordered by
magnitude.
+ * The standard precision sum must be provided.
+ *
+ * @param a First part of sum.
+ * @param b Second part of sum.
+ * @param sum Sum of the parts (a + b).
+ * @return <code>(b - (sum - (sum - b))) + (a - (sum - b))</code>
+ * @see <a
href="http://www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps">
+ * Shewchuk (1997) Theorum 7</a>
+ */
+ static double twoSumLow(double a, double b, double sum) {
+ final double bVirtual = sum - a;
+ // sum - bVirtual == aVirtual.
+ // a - aVirtual == a round-off
+ // b - bVirtual == b round-off
+ return (a - (sum - bVirtual)) + (b - bVirtual);
+ }
+}
diff --git
a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/LinearCombination.java
b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/LinearCombination.java
index 052857c..fa9ddfb 100644
---
a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/LinearCombination.java
+++
b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/LinearCombination.java
@@ -23,8 +23,8 @@ package org.apache.commons.numbers.arrays;
* It does so by using specific multiplication and addition algorithms to
* preserve accuracy and reduce cancellation effects.
*
- * It is based on the 2005 paper
- * <a href="http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.2.1547">
+ * <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>.
*/
@@ -32,15 +32,30 @@ public final class LinearCombination {
/*
* Caveat:
*
- * The code below is split in many additions/subtractions that may
+ * 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.
- * The variables naming conventions are that xyzHigh contains the most
significant
- * bits of xyz and xyzLow contains its least significant bits. So
theoretically
- * xyz is the sum xyzHigh + xyzLow, but in many cases below, this sum
cannot
- * be represented in only one double precision number so we preserve two
numbers
- * to hold it as long as we can, combining the high and low order bits
together
- * only at the end, after cancellation may have occurred on high order bits
+ *
+ * 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. */
@@ -60,57 +75,26 @@ public final class LinearCombination {
throw new IllegalArgumentException("Dimension mismatch: " +
a.length + " != " + b.length);
}
+ // Implement dot2s (Algorithm 5.4) from Ogita et al (2005).
final int len = a.length;
- if (len == 1) {
- // Revert to scalar multiplication.
- return a[0] * b[0];
- }
-
- final double[] prodHigh = new double[len];
- double prodLowSum = 0;
-
- for (int i = 0; i < len; i++) {
- final double ai = a[i];
- final double aHigh = highPart(ai);
- final double aLow = ai - aHigh;
-
- final double bi = b[i];
- final double bHigh = highPart(bi);
- final double bLow = bi - bHigh;
- prodHigh[i] = ai * bi;
- final double prodLow = prodLow(aLow, bLow, prodHigh[i], aHigh,
bHigh);
- prodLowSum += prodLow;
- }
+ // 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 prodHighCur = prodHigh[0];
- double prodHighNext = prodHigh[1];
- double sHighPrev = prodHighCur + prodHighNext;
- double sPrime = sHighPrev - prodHighNext;
- double sLowSum = (prodHighNext - (sHighPrev - sPrime)) + (prodHighCur
- sPrime);
-
- final int lenMinusOne = len - 1;
- for (int i = 1; i < lenMinusOne; i++) {
- prodHighNext = prodHigh[i + 1];
- final double sHighCur = sHighPrev + prodHighNext;
- sPrime = sHighCur - prodHighNext;
- sLowSum += (prodHighNext - (sHighCur - sPrime)) + (sHighPrev -
sPrime);
- sHighPrev = sHighCur;
+ final double x = p + h;
+ // s_i = s_(i-1) + (q_i + r_i)
+ s += ExtendedPrecision.twoSumLow(p, h, x) + r;
+ p = x;
}
- double result = sHighPrev + (prodLowSum + sLowSum);
-
- if (Double.isNaN(result)) {
- // either we have split infinite numbers or some coefficients were
NaNs,
- // just rely on the naive implementation and let IEEE754 handle
this
- result = 0;
- for (int i = 0; i < len; ++i) {
- result += a[i] * b[i];
- }
- }
-
- return result;
+ return getSum(p, p + s);
}
/**
@@ -126,42 +110,17 @@ public final class LinearCombination {
*/
public static double value(double a1, double b1,
double a2, double b2) {
- // split a1 and b1 as one 26 bits number and one 27 bits number
- final double a1High = highPart(a1);
- final double a1Low = a1 - a1High;
- final double b1High = highPart(b1);
- final double b1Low = b1 - b1High;
-
- // accurate multiplication a1 * b1
- final double prod1High = a1 * b1;
- final double prod1Low = prodLow(a1Low, b1Low, prod1High, a1High,
b1High);
-
- // split a2 and b2 as one 26 bits number and one 27 bits number
- final double a2High = highPart(a2);
- final double a2Low = a2 - a2High;
- final double b2High = highPart(b2);
- final double b2Low = b2 - b2High;
-
- // accurate multiplication a2 * b2
- final double prod2High = a2 * b2;
- final double prod2Low = prodLow(a2Low, b2Low, prod2High, a2High,
b2High);
-
- // accurate addition a1 * b1 + a2 * b2
- final double s12High = prod1High + prod2High;
- final double s12Prime = s12High - prod2High;
- final double s12Low = (prod2High - (s12High - s12Prime)) +
(prod1High - s12Prime);
-
- // final rounding, s12 may have suffered many cancellations, we try
- // to recover some bits from the extra words we have saved up to now
- double result = s12High + (prod1Low + prod2Low + s12Low);
-
- if (Double.isNaN(result)) {
- // either we have split infinite numbers or some coefficients were
NaNs,
- // just rely on the naive implementation and let IEEE754 handle
this
- result = a1 * b1 + a2 * b2;
- }
-
- return result;
+ // 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);
}
/**
@@ -180,57 +139,22 @@ public final class LinearCombination {
public static double value(double a1, double b1,
double a2, double b2,
double a3, double b3) {
- // split a1 and b1 as one 26 bits number and one 27 bits number
- final double a1High = highPart(a1);
- final double a1Low = a1 - a1High;
- final double b1High = highPart(b1);
- final double b1Low = b1 - b1High;
-
- // accurate multiplication a1 * b1
- final double prod1High = a1 * b1;
- final double prod1Low = prodLow(a1Low, b1Low, prod1High, a1High,
b1High);
-
- // split a2 and b2 as one 26 bits number and one 27 bits number
- final double a2High = highPart(a2);
- final double a2Low = a2 - a2High;
- final double b2High = highPart(b2);
- final double b2Low = b2 - b2High;
-
- // accurate multiplication a2 * b2
- final double prod2High = a2 * b2;
- final double prod2Low = prodLow(a2Low, b2Low, prod2High, a2High,
b2High);
-
- // split a3 and b3 as one 26 bits number and one 27 bits number
- final double a3High = highPart(a3);
- final double a3Low = a3 - a3High;
- final double b3High = highPart(b3);
- final double b3Low = b3 - b3High;
-
- // accurate multiplication a3 * b3
- final double prod3High = a3 * b3;
- final double prod3Low = prodLow(a3Low, b3Low, prod3High, a3High,
b3High);
-
- // accurate addition a1 * b1 + a2 * b2
- final double s12High = prod1High + prod2High;
- final double s12Prime = s12High - prod2High;
- final double s12Low = (prod2High - (s12High - s12Prime)) +
(prod1High - s12Prime);
-
- // accurate addition a1 * b1 + a2 * b2 + a3 * b3
- final double s123High = s12High + prod3High;
- final double s123Prime = s123High - prod3High;
- final double s123Low = (prod3High - (s123High - s123Prime)) +
(s12High - s123Prime);
-
- // final rounding, s123 may have suffered many cancellations, we try
- // to recover some bits from the extra words we have saved up to now
- double result = s123High + (prod1Low + prod2Low + prod3Low + s12Low +
s123Low);
-
- if (Double.isNaN(result)) {
- // either we have split infinite numbers or some coefficients were
NaNs,
- // just rely on the naive implementation and let IEEE754 handle
this
- result = a1 * b1 + a2 * b2 + a3 * b3;
- }
-
- return result;
+ // 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);
}
/**
@@ -252,95 +176,47 @@ public final class LinearCombination {
double a2, double b2,
double a3, double b3,
double a4, double b4) {
- // split a1 and b1 as one 26 bits number and one 27 bits number
- final double a1High = highPart(a1);
- final double a1Low = a1 - a1High;
- final double b1High = highPart(b1);
- final double b1Low = b1 - b1High;
-
- // accurate multiplication a1 * b1
- final double prod1High = a1 * b1;
- final double prod1Low = prodLow(a1Low, b1Low, prod1High, a1High,
b1High);
-
- // split a2 and b2 as one 26 bits number and one 27 bits number
- final double a2High = highPart(a2);
- final double a2Low = a2 - a2High;
- final double b2High = highPart(b2);
- final double b2Low = b2 - b2High;
-
- // accurate multiplication a2 * b2
- final double prod2High = a2 * b2;
- final double prod2Low = prodLow(a2Low, b2Low, prod2High, a2High,
b2High);
-
- // split a3 and b3 as one 26 bits number and one 27 bits number
- final double a3High = highPart(a3);
- final double a3Low = a3 - a3High;
- final double b3High = highPart(b3);
- final double b3Low = b3 - b3High;
-
- // accurate multiplication a3 * b3
- final double prod3High = a3 * b3;
- final double prod3Low = prodLow(a3Low, b3Low, prod3High, a3High,
b3High);
-
- // split a4 and b4 as one 26 bits number and one 27 bits number
- final double a4High = highPart(a4);
- final double a4Low = a4 - a4High;
- final double b4High = highPart(b4);
- final double b4Low = b4 - b4High;
-
- // accurate multiplication a4 * b4
- final double prod4High = a4 * b4;
- final double prod4Low = prodLow(a4Low, b4Low, prod4High, a4High,
b4High);
-
- // accurate addition a1 * b1 + a2 * b2
- final double s12High = prod1High + prod2High;
- final double s12Prime = s12High - prod2High;
- final double s12Low = (prod2High - (s12High - s12Prime)) +
(prod1High - s12Prime);
-
- // accurate addition a1 * b1 + a2 * b2 + a3 * b3
- final double s123High = s12High + prod3High;
- final double s123Prime = s123High - prod3High;
- final double s123Low = (prod3High - (s123High - s123Prime)) +
(s12High - s123Prime);
-
- // accurate addition a1 * b1 + a2 * b2 + a3 * b3 + a4 * b4
- final double s1234High = s123High + prod4High;
- final double s1234Prime = s1234High - prod4High;
- final double s1234Low = (prod4High - (s1234High - s1234Prime)) +
(s123High - s1234Prime);
-
- // final rounding, s1234 may have suffered many cancellations, we try
- // to recover some bits from the extra words we have saved up to now
- double result = s1234High + (prod1Low + prod2Low + prod3Low + prod4Low
+ s12Low + s123Low + s1234Low);
-
- if (Double.isNaN(result)) {
- // either we have split infinite numbers or some coefficients were
NaNs,
- // just rely on the naive implementation and let IEEE754 handle
this
- result = a1 * b1 + a2 * b2 + a3 * b3 + a4 * b4;
- }
-
- return result;
+ // 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);
}
/**
- * @param value Value.
- * @return the high part of the value.
- */
- private static double highPart(double value) {
- return Double.longBitsToDouble(Double.doubleToRawLongBits(value) &
((-1L) << 27));
- }
-
- /**
- * @param aLow Low part of first factor.
- * @param bLow Low part of second factor.
- * @param prodHigh Product of the factors.
- * @param aHigh High part of first factor.
- * @param bHigh High part of second factor.
- * @return <code>aLow * bLow - (((prodHigh - aHigh * bHigh) - aLow *
bHigh) - aHigh * bLow)</code>
+ * 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 prodLow(double aLow,
- double bLow,
- double prodHigh,
- double aHigh,
- double bHigh) {
- return aLow * bLow - (((prodHigh - aHigh * bHigh) - aLow * bHigh) -
aHigh * bLow);
+ 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-arrays/src/test/java/org/apache/commons/numbers/arrays/ExtendedPrecisionTest.java
b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/ExtendedPrecisionTest.java
new file mode 100644
index 0000000..262d6c6
--- /dev/null
+++
b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/ExtendedPrecisionTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.arrays;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test cases for the {@link ExtendedPrecision} class.
+ */
+class ExtendedPrecisionTest {
+ @Test
+ void testSplitAssumptions() {
+ // The multiplier used to split the double value into high and low
parts.
+ final double scale = (1 << 27) + 1;
+ // The upper limit above which a number may overflow during the split
into a high part.
+ final double limit = 0x1.0p996;
+ Assertions.assertTrue(Double.isFinite(limit * scale));
+ Assertions.assertTrue(Double.isFinite(-limit * scale));
+ // Cannot make the limit the next power up
+ Assertions.assertEquals(Double.POSITIVE_INFINITY, limit * 2 * scale);
+ Assertions.assertEquals(Double.NEGATIVE_INFINITY, -limit * 2 * scale);
+ // Check the level for the safe upper limit of the exponent of the sum
of the absolute
+ // components of the product
+ Assertions.assertTrue(Math.getExponent(2 *
Math.sqrt(Double.MAX_VALUE)) - 2 > 508);
+ }
+
+ @Test
+ void testHighPartUnscaled() {
+ Assertions.assertEquals(Double.NaN,
ExtendedPrecision.highPartUnscaled(Double.POSITIVE_INFINITY));
+ Assertions.assertEquals(Double.NaN,
ExtendedPrecision.highPartUnscaled(Double.NEGATIVE_INFINITY));
+ Assertions.assertEquals(Double.NaN,
ExtendedPrecision.highPartUnscaled(Double.NaN));
+ // Large finite numbers will overflow during the split
+ Assertions.assertEquals(Double.NaN,
ExtendedPrecision.highPartUnscaled(Double.MAX_VALUE));
+ Assertions.assertEquals(Double.NaN,
ExtendedPrecision.highPartUnscaled(-Double.MAX_VALUE));
+ }
+
+ /**
+ * Test {@link ExtendedPrecision#productLow(double, double, double)}
computes the same
+ * result as JDK 9 Math.fma(x, y, -x * y) for edge cases.
+ */
+ @Test
+ void testProductLow() {
+ assertProductLow(0.0, 1.0, Math.nextDown(Double.MIN_NORMAL));
+ assertProductLow(0.0, -1.0, Math.nextDown(Double.MIN_NORMAL));
+ assertProductLow(Double.NaN, 1.0, Double.POSITIVE_INFINITY);
+ assertProductLow(Double.NaN, 1.0, Double.NEGATIVE_INFINITY);
+ assertProductLow(Double.NaN, 1.0, Double.NaN);
+ assertProductLow(0.0, 1.0, Double.MAX_VALUE);
+ assertProductLow(Double.NaN, 2.0, Double.MAX_VALUE);
+ }
+
+ private static void assertProductLow(double expected, double x, double y) {
+ // Requires a delta of 0.0 to assert -0.0 == 0.0
+ Assertions.assertEquals(expected, ExtendedPrecision.productLow(x, y, x
* y), 0.0);
+ }
+
+ @Test
+ void testIsNotNormal() {
+ for (double a : new double[] {Double.MAX_VALUE, 1.0,
Double.MIN_NORMAL}) {
+ Assertions.assertFalse(ExtendedPrecision.isNotNormal(a));
+ Assertions.assertFalse(ExtendedPrecision.isNotNormal(-a));
+ }
+ for (double a : new double[] {Double.POSITIVE_INFINITY, 0.0,
+ Math.nextDown(Double.MIN_NORMAL),
Double.NaN}) {
+ Assertions.assertTrue(ExtendedPrecision.isNotNormal(a));
+ Assertions.assertTrue(ExtendedPrecision.isNotNormal(-a));
+ }
+ }
+
+ /**
+ * This demonstrates splitting a sub normal number with no information in
the upper 26 bits
+ * of the mantissa.
+ */
+ @Test
+ void testSubNormalSplit() {
+ final double a = Double.longBitsToDouble(1L << 25);
+
+ // A split using masking of the mantissa bits computes the high part
incorrectly
+ final double hi1 =
Double.longBitsToDouble(Double.doubleToRawLongBits(a) & ((-1L) << 27));
+ final double lo1 = a - hi1;
+ Assertions.assertEquals(0, hi1);
+ Assertions.assertEquals(a, lo1);
+ Assertions.assertFalse(Math.abs(hi1) > Math.abs(lo1));
+
+ // Dekker's split
+ final double hi2 = ExtendedPrecision.highPartUnscaled(a);
+ final double lo2 = a - hi2;
+ Assertions.assertEquals(a, hi2);
+ Assertions.assertEquals(0, lo2);
+ Assertions.assertTrue(Math.abs(hi2) > Math.abs(lo2));
+ }
+}
diff --git
a/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/LinearCombinationTest.java
b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/LinearCombinationTest.java
index 6711b0c..855c46c 100644
---
a/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/LinearCombinationTest.java
+++
b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/LinearCombinationTest.java
@@ -27,6 +27,12 @@ import org.apache.commons.numbers.fraction.BigFraction;
* 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() {
@@ -147,7 +153,7 @@ class LinearCombinationTest {
}
@Test
- void testInfinite() {
+ void testNonFinite() {
final double[][] a = new double[][] {
{1, 2, 3, 4},
{1, Double.POSITIVE_INFINITY, 3, 4},
@@ -156,7 +162,10 @@ class LinearCombinationTest {
{1, 2, 3, 4},
{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},
@@ -166,135 +175,232 @@ class LinearCombinationTest {
{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}
+ {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]),
- 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
- Assertions.assertEquals(22, LinearCombination.value(a[0], b[0]),
1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
- Assertions.assertEquals(Double.NEGATIVE_INFINITY,
LinearCombination.value(a[1], b[1]), 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
- Assertions.assertEquals(Double.POSITIVE_INFINITY,
LinearCombination.value(a[2], b[2]), 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
- Assertions.assertEquals(Double.NEGATIVE_INFINITY,
LinearCombination.value(a[3], b[3]), 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
- Assertions.assertEquals(Double.POSITIVE_INFINITY,
LinearCombination.value(a[4], b[4]), 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
- Assertions.assertEquals(Double.POSITIVE_INFINITY,
LinearCombination.value(a[5], b[5]), 1e-10);
+ 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]),
- 1e-10);
+ 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]),
- 1e-10);
- Assertions.assertTrue(Double.isNaN(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.assertTrue(Double.isNaN(LinearCombination.value(a[6],
b[6])));
-
- Assertions.assertTrue(Double.isNaN(LinearCombination.value(a[7][0],
b[7][0],
- a[7][1],
b[7][1])));
- Assertions.assertTrue(Double.isNaN(LinearCombination.value(a[7][0],
b[7][0],
- a[7][1],
b[7][1],
- a[7][2],
b[7][2])));
- Assertions.assertTrue(Double.isNaN(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.assertTrue(Double.isNaN(LinearCombination.value(a[7],
b[7])));
+ 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/src/changes/changes.xml b/src/changes/changes.xml
index 0f93d3b..d58be94 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -68,6 +68,11 @@ Apache Commons Numbers 1.0-beta1 contains the following
library modules:
commons-numbers-quaternion (requires Java 8+)
commons-numbers-rootfinder (requires Java 8+)
">
+ <action dev="aherbert" type="update" issue="NUMBERS-142" due-to="Alex
Herbert">
+ "LinearCombination": Update to use the dot2s algorithm. Avoids
construction of an
+ intermediate array for array dot products. Update the hi-lo splitting
algorithm
+ to use Dekker's split to ensure the product round-off is computed to
exact precision.
+ </action>
<action dev="aherbert" type="fix" issue="NUMBERS-150" due-to="Jin Xu">
"Fraction/BigFraction": Fixed pow(int) to handle Integer.MIN_VALUE and
throw
ArithmeticException for negative exponents to a fraction of zero.