Skip to content
Permalink
Browse files
RNG-174: Improve support for non-zero seeds
Allow specification of a range for array seeds that must be non-zero.

The values for the range are based on the result of
o.a.c.rng.simple.ProvidersCommonParametricTest.testZeroIntArraySeed.

See also the core module for tests using:
- RandomAssert.assertNextIntZeroOutput
- RandomAssert.assertIntArrayConstructorWithSingleBitInPoolIsFunctional

Any generator that fails these tests requires a non-zero seed. In most
cases this was set as the full seed length, or one less for generators
that do not use all the bits of the seed array (WELL_19937_x,
WELL_44497_x).

Notable exceptions:

The KISS generator is reduced to a simple LCG when positions [0, 3) are
all zero. Added a test to demonstrate this. With a zero seed the KISS
LCG passes testZeroIntArraySeed. To avoid a poor generator the seed will
be checked to be non-zero in [0, 3).

The MSWS generator is sensitive to the initial state. Added a test to
show that a zero seed creates zero output. Updating RandomAssert to add
an assertLongArrayConstructorWithSingleBitInPoolIsFunctional test shows
the MSWS fails with single bit seeds. The sub-range has been set to [2,
3) to ensure a non-zero Weyl increment which is the best way to escape a
bad seed. This is a functionally breaking change.
  • Loading branch information
