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


The following commit(s) were added to refs/heads/master by this push:
     new c961b76e RNG-201: Limit the max mean to the    LargeMeanPoissonSampler 
support
c961b76e is described below

commit c961b76e7109cc40216b4f46d49b28c3d5fdd3c2
Author: Alex Herbert <[email protected]>
AuthorDate: Tue Aug 25 09:42:42 2026 +0100

    RNG-201: Limit the max mean to the    LargeMeanPoissonSampler support
---
 .../distribution/LargeMeanPoissonSampler.java      |  2 +-
 .../sampling/distribution/PoissonSamplerCache.java | 38 ++++++---
 .../distribution/PoissonSamplerCacheTest.java      | 98 +++++++++++++++++-----
 src/changes/changes.xml                            |  6 ++
 4 files changed, 111 insertions(+), 33 deletions(-)

diff --git 
a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/LargeMeanPoissonSampler.java
 
b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/LargeMeanPoissonSampler.java
index 10b95d1b..d995befe 100644
--- 
a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/LargeMeanPoissonSampler.java
+++ 
b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/LargeMeanPoissonSampler.java
@@ -46,7 +46,7 @@ import 
org.apache.commons.rng.sampling.distribution.InternalUtils.FactorialLog;
 public class LargeMeanPoissonSampler
     implements SharedStateDiscreteSampler {
     /** Upper bound to avoid truncation. */
-    private static final double MAX_MEAN = 0.5 * Integer.MAX_VALUE;
+    static final double MAX_MEAN = 0.5 * Integer.MAX_VALUE;
     /** Class to compute {@code log(n!)}. This has no cached values. */
     private static final InternalUtils.FactorialLog NO_CACHE_FACTORIAL_LOG;
     /** Used when there is no requirement for a small mean Poisson sampler. */
diff --git 
a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/PoissonSamplerCache.java
 
b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/PoissonSamplerCache.java
index 6198cbe3..27e97dcc 100644
--- 
a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/PoissonSamplerCache.java
+++ 
b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/PoissonSamplerCache.java
@@ -83,7 +83,8 @@ public class PoissonSamplerCache {
      *
      * @param minMean The minimum mean covered by the cache.
      * @param maxMean The maximum mean covered by the cache.
-     * @throws IllegalArgumentException if {@code maxMean < minMean}
+     * @throws IllegalArgumentException if {@code maxMean < minMean}; or
+     * {@code maxMean > 0.5 *} {@link Integer#MAX_VALUE}.
      */
     public PoissonSamplerCache(double minMean,
                                double maxMean) {
@@ -108,10 +109,15 @@ public class PoissonSamplerCache {
             maxN = 0;
             values = null;
         } else {
+            // After validation:
+            // min <= max
+            // max in [-infinity, MAX_MEAN]   (No NaN)
+            // min in [PIVOT, max]            (No NaN)
+
             // Convert the mean into integers.
             // Note the minimum is clipped to the algorithm switch point.
             this.minN = (int) Math.floor(Math.max(minMean, 
PoissonSampler.PIVOT));
-            this.maxN = (int) Math.floor(Math.min(maxMean, Integer.MAX_VALUE));
+            this.maxN = (int) Math.floor(maxMean);
             values = new LargeMeanPoissonSamplerState[maxN - minN + 1];
         }
     }
@@ -133,7 +139,8 @@ public class PoissonSamplerCache {
     }
 
     /**
-     * Check the mean range.
+     * Check the mean range. This may update the {@code minMean} to be a valid
+     * lower bound for the support.
      *
      * <p>This method exists to raise an exception before invocation of the
      * private constructor; this mitigates Finalizer attacks
@@ -141,8 +148,9 @@ public class PoissonSamplerCache {
      *
      * @param minMean The minimum mean covered by the cache.
      * @param maxMean The maximum mean covered by the cache.
-     * @return the minimum mean
-     * @throws IllegalArgumentException if {@code maxMean < minMean}
+     * @return the minimum for the mean support
+     * @throws IllegalArgumentException if {@code maxMean < minMean}; or
+     * {@code maxMean > 0.5 *} {@link Integer#MAX_VALUE}.
      */
     private static double checkMeanRange(double minMean, double maxMean) {
         // Note:
@@ -157,7 +165,14 @@ public class PoissonSamplerCache {
             throw new IllegalArgumentException(
                     "Max mean: " + maxMean + " < " + minMean);
         }
-        return minMean;
+
+        // The maximum mean support must be the same as the support of
+        // the Poisson sampler. As stated above there is no lower bound on the 
mean.
+        InternalUtils.requireRangeClosed(Double.NEGATIVE_INFINITY,
+            LargeMeanPoissonSampler.MAX_MEAN, maxMean, "Max mean");
+
+        // Handle a NaN value for the min; negatives are allowed.
+        return Double.isNaN(minMean) ? PoissonSampler.PIVOT : minMean;
     }
 
     /**
@@ -170,7 +185,7 @@ public class PoissonSamplerCache {
      * @param mean Mean.
      * @return A Poisson sampler
      * @throws IllegalArgumentException if {@code mean <= 0} or
-     * {@code mean >} {@link Integer#MAX_VALUE}.
+     * {@code mean > 0.5 *} {@link Integer#MAX_VALUE}.
      * @deprecated Use {@link #createSharedStateSampler(UniformRandomProvider, 
double)}.
      */
     @Deprecated
@@ -189,7 +204,7 @@ public class PoissonSamplerCache {
      * @param mean Mean.
      * @return A Poisson sampler
      * @throws IllegalArgumentException if {@code mean <= 0} or
-     * {@code mean >} {@link Integer#MAX_VALUE}.
+     * {@code mean > 0.5 *} {@link Integer#MAX_VALUE}.
      * @since 1.4
      */
     public SharedStateDiscreteSampler 
createSharedStateSampler(UniformRandomProvider rng,
@@ -347,7 +362,8 @@ public class PoissonSamplerCache {
      *
      * @param minMean The minimum mean covered by the cache.
      * @param maxMean The maximum mean covered by the cache.
-     * @throws IllegalArgumentException if {@code maxMean < minMean}
+     * @throws IllegalArgumentException if {@code maxMean < minMean}; or
+     * {@code maxMean > 0.5 *} {@link Integer#MAX_VALUE}.
      * @return the poisson sampler cache
      */
     public PoissonSamplerCache withRange(double minMean,
@@ -356,7 +372,7 @@ public class PoissonSamplerCache {
             // Nothing to reuse
             return new PoissonSamplerCache(minMean, maxMean);
         }
-        checkMeanRange(minMean, maxMean);
+        final double min = checkMeanRange(minMean, maxMean);
 
         // The cache can only be used for the LargeMeanPoissonSampler.
         if (maxMean < PoissonSampler.PIVOT) {
@@ -365,7 +381,7 @@ public class PoissonSamplerCache {
 
         // Convert the mean into integers.
         // Note the minimum is clipped to the algorithm switch point.
-        final int withMinN = (int) Math.floor(Math.max(minMean, 
PoissonSampler.PIVOT));
+        final int withMinN = (int) Math.floor(Math.max(min, 
PoissonSampler.PIVOT));
         final int withMaxN = (int) Math.floor(maxMean);
         final LargeMeanPoissonSamplerState[] states =
                 new LargeMeanPoissonSamplerState[withMaxN - withMinN + 1];
diff --git 
a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/PoissonSamplerCacheTest.java
 
b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/PoissonSamplerCacheTest.java
index 982caf8b..c30035ce 100644
--- 
a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/PoissonSamplerCacheTest.java
+++ 
b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/PoissonSamplerCacheTest.java
@@ -16,12 +16,15 @@
  */
 package org.apache.commons.rng.sampling.distribution;
 
+import java.util.stream.DoubleStream;
 import org.apache.commons.rng.RestorableUniformRandomProvider;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.sampling.RandomAssert;
 import org.apache.commons.rng.simple.RandomSource;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 
 /**
  * This test checks the {@link PoissonSamplerCache} functions exactly like the
@@ -39,6 +42,30 @@ class PoissonSamplerCacheTest {
     /** The mid-point of the range of the mean */
     private final int midRange = (minRange + maxRange) / 2;
 
+
+    /**
+     * Return a stream of supported values for the min mean.
+     * All the values should be handled as if the min mean was 0.
+     * This is an invalid mean for a PoissonSampler but valid to bound
+     * the cache construction.
+     *
+     * @return the stream
+     */
+    static DoubleStream supportedMinMean() {
+        return DoubleStream.of(Double.NEGATIVE_INFINITY, Double.NaN, -1, 0);
+    }
+
+    /**
+     * Return a stream of unsupported values for the max mean.
+     * These values are invalid to create a cache or a PoissonSampler.
+     *
+     * @return the stream
+     */
+    static DoubleStream unsupportedMaxMean() {
+        return DoubleStream.of(Double.POSITIVE_INFINITY, Double.NaN,
+            Math.nextUp(LargeMeanPoissonSampler.MAX_MEAN));
+    }
+
     /**
      * Test the cache reports the minimum mean that uses an algorithm that 
supports caching.
      * This mean is the same level as the algorithm switch point in the 
PoissonSampler.
@@ -112,9 +139,9 @@ class PoissonSamplerCacheTest {
      * Test the cache can be created with a min range below 0.
      * In this case the range is truncated to 0.
      */
-    @Test
-    void testConstructorWhenMinBelow0() {
-        final double min = -1;
+    @ParameterizedTest
+    @MethodSource(value = {"supportedMinMean"})
+    void testConstructorWhenMinBelow0(double min) {
         final double max = PoissonSampler.PIVOT + 2;
         final PoissonSamplerCache cache = createPoissonSamplerCache(min, max);
         Assertions.assertTrue(cache.isValidRange());
@@ -137,6 +164,18 @@ class PoissonSamplerCacheTest {
         Assertions.assertEquals(0, cache.getMaxMean());
     }
 
+    /**
+     * Test the cache requires a range within the support of the
+     * PoissonSampler.
+     */
+    @ParameterizedTest
+    @MethodSource(value = {"unsupportedMaxMean"})
+    void testConstructorWhenMaxAbovePoissonSamplerSupport(double max) {
+        final double min = 0;
+        Assertions.assertThrows(IllegalArgumentException.class,
+            () -> createPoissonSamplerCache(min, max));
+    }
+
     /**
      * Test the cache can be created without a range that requires a cache.
      * In this case the cache will be a pass through to the constructor
@@ -200,9 +239,9 @@ class PoissonSamplerCacheTest {
      * Test the cache can be created with a min range below 0.
      * In this case the range is truncated to 0.
      */
-    @Test
-    void testWithRangeConstructorWhenMinBelow0() {
-        final double min = -1;
+    @ParameterizedTest
+    @MethodSource(value = {"supportedMinMean"})
+    void testWithRangeConstructorWhenMinBelow0(double min) {
         final double max = PoissonSampler.PIVOT + 2;
         final PoissonSamplerCache cache = 
createPoissonSamplerCache().withRange(min, max);
         Assertions.assertTrue(cache.isValidRange());
@@ -211,6 +250,33 @@ class PoissonSamplerCacheTest {
                                 cache.getMaxMean());
     }
 
+    /**
+     * Test the cache can be created with a max range below 0.
+     * In this case the range is truncated to 0, i.e. no cache.
+     */
+    @Test
+    void testWithRangeConstructorWhenMaxBelow0() {
+        final double min = -10;
+        final double max = -1;
+        final PoissonSamplerCache cache = 
createPoissonSamplerCache().withRange(min, max);
+        Assertions.assertFalse(cache.isValidRange());
+        Assertions.assertEquals(0, cache.getMinMean());
+        Assertions.assertEquals(0, cache.getMaxMean());
+    }
+
+    /**
+     * Test the cache requires a range within the support of the
+     * PoissonSampler.
+     */
+    @ParameterizedTest
+    @MethodSource(value = {"unsupportedMaxMean"})
+    void testWithRangeConstructorWhenMaxAbovePoissonSamplerSupport(double max) 
{
+        final double min = 0;
+        final PoissonSamplerCache cache = createPoissonSamplerCache();
+        Assertions.assertThrows(IllegalArgumentException.class,
+            () -> cache.withRange(min, max));
+    }
+
     /**
      * Test the cache can be created from a cache with no capacity.
      */
@@ -247,24 +313,14 @@ class PoissonSamplerCacheTest {
     /**
      * Test createSharedStateSampler() with a bad mean.
      *
-     * <p>Note this test actually tests the SmallMeanPoissonSampler throws.
-     */
-    @Test
-    void testCreateSharedStateSamplerThrowsWithZeroMean() {
-        final UniformRandomProvider rng = RandomAssert.seededRNG();
-        final PoissonSamplerCache cache = createPoissonSamplerCache();
-        Assertions.assertThrows(IllegalArgumentException.class,
-            () -> cache.createSharedStateSampler(rng, 0));
-    }
-
-    /**
-     * Test createSharedStateSampler() with a mean that is too large.
+     * <p>Note: The supported min mean values are valid for cache construction
+     * but invalid to create a sampler as they are all equivalent to zero.
      */
-    @Test
-    void testCreateSharedStateSamplerThrowsWithNonIntegerMean() {
+    @ParameterizedTest
+    @MethodSource(value = {"supportedMinMean", "unsupportedMaxMean"})
+    void testCreateSharedStateSamplerThrowsWithBadMean(double mean) {
         final UniformRandomProvider rng = RandomAssert.seededRNG();
         final PoissonSamplerCache cache = createPoissonSamplerCache();
-        final double mean = Integer.MAX_VALUE + 1.0;
         Assertions.assertThrows(IllegalArgumentException.class,
             () -> cache.createSharedStateSampler(rng, mean));
     }
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 62ce5b07..c7a76b93 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -56,6 +56,12 @@ If the output is not quite correct, check for invisible 
trailing spaces!
     <release version="1.8" date="TBD" description="
 New features, updates and bug fixes (requires Java 8).
 ">
+      <action dev="aherbert" type="fix" due-to="Alex Herbert" issue="RNG-201">
+        "PoissonSamplerCache": Limit the maximum mean to the same support
+        as the LargeMeanPoissonSampler. Means above the limit, or NaN, will 
throw
+        when constructing the cache. Existing behaviour to throw when
+        constructing a sampler from the cache is unchanged.
+      </action>
       <action dev="aherbert" type="update" due-to="Alex Herbert" 
issue="RNG-200">
         "JDKRandomBridge": Document lack of support for RandomSource instances
         that require constructor arguments in addition to the seed. This

Reply via email to