LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldes_ssj_variates.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_SOLVERS_LDES_LDES_SSJ_VARIATES_H
6#define LINE_SOLVERS_LDES_LDES_SSJ_VARIATES_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The variate layer of the Java LDES engine, reproduced: SSJ's `randvar`
12 * generators as inverse-CDF functions of one uniform.
13 *
14 * THE MEASUREMENT THAT SHAPED THIS FILE. Every `*Gen` instance `Solver_ssj`
15 * builds -- Exponential, Erlang, Uniform, Weibull, Pareto, Lognormal, Gamma,
16 * Poisson, Binomial, Bernoulli -- consumes EXACTLY ONE uniform per draw. That
17 * was measured, not assumed: a probe generated five variates from a seeded
18 * MRG32k3a and then searched the raw stream for the next value, and every
19 * family had advanced the stream by exactly five. SSJ's instance generators
20 * invert; none of them rejects. The consequence is the whole reason a
21 * bit-compatible C++ engine is feasible: the stream can never desynchronize, so
22 * a family whose quantile this file computes to 1e-13 rather than to the last
23 * bit still leaves every LATER draw identical, and only perturbs its own sample
24 * below the tolerance of any event comparison.
25 *
26 * WHAT IS EXACT AND WHAT IS TO 1e-13. Exponential, Uniform, Pareto, Weibull and
27 * Bernoulli have closed-form quantiles and agree with SSJ to the last bit.
28 * Normal (hence Lognormal) uses Wichura's AS 241, Gamma and Erlang inverting
29 * the regularized incomplete gamma by Newton with a Wilson-Hilferty start, and
30 * Poisson and Binomial by discrete inversion -- the two discrete families return
31 * INTEGERS and so are exact wherever the cdf comparison lands on the same side,
32 * which it does everywhere the test looks.
33 *
34 * WHY NOT CALL SSJ'S OWN FORMULAE BLINDLY. `ExponentialDist.inverseF` is
35 * `-log1p(-u)/lambda` and not `-log(1-u)/lambda`; the two differ in the last
36 * bits for small u, which is exactly where an interarrival time matters most.
37 * Where a spelling like that decides agreement, this file uses SSJ's.
38 *
39 * AND THE SPELLING IS NOT ENOUGH: IT MUST BE THE SAME log1p. `std::log1p` is
40 * allowed a 1-ulp error, and glibc changed which representative it returns
41 * between 2.35 and 2.39, so the SAME `common/ldes` binary on the SAME
42 * model.json at `--seed 23000` gave two different sample paths -- 98.565592 on
43 * a 22.04 host against 98.562490 under the containerized MATLAB's 24.04, which
44 * is how this was found. The quantiles below therefore call
45 * `line::fdlibm::log1p`, the algorithm `StrictMath.log1p` is DEFINED to use and
46 * which `Math.log1p` was measured to match on every one of 400k draws; see
47 * `line/util/fdlibm.h`. That fixes the engine to one arithmetic everywhere AND
48 * puts it on the Java engine's, which is what "bit-compatible" was supposed to
49 * mean. `pow`, `log`, `exp`, `sqrt`, `lgamma` and `tgamma` were measured to
50 * agree across those glibcs and are left to libm.
51 */
52
53#include <cmath>
54#include <string>
55#include <vector>
56
57#include "line/util/error.h"
58#include "line/util/fdlibm.h"
59
60namespace line {
61namespace ldes {
62namespace ssj {
63
64// ---------------------------------------------------------------------------
65// Closed-form quantiles
66// ---------------------------------------------------------------------------
67
68/** ExponentialDist.inverseF(lambda, u), SSJ's log1p spelling. */
69inline double exponential_inverse(double lambda, double u) {
70 if (lambda <= 0.0) throw InputError("ssj::exponential_inverse: lambda must be positive");
71 if (u >= 1.0) return std::numeric_limits<double>::infinity();
72 if (u <= 0.0) return 0.0;
73 return -line::fdlibm::log1p(-u) / lambda;
74}
75
76/** UniformDist.inverseF(a, b, u). */
77inline double uniform_inverse(double a, double b, double u) {
78 return a + (b - a) * u;
79}
80
81/** ParetoDist.inverseF(alpha, beta, u) = beta (1-u)^(-1/alpha). */
82inline double pareto_inverse(double alpha, double beta, double u) {
83 if (alpha <= 0.0 || beta <= 0.0)
84 throw InputError("ssj::pareto_inverse: alpha and beta must be positive");
85 if (u >= 1.0) return std::numeric_limits<double>::infinity();
86 return beta * std::pow(1.0 - u, -1.0 / alpha);
87}
88
89/**
90 * WeibullDist.inverseF(alpha, lambda, delta, u).
91 * SSJ parameterises the scale as lambda with the variate
92 * delta + (1/lambda) (-log(1-u))^(1/alpha).
93 */
94inline double weibull_inverse(double alpha, double lambda, double delta, double u) {
95 if (alpha <= 0.0 || lambda <= 0.0)
96 throw InputError("ssj::weibull_inverse: alpha and lambda must be positive");
97 if (u >= 1.0) return std::numeric_limits<double>::infinity();
98 if (u <= 0.0) return delta;
99 return delta + std::pow(-line::fdlibm::log1p(-u), 1.0 / alpha) / lambda;
100}
101
102/** BernoulliDist.inverseF(p, u): 1 when u exceeds 1 - p. */
103inline double bernoulli_inverse(double p, double u) {
104 if (p < 0.0 || p > 1.0) throw InputError("ssj::bernoulli_inverse: p must lie in [0,1]");
105 return (u <= 1.0 - p) ? 0.0 : 1.0;
106}
107
108// ---------------------------------------------------------------------------
109// Normal, and Lognormal on top of it
110// ---------------------------------------------------------------------------
111
112/**
113 * NormalDist.inverseF(0, 1, u) by Wichura's AS 241 (Applied Statistics 37,
114 * 1988), the algorithm SSJ's `inverseF01` implements. Accurate to about 1e-16
115 * over the whole range, which is what makes the Lognormal agree with SSJ to
116 * the last few bits rather than only to a plotting tolerance.
117 */
118inline double normal_inverse01(double u) {
119 if (u <= 0.0) return -std::numeric_limits<double>::infinity();
120 if (u >= 1.0) return std::numeric_limits<double>::infinity();
121
122 const double q = u - 0.5;
123 double r;
124 if (std::fabs(q) <= 0.425) {
125 r = 0.180625 - q * q;
126 return q *
127 (((((((2509.0809287301226727 * r + 33430.575583588128105) * r +
128 67265.770927008700853) * r + 45921.953931549871457) * r +
129 13731.693765509461125) * r + 1971.5909503065514427) * r +
130 133.14166789178437745) * r + 3.387132872796366608) /
131 (((((((5226.495278852854561 * r + 28729.085735721942674) * r +
132 39307.89580009271061) * r + 21213.794301586595867) * r +
133 5394.1960214247511077) * r + 687.1870074920579083) * r +
134 42.313330701600911252) * r + 1.0);
135 }
136 r = (q < 0.0) ? u : 1.0 - u;
137 r = std::sqrt(-std::log(r));
138 double x;
139 if (r <= 5.0) {
140 r -= 1.6;
141 x = (((((((7.7454501427834140764e-4 * r + 0.0227238449892691845833) * r +
142 0.24178072517745061177) * r + 1.27045825245236838258) * r +
143 3.64784832476320460504) * r + 5.7694972214606914055) * r +
144 4.6303378461565452959) * r + 1.42343711074968357734) /
145 (((((((1.05075007164441684324e-9 * r + 5.475938084995344946e-4) * r +
146 0.0151986665636164571966) * r + 0.14810397642748007459) * r +
147 0.68976733498510000455) * r + 1.6763848301838038494) * r +
148 2.05319162663775882187) * r + 1.0);
149 } else {
150 r -= 5.0;
151 x = (((((((2.01033439929228813265e-7 * r + 2.71155556874348757815e-5) * r +
152 0.0012426609473880784386) * r + 0.026532189526576123093) * r +
153 0.29656057182850489123) * r + 1.7848265399172913358) * r +
154 5.4637849111641143699) * r + 6.6579046435011037772) /
155 (((((((2.04426310338993978564e-15 * r + 1.4215117583164458887e-7) * r +
156 1.8463183175100546818e-5) * r + 7.868691311456132591e-4) * r +
157 0.0148753612908506148525) * r + 0.13692988092273580531) * r +
158 0.59983220655588793769) * r + 1.0);
159 }
160 return (q < 0.0) ? -x : x;
161}
162
163/** NormalDist.inverseF(mu, sigma, u). */
164inline double normal_inverse(double mu, double sigma, double u) {
165 if (sigma <= 0.0) throw InputError("ssj::normal_inverse: sigma must be positive");
166 return mu + sigma * normal_inverse01(u);
167}
168
169/** LognormalDist.inverseF(mu, sigma, u) = exp(mu + sigma Phi^-1(u)). */
170inline double lognormal_inverse(double mu, double sigma, double u) {
171 if (u >= 1.0) return std::numeric_limits<double>::infinity();
172 if (u <= 0.0) return 0.0;
173 return std::exp(normal_inverse(mu, sigma, u));
174}
175
176// ---------------------------------------------------------------------------
177// Gamma, and Erlang as the integer-shape case
178// ---------------------------------------------------------------------------
179
180namespace detail {
181
182/** Regularized lower incomplete gamma P(a, x), by series and continued fraction. */
183inline double gamma_p(double a, double x) {
184 if (x <= 0.0) return 0.0;
185 const double lg = std::lgamma(a);
186 if (x < a + 1.0) {
187 // Series expansion.
188 double ap = a, sum = 1.0 / a, del = sum;
189 for (int n = 0; n < 1000; ++n) {
190 ap += 1.0;
191 del *= x / ap;
192 sum += del;
193 if (std::fabs(del) < std::fabs(sum) * 1e-16) break;
194 }
195 return sum * std::exp(-x + a * std::log(x) - lg);
196 }
197 // Continued fraction for Q(a, x), then P = 1 - Q.
198 const double tiny = 1e-300;
199 double b = x + 1.0 - a, c = 1.0 / tiny, d = 1.0 / b, h = d;
200 for (int i = 1; i <= 1000; ++i) {
201 const double an = -static_cast<double>(i) * (static_cast<double>(i) - a);
202 b += 2.0;
203 d = an * d + b;
204 if (std::fabs(d) < tiny) d = tiny;
205 c = b + an / c;
206 if (std::fabs(c) < tiny) c = tiny;
207 d = 1.0 / d;
208 const double del = d * c;
209 h *= del;
210 if (std::fabs(del - 1.0) < 1e-16) break;
211 }
212 return 1.0 - std::exp(-x + a * std::log(x) - lg) * h;
213}
214
215} // namespace detail
216
217/**
218 * GammaDist.inverseF(alpha, lambda, u): the quantile of a Gamma of shape alpha
219 * and RATE lambda, so the mean is alpha/lambda, which is SSJ's convention.
220 *
221 * Newton on P(alpha, lambda x) = u from a Wilson-Hilferty start, with a
222 * bisection guard: the density vanishes at the origin for alpha > 1 and Newton
223 * alone can step negative there. Converges to about 1e-14 relative, which is
224 * below the resolution at which a service time can reorder two events.
225 */
226inline double gamma_inverse(double alpha, double lambda, double u) {
227 if (alpha <= 0.0 || lambda <= 0.0)
228 throw InputError("ssj::gamma_inverse: alpha and lambda must be positive");
229 if (u <= 0.0) return 0.0;
230 if (u >= 1.0) return std::numeric_limits<double>::infinity();
231
232 // Wilson-Hilferty: a cube-root normal approximation to the Gamma quantile.
233 const double z = normal_inverse01(u);
234 const double wh = alpha * std::pow(1.0 - 1.0 / (9.0 * alpha) + z / (3.0 * std::sqrt(alpha)), 3.0);
235 double x = (wh > 0.0) ? wh : alpha * 0.5;
236
237 double lo = 0.0, hi = 0.0; // hi == 0 means "not yet bracketed"
238 const double lg = std::lgamma(alpha);
239 for (int it = 0; it < 200; ++it) {
240 const double p = detail::gamma_p(alpha, x);
241 if (p < u) {
242 lo = x;
243 } else {
244 hi = x;
245 }
246 const double logpdf = (alpha - 1.0) * std::log(x) - x - lg;
247 const double pdf = std::exp(logpdf);
248 double step = (pdf > 0.0) ? (p - u) / pdf : 0.0;
249 double next = x - step;
250 if (!(next > lo) || (hi > 0.0 && !(next < hi)) || !(next > 0.0) || !std::isfinite(next)) {
251 next = (hi > 0.0) ? 0.5 * (lo + hi) : 2.0 * x;
252 }
253 const double rel = std::fabs(next - x) / (std::fabs(x) + 1e-300);
254 x = next;
255 if (rel < 1e-15) break;
256 }
257 return x / lambda;
258}
259
260/**
261 * ErlangGen(k, lambda): the Gamma of integer shape k and rate lambda. SSJ's
262 * ErlangGen inverts the Gamma rather than summing k exponentials, which is why
263 * it costs ONE uniform and not k -- the measurement above pinned that.
264 */
265inline double erlang_inverse(int k, double lambda, double u) {
266 if (k <= 0) throw InputError("ssj::erlang_inverse: k must be positive");
267 return gamma_inverse(static_cast<double>(k), lambda, u);
268}
269
270// ---------------------------------------------------------------------------
271// Discrete families, by inversion
272// ---------------------------------------------------------------------------
273
274/**
275 * PoissonDist.inverseF(lambda, u): the smallest k whose cdf reaches u.
276 *
277 * Summed forward from k = 0 with the pmf carried recursively. The result is an
278 * INTEGER, so it agrees with SSJ exactly whenever the two cdfs put u on the
279 * same side of a step, which no test has yet found a counterexample to.
280 */
281inline double poisson_inverse(double lambda, double u) {
282 if (lambda <= 0.0) throw InputError("ssj::poisson_inverse: lambda must be positive");
283 if (u <= 0.0) return 0.0;
284 if (u >= 1.0) return std::numeric_limits<double>::infinity();
285 double p = std::exp(-lambda), cdf = p;
286 int k = 0;
287 const int guard = static_cast<int>(lambda + 20.0 * std::sqrt(lambda) + 1000.0);
288 while (cdf < u && k < guard) {
289 ++k;
290 p *= lambda / static_cast<double>(k);
291 cdf += p;
292 }
293 return static_cast<double>(k);
294}
295
296/** BinomialDist.inverseF(n, p, u), by the same forward inversion. */
297inline double binomial_inverse(int n, double p, double u) {
298 if (n < 0) throw InputError("ssj::binomial_inverse: n must be non-negative");
299 if (p < 0.0 || p > 1.0) throw InputError("ssj::binomial_inverse: p must lie in [0,1]");
300 if (u <= 0.0) return 0.0;
301 if (p <= 0.0) return 0.0;
302 if (p >= 1.0) return static_cast<double>(n);
303 double pk = std::pow(1.0 - p, static_cast<double>(n)), cdf = pk;
304 int k = 0;
305 while (cdf < u && k < n) {
306 pk *= (static_cast<double>(n - k) / static_cast<double>(k + 1)) * (p / (1.0 - p));
307 ++k;
308 cdf += pk;
309 }
310 return static_cast<double>(k);
311}
312
313} // namespace ssj
314} // namespace ldes
315} // namespace line
316
317#endif // LINE_SOLVERS_LDES_LDES_SSJ_VARIATES_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
double exponential_inverse(double lambda, double u)
ExponentialDist.inverseF(lambda, u), SSJ's log1p spelling.
double binomial_inverse(int n, double p, double u)
BinomialDist.inverseF(n, p, u), by the same forward inversion.
double erlang_inverse(int k, double lambda, double u)
ErlangGen(k, lambda): the Gamma of integer shape k and rate lambda.
double uniform_inverse(double a, double b, double u)
UniformDist.inverseF(a, b, u).
double weibull_inverse(double alpha, double lambda, double delta, double u)
WeibullDist.inverseF(alpha, lambda, delta, u).
double pareto_inverse(double alpha, double beta, double u)
ParetoDist.inverseF(alpha, beta, u) = beta (1-u)^(-1/alpha).
double gamma_inverse(double alpha, double lambda, double u)
GammaDist.inverseF(alpha, lambda, u): the quantile of a Gamma of shape alpha and RATE lambda,...
double normal_inverse01(double u)
NormalDist.inverseF(0, 1, u) by Wichura's AS 241 (Applied Statistics 37, 1988), the algorithm SSJ's i...
double bernoulli_inverse(double p, double u)
BernoulliDist.inverseF(p, u): 1 when u exceeds 1 - p.
double normal_inverse(double mu, double sigma, double u)
NormalDist.inverseF(mu, sigma, u).
double lognormal_inverse(double mu, double sigma, double u)
LognormalDist.inverseF(mu, sigma, u) = exp(mu + sigma Phi^-1(u)).
double poisson_inverse(double lambda, double u)
PoissonDist.inverseF(lambda, u): the smallest k whose cdf reaches u.