aherbert committed Apr 5, 2022
1 parent 950febd commit ab6050d992dd9e8057e0d3e3e29d534839c51440
Showing 14 changed files with 800 additions and 175 deletions.
@@ -353,7 +353,7 @@ private static void assertNextLongNonZeroOutputFromSingleBitSeed(
final int[] seed = new int[size];
int remaining = k;
for (int i = 0; i < seed.length; i++) {
seed[i] = 0x80000000;
seed[i] = Integer.MIN_VALUE;
for (int j = 0; j < 32; j++) {
final UniformRandomProvider rng = constructor.newInstance(seed);
RandomAssert.assertNextLongNonZeroOutputFromSingleBitSeed(rng, 2 * size, 2 * size, i, 31 - j);
@@ -383,17 +383,41 @@ private static void assertNextLongNonZeroOutputFromSingleBitSeed(
*/
public static <T extends UniformRandomProvider> void
assertLongArrayConstructorWithSingleBitSeedIsFunctional(Class<T> type, int size) {
assertLongArrayConstructorWithSingleBitInPoolIsFunctional(type, 64 * size);
}

/**
* Assert that the random generator created using an {@code long[]} seed with a
* single bit set is functional. This is tested using the
* {@link #assertNextLongNonZeroOutput(UniformRandomProvider, int, int)} using
* two times the seed size as the warm-up and test cycles.
*
* <p>The seed size is determined from the size of the bit pool. Bits are set for every position
* in the pool from most significant first. If the pool size is not a multiple of 64 then the
* remaining lower significant bits of the last seed position are not tested.</p>
*
* @param type Class of the generator.
* @param k Number of bits in the pool (not necessarily a multiple of 64).
*/
public static <T extends UniformRandomProvider> void
assertLongArrayConstructorWithSingleBitInPoolIsFunctional(Class<T> type, int k) {
try {
// Find the long[] constructor
final Constructor<T> constructor = type.getConstructor(long[].class);
final int size = (k + 63) / 64;
final long[] seed = new long[size];
for (int i = 0; i < size; i++) {
seed[i] = 0x8000000000000000L;
int remaining = k;
for (int i = 0; i < seed.length; i++) {
seed[i] = Long.MIN_VALUE;
for (int j = 0; j < 64; j++) {
final UniformRandomProvider rng = constructor.newInstance(seed);
RandomAssert.assertNextLongNonZeroOutputFromSingleBitSeed(rng, 2 * size, 2 * size, i, 63 - j);
// Eventually reset to zero
seed[i] >>>= 1;
// Terminate when the entire bit pool has been tested
if (--remaining == 0) {
return;
}
}
Assertions.assertEquals(0L, seed[i], "Seed element was not reset");
}
@@ -17,6 +17,7 @@
package org.apache.commons.rng.core.source32;

import org.apache.commons.rng.core.RandomAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class KISSRandomTest {
@@ -162,4 +163,20 @@ void testMarsaglia() {

RandomAssert.assertEquals(expectedSequence, new KISSRandom(seed));
}

@Test
void testConstructorWithZeroSeedInSubRangeIsPartiallyFunctional() {
// If the first 3 positions are zero then the output is a simple LCG.
// The seeding routine in commons-rng-simple should ensure the first 3 seed
// positions are not all zero.
final int m = 69069;
final int c = 1234567;
int s = 42;
final int[] seed = new int[] {0, 0, 0, s};
final KISSRandom rng = new KISSRandom(seed);
for (int i = 0; i < 200; i++) {
s = m * s + c;
Assertions.assertEquals(s, rng.nextInt());
}
}
}
@@ -22,6 +22,9 @@
import org.junit.jupiter.api.Test;

class MiddleSquareWeylSequenceTest {
/** The size of the array seed. */
private static final int SEED_SIZE = 3;

@Test
void testReferenceCode() {
/*
@@ -97,4 +100,9 @@ void testNextLong() {
rng2.nextLong());
}
}

@Test
void testConstructorWithZeroSeedIsNonFunctional() {
RandomAssert.assertNextIntZeroOutput(new MiddleSquareWeylSequence(new long[SEED_SIZE]), 2 * SEED_SIZE);
}
}
@@ -34,7 +34,7 @@ final class Conversions {
* </pre>
* @see <a href="https://en.wikipedia.org/wiki/Golden_ratio">Golden ratio</a>
*/
private static final long GOLDEN_RATIO = 0x9e3779b97f4a7c15L;
private static final long GOLDEN_RATIO = MixFunctions.GOLDEN_RATIO_64;

/** No instances. */
private Conversions() {}
@@ -100,24 +100,6 @@ static int longSizeFromIntSize(int size) {
return (size + 1) >>> 1;
}

/**
* Perform variant 13 of David Stafford's 64-bit mix function.
* This is the mix function used in the {@link SplitMix64} RNG.
*
* <p>This is ranked first of the top 14 Stafford mixers.
*
* @param z the input value
* @return the output value
* @see <a href="http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html">Better
* Bit Mixing - Improving on MurmurHash3&#39;s 64-bit Finalizer.</a>
*/
private static long stafford13(long z) {
long x = z;
x = (x ^ (x >>> 30)) * 0xbf58476d1ce4e5b9L;
x = (x ^ (x >>> 27)) * 0x94d049bb133111ebL;
return x ^ (x >>> 31);
}

/**
* Creates a {@code long} value from an {@code int}. The conversion
* is made as if seeding a SplitMix64 RNG and calling nextLong().
@@ -126,7 +108,7 @@ private static long stafford13(long z) {
* @return a {@code long}.
*/
static long int2Long(int input) {
return stafford13(input + GOLDEN_RATIO);
return MixFunctions.stafford13(input + GOLDEN_RATIO);
}

/**
@@ -189,13 +171,13 @@ static int[] long2IntArray(long input, int length) {
// Process pairs
final int n = length & ~0x1;
for (int i = 0; i < n; i += 2) {
final long x = stafford13(v += GOLDEN_RATIO);
final long x = MixFunctions.stafford13(v += GOLDEN_RATIO);
output[i] = (int) x;
output[i + 1] = (int) (x >>> 32);
}
// Final value
if (n < length) {
output[n] = (int) stafford13(v + GOLDEN_RATIO);
output[n] = (int) MixFunctions.stafford13(v + GOLDEN_RATIO);
}
return output;
}
@@ -213,7 +195,7 @@ static long[] long2LongArray(long input, int length) {
long v = input;
final long[] output = new long[length];
for (int i = 0; i < length; i++) {
output[i] = stafford13(v += GOLDEN_RATIO);
output[i] = MixFunctions.stafford13(v += GOLDEN_RATIO);
}
return output;
}
@@ -0,0 +1,73 @@
/*
* 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.simple.internal;

/**
* Performs mixing of bits.
*
* @since 1.5
*/
final class MixFunctions {
/**
* The fractional part of the the golden ratio, phi, scaled to 64-bits and rounded to odd.
* This can be used as an increment for a Weyl sequence.
*
* @see <a href="https://en.wikipedia.org/wiki/Golden_ratio">Golden ratio</a>
*/
static final long GOLDEN_RATIO_64 = 0x9e3779b97f4a7c15L;
/**
* The fractional part of the the golden ratio, phi, scaled to 32-bits and rounded to odd.
* This can be used as an increment for a Weyl sequence.
*
* @see <a href="https://en.wikipedia.org/wiki/Golden_ratio">Golden ratio</a>
*/
static final int GOLDEN_RATIO_32 = 0x9e3779b9;

/** No instances. */
private MixFunctions() {}

/**
* Perform variant 13 of David Stafford's 64-bit mix function.
* This is the mix function used in the
* {@link org.apache.commons.rng.core.source64.SplitMix64 SplitMix64} RNG.
*
* <p>This is ranked first of the top 14 Stafford mixers.
*
* @param x the input value
* @return the output value
* @see <a href="http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html">Better
* Bit Mixing - Improving on MurmurHash3&#39;s 64-bit Finalizer.</a>
*/
static long stafford13(long x) {
x = (x ^ (x >>> 30)) * 0xbf58476d1ce4e5b9L;
x = (x ^ (x >>> 27)) * 0x94d049bb133111ebL;
return x ^ (x >>> 31);
}

/**
* Perform the finalising 32-bit mix function of Austin Appleby's MurmurHash3.
*
* @param x the input value
* @return the output value
* @see <a href="https://github.com/aappleby/smhasher">SMHasher</a>
*/
static int murmur3(int x) {
x = (x ^ (x >>> 16)) * 0x85ebca6b;
x = (x ^ (x >>> 13)) * 0xc2b2ae35;
return x ^ (x >>> 16);
}
}
@@ -45,7 +45,7 @@ public enum NativeSeedType {
/** The seed type is {@code Integer}. */
INT(Integer.class, 4) {
@Override
public Integer createSeed(int size) {
public Integer createSeed(int size, int from, int to) {
return SeedFactory.createInt();
}
@Override
@@ -72,7 +72,7 @@ protected Integer convert(byte[] seed, int size) {
/** The seed type is {@code Long}. */
LONG(Long.class, 8) {
@Override
public Long createSeed(int size) {
public Long createSeed(int size, int from, int to) {
return SeedFactory.createLong();
}
@Override
@@ -99,10 +99,11 @@ protected Long convert(byte[] seed, int size) {
/** The seed type is {@code int[]}. */
INT_ARRAY(int[].class, 4) {
@Override
public int[] createSeed(int size) {
public int[] createSeed(int size, int from, int to) {
// Limit the number of calls to the synchronized method. The generator
// will support self-seeding.
return SeedFactory.createIntArray(Math.min(size, RANDOM_SEED_ARRAY_SIZE));
return SeedFactory.createIntArray(Math.min(size, RANDOM_SEED_ARRAY_SIZE),
from, to);
}
@Override
protected int[] convert(Integer seed, int size) {
@@ -132,10 +133,11 @@ protected int[] convert(byte[] seed, int size) {
/** The seed type is {@code long[]}. */
LONG_ARRAY(long[].class, 8) {
@Override
public long[] createSeed(int size) {
public long[] createSeed(int size, int from, int to) {
// Limit the number of calls to the synchronized method. The generator
// will support self-seeding.
return SeedFactory.createLongArray(Math.min(size, RANDOM_SEED_ARRAY_SIZE));
return SeedFactory.createLongArray(Math.min(size, RANDOM_SEED_ARRAY_SIZE),
from, to);
}
@Override
protected long[] convert(Integer seed, int size) {
@@ -214,7 +216,24 @@ public int getBytes() {
* @param size The size of the seed (array types only).
* @return the seed
*/
public abstract Object createSeed(int size);
public Object createSeed(int size) {
// Maintain behaviour since 1.3 to ensure position [0] of array seeds is non-zero.
return createSeed(size, 0, Math.min(size, 1));
}

/**
* Creates the seed. The output seed type is determined by the native seed type. If
* the output is an array the required size of the array can be specified and a
* sub-range that must not be all-zero.
*
* @param size The size of the seed (array types only).
* @param from The start of the not all-zero sub-range (inclusive; array types only).
* @param to The end of the not all-zero sub-range (exclusive; array types only).
* @return the seed
* @throws IndexOutOfBoundsException if the sub-range is out of bounds
* @since 1.5
*/
public abstract Object createSeed(int size, int from, int to);

/**
* Converts the input seed from any of the supported seed types to the native seed type.

0 comments on commit ab6050d

Please sign in to comment.