Skip to content

Commit c7c4db8

Browse files
committed
RNG-189: Add interface ArbitrarilyJumpableUniformRandomProvider
1 parent 4bf6e71 commit c7c4db8

8 files changed

Lines changed: 373 additions & 3 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.commons.rng;
18+
19+
import java.util.stream.Stream;
20+
21+
/**
22+
* Applies to generators that can be advanced an arbitrary number of steps of the output
23+
* sequence in a single operation.
24+
*
25+
* <p>Implementations must ensure that a jump of a specified {@code distance} will advance
26+
* the state cycle sufficiently that an equivalent number of sequential calls to the
27+
* original provider will <strong>not overlap</strong> output from the advanced
28+
* provider.</p>
29+
*
30+
* <p>For many applications, it suffices to jump forward by a power of two or some small
31+
* multiple of a power of two, but this power of two may not be representable as a
32+
* {@code long} value. To avoid the use of {@link java.math.BigInteger BigInteger} values
33+
* as jump distances, double values are used instead.</p>
34+
*
35+
* <p>Typical usage in a multithreaded application is to create a single
36+
* {@link ArbitrarilyJumpableUniformRandomProvider} and {@link #jump(double) jump} the
37+
* generator forward while passing each copy generator to a worker thread. The jump
38+
* {@code distance} should be sufficient to cover all expected output by each worker.
39+
* Since each copy generator is also a {@link ArbitrarilyJumpableUniformRandomProvider}
40+
* with care it is possible to further distribute generators within the original jump
41+
* {@code distance} and use the entire state cycle in different ways.</p>
42+
*
43+
* @since 1.7
44+
*/
45+
public interface ArbitrarilyJumpableUniformRandomProvider extends UniformRandomProvider {
46+
/**
47+
* Creates a copy of the {@link ArbitrarilyJumpableUniformRandomProvider} and then advances
48+
* the state cycle of the current instance by the specified {@code distance}.
49+
* The copy is returned.
50+
*
51+
* <p>The current state will be advanced in a single operation by the equivalent of a
52+
* number of sequential calls to a method that updates the state cycle of the provider.</p>
53+
*
54+
* <p>Repeat invocations of this method will create a series of generators
55+
* that are uniformly spaced at intervals of the output sequence. Each generator provides
56+
* non-overlapping output for the length specified by {@code distance} for use in parallel
57+
* computations.</p>
58+
*
59+
* @param distance Distance to jump forward with the state cycle.
60+
* @return A copy of the current state.
61+
* @throws IllegalArgumentException if {@code distance} is negative,
62+
* or is greater than the period of this generator.
63+
*/
64+
ArbitrarilyJumpableUniformRandomProvider jump(double distance);
65+
66+
/**
67+
* Creates a copy of the {@link ArbitrarilyJumpableUniformRandomProvider} and then advances
68+
* the state cycle of the current instance by a distance equal to 2<sup>{@code logDistance}</sup>.
69+
* The copy is returned.
70+
*
71+
* <p>The current state will be advanced in a single operation by the equivalent of a
72+
* number of sequential calls to a method that updates the state cycle of the provider.</p>
73+
*
74+
* <p>Repeat invocations of this method will create a series of generators
75+
* that are uniformly spaced at intervals of the output sequence. Each generator provides
76+
* non-overlapping output for the length specified by 2<sup>{@code logDistance}</sup> for use
77+
* in parallel computations.</p>
78+
*
79+
* @param logDistance Base-2 logarithm of the distance to jump forward with the state cycle.
80+
* @return A copy of the current state.
81+
* @throws IllegalArgumentException if 2<sup>{@code logDistance}</sup>
82+
* is greater than the period of this generator.
83+
*/
84+
ArbitrarilyJumpableUniformRandomProvider jumpPowerOfTwo(int logDistance);
85+
86+
/**
87+
* Returns an effectively unlimited stream of new random generators, each of which
88+
* implements the {@link ArbitrarilyJumpableUniformRandomProvider} interface. The
89+
* generators are output at integer multiples of the specified jump {@code distance}
90+
* in the generator's state cycle.
91+
*
92+
* @param distance Distance to jump forward with the state cycle.
93+
* @return a stream of random generators.
94+
* @throws IllegalArgumentException if {@code distance} is negative,
95+
* or is greater than the period of this generator.
96+
*/
97+
default Stream<ArbitrarilyJumpableUniformRandomProvider> jumps(double distance) {
98+
UniformRandomProviderSupport.validateJumpDistance(distance);
99+
return Stream.generate(() -> jump(distance)).sequential();
100+
}
101+
102+
/**
103+
* Returns a stream producing the given {@code streamSize} number of new random
104+
* generators, each of which implements the {@link ArbitrarilyJumpableUniformRandomProvider}
105+
* interface. The generators are output at integer multiples of the specified jump
106+
* {@code distance} in the generator's state cycle.
107+
*
108+
* @param streamSize Number of objects to generate.
109+
* @param distance Distance to jump forward with the state cycle.
110+
* @return a stream of random generators; the stream is limited to the given
111+
* {@code streamSize}.
112+
* @throws IllegalArgumentException if {@code streamSize} is negative;
113+
* or if {@code distance} is negative, or is greater than the period of this generator.
114+
*/
115+
default Stream<ArbitrarilyJumpableUniformRandomProvider> jumps(long streamSize,
116+
double distance) {
117+
UniformRandomProviderSupport.validateStreamSize(streamSize);
118+
return jumps(distance).limit(streamSize);
119+
}
120+
}

commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProviderSupport.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
final class UniformRandomProviderSupport {
3535
/** Message for an invalid stream size. */
3636
private static final String INVALID_STREAM_SIZE = "Invalid stream size: ";
37+
/** Message for an invalid jump distance. */
38+
private static final String INVALID_JUMP_DISTANCE = "Invalid jump distance: ";
3739
/** Message for an invalid upper bound (must be positive, finite and above zero). */
3840
private static final String INVALID_UPPER_BOUND = "Upper bound must be above zero: ";
3941
/** Message format for an invalid range for lower inclusive and upper exclusive. */
@@ -58,6 +60,22 @@ static void validateStreamSize(long size) {
5860
}
5961
}
6062

63+
/**
64+
* Validate the jump distance.
65+
*
66+
* <p>Note it is not possible to validate if the jump distance is greater
67+
* than the period of the generator as this is unknown.
68+
*
69+
* @param distance Jump distance.
70+
* @throws IllegalArgumentException if {@code distance} is negative, or non-finite.
71+
*/
72+
static void validateJumpDistance(double distance) {
73+
// Negation of logic will detect NaN
74+
if (!(distance >= 0 && distance <= Double.MAX_VALUE)) {
75+
throw new IllegalArgumentException(INVALID_JUMP_DISTANCE + distance);
76+
}
77+
}
78+
6179
/**
6280
* Validate the upper bound.
6381
*

commons-rng-client-api/src/main/java/org/apache/commons/rng/package-info.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,29 @@
1919
* This package contains the library's interface to be used by client
2020
* code that needs a generator of sequences of pseudo-random numbers
2121
* that are <i>uniformly distributed</i> in a specified range.
22+
*
23+
* <p><strong>Jumpable Generators</strong>
24+
*
25+
* <p>Interfaces are specified for generators that are capable of advancing
26+
* forward a large number of steps in the output period of the generator in a
27+
* single jump. This can be used to create a series of non-overlapping generators
28+
* for use in multithreaded applications.
29+
*
30+
* <p>Note that there is not a one-to-one relationship between the number of output random
31+
* values from a provider and the number of steps from the underlying state cycle. This
32+
* is due to:
33+
* <ol>
34+
* <li>Possible use of rejection algorithms to output a random value using multiple
35+
* values from the state cycle.</li>
36+
* <li>The number of bits required to generate a random value differing from the
37+
* number of bits generated by the underlying source of randomness. For example
38+
* generation of a 64-bit {@code long} value using a 32-bit source of randomness.</li>
39+
* </ol>
40+
*
41+
* <p>Users are advised to use jumping generators with care to avoid
42+
* overlapping output of multiple generators in parallel computations.
43+
* A cautious approach is to use a jump distance far larger than the expected
44+
* output length used by each generator.
2245
*/
2346

