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

asf-gitbox-commits 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 bcbc8f12 Add reference error test for DD.pow
bcbc8f12 is described below

commit bcbc8f12713dc7867d36b78c6f56a1b752241c44
Author: Alex Herbert <[email protected]>
AuthorDate: Mon Oct 19 23:52:39 2026 +0100

    Add reference error test for DD.pow
    
    Add alternative pow implementation to DDExt test class for comparison.
    The current implementation has lower RMS and max error.
---
 .../org/apache/commons/numbers/core/DDExt.java     | 166 +++++++++++++++++++++
 .../org/apache/commons/numbers/core/DDTest.java    |  88 ++++++++++-
 .../org/apache/commons/numbers/core/TestUtils.java |   4 +-
 3 files changed, 254 insertions(+), 4 deletions(-)

diff --git 
a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/DDExt.java 
b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/DDExt.java
index 133237c1..c3bbddd4 100644
--- 
a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/DDExt.java
+++ 
b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/DDExt.java
@@ -732,4 +732,170 @@ public final class DDExt {
         exp[0] = b * n + qb + rb + expi[0];
         return f;
     }
+
+    /**
+     * Compute number {@code x} raised to the power {@code n}.
+     *
+     * <p>Note: This is an alternative implementation to {@link DD#pow(int, 
long[])}.
+     * Accuracy is lower (see {@link DDTest#computePowError()}.
+     *
+     * <p>The value is returned as fractional {@code f} and integral
+     * {@code 2^exp} components.
+     * <pre>
+     * (x+xx)^n = (f+ff) * 2^exp
+     * </pre>
+     *
+     * <p>The combined fractional part (f, ff) is in the range {@code [0.5, 
1)}.
+     *
+     * <p>Special cases:
+     * <ul>
+     *  <li>If {@code (x, xx)} is zero the high part of the fractional part is
+     *      computed using {@link Math#pow(double, double) Math.pow(x, n)} and 
the exponent is 0.</li>
+     *  <li>If {@code n = 0} the fractional part is 0.5 and the exponent is 
1.</li>
+     *  <li>If {@code (x, xx)} is an exact power of 2 the fractional part is 
0.5 and the exponent
+     *      is the power of 2 minus 1.</li>
+     *  <li>If the result high-part is an exact power of 2 and the low-part 
has an opposite
+     *      signed non-zero magnitude then the fraction high-part {@code f} 
will be {@code +/-1} such that
+     *      the double-double number is in the range {@code [0.5, 1)}.</li>
+     *  <li>If the argument is not finite then a fractional representation is 
not possible.
+     *      In this case the fraction and the scale factor is undefined.</li>
+     * </ul>
+     *
+     * <p>The computed result is approximately {@code 16 * (n - 1) * eps} of 
the exact result
+     * where eps is 2<sup>-106</sup>.
+     *
+     * @param x Number.
+     * @param n Power.
+     * @param exp Result power of two scale factor (integral exponent).
+     * @return Fraction part.
+     * @see DD#frexp(int[])
+     */
+    public static DD pow(DD x, int n, long[] exp) {
+        // Edge cases.
+        if (n == 0) {
+            exp[0] = 1;
+            return DD.of(0.5, 0);
+        }
+        // IEEE result for non-finite or zero
+        if (!Double.isFinite(x.hi()) || x.hi() == 0) {
+            exp[0] = 0;
+            return DD.of(Math.pow(x.hi(), n), 0);
+        }
+        // Here the number is non-zero finite
+        final int[] ie = {0};
+        DD f = x.frexp(ie);
+        final long b = ie[0];
+        // Handle exact powers of 2
+        if (Math.abs(f.hi()) == 0.5 && f.lo() == 0) {
+            // (f * 2^b)^n = (2f)^n * 2^(b-1)^n
+            // Use Math.pow to create the sign.
+            // Note the result must be scaled to the fractional representation
+            // by multiplication by 0.5 and addition of 1 to the exponent.
+            final double y0 = 0.5 * Math.pow(2 * f.hi(), n);
+            // Propagate sign change (y0*f.hi()) to the original zero 
(this.lo())
+            final double y1 = Math.copySign(0.0, y0 * f.hi() * x.lo());
+            exp[0] = 1 + (b - 1) * n;
+            return DD.of(y0, y1);
+        }
+        if (n < 0) {
+            f = computePowScaled(b, f.hi(), f.lo(), -n, exp);
+            // Result is a non-zero fraction part so inversion is safe
+            f = DD.of(f.hi(), f.lo()).reciprocal();
+            // Rescale to [0.5, 1.0)
+            f = f.frexp(ie);
+            exp[0] = ie[0] - exp[0];
+            return f;
+        }
+        return computePowScaled(b, f.hi(), f.lo(), n, exp);
+    }
+
+    /**
+     * Compute the number {@code x} (non-zero finite) raised to the power 
{@code n}.
+     *
+     * <p>The input power is treated as an unsigned integer. Thus the negative 
value
+     * {@link Integer#MIN_VALUE} is 2^31.
+     *
+     * @param b Integral component 2^exp of x.
+     * @param x Fractional high part of x.
+     * @param xx Fractional low part of x.
+     * @param n Power (in [2, 2^31]).
+     * @param exp Result power of two scale factor (integral exponent).
+     * @return Fraction part.
+     */
+    private static DD computePowScaled(long b, double x, double xx, int n, 
long[] exp) {
+        // Compute the power by multiplication (keeping track of the scale):
+        // 13 = 1101
+        // x^13 = x^8 * x^4 * x^1
+        //
+        // 21 = 10101
+        // x^21 = x^16 * x^4 * x^1
+        //
+        // 1. Initialise result r as 1
+        // 2. Initialise exponent nn as n
+        // 3. Initialise square x2 as x
+        // 4. while remaining bits (0 or 1):
+        //    - if the lowest bit is 1 then multiply r by x2
+        //    - consume bit and if none left stop
+        //    - square x2
+
+        long re = 0;
+        DD r = DD.ONE;
+
+        // Initialise the square as x^1. Represented as 2^fe * f.
+        // Scale the input in [0.5, 1) to be above 1 so multiplication 
increases magnitude.
+        long fe = b - 1;
+        double f0 = x * 2;
+        double f1 = xx * 2;
+
+        double u;
+        double v;
+        double w;
+
+        int nn = n;
+        while (true) {
+            if ((nn & 0x1) != 0) {
+                // r = r * x2
+                r = r.multiply(DD.of(f0, f1));
+                re += fe;
+                if (Math.abs(r.hi()) > SAFE_MULTIPLY) {
+                    // Scale back to the [1, 2) range. As safe multiply is 
2^500
+                    // the exponent should be < 1001 so the twoPow scaling 
factor is supported.
+                    final int e = Math.getExponent(r.hi());
+                    final double s = DD.twoPow(-e);
+                    re += e;
+                    r = DD.of(r.hi() * s, r.lo() * s);
+                }
+
+            }
+            nn >>>= 1;
+            if (nn == 0) {
+                break;
+            }
+            // Square x2
+            // Inline multiply(f0, f1, f0, f1), adapted for squaring
+            fe <<= 1;
+            u = f0 * f0;
+            v = DD.twoSquareLow(f0, u);
+            // Inline fastTwoSum(hi, lo + (2 * f0 * f1), f)
+            w = v + (2 * f0 * f1);
+            f0 = u + w;
+            f1 = DD.fastTwoSumLow(u, w, f0);
+            // Rescale
+            if (Math.abs(f0) > SAFE_MULTIPLY) {
+                // Scale back to the [1, 2) range. As safe multiply is 2^500
+                // the exponent should be < 1001 so the twoPow scaling factor 
is supported.
+                final int e = Math.getExponent(f0);
+                final double s = DD.twoPow(-e);
+                fe += e;
+                f0 *= s;
+                f1 *= s;
+            }
+        }
+
+        // Normalise the return
+        final int[] e = {0};
+        r = r.frexp(e);
+        exp[0] = re + e[0];
+        return r;
+    }
 }
