| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.rng.sampling.distribution; |
| |
| import org.apache.commons.rng.UniformRandomProvider; |
| import org.apache.commons.rng.core.source32.IntProvider; |
| import org.apache.commons.rng.sampling.RandomAssert; |
| import org.apache.commons.rng.simple.RandomSource; |
| import org.junit.Assert; |
| import org.junit.Test; |
| |
| import java.util.Locale; |
| |
| /** |
| * Test for the {@link DiscreteUniformSampler}. The tests hit edge cases for the sampler |
| * and demonstrates uniformity of output when the underlying RNG output is uniform. |
| */ |
| public class DiscreteUniformSamplerTest { |
| /** |
| * Test the constructor with a bad range. |
| */ |
| @Test(expected = IllegalArgumentException.class) |
| public void testConstructorThrowsWithLowerAboveUpper() { |
| final int upper = 55; |
| final int lower = upper + 1; |
| final UniformRandomProvider rng = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| DiscreteUniformSampler.of(rng, lower, upper); |
| } |
| |
| @Test |
| public void testSamplesWithRangeOf1() { |
| final int upper = 99; |
| final int lower = upper; |
| final UniformRandomProvider rng = RandomSource.create(RandomSource.SPLIT_MIX_64); |
| final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng, lower, upper); |
| for (int i = 0; i < 5; i++) { |
| Assert.assertEquals(lower, sampler.sample()); |
| } |
| } |
| |
| /** |
| * Test samples with a full integer range. |
| * The output should be the same as the int values produced from a RNG. |
| */ |
| @Test |
| public void testSamplesWithFullRange() { |
| final int upper = Integer.MAX_VALUE; |
| final int lower = Integer.MIN_VALUE; |
| final UniformRandomProvider rng1 = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| final UniformRandomProvider rng2 = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng2, lower, upper); |
| for (int i = 0; i < 5; i++) { |
| Assert.assertEquals(rng1.nextInt(), sampler.sample()); |
| } |
| } |
| |
| @Test |
| public void testSamplesWithPowerOf2Range() { |
| final UniformRandomProvider rngZeroBits = new IntProvider() { |
| @Override |
| public int next() { |
| return 0; |
| } |
| }; |
| final UniformRandomProvider rngAllBits = new IntProvider() { |
| @Override |
| public int next() { |
| return 0xffffffff; |
| } |
| }; |
| |
| final int lower = -3; |
| DiscreteUniformSampler sampler; |
| // The upper range for a positive integer is 2^31-1. So the max positive power of |
| // 2 is 2^30. However the sampler should handle a bit shift of 31 to create a range |
| // of Integer.MIN_VALUE (0x80000000) as this is a power of 2 as an unsigned int (2^31). |
| for (int i = 0; i < 32; i++) { |
| final int range = 1 << i; |
| final int upper = lower + range - 1; |
| sampler = new DiscreteUniformSampler(rngZeroBits, lower, upper); |
| Assert.assertEquals("Zero bits sample", lower, sampler.sample()); |
| sampler = new DiscreteUniformSampler(rngAllBits, lower, upper); |
| Assert.assertEquals("All bits sample", upper, sampler.sample()); |
| } |
| } |
| |
| @Test |
| public void testOffsetSamplesWithNonPowerOf2Range() { |
| assertOffsetSamples(257); |
| } |
| |
| @Test |
| public void testOffsetSamplesWithPowerOf2Range() { |
| assertOffsetSamples(256); |
| } |
| |
| @Test |
| public void testOffsetSamplesWithRangeOf1() { |
| assertOffsetSamples(1); |
| } |
| |
| private static void assertOffsetSamples(int range) { |
| final Long seed = RandomSource.createLong(); |
| final UniformRandomProvider rng1 = RandomSource.create(RandomSource.SPLIT_MIX_64, seed); |
| final UniformRandomProvider rng2 = RandomSource.create(RandomSource.SPLIT_MIX_64, seed); |
| final UniformRandomProvider rng3 = RandomSource.create(RandomSource.SPLIT_MIX_64, seed); |
| |
| // Since the upper limit is inclusive |
| range = range - 1; |
| final int offsetLo = -13; |
| final int offsetHi = 42; |
| final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng1, 0, range); |
| final SharedStateDiscreteSampler samplerLo = DiscreteUniformSampler.of(rng2, offsetLo, offsetLo + range); |
| final SharedStateDiscreteSampler samplerHi = DiscreteUniformSampler.of(rng3, offsetHi, offsetHi + range); |
| for (int i = 0; i < 10; i++) { |
| final int sample1 = sampler.sample(); |
| final int sample2 = samplerLo.sample(); |
| final int sample3 = samplerHi.sample(); |
| Assert.assertEquals("Incorrect negative offset sample", sample1 + offsetLo, sample2); |
| Assert.assertEquals("Incorrect positive offset sample", sample1 + offsetHi, sample3); |
| } |
| } |
| |
| /** |
| * Test the sample uniformity when using a small range that is not a power of 2. |
| */ |
| @Test |
| public void testSampleUniformityWithNonPowerOf2Range() { |
| // Test using a RNG that outputs an evenly spaced set of integers. |
| // Create a Weyl sequence using George Marsaglia’s increment from: |
| // Marsaglia, G (July 2003). "Xorshift RNGs". Journal of Statistical Software. 8 (14). |
| // https://en.wikipedia.org/wiki/Weyl_sequence |
| final UniformRandomProvider rng = new IntProvider() { |
| private final int increment = 362437; |
| // Start at the highest positive number |
| private final int start = Integer.MIN_VALUE - increment; |
| |
| private int bits = start; |
| |
| @Override |
| public int next() { |
| // Output until the first wrap. The entire sequence will be uniform. |
| // Note this is not the full period of the sequence. |
| // Expect ((1L << 32) / increment) numbers = 11850 |
| int result = bits += increment; |
| if (result < start) { |
| return result; |
| } |
| throw new IllegalStateException("end of sequence"); |
| } |
| }; |
| |
| // n = upper range exclusive |
| final int n = 37; // prime |
| final int[] histogram = new int[n]; |
| |
| final int lower = 0; |
| final int upper = n - 1; |
| |
| final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng, lower, upper); |
| |
| try { |
| while (true) { |
| histogram[sampler.sample()]++; |
| } |
| } catch (IllegalStateException ex) { |
| // Expected end of sequence |
| } |
| |
| // The sequence will result in either x or (x+1) samples in each bin (i.e. uniform). |
| int min = histogram[0]; |
| int max = histogram[0]; |
| for (int value : histogram) { |
| min = Math.min(min, value); |
| max = Math.max(max, value); |
| } |
| Assert.assertTrue("Not uniform, max = " + max + ", min=" + min, max - min <= 1); |
| } |
| |
| /** |
| * Test the sample uniformity when using a small range that is a power of 2. |
| */ |
| @Test |
| public void testSampleUniformityWithPowerOf2Range() { |
| // Test using a RNG that outputs a counter of integers. |
| final UniformRandomProvider rng = new IntProvider() { |
| private int bits = 0; |
| |
| @Override |
| public int next() { |
| // We reverse the bits because the most significant bits are used |
| return Integer.reverse(bits++); |
| } |
| }; |
| |
| // n = upper range exclusive |
| final int n = 32; // power of 2 |
| final int[] histogram = new int[n]; |
| |
| final int lower = 0; |
| final int upper = n - 1; |
| |
| final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng, lower, upper); |
| |
| final int expected = 2; |
| for (int i = expected * n; i-- > 0;) { |
| histogram[sampler.sample()]++; |
| } |
| |
| // This should be even across the entire range |
| for (int value : histogram) { |
| Assert.assertEquals(expected, value); |
| } |
| } |
| |
| /** |
| * Test the sample rejection when using a range that is not a power of 2. The rejection |
| * algorithm of Lemire (2019) splits the entire 32-bit range into intervals of size 2^32/n. It |
| * will reject the lowest value in each interval that is over sampled. This test uses 0 |
| * as the first value from the RNG and tests it is rejected. |
| */ |
| @Test |
| public void testSampleRejectionWithNonPowerOf2Range() { |
| // Test using a RNG that returns a sequence. |
| // The first value of zero should produce a sample that is rejected. |
| final int[] value = new int[1]; |
| final UniformRandomProvider rng = new IntProvider() { |
| @Override |
| public int next() { |
| return value[0]++; |
| } |
| }; |
| |
| // n = upper range exclusive. |
| // Use a prime number to select the rejection algorithm. |
| final int n = 37; |
| final int lower = 0; |
| final int upper = n - 1; |
| |
| final SharedStateDiscreteSampler sampler = DiscreteUniformSampler.of(rng, lower, upper); |
| |
| final int sample = sampler.sample(); |
| |
| Assert.assertEquals("Sample is incorrect", 0, sample); |
| Assert.assertEquals("Sample should be produced from 2nd value", 2, value[0]); |
| } |
| |
| /** |
| * Test the SharedStateSampler implementation. |
| */ |
| @Test |
| public void testSharedStateSamplerWithSmallRange() { |
| testSharedStateSampler(5, 67); |
| } |
| |
| /** |
| * Test the SharedStateSampler implementation. |
| */ |
| @Test |
| public void testSharedStateSamplerWithLargeRange() { |
| // Set the range so rejection below or above the threshold occurs with approximately p=0.25 |
| testSharedStateSampler(Integer.MIN_VALUE / 2 - 1, Integer.MAX_VALUE / 2 + 1); |
| } |
| |
| /** |
| * Test the SharedStateSampler implementation. |
| */ |
| @Test |
| public void testSharedStateSamplerWithPowerOf2Range() { |
| testSharedStateSampler(0, 31); |
| } |
| |
| /** |
| * Test the SharedStateSampler implementation. |
| */ |
| @Test |
| public void testSharedStateSamplerWithRangeOf1() { |
| testSharedStateSampler(9, 9); |
| } |
| |
| /** |
| * Test the SharedStateSampler implementation. |
| * |
| * @param lower Lower. |
| * @param upper Upper. |
| */ |
| private static void testSharedStateSampler(int lower, int upper) { |
| final UniformRandomProvider rng1 = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| final UniformRandomProvider rng2 = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| // Use instance constructor not factory constructor to exercise 1.X public API |
| final SharedStateDiscreteSampler sampler1 = |
| new DiscreteUniformSampler(rng1, lower, upper); |
| final SharedStateDiscreteSampler sampler2 = sampler1.withUniformRandomProvider(rng2); |
| RandomAssert.assertProduceSameSequence(sampler1, sampler2); |
| } |
| |
| @Test |
| public void testToStringWithSmallRange() { |
| assertToString(5, 67); |
| } |
| |
| @Test |
| public void testToStringWithLargeRange() { |
| assertToString(-99999999, Integer.MAX_VALUE); |
| } |
| |
| @Test |
| public void testToStringWithPowerOf2Range() { |
| // Note the range is upper - lower + 1 |
| assertToString(0, 31); |
| } |
| |
| @Test |
| public void testToStringWithRangeOf1() { |
| assertToString(9, 9); |
| } |
| |
| /** |
| * Test the toString method. This is added to ensure coverage as the factory constructor |
| * used in other tests does not create an instance of the wrapper class. |
| * |
| * @param lower Lower. |
| * @param upper Upper. |
| */ |
| private static void assertToString(int lower, int upper) { |
| final UniformRandomProvider rng = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| final DiscreteUniformSampler sampler = |
| new DiscreteUniformSampler(rng, lower, upper); |
| Assert.assertTrue(sampler.toString().toLowerCase(Locale.US).contains("uniform")); |
| } |
| } |