LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
rng_ssj.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_UTIL_RNG_SSJ_H
6#define LINE_UTIL_RNG_SSJ_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * The two random number generators the Java LDES engine draws from, reproduced
12 * exactly: SSJ's `MRG32k3a` and `java.util.Random`.
13 *
14 * WHY THIS EXISTS. `common/ldes` is to become the C++ engine compiled, in place
15 * of a GraalVM image of the Java one, and a simulator that answers with a
16 * different sample path at the same seed is not a replacement -- every seeded
17 * golden in MATLAB, Python and the parity harness would re-baseline, and
18 * "LDES agrees across the codebases at a given seed", which `CLAUDE.md` states
19 * as an invariant, would weaken to "agrees in distribution". The C++ engine
20 * previously drew from `std::mt19937_64`; this header is the first half of
21 * closing that gap, and `ldes_sampler.h` is the second (the variate algorithms
22 * must consume the same uniforms in the same order).
23 *
24 * WHAT MADE IT TRACTABLE. `Solver_ssj` never relies on SSJ's package-level
25 * stream sequence: every stream is created and then given an EXPLICIT six-long
26 * seed derived from the run seed and a per-(node, class) offset, e.g.
27 *
28 * long offset = ((long) (numSources + svcIdx) * numClasses + k) * 10 + 1000;
29 * stream.setSeed(LdesStreamSeeds.mrg32k3a(seed, offset));
30 *
31 * so none of SSJ's 2^127 jump-ahead machinery is on the path. Matching the
32 * engine means matching the recurrence, `nextValue`/`nextDouble`, those
33 * offsets and the hash that turns (seed, offset) into the six seeds
34 * (`SeedHash`), not the stream hierarchy.
35 *
36 * MRG32k3a is L'Ecuyer's combined multiple recursive generator (Operations
37 * Research 47(1), 1999): two order-three recurrences modulo m1 = 2^32 - 209 and
38 * m2 = 2^32 - 22853, combined by subtraction. SSJ computes it in DOUBLES with
39 * the multipliers split so every intermediate stays exactly representable, and
40 * that arithmetic is reproduced here rather than an integer rewrite: the double
41 * form is what the reference runs, and the two agree only if the rounding does.
42 *
43 * `java.util.Random` is the specified 48-bit LCG of the Java Language
44 * Specification: seed scrambling by 0x5DEECE66D, `next(bits)` taking the high
45 * bits, `nextDouble` from a 26-bit and a 27-bit draw, and `nextInt(bound)` with
46 * its rejection loop for non-power-of-two bounds. All of it is normative, so a
47 * faithful transcription is exact by construction.
48 */
49
50#include <cmath>
51#include <cstdint>
52#include <string>
53
54#include "line/util/error.h"
55
56namespace line {
57namespace rng {
58
59// ---------------------------------------------------------------------------
60// Stream seeds
61// ---------------------------------------------------------------------------
62
63/**
64 * splitmix64 keyed by (run seed, stream offset): where every LDES stream's seeds come from.
65 *
66 * The offset only NAMES a stream. It used to be added to the seed directly,
67 * `{seed+off, ..., seed+off+5}`, and that is wrong for MRG32k3a: its two
68 * recurrences are linear, so streams whose seed vectors are EQUALLY SPACED are
69 * linearly dependent, the draws of offsets o, o+10 and o+20 satisfying
70 * u1 - 2*u2 + u3 = 0 (mod 1) draw for draw. Sibling stations sit at exactly
71 * such offsets, so the branches of a 3-way fork drew in lockstep and E[max] of
72 * three Exp(1) came out 1.845 against 11/6 (|t| = 14 over 8 seeds).
73 *
74 * Java's `jline.solvers.ldes.handlers.LdesStreamSeeds` is the same function,
75 * draw for draw, which is what keeps the flat engines on one sample path. The
76 * layered engine (`engine::ln_rng`) keeps drawing from the same hash after the
77 * six MRG seeds for its java.util.Random and McRng.
78 */
79class SeedHash {
80public:
81 SeedHash(long long seed, long long offset)
82 : z_(static_cast<std::uint64_t>(seed) * kGolden ^ static_cast<std::uint64_t>(offset)) {}
83
84 /** The next splitmix64 output. */
85 std::uint64_t next() {
86 z_ += kGolden;
87 std::uint64_t x = z_;
88 x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
89 x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
90 return x ^ (x >> 31);
91 }
92
93 /**
94 * Six MRG32k3a seeds: the first three in [1, m1), the last three in [1, m2),
95 * so neither component can start from the all-zero state SSJ rejects.
96 */
97 void mrg_seeds(long long s[6]) {
98 for (int i = 0; i < 6; ++i) {
99 const std::uint64_t m = (i < 3) ? 4294967087ULL : 4294944443ULL; // SSJ's m1, m2
100 s[i] = static_cast<long long>(1 + next() % (m - 1));
101 }
102 }
103
104private:
105 static constexpr std::uint64_t kGolden = 0x9E3779B97F4A7C15ULL;
106 std::uint64_t z_;
107};
108
109// ---------------------------------------------------------------------------
110// SSJ MRG32k3a
111// ---------------------------------------------------------------------------
112
113/**
114 * SSJ's `umontreal.ssj.rng.MRG32k3a`, state and all.
115 *
116 * The state is the six doubles (Cg[0..5]) SSJ keeps; `set_seed` takes the same
117 * six-long vector `setSeed(long[])` takes and applies the same validation, so a
118 * call site can be transcribed from the Java verbatim.
119 */
120class Mrg32k3a {
121public:
122 Mrg32k3a() { reset_to_default(); }
123
124 /** SSJ's setSeed(long[6]). Values are taken modulo the two moduli. */
125 void set_seed(const long long s[6]) {
126 validate(s);
127 for (int i = 0; i < 6; ++i) cg_[i] = static_cast<double>(s[i]);
128 }
129
130 /** The stream an LDES engine names by (run seed, offset): its seeds hashed by `SeedHash`. */
131 void set_seed_offset(long long seed, long long offset) {
132 long long s[6];
133 SeedHash(seed, offset).mrg_seeds(s);
134 set_seed(s);
135 }
136
137 /**
138 * SSJ's nextValue(): the combined generator, returning a double in (0, 1).
139 * Never returns 0; may return values arbitrarily close to 1.
140 */
141 double next_double() {
142 // Component 1
143 double p1 = kA12 * cg_[1] - kA13n * cg_[0];
144 double k = std::floor(p1 / kM1);
145 p1 -= k * kM1;
146 if (p1 < 0.0) p1 += kM1;
147 cg_[0] = cg_[1];
148 cg_[1] = cg_[2];
149 cg_[2] = p1;
150
151 // Component 2
152 double p2 = kA21 * cg_[5] - kA23n * cg_[3];
153 k = std::floor(p2 / kM2);
154 p2 -= k * kM2;
155 if (p2 < 0.0) p2 += kM2;
156 cg_[3] = cg_[4];
157 cg_[4] = cg_[5];
158 cg_[5] = p2;
159
160 // Combination
161 return ((p1 > p2) ? (p1 - p2) * kNorm : (p1 - p2 + kM1) * kNorm);
162 }
163
164 /** SSJ's nextInt(i, j): a uniform integer on [i, j]. */
165 int next_int(int i, int j) {
166 if (i > j) throw InputError("Mrg32k3a::next_int: empty range");
167 return i + static_cast<int>(next_double() * (static_cast<double>(j - i) + 1.0));
168 }
169
170 /** The six state doubles, for tests that pin the state and not only the draws. */
171 double state(int i) const { return cg_[i]; }
172
173private:
174 void reset_to_default() {
175 // SSJ's default initial seed (12345 in all six slots).
176 for (int i = 0; i < 6; ++i) cg_[i] = 12345.0;
177 }
178
179 static void validate(const long long s[6]) {
180 for (int i = 0; i < 3; ++i) {
181 if (s[i] < 0 || static_cast<double>(s[i]) >= kM1)
182 throw InputError("Mrg32k3a::set_seed: the first three seeds must be in [0, m1)");
183 }
184 for (int i = 3; i < 6; ++i) {
185 if (s[i] < 0 || static_cast<double>(s[i]) >= kM2)
186 throw InputError("Mrg32k3a::set_seed: the last three seeds must be in [0, m2)");
187 }
188 if (s[0] == 0 && s[1] == 0 && s[2] == 0)
189 throw InputError("Mrg32k3a::set_seed: the first three seeds must not all be zero");
190 if (s[3] == 0 && s[4] == 0 && s[5] == 0)
191 throw InputError("Mrg32k3a::set_seed: the last three seeds must not all be zero");
192 }
193
194 // L'Ecuyer's constants, exactly as SSJ spells them.
195 static constexpr double kM1 = 4294967087.0;
196 static constexpr double kM2 = 4294944443.0;
197 static constexpr double kA12 = 1403580.0;
198 static constexpr double kA13n = 810728.0;
199 static constexpr double kA21 = 527612.0;
200 static constexpr double kA23n = 1370589.0;
201 /** 1/(m1+1), SSJ's norm, so the result lies strictly inside (0,1). */
202 static constexpr double kNorm = 2.328306549295727688e-10;
203
204 double cg_[6];
205};
206
207// ---------------------------------------------------------------------------
208// java.util.Random
209// ---------------------------------------------------------------------------
210
211/**
212 * `java.util.Random`, the 48-bit LCG of the Java Language Specification.
213 *
214 * The engine uses 23 of these alongside the MRG streams, for the choices that
215 * are draws rather than variates: which batch size a BMAP service releases,
216 * which branch a probabilistic route takes, and so on. Transcribed rather than
217 * approximated, for the same reason as above.
218 */
220public:
221 explicit JavaRandom(long long seed = 0) { set_seed(seed); }
222
223 /** Java's setSeed: scramble by 0x5DEECE66D and mask to 48 bits. */
224 void set_seed(long long seed) {
225 seed_ = (static_cast<uint64_t>(seed) ^ 0x5DEECE66DULL) & ((1ULL << 48) - 1);
226 have_next_gaussian_ = false;
227 }
228
229 /** Java's next(bits). */
230 int32_t next(int bits) {
231 seed_ = (seed_ * 0x5DEECE66DULL + 0xBULL) & ((1ULL << 48) - 1);
232 return static_cast<int32_t>(static_cast<int64_t>(seed_) >> (48 - bits));
233 }
234
235 /** Java's nextInt(). */
236 int32_t next_int() { return next(32); }
237
238 /** Java's nextInt(bound), including the rejection loop it documents. */
239 int32_t next_int(int32_t bound) {
240 if (bound <= 0) throw InputError("JavaRandom::next_int: bound must be positive");
241 if ((bound & -bound) == bound) { // power of two
242 return static_cast<int32_t>((static_cast<int64_t>(bound) * next(31)) >> 31);
243 }
244 int32_t bits, val;
245 do {
246 bits = next(31);
247 val = bits % bound;
248 } while (bits - val + (bound - 1) < 0);
249 return val;
250 }
251
252 /** Java's nextDouble(): a 26-bit and a 27-bit draw. */
253 double next_double() {
254 return static_cast<double>((static_cast<int64_t>(next(26)) << 27) + next(27)) /
255 static_cast<double>(1LL << 53);
256 }
257
258 /** Java's nextLong(). */
259 int64_t next_long() {
260 return (static_cast<int64_t>(next(32)) << 32) + next(32);
261 }
262
263 /** Java's nextBoolean(). */
264 bool next_boolean() { return next(1) != 0; }
265
266 /** Java's nextGaussian(), the polar method with its cached second value. */
267 double next_gaussian() {
268 if (have_next_gaussian_) {
269 have_next_gaussian_ = false;
270 return next_gaussian_;
271 }
272 double v1, v2, s;
273 do {
274 v1 = 2.0 * next_double() - 1.0;
275 v2 = 2.0 * next_double() - 1.0;
276 s = v1 * v1 + v2 * v2;
277 } while (s >= 1.0 || s == 0.0);
278 const double multiplier = std::sqrt(-2.0 * std::log(s) / s);
279 next_gaussian_ = v2 * multiplier;
280 have_next_gaussian_ = true;
281 return v1 * multiplier;
282 }
283
284private:
285 uint64_t seed_ = 0;
286 bool have_next_gaussian_ = false;
287 double next_gaussian_ = 0.0;
288};
289
290} // namespace rng
291} // namespace line
292
293#endif // LINE_UTIL_RNG_SSJ_H
InputError(const std::string &what)
Definition error.h:39
JavaRandom(long long seed=0)
Definition rng_ssj.h:221
bool next_boolean()
Java's nextBoolean().
Definition rng_ssj.h:264
int32_t next(int bits)
Java's next(bits).
Definition rng_ssj.h:230
void set_seed(long long seed)
Java's setSeed: scramble by 0x5DEECE66D and mask to 48 bits.
Definition rng_ssj.h:224
double next_gaussian()
Java's nextGaussian(), the polar method with its cached second value.
Definition rng_ssj.h:267
int32_t next_int(int32_t bound)
Java's nextInt(bound), including the rejection loop it documents.
Definition rng_ssj.h:239
double next_double()
Java's nextDouble(): a 26-bit and a 27-bit draw.
Definition rng_ssj.h:253
int64_t next_long()
Java's nextLong().
Definition rng_ssj.h:259
int32_t next_int()
Java's nextInt().
Definition rng_ssj.h:236
void set_seed(const long long s[6])
SSJ's setSeed(long[6]).
Definition rng_ssj.h:125
int next_int(int i, int j)
SSJ's nextInt(i, j): a uniform integer on [i, j].
Definition rng_ssj.h:165
double next_double()
SSJ's nextValue(): the combined generator, returning a double in (0, 1).
Definition rng_ssj.h:141
double state(int i) const
The six state doubles, for tests that pin the state and not only the draws.
Definition rng_ssj.h:171
void set_seed_offset(long long seed, long long offset)
The stream an LDES engine names by (run seed, offset): its seeds hashed by SeedHash.
Definition rng_ssj.h:131
std::uint64_t next()
The next splitmix64 output.
Definition rng_ssj.h:85
void mrg_seeds(long long s[6])
Six MRG32k3a seeds: the first three in [1, m1), the last three in [1, m2), so neither component can s...
Definition rng_ssj.h:97
SeedHash(long long seed, long long offset)
Definition rng_ssj.h:81
The exception types the port throws.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52