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-rng.git
The following commit(s) were added to refs/heads/master by this push: new 81b07fa RNG-145: ContinuousUniformSampler to support an open bound 81b07fa is described below commit 81b07fadbfba6d6afe7bb74ec3e8a5a43a40c343 Author: aherbert <aherb...@apache.org> AuthorDate: Mon Jun 14 16:53:27 2021 +0100 RNG-145: ContinuousUniformSampler to support an open bound --- .../distribution/ContinuousUniformSampler.java | 83 +++++++++++++++++++++- .../rng/sampling/distribution/InternalUtils.java | 26 +++++++ .../distribution/ContinuousUniformSamplerTest.java | 68 +++++++++++++++++- 3 files changed, 175 insertions(+), 2 deletions(-) diff --git a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/ContinuousUniformSampler.java b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/ContinuousUniformSampler.java index feaddc2..6a8a9a9 100644 --- a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/ContinuousUniformSampler.java +++ b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/ContinuousUniformSampler.java @@ -36,6 +36,30 @@ public class ContinuousUniformSampler private final UniformRandomProvider rng; /** + * Specialization to sample from an open interval {@code (lo, hi)}. + */ + private static class OpenIntervalContinuousUniformSampler extends ContinuousUniformSampler { + /** + * @param rng Generator of uniformly distributed random numbers. + * @param lo Lower bound. + * @param hi Higher bound. + */ + OpenIntervalContinuousUniformSampler(UniformRandomProvider rng, double lo, double hi) { + super(rng, lo, hi); + } + + @Override + double getU() { + return InternalUtils.nextDouble01(getRng()); + } + + @Override + public SharedStateContinuousSampler withUniformRandomProvider(UniformRandomProvider rng) { + return new OpenIntervalContinuousUniformSampler(rng, getLo(), getHi()); + } + } + + /** * @param rng Generator of uniformly distributed random numbers. * @param lo Lower bound. * @param hi Higher bound. @@ -52,10 +76,47 @@ public class ContinuousUniformSampler /** {@inheritDoc} */ @Override public double sample() { - final double u = rng.nextDouble(); + final double u = getU(); return u * hi + (1 - u) * lo; } + /** + * Gets the uniform deviate {@code u} the interval 0 to 1. + * The interval may be open or closed depending on the implementation. + * + * @return u + */ + double getU() { + return rng.nextDouble(); + } + + /** + * Gets the lower bound. This is deliberately scoped as package private. + * + * @return the lower bound + */ + double getLo() { + return lo; + } + + /** + * Gets the higher bound. This is deliberately scoped as package private. + * + * @return the higher bound + */ + double getHi() { + return hi; + } + + /** + * Gets the RNG. This is deliberately scoped as package private. + * + * @return the rng + */ + UniformRandomProvider getRng() { + return rng; + } + /** {@inheritDoc} */ @Override public String toString() { @@ -86,4 +147,24 @@ public class ContinuousUniformSampler double hi) { return new ContinuousUniformSampler(rng, lo, hi); } + + /** + * Creates a new continuous uniform distribution sampler. + * The bounds can be optionally excluded. + * + * @param rng Generator of uniformly distributed random numbers. + * @param lo Lower bound. + * @param hi Higher bound. + * @param excludeBounds Set to {@code true} to use the open interval {@code (lower, upper)}. + * @return the sampler + * @since 1.4 + */ + public static SharedStateContinuousSampler of(UniformRandomProvider rng, + double lo, + double hi, + boolean excludeBounds) { + return excludeBounds ? + new OpenIntervalContinuousUniformSampler(rng, lo, hi) : + new ContinuousUniformSampler(rng, lo, hi); + } } diff --git a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/InternalUtils.java b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/InternalUtils.java index af4c049..02eed2f 100644 --- a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/InternalUtils.java +++ b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/InternalUtils.java @@ -38,6 +38,13 @@ final class InternalUtils { // Class is package-private on purpose; do not make /** The first array index with a non-zero log factorial. */ private static final int BEGIN_LOG_FACTORIALS = 2; + /** + * The multiplier to convert the least significant 53-bits of a {@code long} to a {@code double}. + * See {@link #makeDouble(long)} and {@link #makeDouble(int, int)}. + * + * <p>This is equivalent to 1.0 / (1L << 53). + */ + private static final double DOUBLE_MULTIPLIER = 0x1.0p-53d; /** Utility class. */ private InternalUtils() {} @@ -119,6 +126,25 @@ final class InternalUtils { // Class is package-private on purpose; do not make } /** + * Gets a uniform random variable in the open interval {@code (0, 1)}. + * + * @param rng Generator of uniformly distributed random numbers. + * @return u + */ + static double nextDouble01(UniformRandomProvider rng) { + // See o.a.c.rng.core.util.NumberFactory.createDouble(long) + // Require the least significant 53-bits so shift the higher bits across + final long x = rng.nextLong() >>> 11; + if (x == 0) { + // Recursive call will create a stack overflow if the random generator + // is broken (always returns 0-bits). The alternative would be an infinite loop. + return nextDouble01(rng); + } + // Generate a double in the 2^53-1 dyadic rationals in (0, 1). + return x * DOUBLE_MULTIPLIER; + } + + /** * Class for computing the natural logarithm of the factorial of {@code n}. * It allows to allocate a cache of precomputed values. * In case of cache miss, computation is performed by a call to diff --git a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/ContinuousUniformSamplerTest.java b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/ContinuousUniformSamplerTest.java index 59f12c2..f1cf6f9 100644 --- a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/ContinuousUniformSamplerTest.java +++ b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/ContinuousUniformSamplerTest.java @@ -17,6 +17,7 @@ package org.apache.commons.rng.sampling.distribution; import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.core.source64.SplitMix64; import org.apache.commons.rng.sampling.RandomAssert; import org.apache.commons.rng.simple.RandomSource; import org.junit.Assert; @@ -50,17 +51,82 @@ public class ContinuousUniformSamplerTest { } /** + * Test the sampler excludes the bounds when the underlying generator returns long values + * that produce the limit of the uniform double output. + */ + @Test + public void testExcludeBounds() { + // A broken RNG that will return in an alternating sequence from 0 up or -1 down. + // This is either zero bits or all the bits + final UniformRandomProvider rng = new SplitMix64(0L) { + private long l1; + private long l2; + @Override + public long nextLong() { + if (l1 > l2) { + l2++; + // Descending sequence: -1, -2, -3, ... + return -l2; + } + // Ascending sequence: 0, 1, 2, ... + l1++; + // Shift by 11 bits to reverse the shift performed when computing the next + // double from a long. + return l1 << 11; + } + }; + final double low = 3.18; + final double high = 5.23; + final SharedStateContinuousSampler sampler = + ContinuousUniformSampler.of(rng, low, high, true); + // Test the sampler excludes the end points + for (int i = 0; i < 10; i++) { + final double value = sampler.sample(); + Assert.assertTrue("Value not in range", value >= low && value <= high); + } + } + + /** * Test the SharedStateSampler implementation. */ @Test public void testSharedStateSampler() { + testSharedStateSampler(false); + testSharedStateSampler(true); + } + + /** + * Test the SharedStateSampler implementation. + * + * @param excludedBounds Set to true to exclude the bounds. + */ + private static void testSharedStateSampler(boolean excludedBounds) { final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L); final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L); final double low = 1.23; final double high = 4.56; final SharedStateContinuousSampler sampler1 = - ContinuousUniformSampler.of(rng1, low, high); + ContinuousUniformSampler.of(rng1, low, high, excludedBounds); final SharedStateContinuousSampler sampler2 = sampler1.withUniformRandomProvider(rng2); RandomAssert.assertProduceSameSequence(sampler1, sampler2); } + + /** + * Test the sampler implementation with bounds excluded matches that with bounds included + * when the generator does not produce the limit of the uniform double output. + */ + @Test + public void testSamplerWithBoundsExcluded() { + // SplitMix64 only returns zero once in the output. Seeded with zero it outputs zero + // at the end of the period. + final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L); + final double low = 1.23; + final double high = 4.56; + final SharedStateContinuousSampler sampler1 = + ContinuousUniformSampler.of(rng1, low, high, false); + final SharedStateContinuousSampler sampler2 = + ContinuousUniformSampler.of(rng2, low, high, true); + RandomAssert.assertProduceSameSequence(sampler1, sampler2); + } }