blob: 0cd7076506d3c2f00dfa8f966a904d53e4f4d890 [file] [log] [blame]
/*
* 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(", ", "[", "]"))));
}
}
}