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