You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ah...@apache.org on 2021/06/14 15:53:31 UTC

[commons-rng] branch master updated: RNG-145: ContinuousUniformSampler to support an open bound

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 <ah...@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);
+    }
 }