diff --git 
a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/DDTest.java
 
b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/DDTest.java
index 06b047e2..467b93ac 100644
--- 
a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/DDTest.java
+++ 
b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/DDTest.java
@@ -3033,6 +3033,29 @@ class DDTest {
         //    a -> a.abs().sqrt(MC_DIVIDE), a -> a.abs().sqrt(), a -> 
DDExt.sqrt(a.abs()));
     }
 
+    /**
+     * Compute the max and mean error of scaled power operations.
+     * Note: This is not a test and is included for reference.
+     */
+    @Test
+    @org.junit.jupiter.api.Disabled("This fixture creates reference data")
+    void computePowError() {
+        final int n = 10_000;
+        final int samples = 1_000_000;
+        final UniformRandomProvider rng = createRNG();
+        final DD[] dd = IntStream.range(0, n).mapToObj(i ->
+            signedNormalDoubleDouble(rng)
+        ).toArray(DD[]::new);
+        final BigDecimal[] bd = 
Arrays.stream(dd).map(DD::bigDecimalValue).toArray(BigDecimal[]::new);
+        // BigDecimal power supported up to 2^(2^29)
+        for (int b = 2; b < 29; b++) {
+            computePowError(rng, dd, bd, Math.min(n, samples), b);
+        }
+        for (int b = 2; b < 29; b++) {
+            computePowError(rng, dd, bd, Math.min(n, samples), -b);
+        }
+    }
+
     /**
      * Compute the max and mean error of the operators. This method supports 
two operators
      * so they can be compared to a reference implementation.
@@ -3042,7 +3065,7 @@ class DDTest {
      * @param bd DD data converted to BigDecimal
      * @param samples Number of samples
      * @param operatorName Operator name
-     * @param expected Operator to compute the expected result
+     * @param reference Operator to compute the expected result
      * @param op1 First operator
      * @param op2 Second operator
      */