2447
package org.apache.commons.rng;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.commons.rng;
18+
19+
import java.util.HashSet;
20+
import java.util.Set;
21+
import java.util.stream.Stream;
22+
import org.junit.jupiter.api.Assertions;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
import org.junit.jupiter.params.provider.MethodSource;
26+
import org.junit.jupiter.params.provider.ValueSource;
27+
28+
/**
29+
* Tests for default method implementations in
30+
* {@link ArbitrarilyJumpableUniformRandomProvider}.
31+
*
32+
* <p>Streams methods are asserted to call the corresponding jump method in the
33+
* interface.
34+
*/
35+
class ArbitrarilyJumpableUniformRandomProviderTest {
36+
/**
37+
* Class for checking the behavior of the ArbitrarilyJumpableUniformRandomProvider.
38+
* This generator returns a fixed value. The value is incremented by jumping.
39+
*/
40+
private static class JumpableGenerator implements ArbitrarilyJumpableUniformRandomProvider {
41+
/** The value for nextLong(). */
42+
private long value;
43+
44+
JumpableGenerator(long seed) {
45+
this.value = seed;
46+
}
47+
48+
@Override
49+
public long nextLong() {
50+
return value;
51+
}
52+
53+
@Override
54+
public ArbitrarilyJumpableUniformRandomProvider jump(double distance) {
55+
final JumpableGenerator copy = new JumpableGenerator(value);
56+
// Support small distances as a long
57+
value += (long) distance;
58+
return copy;
59+
}
60+
61+
@Override
62+
public ArbitrarilyJumpableUniformRandomProvider jumpPowerOfTwo(int logDistance) {
63+
throw new IllegalStateException("Not required by default methods");
64+
}
65+
}
66+
67+
interface JumpsFunction {
68+
/**
69+
* Create a stream of generators separated by the jump distance.
70+
*
71+
* @param rng Jumpable generator.
72+
* @param streamSize Number of objects to generate.
73+
* @param distance Distance to jump forward with the state cycle.
74+
* @return the stream
75+
*/
76+
Stream<ArbitrarilyJumpableUniformRandomProvider> apply(ArbitrarilyJumpableUniformRandomProvider rng, long size, double distance);
77+
}
78+
79+
/**
80+
* Return a stream of jump arguments, each of the arguments consisting of the size of
81+
* the stream, the seed value for the jumpable generator, and the jump distance.
82+
*
83+
* @return the stream of arguments
84+
*/
85+
static Stream<Arguments> jumpArguments() {
86+
return Stream.of(
87+
// size, seed
88+
Arguments.of(0, 0, 0.0),
89+
Arguments.of(1, 62317845757L, 2.0),
90+
Arguments.of(5, -12683127894356L, 42.0)
91+
);
92+
}
93+
94+
@ParameterizedTest
95+
@ValueSource(longs = {-1, -2, Long.MIN_VALUE})
96+
void testInvalidStreamSizeThrows(long size) {
97+
final ArbitrarilyJumpableUniformRandomProvider rng = new JumpableGenerator(0);
98+
final double distance = 10;
99+
Assertions.assertThrows(IllegalArgumentException.class, () -> rng.jumps(size, distance));
100+
}
101+
102+
@ParameterizedTest
103+
@ValueSource(doubles = {-1, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NaN})
104+
void testInvalidDistanceThrows(double distance) {
105+
final ArbitrarilyJumpableUniformRandomProvider rng = new JumpableGenerator(0);
106+
final long size = 1;
107+
Assertions.assertThrows(IllegalArgumentException.class, () -> rng.jumps(size, distance));
108+
}
109+
110+
@ParameterizedTest
111+
@MethodSource(value = {"jumpArguments"})
112+
void testJumps(int size, long seed, double distance) {
113+
assertJumps(size, seed, distance,
114+
(rng, n, d) -> rng.jumps(d).limit(n));
115+
}
116+
117+
@ParameterizedTest
118+
@MethodSource(value = {"jumpArguments"})
119+
void testJumpsWithSize(int size, long seed, double distance) {
120+
assertJumps(size, seed, distance,
121+
ArbitrarilyJumpableUniformRandomProvider::jumps);
122+
}
123+
124+
/**
125+
* Assert that successive calls to the generator jump function will create a series of
126+
* generators that matches the stream function.
127+
*
128+
* @param size Number of jumps.
129+
* @param seed Seed for the generator.
130+
* @param distance Jump distance.
131+
* @param streamFunction Stream function to create a series of generators spaced
132+
* using the jump function.
133+
*/
134+
private static void assertJumps(int size, long seed, double distance,
135+
JumpsFunction streamFunction) {
136+
// Manually jump
137+
final JumpableGenerator jumpingRNG = new JumpableGenerator(seed);
138+
final long[] expected = new long[size];
139+
for (int i = 0; i < size; i++) {
140+
final UniformRandomProvider copy = jumpingRNG.jump(distance);
141+
Assertions.assertNotSame(jumpingRNG, copy, "Jump function should return a copy");
142+
expected[i] = copy.nextLong();
143+
}
144+
145+
// Stream (must be sequential)
146+
final JumpableGenerator streamingRNG = new JumpableGenerator(seed);
147+
final Stream<? extends UniformRandomProvider> stream =
148+
streamFunction.apply(streamingRNG, size, distance);
149+
Assertions.assertFalse(stream.isParallel(), "Jumping requires a non-parallel stream");
150+
151+
// Stream should create unique generators
152+
final Set<UniformRandomProvider> set = new HashSet<>();
153+
final long[] actual = stream.map(x -> addAndReturn(set, x))
154+
.mapToLong(UniformRandomProvider::nextLong)
155+
.toArray();
156+
Assertions.assertEquals(size, set.size(), "Stream should have unique generators");
157+
Assertions.assertFalse(set.contains(streamingRNG), "Stream contains the source of the stream as a generator");
158+
159+
Assertions.assertArrayEquals(expected, actual, "Stream function did not match the jump function");
160+
}
161+
162+
/**
163+
* Add the generator to the set and return the generator. This is a convenience
164+
* method used to check generator objects in a stream are unique and not the same as
165+
* the generator that created the stream.
166+
*
167+
* @param set Set
168+
* @param x Generator
169+
* @return the generator
170+
*/
171+
private static UniformRandomProvider addAndReturn(Set<UniformRandomProvider> set, UniformRandomProvider x) {
172+
set.add(x);
173+
return x;
174+
}
175+
}

