View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.rng.sampling.distribution;
18  
19  import java.util.Locale;
20  import org.apache.commons.rng.UniformRandomProvider;
21  import org.apache.commons.rng.core.source64.LongProvider;
22  import org.apache.commons.rng.core.source64.SplitMix64;
23  import org.apache.commons.rng.sampling.RandomAssert;
24  import org.junit.jupiter.api.Assertions;
25  import org.junit.jupiter.api.Test;
26  import org.junit.jupiter.params.ParameterizedTest;
27  import org.junit.jupiter.params.provider.ValueSource;
28  
29  /**
30   * Test for the {@link UniformLongSampler}. The tests hit edge cases for the sampler
31   * and demonstrates uniformity of output when the underlying RNG output is uniform.
32   *
33   * <p>Note: No statistical tests for uniformity are performed on the output. The tests
34   * are constructed on the premise that the underlying sampling methods correctly
35   * use the random bits from {@link UniformRandomProvider}. Correctness
36   * for a small range is tested against {@link UniformRandomProvider#nextLong(long)}
37   * and correctness for a large range is tested that the {@link UniformRandomProvider#nextLong()}
38   * is within the range limits. Power of two ranges are tested against a bit shift
39   * of a random long.
40   */
41  class UniformLongSamplerTest {
42      /**
43       * Test the constructor with a bad range.
44       */
45      @Test
46      void testConstructorThrowsWithLowerAboveUpper() {
47          final long upper = 55;
48          final long lower = upper + 1;
49          final UniformRandomProvider rng = RandomAssert.seededRNG();
50          Assertions.assertThrows(IllegalArgumentException.class,
51              () -> UniformLongSampler.of(rng, lower, upper));
52      }
53  
54      @Test
55      void testSamplesWithRangeOf1() {
56          final long upper = 99;
57          final long lower = upper;
58          final UniformRandomProvider rng = RandomAssert.createRNG();
59          final UniformLongSampler sampler = UniformLongSampler.of(rng, lower, upper);
60          for (int i = 0; i < 5; i++) {
61              Assertions.assertEquals(lower, sampler.sample());
62          }
63      }
64  
65      /**
66       * Test samples with a full long range.
67       * The output should be the same as the long values produced from a RNG.
68       */
69      @Test
70      void testSamplesWithFullRange() {
71          final long upper = Long.MAX_VALUE;
72          final long lower = Long.MIN_VALUE;
73          final UniformRandomProvider rng1 = RandomAssert.seededRNG();
74          final UniformRandomProvider rng2 = RandomAssert.seededRNG();
75          final UniformLongSampler sampler = UniformLongSampler.of(rng2, lower, upper);
76          for (int i = 0; i < 10; i++) {
77              Assertions.assertEquals(rng1.nextLong(), sampler.sample());
78          }
79      }
80  
81      /**
82       * Test samples with a non-power of 2 range.
83       * The output should be the same as the long values produced from a RNG
84       * based on o.a.c.rng.core.BaseProvider as the rejection algorithm is
85       * the same.
86       */
87      @ParameterizedTest
88      @ValueSource(longs = {234293789329234L, 145, 69987, 12673586268L, 234785389445435L, Long.MAX_VALUE})
89      void testSamplesWithSmallNonPowerOf2Range(long upper) {
90          for (final long lower : new long[] {-13, 0, 13}) {
91              final long n = upper - lower + 1;
92              // Skip overflow ranges
93              if (n < 0) {
94                  continue;
95              }
96  
97              Assertions.assertNotEquals(0, n & (n - 1), "Power of 2 is invalid here");
98  
99              // Largest multiple of the upper bound.
100             // floor(2^63 / range) * range
101             // Computed as 2^63 - 2 % 63 with unsigned integers.
102             final long m = Long.MIN_VALUE - Long.remainderUnsigned(Long.MIN_VALUE, n);
103 
104             // Check the method used in the sampler
105             Assertions.assertEquals(m, (Long.MIN_VALUE / n) * -n);
106 
107             // Use an RNG that forces the rejection path on the first few samples
108             // This occurs when the positive value is above the limit set by the
109             // largest multiple of upper that does not overflow.
110             final UniformRandomProvider rng1 = createRngWithFullBitsOnFirstCall(m);
111             final UniformRandomProvider rng2 = createRngWithFullBitsOnFirstCall(m);
112             final UniformLongSampler sampler = UniformLongSampler.of(rng2, lower, upper);
113             for (int i = 0; i < 100; i++) {
114                 Assertions.assertEquals(lower + rng1.nextLong(n), sampler.sample());
115             }
116         }
117     }
118 
119     /**
120      * Creates a RNG which will return full bits for the first sample, then bits
121      * too high for the configured limit for a few iterations.
122      *
123      * @param m Upper limit for a sample
124      * @return the uniform random provider
125      */
126     private static UniformRandomProvider createRngWithFullBitsOnFirstCall(long m) {
127         return new SplitMix64(0L) {
128             private int i;
129             @Override
130             public long next() {
131                 int j = i++;
132                 if (j == 0) {
133                     // Full bits
134                     return -1L;
135                 } else if (j < 6) {
136                     // A value just above or below the limit.
137                     // Assumes m is positive and the sampler uses >>> 1 to extract
138                     // a positive value.
139                     return (m + 3 - j) << 1;
140                 }
141                 return super.next();
142             }
143         };
144     }
145 
146     /**
147      * Test samples with a power of 2 range.
148      * This tests the minimum and maximum output should be the range limits.
149      */
150     @Test
151     void testSamplesWithPowerOf2Range() {
152         final UniformRandomProvider rngZeroBits = new LongProvider() {
153             @Override
154             public long next() {
155                 // No bits
156                 return 0L;
157             }
158         };
159         final UniformRandomProvider rngAllBits = new LongProvider() {
160             @Override
161             public long next() {
162                 // All bits
163                 return -1L;
164             }
165         };
166 
167         final long lower = -3;
168         UniformLongSampler sampler;
169         // The upper range for a positive long is 2^63-1. So the max positive power of
170         // 2 is 2^62. However the sampler should handle a bit shift of 63 to create a range
171         // of Long.MIN_VALUE as this is a power of 2 as an unsigned long (2^63).
172         for (int i = 0; i < 64; i++) {
173             final long range = 1L << i;
174             final long upper = lower + range - 1;
175             sampler = UniformLongSampler.of(rngZeroBits, lower, upper);
176             Assertions.assertEquals(lower, sampler.sample(), "Zero bits sample");
177             sampler = UniformLongSampler.of(rngAllBits, lower, upper);
178             Assertions.assertEquals(upper, sampler.sample(), "All bits sample");
179         }
180     }
181 
182     /**
183      * Test samples with a power of 2 range.
184      * This tests the output is created using a bit shift.
185      */
186     @Test
187     void testSamplesWithPowerOf2RangeIsBitShift() {
188         final long lower = 0;
189         UniformLongSampler sampler;
190         // Power of 2 sampler used for a bit shift of 1 to 63.
191         for (int i = 1; i <= 63; i++) {
192             // Upper is inclusive so subtract 1
193             final long upper = (1L << i) - 1;
194             final int shift = 64 - i;
195             final UniformRandomProvider rng1 = RandomAssert.seededRNG();
196             final UniformRandomProvider rng2 = RandomAssert.seededRNG();
197             sampler = UniformLongSampler.of(rng2, lower, upper);
198             for (int j = 0; j < 10; j++) {
199                 Assertions.assertEquals(rng1.nextLong() >>> shift, sampler.sample());
200             }
201         }
202     }
203 
204     /**
205      * Test samples with a large non-power of 2 range.
206      * This tests the large range algorithm uses a rejection method.
207      */
208     @Test
209     void testSamplesWithLargeNonPowerOf2RangeIsRejectionMethod() {
210         // Create a range bigger than 2^63
211         final long upper = Long.MAX_VALUE / 2 + 1;
212         final long lower = Long.MIN_VALUE / 2 - 1;
213         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
214         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
215         final UniformLongSampler sampler = UniformLongSampler.of(rng2, lower, upper);
216         for (int i = 0; i < 10; i++) {
217             // Get the expected value by the rejection method
218             long expected;
219             do {
220                 expected = rng1.nextLong();
221             } while (expected < lower || expected > upper);
222             Assertions.assertEquals(expected, sampler.sample());
223         }
224     }
225 
226     @Test
227     void testOffsetSamplesWithNonPowerOf2Range() {
228         assertOffsetSamples(257);
229     }
230 
231     @Test
232     void testOffsetSamplesWithPowerOf2Range() {
233         assertOffsetSamples(256);
234     }
235 
236     @Test
237     void testOffsetSamplesWithRangeOf1() {
238         assertOffsetSamples(1);
239     }
240 
241     private static void assertOffsetSamples(long range) {
242         final UniformRandomProvider[] rngs = RandomAssert.createRNG(3);
243         final UniformRandomProvider rng1 = rngs[0];
244         final UniformRandomProvider rng2 = rngs[1];
245         final UniformRandomProvider rng3 = rngs[2];
246 
247         // Since the upper limit is inclusive
248         range = range - 1;
249         final long offsetLo = -13;
250         final long offsetHi = 42;
251         final UniformLongSampler sampler = UniformLongSampler.of(rng1, 0, range);
252         final UniformLongSampler samplerLo = UniformLongSampler.of(rng2, offsetLo, offsetLo + range);
253         final UniformLongSampler samplerHi = UniformLongSampler.of(rng3, offsetHi, offsetHi + range);
254         for (int i = 0; i < 10; i++) {
255             final long sample1 = sampler.sample();
256             final long sample2 = samplerLo.sample();
257             final long sample3 = samplerHi.sample();
258             Assertions.assertEquals(sample1 + offsetLo, sample2, "Incorrect negative offset sample");
259             Assertions.assertEquals(sample1 + offsetHi, sample3, "Incorrect positive offset sample");
260         }
261     }
262 
263     /**
264      * Test the sample uniformity when using a small range that is a power of 2.
265      */
266     @Test
267     void testSampleUniformityWithPowerOf2Range() {
268         // Test using a RNG that outputs a counter of integers.
269         // The n most significant bits will be represented uniformly over a
270         // sequence that is a 2^n long.
271         final UniformRandomProvider rng = new LongProvider() {
272             private long bits = 0;
273 
274             @Override
275             public long next() {
276                 // We reverse the bits because the most significant bits are used
277                 return Long.reverse(bits++);
278             }
279         };
280 
281         // n = upper range exclusive
282         final int n = 32; // power of 2
283         final int[] histogram = new int[n];
284 
285         final long lower = 0;
286         final long upper = n - 1;
287 
288         final UniformLongSampler sampler = UniformLongSampler.of(rng, lower, upper);
289 
290         final int expected = 2;
291         for (int i = expected * n; i-- > 0;) {
292             histogram[(int) sampler.sample()]++;
293         }
294 
295         // This should be even across the entire range
296         for (int value : histogram) {
297             Assertions.assertEquals(expected, value);
298         }
299     }
300 
301     @Test
302     void testSharedStateSamplerWithSmallRange() {
303         assertSharedStateSampler(5, 67);
304     }
305 
306     @Test
307     void testSharedStateSamplerWithLargeRange() {
308         // Set the range so rejection below or above the threshold occurs with approximately
309         // p=0.25 for each bound.
310         assertSharedStateSampler(Long.MIN_VALUE / 2 - 1, Long.MAX_VALUE / 2 + 1);
311     }
312 
313     @Test
314     void testSharedStateSamplerWithPowerOf2Range() {
315         assertSharedStateSampler(0, (1L << 45) - 1);
316     }
317 
318     @Test
319     void testSharedStateSamplerWithRangeOf1() {
320         assertSharedStateSampler(968757657572323L, 968757657572323L);
321     }
322 
323     /**
324      * Test the SharedStateSampler implementation returns the same sequence as the source sampler
325      * when using an identical RNG.
326      *
327      * @param lower Lower.
328      * @param upper Upper.
329      */
330     private static void assertSharedStateSampler(long lower, long upper) {
331         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
332         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
333         final UniformLongSampler sampler1 = UniformLongSampler.of(rng1, lower, upper);
334         final UniformLongSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
335         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
336     }
337 
338     @Test
339     void testToStringWithSmallRange() {
340         assertToString(5, 67);
341     }
342 
343     @Test
344     void testToStringWithLargeRange() {
345         assertToString(-99999999, Long.MAX_VALUE);
346     }
347 
348     @Test
349     void testToStringWithPowerOf2Range() {
350         // Note the range is upper - lower + 1
351         assertToString(0, 31);
352     }
353 
354     @Test
355     void testToStringWithRangeOf1() {
356         assertToString(9, 9);
357     }
358 
359     /**
360      * Test the toString method contains the term "uniform". This is true of all samplers
361      * even for a fixed sample from a range of 1.
362      *
363      * @param lower Lower.
364      * @param upper Upper.
365      */
366     private static void assertToString(long lower, long upper) {
367         final UniformRandomProvider rng = RandomAssert.seededRNG();
368         final UniformLongSampler sampler = UniformLongSampler.of(rng, lower, upper);
369         Assertions.assertTrue(sampler.toString().toLowerCase(Locale.US).contains("uniform"));
370     }
371 }