@@ -3083,7 +3106,7 @@ class DDTest {
      * @param bd DD data converted to BigDecimal
      * @param samples Number of samples
      * @param operatorName Operator name
-     * @param expected Operator to compute the expected result
+     * @param reference Operator to compute the expected result
      * @param op1 First operator
      * @param op2 Second operator
      */
@@ -3116,6 +3139,67 @@ class DDTest {
             1e-9 * time);
     }
 
+    /**
+     * Compute the max and mean error of the pow operator.
+     * The javadoc states 16 * (n - 1) eps. This is an upper error bound.
+     * This reports RMS error ~ n/4; Max error ~ 2n.
+     *
+     * <p>The alternative implementation is {@link DDExt#pow(DD, int, 
long[])}. This computes
+     * the result starting at 1 and multiplying by increasing powers of 2 if 
required. The error is
+     * worse than the DD power implementation which starts at x^1 and 
multiplies by x if required,
+     * squaring the current result each iteration. Due to pre-computation of 
factors to multiply
+     * by x, the current DD implementation is also faster.
+     *
+     * <p>This method can be used to show DDMath.pow is accurate to ~ 0.5 eps.
+     *
+     * @param rng Source of randomness
+     * @param dd DD data
+     * @param bd DD data converted to BigDecimal
+     * @param samples Number of samples
+     * @param b Maximum bit in the power
+     */
+    private static void computePowError(UniformRandomProvider rng,
+        DD[] dd, BigDecimal[] bd, int samples, int b) {
+        final int absB = Math.abs(b);
+        Assertions.assertTrue(-absB >= -30, "b is not an exponent; exponent is 
2^b");
+        final ErrorStatistics e1 = new ErrorStatistics();
+        final ErrorStatistics e2 = new ErrorStatistics();
+        final long start = System.nanoTime();
+        final int[] count = {0};
+        final int base = 1 << absB;
+        final IntSupplier next = samples == dd.length ? () -> count[0]++ : () 
-> rng.nextInt(dd.length);
+        final long[] exp = {0};
+        final MathContext mc = MathContext.DECIMAL128;
+        final BigDecimal two = BigDecimal.valueOf(2);
+        final BigDecimal half = new BigDecimal(0.5);
+        for (int n = 0; n < samples; n++) {
+            final int i = next.getAsInt();
+            int y = base + rng.nextInt(base);
+            if (b < 0) {
+                y = -y;
+            }
+            BigDecimal expected = bd[i].pow(y, mc);
+            final DD actual1 = dd[i].pow(y, exp);
+            e1.add(TestUtils.assertScaledEquals(expected, actual1, exp[0], 
-1.0, () -> "DD.pow"));
+            final DD actual2 = DDExt.pow(dd[i], y, exp);
+            // Max ULP error ~ 0.55:
+            // final DD actual2 = DDMath.pow(dd[i], y, exp);
+            e2.add(TestUtils.assertScaledEquals(expected, actual2, exp[0], 
-1.0, () -> "DDExt.pow"));
+        }
+        final long time = System.nanoTime() - start;
+        final double rms1 = e1.getRMS() / EPS;
+        final double max1 = e1.getMaxAbs() / EPS;
+        final double rms2 = e2.getRMS() / EPS;
+        final double max2 = e2.getMaxAbs() / EPS;
+        // Report errors scaled by the maximum power
+        final double scale = Math.scalb(1.0, -absB - 1);
+        TestUtils.printf("pow 2^%-3d  %12.6g (%4.2f) %12.6g (%4.2f) %12.6g  
%12.6g (%4.2f) %12.6g (%4.2f) %12.6g   (%.3fs)%n",
+            b,
+            rms1, scale * rms1, max1, scale * max1, e1.getMean() / EPS,
+            rms2, scale * rms2, max2, scale * max2, e2.getMean() / EPS,
+            1e-9 * time);
+    }
+
     /**
      * Class to compute the error statistics.
      *
diff --git 
a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/TestUtils.java
 
b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/TestUtils.java
index f936a9c8..8615308e 100644
--- 
a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/TestUtils.java
+++ 
b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/TestUtils.java
@@ -218,9 +218,9 @@ public final class TestUtils {
         // e * 2^exp = expected
         // e = expected * 2^-exp
         if (exp < 0) {
-            e = expected.multiply(TWO.pow((int) -exp), MC_SCALING);
+            e = expected.multiply(TWO.pow((int) -exp, MC_SCALING), MC_SCALING);
         } else {
-            e = expected.multiply(HALF.pow((int) exp), MC_SCALING);
+            e = expected.multiply(HALF.pow((int) exp, MC_SCALING), MC_SCALING);
         }
         return TestUtils.assertEquals(e, actual, eps, () -> prefix(msg) + 
"scale=2^" + exp);
     }

Reply via email to