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(new long[] { seed + offset, ..., seed + offset + 5 });
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`, and those
33 * offsets -- not the stream hierarchy.
34 *
35 * MRG32k3a is L'Ecuyer's combined multiple recursive generator (Operations
36 * Research 47(1), 1999): two order-three recurrences modulo m1 = 2^32 - 209 and
37 * m2 = 2^32 - 22853, combined by subtraction. SSJ computes it in DOUBLES with
38 * the multipliers split so every intermediate stays exactly representable, and
39 * that arithmetic is reproduced here rather than an integer rewrite: the double
40 * form is what the reference runs, and the two agree only if the rounding does.
41 *
42 * `java.util.Random` is the specified 48-bit LCG of the Java Language
43 * Specification: seed scrambling by 0x5DEECE66D, `next(bits)` taking the high
44 * bits, `nextDouble` from a 26-bit and a 27-bit draw, and `nextInt(bound)` with
45 * its rejection loop for non-power-of-two bounds. All of it is normative, so a
46 * faithful transcription is exact by construction.
47 */
48
49#include <cmath>
50#include <cstdint>
51#include <string>
52
53#include "line/util/error.h"
54
55namespace line {
56namespace rng {
57
58// ---------------------------------------------------------------------------
59// SSJ MRG32k3a
60// ---------------------------------------------------------------------------
61
62/**
63 * SSJ's `umontreal.ssj.rng.MRG32k3a`, state and all.
64 *
65 * The state is the six doubles (Cg[0..5]) SSJ keeps; `set_seed` takes the same
66 * six-long vector `setSeed(long[])` takes and applies the same validation, so a
67 * call site can be transcribed from the Java verbatim.
68 */
69class Mrg32k3a {
70public:
71 Mrg32k3a() { reset_to_default(); }
72
73 /** SSJ's setSeed(long[6]). Values are taken modulo the two moduli. */
74 void set_seed(const long long s[6]) {
75 validate(s);
76 for (int i = 0; i < 6; ++i) cg_[i] = static_cast<double>(s[i]);
77 }
78
79 /** Convenience for the engine's `{seed+off, ..., seed+off+5}` idiom. */
80 void set_seed_offset(long long seed, long long offset) {
81 long long s[6];
82 for (int i = 0; i < 6; ++i) s[i] = seed + offset + i;
83 set_seed(s);
84 }
85
86 /**
87 * SSJ's nextValue(): the combined generator, returning a double in (0, 1).
88 * Never returns 0; may return values arbitrarily close to 1.
89 */
90 double next_double() {
91 // Component 1
92 double p1 = kA12 * cg_[1] - kA13n * cg_[0];
93 double k = std::floor(p1 / kM1);
94 p1 -= k * kM1;
95 if (p1 < 0.0) p1 += kM1;
96 cg_[0] = cg_[1];
97 cg_[1] = cg_[2];
98 cg_[2] = p1;
99
100 // Component 2
101 double p2 = kA21 * cg_[5] - kA23n * cg_[3];
102 k = std::floor(p2 / kM2);
103 p2 -= k * kM2;
104 if (p2 < 0.0) p2 += kM2;
105 cg_[3] = cg_[4];
106 cg_[4] = cg_[5];
107 cg_[5] = p2;
108
109 // Combination
110 return ((p1 > p2) ? (p1 - p2) * kNorm : (p1 - p2 + kM1) * kNorm);
111 }
112
113 /** SSJ's nextInt(i, j): a uniform integer on [i, j]. */
114 int next_int(int i, int j) {
115 if (i > j) throw InputError("Mrg32k3a::next_int: empty range");
116 return i + static_cast<int>(next_double() * (static_cast<double>(j - i) + 1.0));
117 }
118
119 /** The six state doubles, for tests that pin the state and not only the draws. */
120 double state(int i) const { return cg_[i]; }
121
122private:
123 void reset_to_default() {
124 // SSJ's default initial seed (12345 in all six slots).
125 for (int i = 0; i < 6; ++i) cg_[i] = 12345.0;
126 }
127
128 static void validate(const long long s[6]) {
129 for (int i = 0; i < 3; ++i) {
130 if (s[i] < 0 || static_cast<double>(s[i]) >= kM1)
131 throw InputError("Mrg32k3a::set_seed: the first three seeds must be in [0, m1)");
132 }
133 for (int i = 3; i < 6; ++i) {
134 if (s[i] < 0 || static_cast<double>(s[i]) >= kM2)
135 throw InputError("Mrg32k3a::set_seed: the last three seeds must be in [0, m2)");
136 }
137 if (s[0] == 0 && s[1] == 0 && s[2] == 0)
138 throw InputError("Mrg32k3a::set_seed: the first three seeds must not all be zero");
139 if (s[3] == 0 && s[4] == 0 && s[5] == 0)
140 throw InputError("Mrg32k3a::set_seed: the last three seeds must not all be zero");
141 }
142
143 // L'Ecuyer's constants, exactly as SSJ spells them.
144 static constexpr double kM1 = 4294967087.0;
145 static constexpr double kM2 = 4294944443.0;
146 static constexpr double kA12 = 1403580.0;
147 static constexpr double kA13n = 810728.0;
148 static constexpr double kA21 = 527612.0;
149 static constexpr double kA23n = 1370589.0;
150 /** 1/(m1+1), SSJ's norm, so the result lies strictly inside (0,1). */
151 static constexpr double kNorm = 2.328306549295727688e-10;
152
153 double cg_[6];
154};
155
156// ---------------------------------------------------------------------------
157// java.util.Random
158// ---------------------------------------------------------------------------
159
160/**
161 * `java.util.Random`, the 48-bit LCG of the Java Language Specification.
162 *
163 * The engine uses 23 of these alongside the MRG streams, for the choices that
164 * are draws rather than variates: which batch size a BMAP service releases,
165 * which branch a probabilistic route takes, and so on. Transcribed rather than
166 * approximated, for the same reason as above.
167 */
169public:
170 explicit JavaRandom(long long seed = 0) { set_seed(seed); }
171
172 /** Java's setSeed: scramble by 0x5DEECE66D and mask to 48 bits. */
173 void set_seed(long long seed) {
174 seed_ = (static_cast<uint64_t>(seed) ^ 0x5DEECE66DULL) & ((1ULL << 48) - 1);
175 have_next_gaussian_ = false;
176 }
177
178 /** Java's next(bits). */
179 int32_t next(int bits) {
180 seed_ = (seed_ * 0x5DEECE66DULL + 0xBULL) & ((1ULL << 48) - 1);
181 return static_cast<int32_t>(static_cast<int64_t>(seed_) >> (48 - bits));
182 }
183
184 /** Java's nextInt(). */
185 int32_t next_int() { return next(32); }
186
187 /** Java's nextInt(bound), including the rejection loop it documents. */
188 int32_t next_int(int32_t bound) {
189 if (bound <= 0) throw InputError("JavaRandom::next_int: bound must be positive");
190 if ((bound & -bound) == bound) { // power of two
191 return static_cast<int32_t>((static_cast<int64_t>(bound) * next(31)) >> 31);
192 }
193 int32_t bits, val;
194 do {
195 bits = next(31);
196 val = bits % bound;
197 } while (bits - val + (bound - 1) < 0);
198 return val;
199 }
200
201 /** Java's nextDouble(): a 26-bit and a 27-bit draw. */
202 double next_double() {
203 return static_cast<double>((static_cast<int64_t>(next(26)) << 27) + next(27)) /
204 static_cast<double>(1LL << 53);
205 }
206
207 /** Java's nextLong(). */
208 int64_t next_long() {
209 return (static_cast<int64_t>(next(32)) << 32) + next(32);
210 }
211
212 /** Java's nextBoolean(). */
213 bool next_boolean() { return next(1) != 0; }
214
215 /** Java's nextGaussian(), the polar method with its cached second value. */
216 double next_gaussian() {
217 if (have_next_gaussian_) {
218 have_next_gaussian_ = false;
219 return next_gaussian_;
220 }
221 double v1, v2, s;
222 do {
223 v1 = 2.0 * next_double() - 1.0;
224 v2 = 2.0 * next_double() - 1.0;
225 s = v1 * v1 + v2 * v2;
226 } while (s >= 1.0 || s == 0.0);
227 const double multiplier = std::sqrt(-2.0 * std::log(s) / s);
228 next_gaussian_ = v2 * multiplier;
229 have_next_gaussian_ = true;
230 return v1 * multiplier;
231 }
232
233private:
234 uint64_t seed_ = 0;
235 bool have_next_gaussian_ = false;
236 double next_gaussian_ = 0.0;
237};
238
239} // namespace rng
240} // namespace line
241
242#endif // LINE_UTIL_RNG_SSJ_H
InputError(const std::string &what)
Definition error.h:39
JavaRandom(long long seed=0)
Definition rng_ssj.h:170
bool next_boolean()
Java's nextBoolean().
Definition rng_ssj.h:213
int32_t next(int bits)
Java's next(bits).
Definition rng_ssj.h:179
void set_seed(long long seed)
Java's setSeed: scramble by 0x5DEECE66D and mask to 48 bits.
Definition rng_ssj.h:173
double next_gaussian()
Java's nextGaussian(), the polar method with its cached second value.
Definition rng_ssj.h:216
int32_t next_int(int32_t bound)
Java's nextInt(bound), including the rejection loop it documents.
Definition rng_ssj.h:188
double next_double()
Java's nextDouble(): a 26-bit and a 27-bit draw.
Definition rng_ssj.h:202
int64_t next_long()
Java's nextLong().
Definition rng_ssj.h:208
int32_t next_int()
Java's nextInt().
Definition rng_ssj.h:185
void set_seed(const long long s[6])
SSJ's setSeed(long[6]).
Definition rng_ssj.h:74
int next_int(int i, int j)
SSJ's nextInt(i, j): a uniform integer on [i, j].
Definition rng_ssj.h:114
double next_double()
SSJ's nextValue(): the combined generator, returning a double in (0, 1).
Definition rng_ssj.h:90
double state(int i) const
The six state doubles, for tests that pin the state and not only the draws.
Definition rng_ssj.h:120
void set_seed_offset(long long seed, long long offset)
Convenience for the engine's {seed+off, ..., seed+off+5} idiom.
Definition rng_ssj.h:80
The exception types the port throws.