commons-rng-simple/src/main/java/org/apache/commons/rng/simple/RandomSource.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@
157157
* non-overlapping sequences by copying the generator and then advancing a large number
158158
* of steps in the generator sequence. Repeated jumps can create a series of
159159
* child generators that will output non-overlapping sequences over a specified number
160-
* of outputs. These implementations are identified using the {@link #isJumpable()}
161-
* and {@link #isLongJumpable()} methods.
160+
* of outputs. These implementations are identified using the {@link #isJumpable()},
161+
* {@link #isLongJumpable()} and {@link #isArbitrarilyJumpable()} methods.
162162
* </p>
163163
* <pre><code>
164164
* RandomSource source = RandomSource.XO_RO_SHI_RO_128_SS; // Known to be jumpable.
@@ -802,6 +802,29 @@ public boolean isLongJumpable() {
802802
return isAssignableTo(org.apache.commons.rng.LongJumpableUniformRandomProvider.class);
803803
}
804804

805+
/**
806+
* Checks whether the implementing class represented by this random source
807+
* supports the {@link org.apache.commons.rng.ArbitrarilyJumpableUniformRandomProvider
808+
* ArbitrarilyJumpableUniformRandomProvider} interface. If {@code true} the instance returned
809+
* by {@link #create(RandomSource)} may be cast to the interface; otherwise a class
810+
* cast exception will occur.
811+
*
812+
* <p>Usage example:</p>
813+
* <pre><code>
814+
* RandomSource source = ...;
815+
* if (source.isJumpable()) {
816+
* ArbitrarilyJumpableUniformRandomProvider rng =
817+
* (ArbitrarilyJumpableUniformRandomProvider) source.create();
818+
* }
819+
* </code></pre>
820+
*
821+
* @return {@code true} if arbitrarily jumpable
822+
* @since 1.7
823+
*/
824+
public boolean isArbitrarilyJumpable() {
825+
return isAssignableTo(org.apache.commons.rng.ArbitrarilyJumpableUniformRandomProvider.class);
826+
}
827+
805828
/**
806829
* Checks whether the implementing class represented by this random source
807830
* supports the {@link org.apache.commons.rng.SplittableUniformRandomProvider

0 commit comments

Comments
 (0)