| /* |
| * 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; |
| |
| import java.math.BigDecimal; |
| import java.math.MathContext; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.SplittableRandom; |
| import java.util.concurrent.ThreadLocalRandom; |
| import java.util.function.LongSupplier; |
| import java.util.stream.Collectors; |
| import java.util.stream.DoubleStream; |
| import java.util.stream.IntStream; |
| import java.util.stream.LongStream; |
| import java.util.stream.Stream; |
| import org.junit.jupiter.api.Assertions; |
| import org.junit.jupiter.api.Test; |
| import org.junit.jupiter.params.ParameterizedTest; |
| import org.junit.jupiter.params.provider.Arguments; |
| import org.junit.jupiter.params.provider.CsvSource; |
| import org.junit.jupiter.params.provider.MethodSource; |
| import org.junit.jupiter.params.provider.ValueSource; |
| |
| /** |
| * Tests for default method implementations in {@link UniformRandomProvider}. |
| * |
| * <p>This class verifies all exception conditions for the range methods and |
| * the range arguments to the stream methods. Streams methods are asserted |
| * to call the corresponding single value generation method in the interface. |
| * Single value generation methods are asserted using a test of uniformity |
| * from multiple samples. |
| */ |
| class UniformRandomProviderTest { |
| private static final long STREAM_SIZE_ONE = 1; |
| /** Sample size for statistical uniformity tests. */ |
| private static final int SAMPLE_SIZE = 1000; |
| /** Sample size for statistical uniformity tests as a BigDecimal. */ |
| private static final BigDecimal SAMPLE_SIZE_BD = BigDecimal.valueOf(SAMPLE_SIZE); |
| /** Relative error used to verify the sum of expected frequencies matches the sample size. */ |
| private static final double RELATIVE_ERROR = Math.ulp(1.0) * 2; |
| |
| /** |
| * Dummy class for checking the behavior of the UniformRandomProvider. |
| */ |
| private static class DummyGenerator implements UniformRandomProvider { |
| /** An instance. */ |
| static final DummyGenerator INSTANCE = new DummyGenerator(); |
| |
| @Override |
| public long nextLong() { |
| throw new UnsupportedOperationException("The nextLong method should not be invoked"); |
| } |
| } |
| |
| static int[] invalidNextIntBound() { |
| return new int[] {0, -1, Integer.MIN_VALUE}; |
| } |
| |
| static Stream<Arguments> invalidNextIntOriginBound() { |
| return Stream.of( |
| Arguments.of(1, 1), |
| Arguments.of(2, 1), |
| Arguments.of(-1, -1), |
| Arguments.of(-1, -2) |
| ); |
| } |
| |
| static long[] invalidNextLongBound() { |
| return new long[] {0, -1, Long.MIN_VALUE}; |
| } |
| |
| static Stream<Arguments> invalidNextLongOriginBound() { |
| return Stream.of( |
| Arguments.of(1L, 1L), |
| Arguments.of(2L, 1L), |
| Arguments.of(-1L, -1L), |
| Arguments.of(-1L, -2L) |
| ); |
| } |
| |
| static float[] invalidNextFloatBound() { |
| return new float[] {0, -1, -Float.MAX_VALUE, Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY}; |
| } |
| |
| static Stream<Arguments> invalidNextFloatOriginBound() { |
| return Stream.of( |
| Arguments.of(1f, 1f), |
| Arguments.of(2f, 1f), |
| Arguments.of(-1f, -1f), |
| Arguments.of(-1f, -2f), |
| Arguments.of(Float.NEGATIVE_INFINITY, 0f), |
| Arguments.of(0f, Float.POSITIVE_INFINITY), |
| Arguments.of(0f, Float.NaN), |
| Arguments.of(Float.NaN, 1f) |
| ); |
| } |
| |
| static double[] invalidNextDoubleBound() { |
| return new double[] {0, -1, -Double.MAX_VALUE, Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}; |
| } |
| |
| static Stream<Arguments> invalidNextDoubleOriginBound() { |
| return Stream.of( |
| Arguments.of(1, 1), |
| Arguments.of(2, 1), |
| Arguments.of(-1, -1), |
| Arguments.of(-1, -2), |
| Arguments.of(Double.NEGATIVE_INFINITY, 0), |
| Arguments.of(0, Double.POSITIVE_INFINITY), |
| Arguments.of(0, Double.NaN), |
| Arguments.of(Double.NaN, 1) |
| ); |
| } |
| |
| static long[] streamSizes() { |
| return new long[] {0, 1, 13}; |
| } |
| |
| /** |
| * Creates a functional random generator by implementing the |
| * {@link UniformRandomProvider#nextLong} method with a high quality source of randomness. |
| * |
| * @param seed Seed for the generator. |
| * @return the random generator |
| */ |
| private static UniformRandomProvider createRNG(long seed) { |
| // The algorithm for SplittableRandom with the default increment passes: |
| // - Test U01 BigCrush |
| // - PractRand with at least 2^42 bytes (4 TiB) of output |
| return new SplittableRandom(seed)::nextLong; |
| } |
| |
| @Test |
| void testNextBytesThrows() { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(NullPointerException.class, () -> rng.nextBytes(null)); |
| Assertions.assertThrows(NullPointerException.class, () -> rng.nextBytes(null, 0, 1)); |
| // Invalid range |
| final int length = 10; |
| final byte[] bytes = new byte[length]; |
| Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, -1, 1), "start < 0"); |
| Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, length, 1), "start >= length"); |
| Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, 0, -1), "len < 0"); |
| Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, 5, 10), "start + len > length"); |
| Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, 5, Integer.MAX_VALUE), "start + len > length, taking into account integer overflow"); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextIntBound"}) |
| void testNextIntBoundThrows(int bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextInt(bound)); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextIntOriginBound"}) |
| void testNextIntOriginBoundThrows(int origin, int bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextInt(origin, bound)); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextLongBound"}) |
| void testNextLongBoundThrows(long bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextLong(bound)); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextLongOriginBound"}) |
| void testNextLongOriginBoundThrows(long origin, long bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextLong(origin, bound)); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextFloatBound"}) |
| void testNextFloatBoundThrows(float bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextFloat(bound)); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextFloatOriginBound"}) |
| void testNextFloatOriginBoundThrows(float origin, float bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextFloat(origin, bound)); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextDoubleBound"}) |
| void testNextDoubleBoundThrows(double bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextDouble(bound)); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextDoubleOriginBound"}) |
| void testNextDoubleOriginBoundThrows(double origin, double bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextDouble(origin, bound)); |
| } |
| |
| @Test |
| void testNextFloatExtremes() { |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| private int[] values = {0, -1, 1 << 8}; |
| @Override |
| public int nextInt() { |
| return values[i++]; |
| } |
| }; |
| Assertions.assertEquals(0, rng.nextFloat(), "Expected zero bits to return 0"); |
| Assertions.assertEquals(Math.nextDown(1f), rng.nextFloat(), "Expected all bits to return ~1"); |
| // Assumes the value is created from the high 24 bits of nextInt |
| Assertions.assertEquals(1f - Math.nextDown(1f), rng.nextFloat(), "Expected 1 bit (shifted) to return ~0"); |
| } |
| |
| @ParameterizedTest |
| @ValueSource(floats = {Float.MIN_NORMAL, Float.MIN_NORMAL / 2}) |
| void testNextFloatBoundRounding(float bound) { |
| // This method will be used |
| final UniformRandomProvider rng = new DummyGenerator() { |
| @Override |
| public float nextFloat() { |
| return Math.nextDown(1.0f); |
| } |
| }; |
| Assertions.assertEquals(bound, rng.nextFloat() * bound, "Expected result to be rounded up"); |
| Assertions.assertEquals(Math.nextDown(bound), rng.nextFloat(bound), "Result was not correctly rounded down"); |
| } |
| |
| @Test |
| void testNextFloatOriginBoundInfiniteRange() { |
| // This method will be used |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| private float[] values = {0, 0.25f, 0.5f, 0.75f, 1}; |
| @Override |
| public float nextFloat() { |
| return values[i++]; |
| } |
| }; |
| final float x = Float.MAX_VALUE; |
| Assertions.assertEquals(-x, rng.nextFloat(-x, x)); |
| Assertions.assertEquals(-x / 2, rng.nextFloat(-x, x), Math.ulp(x / 2)); |
| Assertions.assertEquals(0, rng.nextFloat(-x, x)); |
| Assertions.assertEquals(x / 2, rng.nextFloat(-x, x), Math.ulp(x / 2)); |
| Assertions.assertEquals(Math.nextDown(x), rng.nextFloat(-x, x)); |
| } |
| |
| @Test |
| void testNextFloatOriginBoundRounding() { |
| // This method will be used |
| final float v = Math.nextDown(1.0f); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| @Override |
| public float nextFloat() { |
| return v; |
| } |
| }; |
| float origin; |
| float bound; |
| |
| // Simple case |
| origin = 3.5f; |
| bound = 4.5f; |
| Assertions.assertEquals(bound, origin + v * (bound - origin), "Expected result to be rounded up"); |
| Assertions.assertEquals(Math.nextDown(bound), rng.nextFloat(origin, bound), "Result was not correctly rounded down"); |
| |
| // Alternate formula: |
| // requires sub-normal result for 'origin * v' to force rounding |
| origin = -Float.MIN_NORMAL / 2; |
| bound = Float.MIN_NORMAL / 2; |
| Assertions.assertEquals(bound, origin * (1 - v) + v * bound, "Expected result to be rounded up"); |
| Assertions.assertEquals(Math.nextDown(bound), rng.nextFloat(origin, bound), "Result was not correctly rounded down"); |
| } |
| |
| @Test |
| void testNextDoubleExtremes() { |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| private long[] values = {0, -1, 1L << 11}; |
| @Override |
| public long nextLong() { |
| return values[i++]; |
| } |
| }; |
| Assertions.assertEquals(0, rng.nextDouble(), "Expected zero bits to return 0"); |
| Assertions.assertEquals(Math.nextDown(1.0), rng.nextDouble(), "Expected all bits to return ~1"); |
| // Assumes the value is created from the high 53 bits of nextInt |
| Assertions.assertEquals(1.0 - Math.nextDown(1.0), rng.nextDouble(), "Expected 1 bit (shifted) to return ~0"); |
| } |
| |
| @ParameterizedTest |
| @ValueSource(doubles = {Double.MIN_NORMAL, Double.MIN_NORMAL / 2}) |
| void testNextDoubleBoundRounding(double bound) { |
| // This method will be used |
| final UniformRandomProvider rng = new DummyGenerator() { |
| @Override |
| public double nextDouble() { |
| return Math.nextDown(1.0); |
| } |
| }; |
| Assertions.assertEquals(bound, rng.nextDouble() * bound, "Expected result to be rounded up"); |
| Assertions.assertEquals(Math.nextDown(bound), rng.nextDouble(bound), "Result was not correctly rounded down"); |
| } |
| |
| @Test |
| void testNextDoubleOriginBoundInfiniteRange() { |
| // This method will be used |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| private double[] values = {0, 0.25, 0.5, 0.75, 1}; |
| @Override |
| public double nextDouble() { |
| return values[i++]; |
| } |
| }; |
| final double x = Double.MAX_VALUE; |
| Assertions.assertEquals(-x, rng.nextDouble(-x, x)); |
| Assertions.assertEquals(-x / 2, rng.nextDouble(-x, x), Math.ulp(x / 2)); |
| Assertions.assertEquals(0, rng.nextDouble(-x, x)); |
| Assertions.assertEquals(x / 2, rng.nextDouble(-x, x), Math.ulp(x / 2)); |
| Assertions.assertEquals(Math.nextDown(x), rng.nextDouble(-x, x)); |
| } |
| |
| @Test |
| void testNextDoubleOriginBoundRounding() { |
| // This method will be used |
| final double v = Math.nextDown(1.0); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| @Override |
| public double nextDouble() { |
| return v; |
| } |
| }; |
| double origin; |
| double bound; |
| |
| // Simple case |
| origin = 3.5; |
| bound = 4.5; |
| Assertions.assertEquals(bound, origin + v * (bound - origin), "Expected result to be rounded up"); |
| Assertions.assertEquals(Math.nextDown(bound), rng.nextDouble(origin, bound), "Result was not correctly rounded down"); |
| |
| // Alternate formula: |
| // requires sub-normal result for 'origin * v' to force rounding |
| origin = -Double.MIN_NORMAL / 2; |
| bound = Double.MIN_NORMAL / 2; |
| Assertions.assertEquals(bound, origin * (1 - v) + v * bound, "Expected result to be rounded up"); |
| Assertions.assertEquals(Math.nextDown(bound), rng.nextDouble(origin, bound), "Result was not correctly rounded down"); |
| } |
| |
| @Test |
| void testNextBooleanIsIntSignBit() { |
| final int size = 10; |
| final int[] values = ThreadLocalRandom.current().ints(size).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public int nextInt() { |
| return values[i++]; |
| } |
| }; |
| for (int i = 0; i < size; i++) { |
| Assertions.assertEquals(values[i] < 0, rng.nextBoolean()); |
| } |
| } |
| |
| @ParameterizedTest |
| @ValueSource(longs = {-1, -2, Long.MIN_VALUE}) |
| void testInvalidStreamSizeThrows(long size) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(size), "ints"); |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(size, 1, 42), "ints(lower, upper)"); |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(size), "longs"); |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(size, 3L, 33L), "longs(lower, upper)"); |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(size), "doubles"); |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(size, 1.5, 2.75), "doubles(lower, upper)"); |
| } |
| |
| @Test |
| void testUnlimitedStreamSize() { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertEquals(Long.MAX_VALUE, rng.ints().spliterator().estimateSize(), "ints"); |
| Assertions.assertEquals(Long.MAX_VALUE, rng.ints(1, 42).spliterator().estimateSize(), "ints(lower, upper)"); |
| Assertions.assertEquals(Long.MAX_VALUE, rng.longs().spliterator().estimateSize(), "longs"); |
| Assertions.assertEquals(Long.MAX_VALUE, rng.longs(3L, 33L).spliterator().estimateSize(), "longs(lower, upper)"); |
| Assertions.assertEquals(Long.MAX_VALUE, rng.doubles().spliterator().estimateSize(), "doubles"); |
| Assertions.assertEquals(Long.MAX_VALUE, rng.doubles(1.5, 2.75).spliterator().estimateSize(), "doubles(lower, upper)"); |
| } |
| |
| // Test stream methods throw immediately for invalid range arguments. |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextIntOriginBound"}) |
| void testIntsOriginBoundThrows(int origin, int bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(origin, bound)); |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(STREAM_SIZE_ONE, origin, bound)); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextLongOriginBound"}) |
| void testLongsOriginBoundThrows(long origin, long bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(origin, bound)); |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(STREAM_SIZE_ONE, origin, bound)); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"invalidNextDoubleOriginBound"}) |
| void testDoublesOriginBoundThrows(double origin, double bound) { |
| final UniformRandomProvider rng = DummyGenerator.INSTANCE; |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(origin, bound)); |
| Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(STREAM_SIZE_ONE, origin, bound)); |
| } |
| |
| // Test stream methods call the correct generation method in the UniformRandomProvider. |
| // If range arguments are supplied they are asserted to be passed through. |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testInts(long streamSize) { |
| final int[] values = ThreadLocalRandom.current().ints(streamSize).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public int nextInt() { |
| return values[i++]; |
| } |
| }; |
| |
| final IntStream stream = rng.ints(); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testIntsOriginBound(long streamSize) { |
| final int origin = 13; |
| final int bound = 42; |
| final int[] values = ThreadLocalRandom.current().ints(streamSize, origin, bound).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public int nextInt(int o, int b) { |
| Assertions.assertEquals(origin, o, "origin"); |
| Assertions.assertEquals(bound, b, "bound"); |
| return values[i++]; |
| } |
| }; |
| |
| final IntStream stream = rng.ints(origin, bound); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testIntsWithSize(long streamSize) { |
| final int[] values = ThreadLocalRandom.current().ints(streamSize).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public int nextInt() { |
| return values[i++]; |
| } |
| }; |
| |
| final IntStream stream = rng.ints(streamSize); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testIntsOriginBoundWithSize(long streamSize) { |
| final int origin = 13; |
| final int bound = 42; |
| final int[] values = ThreadLocalRandom.current().ints(streamSize, origin, bound).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public int nextInt(int o, int b) { |
| Assertions.assertEquals(origin, o, "origin"); |
| Assertions.assertEquals(bound, b, "bound"); |
| return values[i++]; |
| } |
| }; |
| |
| final IntStream stream = rng.ints(streamSize, origin, bound); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testLongs(long streamSize) { |
| final long[] values = ThreadLocalRandom.current().longs(streamSize).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public long nextLong() { |
| return values[i++]; |
| } |
| }; |
| |
| final LongStream stream = rng.longs(); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testLongsOriginBound(long streamSize) { |
| final long origin = 13; |
| final long bound = 42; |
| final long[] values = ThreadLocalRandom.current().longs(streamSize, origin, bound).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public long nextLong(long o, long b) { |
| Assertions.assertEquals(origin, o, "origin"); |
| Assertions.assertEquals(bound, b, "bound"); |
| return values[i++]; |
| } |
| }; |
| |
| final LongStream stream = rng.longs(origin, bound); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testLongsWithSize(long streamSize) { |
| final long[] values = ThreadLocalRandom.current().longs(streamSize).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public long nextLong() { |
| return values[i++]; |
| } |
| }; |
| |
| final LongStream stream = rng.longs(streamSize); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testLongsOriginBoundWithSize(long streamSize) { |
| final long origin = 13; |
| final long bound = 42; |
| final long[] values = ThreadLocalRandom.current().longs(streamSize, origin, bound).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public long nextLong(long o, long b) { |
| Assertions.assertEquals(origin, o, "origin"); |
| Assertions.assertEquals(bound, b, "bound"); |
| return values[i++]; |
| } |
| }; |
| |
| final LongStream stream = rng.longs(streamSize, origin, bound); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testDoubles(long streamSize) { |
| final double[] values = ThreadLocalRandom.current().doubles(streamSize).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public double nextDouble() { |
| return values[i++]; |
| } |
| }; |
| |
| final DoubleStream stream = rng.doubles(); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testDoublesOriginBound(long streamSize) { |
| final double origin = 13; |
| final double bound = 42; |
| final double[] values = ThreadLocalRandom.current().doubles(streamSize, origin, bound).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public double nextDouble(double o, double b) { |
| Assertions.assertEquals(origin, o, "origin"); |
| Assertions.assertEquals(bound, b, "bound"); |
| return values[i++]; |
| } |
| }; |
| |
| final DoubleStream stream = rng.doubles(origin, bound); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testDoublesWithSize(long streamSize) { |
| final double[] values = ThreadLocalRandom.current().doubles(streamSize).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public double nextDouble() { |
| return values[i++]; |
| } |
| }; |
| |
| final DoubleStream stream = rng.doubles(streamSize); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.toArray()); |
| } |
| |
| @ParameterizedTest |
| @MethodSource(value = {"streamSizes"}) |
| void testDoublesOriginBoundWithSize(long streamSize) { |
| final double origin = 13; |
| final double bound = 42; |
| final double[] values = ThreadLocalRandom.current().doubles(streamSize, origin, bound).toArray(); |
| final UniformRandomProvider rng = new DummyGenerator() { |
| private int i; |
| @Override |
| public double nextDouble(double o, double b) { |
| Assertions.assertEquals(origin, o, "origin"); |
| Assertions.assertEquals(bound, b, "bound"); |
| return values[i++]; |
| } |
| }; |
| |
| final DoubleStream stream = rng.doubles(streamSize, origin, bound); |
| Assertions.assertFalse(stream.isParallel()); |
| Assertions.assertArrayEquals(values, stream.toArray()); |
| } |
| |
| // Statistical tests for uniform distribution |
| |
| // Monobit tests |
| |
| @ParameterizedTest |
| @ValueSource(longs = {263784628482L, -2563472, -2367482842368L}) |
| void testNextIntMonobit(long seed) { |
| final UniformRandomProvider rng = createRNG(seed); |
| int bitCount = 0; |
| final int n = 100; |
| for (int i = 0; i < n; i++) { |
| bitCount += Integer.bitCount(rng.nextInt()); |
| } |
| final int numberOfBits = n * Integer.SIZE; |
| assertMonobit(bitCount, numberOfBits); |
| } |
| |
| @ParameterizedTest |
| @ValueSource(longs = {263784628L, 253674, -23568426834L}) |
| void testNextDoubleMonobit(long seed) { |
| final UniformRandomProvider rng = createRNG(seed); |
| int bitCount = 0; |
| final int n = 100; |
| for (int i = 0; i < n; i++) { |
| bitCount += Long.bitCount((long) (rng.nextDouble() * (1L << 53))); |
| } |
| final int numberOfBits = n * 53; |
| assertMonobit(bitCount, numberOfBits); |
| } |
| |
| @ParameterizedTest |
| @ValueSource(longs = {265342L, -234232, -672384648284L}) |
| void testNextBooleanMonobit(long seed) { |
| final UniformRandomProvider rng = createRNG(seed); |
| int bitCount = 0; |
| final int n = 1000; |
| for (int i = 0; i < n; i++) { |
| if (rng.nextBoolean()) { |
| bitCount++; |
| } |
| } |
| final int numberOfBits = n; |
| assertMonobit(bitCount, numberOfBits); |
| } |
| |
| @ParameterizedTest |
| @ValueSource(longs = {-1526731, 263846, 4545}) |
| void testNextFloatMonobit(long seed) { |
| final UniformRandomProvider rng = createRNG(seed); |
| int bitCount = 0; |
| final int n = 100; |
| for (int i = 0; i < n; i++) { |
| bitCount += Integer.bitCount((int) (rng.nextFloat() * (1 << 24))); |
| } |
| final int numberOfBits = n * 24; |
| assertMonobit(bitCount, numberOfBits); |
| } |
| |
| @ParameterizedTest |
| @CsvSource({ |
| "-2638468223894, 16", |
| "235647674, 17", |
| "-928738475, 23", |
| }) |
| void testNextBytesMonobit(long seed, int range) { |
| final UniformRandomProvider rng = createRNG(seed); |
| final byte[] bytes = new byte[range]; |
| int bitCount = 0; |
| final int n = 100; |
| for (int i = 0; i < n; i++) { |
| rng.nextBytes(bytes); |
| for (final byte b1 : bytes) { |
| bitCount += Integer.bitCount(b1 & 0xff); |
| } |
| } |
| final int numberOfBits = n * Byte.SIZE * range; |
| assertMonobit(bitCount, numberOfBits); |
| } |
| |
| /** |
| * Assert that the number of 1 bits is approximately 50%. This is based upon a |
| * fixed-step "random walk" of +1/-1 from zero. |
| * |
| * <p>The test is equivalent to the NIST Monobit test with a fixed p-value of |
| * 0.01. The number of bits is recommended to be above 100.</p> |
| * |
| * @see <A |
| * href="https://csrc.nist.gov/publications/detail/sp/800-22/rev-1a/final">Bassham, |
| * et al (2010) NIST SP 800-22: A Statistical Test Suite for Random and |
| * Pseudorandom Number Generators for Cryptographic Applications. Section |
| * 2.1.</a> |
| * |
| * @param bitCount The bit count. |
| * @param numberOfBits Number of bits. |
| */ |
| private static void assertMonobit(int bitCount, int numberOfBits) { |
| // Convert the bit count into a number of +1/-1 steps. |
| final double sum = 2.0 * bitCount - numberOfBits; |
| // The reference distribution is Normal with a standard deviation of sqrt(n). |
| // Check the absolute position is not too far from the mean of 0 with a fixed |
| // p-value of 0.01 taken from a 2-tailed Normal distribution. Computation of |
| // the p-value requires the complimentary error function. |
| final double absSum = Math.abs(sum); |
| final double max = Math.sqrt(numberOfBits) * 2.5758293035489004; |
| Assertions.assertTrue(absSum <= max, |
| () -> "Walked too far astray: " + absSum + " > " + max + |
| " (test will fail randomly about 1 in 100 times)"); |
| } |
| |
| // Uniformity tests |
| |
| @ParameterizedTest |
| @CsvSource({ |
| "263746283, 23, 0, 23", |
| "-126536861889, 16, 0, 16", |
| "617868181124, 1234, 567, 89", |
| "-56788, 512, 0, 233", |
| "6787535424, 512, 233, 279", |
| }) |
| void testNextBytesUniform(long seed, |
| int length, int start, int size) { |
| final UniformRandomProvider rng = createRNG(seed); |
| final byte[] buffer = new byte[length]; |
| |
| final Runnable nextMethod = start == 0 && size == length ? |
| () -> rng.nextBytes(buffer) : |
| () -> rng.nextBytes(buffer, start, size); |
| |
| final int last = start + size; |
| Assertions.assertTrue(isUniformNextBytes(buffer, start, last, nextMethod), |
| "Expected uniform bytes"); |
| |
| // The parts of the buffer where no values are put should be zero. |
| for (int i = 0; i < start; i++) { |
| Assertions.assertEquals(0, buffer[i], () -> "Filled < start: " + start); |
| } |
| for (int i = last; i < length; i++) { |
| Assertions.assertEquals(0, buffer[i], () -> "Filled >= last: " + last); |
| } |
| } |
| |
| /** |
| * Checks that the generator values can be placed into 256 bins with |
| * approximately equal number of counts. |
| * Test allows to select only part of the buffer for performing the |
| * statistics. |
| * |
| * @param buffer Buffer to be filled. |
| * @param first First element (included) of {@code buffer} range for |
| * which statistics must be taken into account. |
| * @param last Last element (excluded) of {@code buffer} range for |
| * which statistics must be taken into account. |
| * @param nextMethod Method that fills the given {@code buffer}. |
| * @return {@code true} if the distribution is uniform. |
| */ |
| private static boolean isUniformNextBytes(byte[] buffer, |
| int first, |
| int last, |
| Runnable nextMethod) { |
| final int sampleSize = 10000; |
| |
| // Number of possible values (do not change). |
| final int byteRange = 256; |
| // Chi-square critical value with 255 degrees of freedom |
| // and 1% significance level. |
| final double chi2CriticalValue = 310.45738821990585; |
| |
| // Bins. |
| final long[] observed = new long[byteRange]; |
| final double[] expected = new double[byteRange]; |
| |
| Arrays.fill(expected, sampleSize * (last - first) / (double) byteRange); |
| |
| for (int k = 0; k < sampleSize; k++) { |
| nextMethod.run(); |
| |
| for (int i = first; i < last; i++) { |
| // Convert byte to an index in [0, 255] |
| ++observed[buffer[i] & 0xff]; |
| } |
| } |
| |
| // Compute chi-square. |
| double chi2 = 0; |
| for (int k = 0; k < byteRange; k++) { |
| final double diff = observed[k] - expected[k]; |
| chi2 += diff * diff / expected[k]; |
| } |
| |
| // Statistics check. |
| return chi2 <= chi2CriticalValue; |
| } |
| |
| // Add range tests |
| |
| @ParameterizedTest |
| @CsvSource({ |
| // No lower bound |
| "2673846826, 0, 10", |
| "-23658268, 0, 12", |
| "263478624, 0, 31", |
| "1278332, 0, 32", |
| "99734765, 0, 2016128993", |
| "-63485384, 0, 1834691456", |
| "3876457638, 0, 869657561", |
| "-126784782, 0, 1570504788", |
| "2637846, 0, 2147483647", |
| // Small range |
| "2634682, 567576, 567586", |
| "-56757798989, -1000, -100", |
| "-97324785, -54656, 12", |
| "23423235, -526783468, 257", |
| // Large range |
| "-2634682, -1073741824, 1073741824", |
| "6786868132, -1263842626, 1237846372", |
| "-263846723, -368268, 2147483647", |
| "7352352, -2147483648, 61523457", |
| }) |
| void testNextIntUniform(long seed, int origin, int bound) { |
| final UniformRandomProvider rng = createRNG(seed); |
| final LongSupplier nextMethod = origin == 0 ? |
| () -> rng.nextInt(bound) : |
| () -> rng.nextInt(origin, bound); |
| checkNextInRange("nextInt", origin, bound, nextMethod); |
| } |
| |
| @ParameterizedTest |
| @CsvSource({ |
| // No lower bound |
| "2673846826, 0, 11", |
| "-23658268, 0, 19", |
| "263478624, 0, 31", |
| "1278332, 0, 32", |
| "99734765, 0, 2326378468282368421", |
| "-63485384, 0, 4872347624242243222", |
| "3876457638, 0, 6263784682638866843", |
| "-126784782, 0, 7256684297832668332", |
| "2637846, 0, 9223372036854775807", |
| // Small range |
| "2634682, 567576, 567586", |
| "-56757798989, -1000, -100", |
| "-97324785, -54656, 12", |
| "23423235, -526783468, 257", |
| // Large range |
| "-2634682, -4611686018427387904, 4611686018427387904", |
| "6786868132, -4962836478223688590, 6723648246224929947", |
| "-263846723, -368268, 9223372036854775807", |
| "7352352, -9223372036854775808, 61523457", |
| }) |
| void testNextLongUniform(long seed, long origin, long bound) { |
| final UniformRandomProvider rng = createRNG(seed); |
| final LongSupplier nextMethod = origin == 0 ? |
| () -> rng.nextLong(bound) : |
| () -> rng.nextLong(origin, bound); |
| checkNextInRange("nextLong", origin, bound, nextMethod); |
| } |
| |
| @ParameterizedTest |
| @CsvSource({ |
| // Note: If the range limits are integers above 2^24 (16777216) it is not possible |
| // to represent all the values with a float. This has no effect on sampling into bins |
| // but should be avoided when generating integers for use in production code. |
| |
| // No lower bound. |
| "2673846826, 0, 11", |
| "-23658268, 0, 19", |
| "263478624, 0, 31", |
| "1278332, 0, 32", |
| "99734765, 0, 1234", |
| "-63485384, 0, 578", |
| "3876457638, 0, 10000", |
| "-126784782, 0, 2983423", |
| "2637846, 0, 16777216", |
| // Range |
| "2634682, 567576, 567586", |
| "-56757798989, -1000, -100", |
| "-97324785, -54656, 12", |
| "23423235, -526783468, 257", |
| "-2634682, -688689797, -516827", |
| "6786868132, -67, 67", |
| "-263846723, -5678, 42", |
| "7352352, 678687, 61523457", |
| }) |
| void testNextFloatUniform(long seed, float origin, float bound) { |
| Assertions.assertEquals((long) origin, origin, "origin"); |
| Assertions.assertEquals((long) bound, bound, "bound"); |
| final UniformRandomProvider rng = createRNG(seed); |
| // Note casting as long will round towards zero. |
| // If the upper bound is negative then this can create a domain error so use floor. |
| final LongSupplier nextMethod = origin == 0 ? |
| () -> (long) rng.nextFloat(bound) : |
| () -> (long) Math.floor(rng.nextFloat(origin, bound)); |
| checkNextInRange("nextFloat", (long) origin, (long) bound, nextMethod); |
| } |
| |
| |
| @ParameterizedTest |
| @CsvSource({ |
| // Note: If the range limits are integers above 2^53 (9007199254740992) it is not possible |
| // to represent all the values with a double. This has no effect on sampling into bins |
| // but should be avoided when generating integers for use in production code. |
| |
| // No lower bound. |
| "2673846826, 0, 11", |
| "-23658268, 0, 19", |
| "263478624, 0, 31", |
| "1278332, 0, 32", |
| "99734765, 0, 1234", |
| "-63485384, 0, 578", |
| "3876457638, 0, 10000", |
| "-126784782, 0, 2983423", |
| "2637846, 0, 9007199254740992", |
| // Range |
| "2634682, 567576, 567586", |
| "-56757798989, -1000, -100", |
| "-97324785, -54656, 12", |
| "23423235, -526783468, 257", |
| "-2634682, -688689797, -516827", |
| "6786868132, -67, 67", |
| "-263846723, -5678, 42", |
| "7352352, 678687, 61523457", |
| }) |
| void testNextDoubleUniform(long seed, double origin, double bound) { |
| Assertions.assertEquals((long) origin, origin, "origin"); |
| Assertions.assertEquals((long) bound, bound, "bound"); |
| final UniformRandomProvider rng = createRNG(seed); |
| // Note casting as long will round towards zero. |
| // If the upper bound is negative then this can create a domain error so use floor. |
| final LongSupplier nextMethod = origin == 0 ? |
| () -> (long) rng.nextDouble(bound) : |
| () -> (long) Math.floor(rng.nextDouble(origin, bound)); |
| checkNextInRange("nextDouble", (long) origin, (long) bound, nextMethod); |
| } |
| |
| /** |
| * Tests uniformity of the distribution produced by the given |
| * {@code nextMethod}. |
| * It performs a chi-square test of homogeneity of the observed |
| * distribution with the expected uniform distribution. |
| * Repeat tests are performed at the 1% level and the total number of failed |
| * tests is tested at the 0.5% significance level. |
| * |
| * @param method Generator method. |
| * @param origin Lower bound (inclusive). |
| * @param bound Upper bound (exclusive). |
| * @param nextMethod method to call. |
| */ |
| private static void checkNextInRange(String method, |
| long origin, |
| long bound, |
| LongSupplier nextMethod) { |
| // Do not change |
| // (statistical test assumes that 500 repeats are made with dof = 9). |
| final int numTests = 500; |
| final int numBins = 10; // dof = numBins - 1 |
| |
| // Set up bins. |
| final long[] binUpperBounds = new long[numBins]; |
| // Range may be above a positive long: step = (bound - origin) / bins |
| final BigDecimal range = BigDecimal.valueOf(bound) |
| .subtract(BigDecimal.valueOf(origin)); |
| final double step = range.divide(BigDecimal.TEN).doubleValue(); |
| for (int k = 1; k < numBins; k++) { |
| binUpperBounds[k - 1] = origin + (long) (k * step); |
| } |
| // Final bound |
| binUpperBounds[numBins - 1] = bound; |
| |
| // Create expected frequencies |
| final double[] expected = new double[numBins]; |
| long previousUpperBound = origin; |
| final double scale = SAMPLE_SIZE_BD.divide(range, MathContext.DECIMAL128).doubleValue(); |
| double sum = 0; |
| for (int k = 0; k < numBins; k++) { |
| final long binWidth = binUpperBounds[k] - previousUpperBound; |
| expected[k] = scale * binWidth; |
| sum += expected[k]; |
| previousUpperBound = binUpperBounds[k]; |
| } |
| Assertions.assertEquals(SAMPLE_SIZE, sum, SAMPLE_SIZE * RELATIVE_ERROR, "Invalid expected frequencies"); |
| |
| final int[] observed = new int[numBins]; |
| // Chi-square critical value with 9 degrees of freedom |
| // and 1% significance level. |
| final double chi2CriticalValue = 21.665994333461924; |
| |
| // For storing chi2 larger than the critical value. |
| final List<Double> failedStat = new ArrayList<>(); |
| try { |
| final int lastDecileIndex = numBins - 1; |
| for (int i = 0; i < numTests; i++) { |
| Arrays.fill(observed, 0); |
| SAMPLE: for (int j = 0; j < SAMPLE_SIZE; j++) { |
| final long value = nextMethod.getAsLong(); |
| if (value < origin) { |
| Assertions.fail(String.format("Sample %d not within bound [%d, %d)", |
| value, origin, bound)); |
| } |
| |
| for (int k = 0; k < lastDecileIndex; k++) { |
| if (value < binUpperBounds[k]) { |
| ++observed[k]; |
| continue SAMPLE; |
| } |
| } |
| if (value >= bound) { |
| Assertions.fail(String.format("Sample %d not within bound [%d, %d)", |
| value, origin, bound)); |
| } |
| ++observed[lastDecileIndex]; |
| } |
| |
| // Compute chi-square. |
| double chi2 = 0; |
| for (int k = 0; k < numBins; k++) { |
| final double diff = observed[k] - expected[k]; |
| chi2 += diff * diff / expected[k]; |
| } |
| |
| // Statistics check. |
| if (chi2 > chi2CriticalValue) { |
| failedStat.add(chi2); |
| } |
| } |
| } catch (Exception e) { |
| // Should never happen. |
| throw new RuntimeException("Unexpected", e); |
| } |
| |
| // The expected number of failed tests can be modelled as a Binomial distribution |
| // B(n, p) with n=500, p=0.01 (500 tests with a 1% significance level). |
| // The cumulative probability of the number of failed tests (X) is: |
| // x P(X>x) |
| // 10 0.0132 |
| // 11 0.00521 |
| // 12 0.00190 |
| |
| if (failedStat.size() > 11) { // Test will fail with 0.5% probability |
| Assertions.fail(String.format( |
| "%s: Too many failures for n = %d, sample size = %d " + |
| "(%d out of %d tests failed, chi2 > %.3f=%s)", |
| method, bound, SAMPLE_SIZE, failedStat.size(), numTests, chi2CriticalValue, |
| failedStat.stream().map(d -> String.format("%.3f", d)) |
| .collect(Collectors.joining(", ", "[", "]")))); |
| } |
| } |
| } |