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-statistics.git


The following commit(s) were added to refs/heads/master by this push:
     new 325a9b04 STATISTICS-99: Configurable allocation size for the exact p 
computation
325a9b04 is described below

commit 325a9b044cb9e0998aabd3cf81e999998cfeeb85
Author: Alex Herbert <[email protected]>
AuthorDate: Sat Aug 29 19:55:24 2026 +0100

    STATISTICS-99: Configurable allocation size for the exact p computation
---
 .../statistics/inference/MannWhitneyUTest.java     | 55 +++++++++++++++++++++-
 .../statistics/inference/MannWhitneyUTestTest.java | 28 ++++++++++-
 src/changes/changes.xml                            |  7 ++-
 3 files changed, 87 insertions(+), 3 deletions(-)

diff --git 
a/commons-statistics-inference/src/main/java/org/apache/commons/statistics/inference/MannWhitneyUTest.java
 
b/commons-statistics-inference/src/main/java/org/apache/commons/statistics/inference/MannWhitneyUTest.java
index 8807a80c..e7f32458 100644
--- 
a/commons-statistics-inference/src/main/java/org/apache/commons/statistics/inference/MannWhitneyUTest.java
+++ 
b/commons-statistics-inference/src/main/java/org/apache/commons/statistics/inference/MannWhitneyUTest.java
@@ -51,6 +51,12 @@ public final class MannWhitneyUTest {
      * modified when holding the lock. When the storage is determined to be 
the correct
      * size it can be returned for read/write to the array when not holding 
the lock. */
     private static SoftReference<double[][][]> cacheF = new 
SoftReference<>(null); // @GuardedBy("LOCK")
+    /** Limit on the tabulation byte allocation size for the exact
+     * p-value computation. The default 2^29, or 512 MiB, is larger than any
+     * tabulation created using the limits configured for the
+     * {@linkplain PValueMethod#AUTO auto} p-value mode. */
+    private static final long MAX_BYTES = Long.getLong(
+        "org.apache.commons.statistics.inference.MannWhitneyUTest.maxBytes", 
1L << 29);
     /** Default instance. */
     private static final MannWhitneyUTest DEFAULT = new MannWhitneyUTest(
         AlternativeHypothesis.TWO_SIDED, PValueMethod.AUTO, true, 0);
@@ -157,6 +163,11 @@ public final class MannWhitneyUTest {
     /**
      * Return an instance with the configured p-value method.
      *
+     * <p><strong>Note:</strong> Use of the {@link PValueMethod#EXACT} method 
may require
+     * a large memory allocation for the computation tables. This is limited 
to 512 MiB
+     * but can be increased using a system property. See the
+     * {@link #test(double[], double[])} documentation for details.
+     *
      * @param v Value.
      * @return an instance
      * @throws IllegalArgumentException if the value is not in the allowed 
options or is null
@@ -259,7 +270,12 @@ public final class MannWhitneyUTest {
      * or {@code min(n, m) <= 37} for any {@code max(n, m)}.
      * An {@link OutOfMemoryError} is not expected using the
      * limits configured for the {@linkplain PValueMethod#AUTO auto} p-value 
computation
-     * as the maximum required memory is approximately 23 MiB.
+     * as the maximum required memory is approximately 2.9 MiB.
+     * To avoid memory errors the tabulation size is limited to {@code 2^29} 
bytes
+     * (512 MiB); a <em>user-requested</em> exact computation that would
+     * exceed this limit reverts to the asymptotic approximation. The limit is
+     * configurable using the system property
+     * {@code 
org.apache.commons.statistics.inference.MannWhitneyUTest.maxBytes}.
      *
      * @param x First sample values.
      * @param y Second sample values.
@@ -409,6 +425,12 @@ public final class MannWhitneyUTest {
         final int n1 = Math.min(m, n);
         final int n2 = Math.max(m, n);
 
+        // Limit the size of the tabulation storage.
+        // Note: the CDF is computed using k <= min(u1, u2).
+        if (isOversizedAllocation(Math.min(u1, u2) + 1.0, n1 + 1.0, n2 + 1.0)) 
{
+            return -1;
+        }
+
         // Return the correct side:
         if (alternative == AlternativeHypothesis.GREATER_THAN) {
             // sf(u1 - 1)
@@ -546,6 +568,22 @@ public final class MannWhitneyUTest {
                 // and copying all old values.
                 final int sn = Math.max(n1, n + 1);
                 final int sk = Math.max(k1, k + 1);
+                // Growing combines the dimensions of the previous and 
requested
+                // storage; this can exceed the table size limit even when each
+                // individual request is below it. In that case discard the 
previous
+                // computation and allocate the requested size (which has been 
checked
+                // against the limit by the caller). Any other thread using the
+                // previous storage is not affected.
+                if (isOversizedAllocation(Math.max(m1, m + 1.0), sn, sk)) {
+                    f = new double[m + 1][n + 1][k + 1];
+                    for (final double[][] a : f) {
+                        for (final double[] b : a) {
+                            initialize(b);
+                        }
+                    }
+                    cacheF = new SoftReference<>(f);
+                    return f;
+                }
                 if (growM) {
                     // Entirely new region
                     f = Arrays.copyOf(f, m + 1);
@@ -587,6 +625,21 @@ public final class MannWhitneyUTest {
         }
     }
 
+    /**
+     * Test an allocation of an array of size [x][y][z] against the
+     * the configured maximum number of bytes.
+     *
+     * <p>Uses floating-point arguments to avoid overflow.
+     *
+     * @param x Size x (assumed to be positive)
+     * @param y Size y (assumed to be positive)
+     * @param z Size z (assumed to be positive)
+     * @return true if the byte size is too large
+     */
+    private static boolean isOversizedAllocation(double x, double y, double z) 
{
+        return x * y * z * Double.BYTES > MAX_BYTES;
+    }
+
     /**
      * Initialize the array for f(m, n, x).
      * Set value to 1 for x=0; otherwise {@link #UNSET}.
diff --git 
a/commons-statistics-inference/src/test/java/org/apache/commons/statistics/inference/MannWhitneyUTestTest.java
 
b/commons-statistics-inference/src/test/java/org/apache/commons/statistics/inference/MannWhitneyUTestTest.java
index f43b5189..28cf5c3f 100644
--- 
a/commons-statistics-inference/src/test/java/org/apache/commons/statistics/inference/MannWhitneyUTestTest.java
+++ 
b/commons-statistics-inference/src/test/java/org/apache/commons/statistics/inference/MannWhitneyUTestTest.java
@@ -350,6 +350,18 @@ class MannWhitneyUTestTest {
         Assertions.assertEquals(-1, MannWhitneyUTest.calculateExactPValue(1L 
<< 32, m, n, AlternativeHypothesis.TWO_SIDED));
     }
 
+    @Test
+    void testCalculateExactPValueTableSizeLimit() {
+        // The tabulation of f, of size (n1+1)*(n2+1)*(min(u1, u2)+1), is 
limited to a
+        // maximum size (2^26 values by default).
+        // Larger user-requested exact computations cannot be computed exactly 
and
+        // revert to the asymptotic approximation. binom(m + n, m) is finite 
for both.
+        Assertions.assertEquals(-1,
+            MannWhitneyUTest.calculateExactPValue(1234, 37, 1000000, 
AlternativeHypothesis.TWO_SIDED));
+        Assertions.assertEquals(-1,
+            MannWhitneyUTest.calculateExactPValue(125000, 500, 500, 
AlternativeHypothesis.TWO_SIDED));
+    }
+
     /**
      * Test the exact CDF computation.
      * This hits all edge cases for expanding the cache of f.
@@ -361,7 +373,7 @@ class MannWhitneyUTestTest {
     @Order(1)
     void testCDF(int u, int m, int n, double p) {
         // Use 'less than' to compute the wilcox distribution CDF(u)
-        TestUtils.assertProbability(p, 
MannWhitneyUTest.calculateExactPValue(u, m, n, 
AlternativeHypothesis.LESS_THAN), 1e-14, "p-value");
+        TestUtils.assertProbability(p, 
MannWhitneyUTest.calculateExactPValue(u, m, n, 
AlternativeHypothesis.LESS_THAN), 5e-14, "p-value");
     }
 
     static Stream<Arguments> testCDF() {
@@ -407,6 +419,20 @@ class MannWhitneyUTestTest {
         builder.add(Arguments.of(7890, 100, 100, 0.99999999999990418775));
         // 22.447 sec
         builder.add(Arguments.of(8901, 100, 100, 1.0));
+
+        // Coverage test for discarding the current f(m, n, k) table.
+        //
+        // Create an allowed allocation that in combination with
+        // the existing table size exceeds the max byte limit.
+        // Max size = 2^26 values = 67,108,864
+        // 601 * 331 * 331 = 65,846,161  (0.981184259)
+        // 601 * 341 * 321 = 65,786,061  (0.980288699)
+        // Combined size is too large:
+        // 601 * 341 * 331 = 67,835,471  (1.010827288)
+        // Expected results from R computed within milliseconds.
+        // These results have the highest relative error of the test cases.
+        builder.add(Arguments.of(600, 330, 330, 6.1399257003662415563e-173)); 
// rel.error 1.36e-14
+        builder.add(Arguments.of(600, 340, 320, 8.3097576360262207539e-173)); 
// rel.error 4.98e-14
         return builder.build();
     }
 
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index ef612916..fc14f6d5 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -53,12 +53,17 @@ If the output is not quite correct, check for invisible 
trailing spaces!
   </properties>
   <body>
     <release version="1.4" date="TBD" description="Adds new features (requires 
Java 8).">
+      <action dev="aherbert" type="fix" due-to="Security scan, Alex Herbert" 
issue="STATISTICS-99">
+        "MannWhitneyUTest": Limit the allocation size for the exact p-value
+        computation. The maximum byte allocation can be configured using a 
system
+        property. Behaviour using the default AUTO p-value computation is 
unchanged.
+      </action>
       <action dev="aherbert" type="update" due-to="Security scan, Alex 
Herbert">
         "KolmogorovSmirnovTest": Bound the maximum number of terms allowed 
during
         the exact p-value computation for the two-sample two-sided statistic 
with
         unequal sample sizes. The caller will fall-back to the aysymptotic
         computation to avoid an impractical computation. Behaviour using the
-        default AUTO limit is unchanged.
+        default AUTO p-value computation is unchanged.
       </action>
       <action dev="aherbert" type="update" due-to="Security scan, Alex 
Herbert">
         "ZipfDistribution": Update documentation on possible long runtimes when

Reply via email to