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.source32.IntProvider;
22  import org.apache.commons.rng.sampling.RandomAssert;
23  import org.apache.commons.rng.simple.RandomSource;
24  import org.junit.jupiter.api.Assertions;
25  import org.junit.jupiter.api.Test;
26  
27  /**
28   * Test for the {@link DiscreteUniformSampler}. The tests hit edge cases for the sampler
29   * and demonstrates uniformity of output when the underlying RNG output is uniform.
30   */
31  class DiscreteUniformSamplerTest {
32      /**
33       * Test the constructor with a bad range.
34       */
35      @Test
36      void testConstructorThrowsWithLowerAboveUpper() {
37          final int upper = 55;
38          final int lower = upper + 1;
39          final UniformRandomProvider rng = RandomAssert.seededRNG();
40          Assertions.assertThrows(IllegalArgumentException.class,
41              () -> DiscreteUniformSampler.of(rng, lower, upper));
42      }
43  
44      @Test
45      void testSamplesWithRangeOf1() {
46          final int upper = 99;
47          final int lower = upper;
48          final UniformRandomProvider rng = RandomAssert.createRNG();
49          final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng, lower, upper);
50          for (int i = 0; i < 5; i++) {
51              Assertions.assertEquals(lower, sampler.sample());
52          }
53      }
54  
55      /**
56       * Test samples with a full integer range.
57       * The output should be the same as the int values produced from a RNG.
58       */
59      @Test
60      void testSamplesWithFullRange() {
61          final int upper = Integer.MAX_VALUE;
62          final int lower = Integer.MIN_VALUE;
63          final UniformRandomProvider rng1 = RandomAssert.seededRNG();
64          final UniformRandomProvider rng2 = RandomAssert.seededRNG();
65          final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng2, lower, upper);
66          for (int i = 0; i < 10; i++) {
67              Assertions.assertEquals(rng1.nextInt(), sampler.sample());
68          }
69      }
70  
71      /**
72       * Test samples with a non-power of 2 range.
73       * The output should be the same as the long values produced from a RNG
74       * based on o.a.c.rng.core.BaseProvider as the rejection algorithm is
75       * the same.
76       */
77      @Test
78      void testSamplesWithSmallNonPowerOf2Range() {
79          final int upper = 257;
80          for (final int lower : new int[] {-13, 0, 13}) {
81              final int n = upper - lower + 1;
82              final UniformRandomProvider rng1 = RandomAssert.seededRNG();
83              final UniformRandomProvider rng2 = RandomAssert.seededRNG();
84              final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng2, lower, upper);
85              for (int i = 0; i < 10; i++) {
86                  Assertions.assertEquals(lower + rng1.nextInt(n), sampler.sample());
87              }
88          }
89      }
90  
91      /**
92       * Test samples with a power of 2 range.
93       * This tests the minimum and maximum output should be the range limits.
94       */
95      @Test
96      void testSamplesWithPowerOf2Range() {
97          final UniformRandomProvider rngZeroBits = new IntProvider() {
98              @Override
99              public int next() {
100                 // No bits
101                 return 0;
102             }
103         };
104         final UniformRandomProvider rngAllBits = new IntProvider() {
105             @Override
106             public int next() {
107                 // All bits
108                 return -1;
109             }
110         };
111 
112         final int lower = -3;
113         DiscreteUniformSampler sampler;
114         // The upper range for a positive integer is 2^31-1. So the max positive power of
115         // 2 is 2^30. However the sampler should handle a bit shift of 31 to create a range
116         // of Integer.MIN_VALUE as this is a power of 2 as an unsigned int (2^31).
117         for (int i = 0; i < 32; i++) {
118             final int range = 1 << i;
119             final int upper = lower + range - 1;
120             sampler = new DiscreteUniformSampler(rngZeroBits, lower, upper);
121             Assertions.assertEquals(lower, sampler.sample(), "Zero bits sample");
122             sampler = new DiscreteUniformSampler(rngAllBits, lower, upper);
123             Assertions.assertEquals(upper, sampler.sample(), "All bits sample");
124         }
125     }
126 
127     /**
128      * Test samples with a power of 2 range.
129      * This tests the output is created using a bit shift.
130      */
131     @Test
132     void testSamplesWithPowerOf2RangeIsBitShift() {
133         final int lower = 0;
134         SharedStateDiscreteSampler sampler;
135         // Power of 2 sampler used for a bit shift of 1 to 31.
136         for (int i = 1; i <= 31; i++) {
137             // Upper is inclusive so subtract 1
138             final int upper = (1 << i) - 1;
139             final int shift = 32 - i;
140             final UniformRandomProvider rng1 = RandomAssert.seededRNG();
141             final UniformRandomProvider rng2 = RandomAssert.seededRNG();
142             sampler = DiscreteUniformSampler.of(rng2, lower, upper);
143             for (int j = 0; j < 10; j++) {
144                 Assertions.assertEquals(rng1.nextInt() >>> shift, sampler.sample());
145             }
146         }
147     }
148 
149     /**
150      * Test samples with a large non-power of 2 range.
151      * This tests the large range algorithm uses a rejection method.
152      */
153     @Test
154     void testSamplesWithLargeNonPowerOf2RangeIsRejectionMethod() {
155         // Create a range bigger than 2^63
156         final int upper = Integer.MAX_VALUE / 2 + 1;
157         final int lower = Integer.MIN_VALUE / 2 - 1;
158         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
159         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
160         final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng2, lower, upper);
161         for (int i = 0; i < 10; i++) {
162             // Get the expected value by the rejection method
163             long expected;
164             do {
165                 expected = rng1.nextInt();
166             } while (expected < lower || expected > upper);
167             Assertions.assertEquals(expected, sampler.sample());
168         }
169     }
170 
171     @Test
172     void testOffsetSamplesWithNonPowerOf2Range() {
173         assertOffsetSamples(257);
174     }
175 
176     @Test
177     void testOffsetSamplesWithPowerOf2Range() {
178         assertOffsetSamples(256);
179     }
180 
181     @Test
182     void testOffsetSamplesWithRangeOf1() {
183         assertOffsetSamples(1);
184     }
185 
186     private static void assertOffsetSamples(int range) {
187         final Long seed = RandomSource.createLong();
188         final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(seed);
189         final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(seed);
190         final UniformRandomProvider rng3 = RandomSource.SPLIT_MIX_64.create(seed);
191 
192         // Since the upper limit is inclusive
193         range = range - 1;
194         final int offsetLo = -13;
195         final int offsetHi = 42;
196         final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng1, 0, range);
197         final SharedStateDiscreteSampler samplerLo = DiscreteUniformSampler.of(rng2, offsetLo, offsetLo + range);
198         final SharedStateDiscreteSampler samplerHi = DiscreteUniformSampler.of(rng3, offsetHi, offsetHi + range);
199         for (int i = 0; i < 10; i++) {
200             final int sample1 = sampler.sample();
201             final int sample2 = samplerLo.sample();
202             final int sample3 = samplerHi.sample();
203             Assertions.assertEquals(sample1 + offsetLo, sample2, "Incorrect negative offset sample");
204             Assertions.assertEquals(sample1 + offsetHi, sample3, "Incorrect positive offset sample");
205         }
206     }
207 
208     /**
209      * Test the sample uniformity when using a small range that is not a power of 2.
210      */
211     @Test
212     void testSampleUniformityWithNonPowerOf2Range() {
213         // Test using a RNG that outputs an evenly spaced set of integers.
214         // Create a Weyl sequence using George Marsaglia’s increment from:
215         // Marsaglia, G (July 2003). "Xorshift RNGs". Journal of Statistical Software. 8 (14).
216         // https://en.wikipedia.org/wiki/Weyl_sequence
217         final UniformRandomProvider rng = new IntProvider() {
218             private final int increment = 362437;
219             // Start at the highest positive number
220             private final int start = Integer.MIN_VALUE - increment;
221 
222             private int bits = start;
223 
224             @Override
225             public int next() {
226                 // Output until the first wrap. The entire sequence will be uniform.
227                 // Note this is not the full period of the sequence.
228                 // Expect ((1L << 32) / increment) numbers = 11850
229                 int result = bits += increment;
230                 if (result < start) {
231                     return result;
232                 }
233                 throw new IllegalStateException("end of sequence");
234             }
235         };
236 
237         // n = upper range exclusive
238         final int n = 37; // prime
239         final int[] histogram = new int[n];
240 
241         final int lower = 0;
242         final int upper = n - 1;
243 
244         final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng, lower, upper);
245 
246         try {
247             while (true) {
248                 histogram[sampler.sample()]++;
249             }
250         } catch (IllegalStateException ex) {
251             // Expected end of sequence
252         }
253 
254         // The sequence will result in either x or (x+1) samples in each bin (i.e. uniform).
255         int min = histogram[0];
256         int max = histogram[0];
257         for (int value : histogram) {
258             min = Math.min(min, value);
259             max = Math.max(max, value);
260         }
261         Assertions.assertTrue(max - min <= 1, "Not uniform, max = " + max + ", min=" + min);
262     }
263 
264     /**
265      * Test the sample uniformity when using a small range that is a power of 2.
266      */
267     @Test
268     void testSampleUniformityWithPowerOf2Range() {
269         // Test using a RNG that outputs a counter of integers.
270         // The n most significant bits will be represented uniformly over a
271         // sequence that is a 2^n long.
272         final UniformRandomProvider rng = new IntProvider() {
273             private int bits = 0;
274 
275             @Override
276             public int next() {
277                 // We reverse the bits because the most significant bits are used
278                 return Integer.reverse(bits++);
279             }
280         };
281 
282         // n = upper range exclusive
283         final int n = 32; // power of 2
284         final int[] histogram = new int[n];
285 
286         final int lower = 0;
287         final int upper = n - 1;
288 
289         final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng, lower, upper);
290 
291         final int expected = 2;
292         for (int i = expected * n; i-- > 0;) {
293             histogram[sampler.sample()]++;
294         }
295 
296         // This should be even across the entire range
297         for (int value : histogram) {
298             Assertions.assertEquals(expected, value);
299         }
300     }
301 
302     /**
303      * Test the sample rejection when using a range that is not a power of 2. The rejection
304      * algorithm of Lemire (2019) splits the entire 32-bit range into intervals of size 2^32/n. It
305      * will reject the lowest value in each interval that is over sampled. This test uses 0
306      * as the first value from the RNG and tests it is rejected.
307      */
308     @Test
309     void testSampleRejectionWithNonPowerOf2Range() {
310         // Test using a RNG that returns a sequence.
311         // The first value of zero should produce a sample that is rejected.
312         final int[] value = new int[1];
313         final UniformRandomProvider rng = new IntProvider() {
314             @Override
315             public int next() {
316                 return value[0]++;
317             }
318         };
319 
320         // n = upper range exclusive.
321         // Use a prime number to select the rejection algorithm.
322         final int n = 37;
323         final int lower = 0;
324         final int upper = n - 1;
325 
326         final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng, lower, upper);
327 
328         final int sample = sampler.sample();
329 
330         Assertions.assertEquals(0, sample, "Sample is incorrect");
331         Assertions.assertEquals(2, value[0], "Sample should be produced from 2nd value");
332     }
333 
334     @Test
335     void testSharedStateSamplerWithSmallRange() {
336         testSharedStateSampler(5, 67);
337     }
338 
339     @Test
340     void testSharedStateSamplerWithLargeRange() {
341         // Set the range so rejection below or above the threshold occurs with approximately p=0.25
342         testSharedStateSampler(Integer.MIN_VALUE / 2 - 1, Integer.MAX_VALUE / 2 + 1);
343     }
344 
345     @Test
346     void testSharedStateSamplerWithPowerOf2Range() {
347         testSharedStateSampler(0, 31);
348     }
349 
350     @Test
351     void testSharedStateSamplerWithRangeOf1() {
352         testSharedStateSampler(9, 9);
353     }
354 
355     /**
356      * Test the SharedStateSampler implementation.
357      *
358      * @param lower Lower.
359      * @param upper Upper.
360      */
361     private static void testSharedStateSampler(int lower, int upper) {
362         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
363         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
364         // Use instance constructor not factory constructor to exercise 1.X public API
365         final SharedStateDiscreteSampler sampler1 =
366             new DiscreteUniformSampler(rng1, lower, upper);
367         final SharedStateDiscreteSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
368         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
369     }
370 
371     @Test
372     void testToStringWithSmallRange() {
373         assertToString(5, 67);
374     }
375 
376     @Test
377     void testToStringWithLargeRange() {
378         assertToString(-99999999, Integer.MAX_VALUE);
379     }
380 
381     @Test
382     void testToStringWithPowerOf2Range() {
383         // Note the range is upper - lower + 1
384         assertToString(0, 31);
385     }
386 
387     @Test
388     void testToStringWithRangeOf1() {
389         assertToString(9, 9);
390     }
391 
392     /**
393      * Test the toString method contains the term "uniform". This is true of all samplers
394      * even for a fixed sample from a range of 1.
395      *
396      * @param lower Lower.
397      * @param upper Upper.
398      */
399     private static void assertToString(int lower, int upper) {
400         final UniformRandomProvider rng = RandomAssert.seededRNG();
401         final DiscreteUniformSampler sampler = new DiscreteUniformSampler(rng, lower, upper);
402         Assertions.assertTrue(sampler.toString().toLowerCase(Locale.US).contains("uniform"));
403     }
404 }