LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
distribution.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_LANG_DISTRIBUTION_H
6#define LINE_LANG_DISTRIBUTION_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * What `refreshProcessRepresentations` and `refreshLST` compute FROM a
12 * distribution: the (D0,D1) pair that reaches `sn.proc`, the arrival-phase
13 * vector `sn.pie`, and the Laplace-Stieltjes transform `sn.lst`.
14 *
15 * These are free functions rather than members of `Distrib` because they need
16 * the api layer -- the stationary vector of a MAP is a linear solve
17 * (`api/mam/map_moment.h`), the Erlang approximation of a non-Markovian
18 * distribution is `map_erlang`, and the Replayer's is an APH fit -- and the
19 * model layer would otherwise depend on the api layer wholesale.
20 *
21 * WHERE THE REFERENCE IS BUG-FOR-BUG REPRODUCED, deliberately. The Weibull and
22 * Lognormal transforms in MATLAB are 1000-point RIGHT-ENDPOINT Riemann sums
23 * over a truncated interval, not converged quadrature: they are biased low by
24 * the tail they drop and by the O(dx) rule. Their values enter M/G/1 waiting
25 * times, so replacing them with an accurate integral would move numbers this
26 * port is supposed to match. The Pareto transform, by contrast, IS converged in
27 * the reference (adaptive Gauss-Kronrod at RelTol 1e-12 over the substitution
28 * u = k/x), and is reproduced as such with the ported `num_integral`.
29 */
30
31#include <cmath>
32#include <complex>
33#include <cstddef>
34#include <vector>
35
44#include "line/num/number.h"
45#include "line/util/error.h"
46#include "line/util/expm.h"
47#include "line/util/matrix.h"
48
49namespace line {
50namespace lang {
51
52/**
53 * The number of Erlang phases `convertToMAP` picks for a non-Markovian
54 * distribution: 20 when the SCV is below CoarseTol (a Det, or near one), and
55 * otherwise ceil(1/SCV) capped at 100.
56 *
57 * The cap is what makes the approximation one-sided: a Pareto of SCV 64 gets a
58 * single phase (an exponential), so the approximation matches the mean and NOT
59 * the SCV whenever the SCV exceeds 1. That is the reference's behaviour and the
60 * reason `sn.scv` is read from the distribution rather than from `sn.proc`.
61 */
62inline unsigned convert_to_map_phases(double scv) {
63 if (scv < GlobalConstants::CoarseTol) return 20;
64 const double n = std::ceil(1.0 / scv);
65 const double capped = n < 1.0 ? 1.0 : (n > 100.0 ? 100.0 : n);
66 return static_cast<unsigned>(capped);
67}
68
69/**
70 * The refusal every lowering of a `Prior` shares.
71 *
72 * A Prior is a set of models, not one law, so there is no (D0,D1), no transform
73 * and no moment of it that a solver could integrate: substituting any single
74 * alternative would answer for a model the caller did not describe, and
75 * collapsing the set to its mixture would answer for a model nobody described.
76 * SolverUQ is the one consumer, and it replaces the Prior before the design
77 * point is solved.
78 */
79inline void reject_prior(const char* who) {
80 throw UnsupportedError(ErrorKind{"Prior"}, std::string(who) +
81 ": the distribution is a Prior, which is a weighted set of alternative "
82 "MODELS rather than one law; solve the model with SolverUQ, which "
83 "replaces each Prior by one alternative per design point");
84}
85
86/**
87 * The (D0,D1) pair that reaches `sn.proc`.
88 *
89 * A type that carries its own representation returns it unchanged. Det,
90 * Uniform, Pareto, Gamma, Weibull and Lognormal are replaced by the Erlang
91 * approximation of `convertToMAP`; a Replayer is fitted by `aph_fit`, which is
92 * what MATLAB's `Replayer.fitAPH` does before taking getProcess.
93 */
94template <class T>
96 if (d.is_prior()) reject_prior("dist_to_map");
97 if (d.disabled) throw InputError("dist_to_map: the distribution is disabled");
99 throw UnsupportedError(ErrorKind{"Normal"},
100 "dist_to_map: a Normal puts mass below zero, so it is not the law of any duration and "
101 "has no Markovian representation. The default arm of this function would hand back the "
102 "Erlang fit of its mean, which is a positive law with the same mean and nothing else "
103 "in common; refusing instead. A Normal reaches this port only as the parameter density "
104 "of a continuous Prior");
105 if (d.has_map()) {
106 mam::Map<T> m;
107 m.D0 = d.D0;
108 m.D1 = d.D1;
109 return m;
110 }
111 if (d.type == ProcessType::REPLAYER) {
112 // MATLAB fits an APH to the trace's first three moments. The fit is
113 // Bobbio-Horvath-Telek, which needs roots and exponentials, so it is
114 // gated: without the guard the static_assert inside aph_fit fires for
115 // EVERY exact-arithmetic caller of dist_to_map, trace or not, because
116 // the branch is instantiated whether or not it is taken.
117 if constexpr (num_traits<T>::has_transcendental) {
118 const T n = num_traits<T>::from_int(static_cast<long>(d.trace.size()));
121 for (const T& x : d.trace) {
122 m1 += x;
123 m2 += T(x * x);
124 m3 += T(x * x * x);
125 }
126 return mam::aph_fit(T(m1 / n), T(m2 / n), T(m3 / n)).aph;
127 } else {
128 throw UnsupportedError(ErrorKind{"Arithmetic"},
129 "dist_to_map: fitting a Replayer trace to an acyclic phase-type needs "
130 "transcendental arithmetic, which exact rational arithmetic does not provide; "
131 "solve in double or Real<n>, or replace the trace by a fitted distribution");
132 }
133 }
135}
136
137/** `sn.pie`: the phase distribution seen by an arriving job. */
138template <class T>
139std::vector<T> dist_pie(const Distrib<T>& d) {
140 return mam::map_pie(dist_to_map(d));
141}
142
143/**
144 * Fill in the first two moments of a distribution given by its matrices.
145 *
146 * `Distrib::map_dist` cannot compute them -- they need the stationary vector --
147 * so a MAP built directly from (D0,D1) leaves mean and scv at their defaults
148 * until this runs. Every builder call that installs such a distribution passes
149 * through here.
150 */
151template <class T>
153 if (d.disabled || !d.has_map()) return;
154 if (d.type == ProcessType::DMAP) {
156 return;
157 }
158 mam::Map<T> m;
159 m.D0 = d.D0;
160 m.D1 = d.D1;
161 d.mean = mam::map_mean(m);
162 d.scv = mam::map_scv(m);
163}
164
165/**
166 * `sn.lst`: the Laplace-Stieltjes transform E[exp(-sX)].
167 *
168 * The phase-type families evaluate the closed form pie (sI - D0)^-1 (-D0) e;
169 * for a MAP that is the transform of its stationary interarrival time, which is
170 * the quantity the M/G/1 analyzers want.
171 */
172template <class T>
173T dist_lst(const Distrib<T>& d, const T& s) {
174 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
175 if (d.is_prior()) reject_prior("dist_lst");
176 if (d.disabled) throw InputError("dist_lst: the distribution is disabled");
177 if (s == zero) return one;
178
179 switch (d.type) {
181 return one;
183 // The EMPIRICAL transform mean(exp(-s x)) over the trace, which is
184 // what MATLAB `Replayer.evalLST` and the JAR/python twins return.
185 // Falling through to the phase-type arm would transform the APH fit
186 // of the first three moments instead, i.e. a different law: on
187 // gallery_replayerm1 that moves the G/M/1 caudal root from 0.332835
188 // to 0.332887 and QLen by 1e-4 relative.
189 if constexpr (!num_traits<T>::has_transcendental) {
190 throw UnsupportedError(ErrorKind{"Arithmetic"},
191 "dist_lst: the transform of a Replayer trace is a mean of exp, which exact "
192 "arithmetic has no representation for; use the double or real backend");
193 } else {
194 const double sv = num_traits<T>::to_double(s);
195 double acc = 0.0;
196 for (const T& x : d.trace) acc += std::exp(-sv * num_traits<T>::to_double(x));
197 return num_traits<T>::from_double(acc / static_cast<double>(d.trace.size()));
198 }
199 }
200 case ProcessType::DET: {
201 if constexpr (!num_traits<T>::has_transcendental) {
202 throw UnsupportedError(ErrorKind{"Arithmetic"},
203 "dist_lst: the transform of a Det is exp(-s m), which exact arithmetic has "
204 "no representation for; use the double or real backend");
205 } else {
208 }
209 }
211 if constexpr (!num_traits<T>::has_transcendental) {
212 throw UnsupportedError(ErrorKind{"Arithmetic"},
213 "dist_lst: the transform of a Uniform evaluates exp, which exact arithmetic "
214 "has no representation for; use the double or real backend");
215 } else {
216 const double a = num_traits<T>::to_double(d.params[0]);
217 const double b = num_traits<T>::to_double(d.params[1]);
218 const double sv = num_traits<T>::to_double(s);
219 return num_traits<T>::from_double((std::exp(-sv * a) - std::exp(-sv * b)) /
220 (sv * (b - a)));
221 }
222 }
223 case ProcessType::NORMAL: {
224 if constexpr (!num_traits<T>::has_transcendental) {
225 throw UnsupportedError(ErrorKind{"Arithmetic"},
226 "dist_lst: the transform of a Normal is a value of exp, which exact arithmetic "
227 "has no representation for; use the double or real backend");
228 } else {
229 // exp(-mu s + sigma^2 s^2 / 2), the Laplace-Stieltjes transform.
230 //
231 // ONE DELIBERATE DIVERGENCE, and it is a defect on the other
232 // side: `Normal.m:96-106` returns exp(+mu s + sigma^2 s^2 / 2),
233 // which is the MGF at +s and not E[exp(-sX)] at all -- its own
234 // comment says "the moment-generating function evaluated at -s",
235 // which that expression is also not. Nothing reads it: a Normal
236 // is never a service process, so no analyzer reaches this arm,
237 // and reproducing the sign would put a wrong transform in the
238 // one place a future caller would trust. Recorded in BUGS.md.
239 const double mu = num_traits<T>::to_double(d.params[0]);
240 const double sg = num_traits<T>::to_double(d.params[1]);
241 const double sv = num_traits<T>::to_double(s);
242 return num_traits<T>::from_double(std::exp(-mu * sv + sg * sg * sv * sv / 2.0));
243 }
244 }
245 case ProcessType::PARETO: {
246 if constexpr (!num_traits<T>::has_transcendental) {
247 throw UnsupportedError(ErrorKind{"Arithmetic"},
248 "dist_lst: the transform of a Pareto is an integral of exp, which exact "
249 "arithmetic has no representation for; use the double or real backend");
250 } else {
251 // alpha * int_0^1 u^(alpha-1) exp(-s k / u) du, the substitution
252 // x = k/u the reference uses to stay accurate as s -> 0.
253 const T alpha = d.params[0], k = d.params[1];
254 const T tiny = num_traits<T>::from_double(1e-300);
255 auto g = [&alpha, &k, &s, &tiny, &one](const T& u) -> T {
256 if (!(u > num_traits<T>::from_int(0))) return num_traits<T>::from_int(0);
257 const T uu = u > tiny ? u : tiny;
258 const double e = std::exp(-num_traits<T>::to_double(T(s * k / uu)));
259 const double p = std::pow(num_traits<T>::to_double(u),
260 num_traits<T>::to_double(T(alpha - one)));
261 return num_traits<T>::from_double(p * e);
262 };
263 const T val = qsys::detail::num_integral<T>(g, zero, one,
265 num_traits<T>::from_double(1e-300), 50);
266 return T(alpha * val);
267 }
268 }
270 if constexpr (!num_traits<T>::has_transcendental) {
271 throw UnsupportedError(ErrorKind{"Arithmetic"},
272 "dist_lst: the transform of a Weibull is a numerical integral, which exact "
273 "arithmetic has no representation for; use the double or real backend");
274 } else {
275 // The reference's 1000-panel right-endpoint sum, reproduced.
276 const double a = num_traits<T>::to_double(d.params[0]);
277 const double r = num_traits<T>::to_double(d.params[1]);
278 const double sv = num_traits<T>::to_double(s);
279 const double upper = a * std::pow(-std::log(1e-10), 1.0 / r);
280 const int n = 1000;
281 const double dx = upper / n;
282 double acc = 0.0;
283 for (int i = 1; i <= n; ++i) {
284 const double x = i * dx;
285 const double pdf =
286 (r / a) * std::pow(x / a, r - 1.0) * std::exp(-std::pow(x / a, r));
287 acc += std::exp(-sv * x) * pdf;
288 }
289 return num_traits<T>::from_double(acc * dx);
290 }
291 }
293 if constexpr (!num_traits<T>::has_transcendental) {
294 throw UnsupportedError(ErrorKind{"Arithmetic"},
295 "dist_lst: the transform of a Lognormal is a numerical integral, which exact "
296 "arithmetic has no representation for; use the double or real backend");
297 } else {
298 const double mu = num_traits<T>::to_double(d.params[0]);
299 const double sg = num_traits<T>::to_double(d.params[1]);
300 const double sv = num_traits<T>::to_double(s);
301 const double upper = std::exp(mu + 5.0 * sg);
302 const int n = 1000;
303 const double dx = upper / n;
304 double acc = 0.0;
305 for (int i = 1; i <= n; ++i) {
306 const double x = i * dx;
307 const double lx = std::log(x);
308 const double pdf = std::exp(-(lx - mu) * (lx - mu) / (2.0 * sg * sg)) /
309 (x * sg * std::sqrt(2.0 * M_PI));
310 acc += std::exp(-sv * x) * pdf;
311 }
312 return num_traits<T>::from_double(acc * dx);
313 }
314 }
315 case ProcessType::GAMMA: {
316 if constexpr (!num_traits<T>::has_transcendental) {
317 throw UnsupportedError(ErrorKind{"Arithmetic"},
318 "dist_lst: the transform of a Gamma is (1 + s theta)^-k, which exact "
319 "arithmetic has no representation for; use the double or real backend");
320 } else {
321 const double shape = num_traits<T>::to_double(d.params[0]);
322 const double scale = num_traits<T>::to_double(d.params[1]);
324 std::pow(1.0 + num_traits<T>::to_double(s) * scale, -shape));
325 }
326 }
327 default:
328 break;
329 }
330
331 // Phase-type / MAP families: pie (sI - D0)^-1 (-D0) e, a rational function
332 // of s and therefore exact wherever the arithmetic is.
333 const mam::Map<T> m = dist_to_map(d);
334 const std::vector<T> pie = mam::map_pie(m);
335 const std::size_t n = m.D0.rows();
336 Matrix<T> A(n, n, zero);
337 for (std::size_t i = 0; i < n; ++i)
338 for (std::size_t j = 0; j < n; ++j) A(i, j) = T((i == j ? s : zero) - m.D0(i, j));
339 // rhs = (-D0) e, the exit-rate vector
340 std::vector<T> rhs(n, zero);
341 for (std::size_t i = 0; i < n; ++i) {
342 T acc = zero;
343 for (std::size_t j = 0; j < n; ++j) acc += m.D0(i, j);
344 rhs[i] = T(-acc);
345 }
346 // Solve A x = rhs by Gaussian elimination with partial pivoting.
347 std::vector<T> x = rhs;
348 for (std::size_t col = 0; col < n; ++col) {
349 std::size_t best = col;
350 double bv = std::fabs(num_traits<T>::to_double(A(col, col)));
351 for (std::size_t r = col + 1; r < n; ++r) {
352 const double v = std::fabs(num_traits<T>::to_double(A(r, col)));
353 if (v > bv) {
354 bv = v;
355 best = r;
356 }
357 }
358 if (best != col) {
359 for (std::size_t j = 0; j < n; ++j) std::swap(A(col, j), A(best, j));
360 std::swap(x[col], x[best]);
361 }
362 if (A(col, col) == zero) throw NumericError(ErrorKind{"Singular"}, "dist_lst: singular transform matrix");
363 for (std::size_t r = 0; r < n; ++r) {
364 if (r == col) continue;
365 const T f = T(A(r, col) / A(col, col));
366 if (f == zero) continue;
367 for (std::size_t j = 0; j < n; ++j) A(r, j) = T(A(r, j) - f * A(col, j));
368 x[r] = T(x[r] - f * x[col]);
369 }
370 }
371 T out = zero;
372 for (std::size_t i = 0; i < n; ++i) out += pie[i] * T(x[i] / A(i, i));
373 return out;
374}
375
376// Forward declarations: the complex transform below reaches the CDF and the raw
377// moments, both defined further down, and a dependent call would resolve only by
378// ADL at instantiation.
379template <class T>
380T dist_cdf(const Distrib<T>& d, const T& x);
381template <class T>
382T dist_moment(const Distrib<T>& d, unsigned k);
383
384/**
385 * `sn.lst` at a COMPLEX argument, E[exp(-sX)] with s off the real axis.
386 *
387 * WHY A SECOND OVERLOAD. A transform is evaluated off the real axis by anything
388 * that inverts it or locates its roots: the Abate-Whitt Euler sum walks the line
389 * Re(s) = A/(2t), and a matrix transform int exp(Ut) dF(t) is read off the
390 * spectrum of U, which is complex in general. `dist_lst(d, s)` above is
391 * templated on the arithmetic type T and returns T, so it cannot answer either;
392 * this twin fixes the argument and the result at std::complex<double>, since a
393 * complex transform is meaningless without transcendental arithmetic anyway.
394 * Parity note: the JAR carries the same capability by widening `sn.lst` to
395 * SerializableFunction<Complex, Complex>, MATLAB by its own closed forms, and
396 * python by Distribution.evalLST accepting a complex argument.
397 *
398 * THE TIERS mirror the real overload exactly: a closed form where the family has
399 * one, the phase-type solve where the law is Markovian, and the CDF-increment
400 * sum otherwise -- the last being a proper measure for ANY law, including one
401 * with an atom and one with no density.
402 */
403template <class T>
404std::complex<double> dist_lst(const Distrib<T>& d, const std::complex<double>& s) {
406 "dist_lst at a complex argument requires transcendental arithmetic");
407 if (d.is_prior()) reject_prior("dist_lst");
408 if (d.disabled) throw InputError("dist_lst: the distribution is disabled");
409 if (std::abs(s) == 0.0) return std::complex<double>(1.0, 0.0);
410
411 switch (d.type) {
413 return std::complex<double>(1.0, 0.0);
415 std::complex<double> acc(0.0, 0.0);
416 for (const T& x : d.trace) acc += std::exp(-s * num_traits<T>::to_double(x));
417 return acc / static_cast<double>(d.trace.size());
418 }
419 case ProcessType::DET:
420 return std::exp(-s * num_traits<T>::to_double(d.mean));
422 const double a = num_traits<T>::to_double(d.params[0]);
423 const double b = num_traits<T>::to_double(d.params[1]);
424 return (std::exp(-s * a) - std::exp(-s * b)) / (s * (b - a));
425 }
426 case ProcessType::NORMAL: {
427 const double mu = num_traits<T>::to_double(d.params[0]);
428 const double sg = num_traits<T>::to_double(d.params[1]);
429 return std::exp(-mu * s + sg * sg * s * s / 2.0);
430 }
431 case ProcessType::GAMMA: {
432 const double shape = num_traits<T>::to_double(d.params[0]);
433 const double scale = num_traits<T>::to_double(d.params[1]);
434 return std::pow(std::complex<double>(1.0, 0.0) + s * scale, -shape);
435 }
436 default:
437 break;
438 }
439
440 if (process_is_markovian(d.type)) {
441 // pie (sI - D0)^-1 (-D0) e, the same rational function as the real
442 // overload, continued to the complex plane.
443 const mam::Map<T> m = dist_to_map(d);
444 const std::vector<T> pie = mam::map_pie(m);
445 const std::size_t n = m.D0.rows();
446 std::vector<std::vector<std::complex<double> > > A(
447 n, std::vector<std::complex<double> >(n, std::complex<double>(0.0, 0.0)));
448 std::vector<std::complex<double> > x(n, std::complex<double>(0.0, 0.0));
449 for (std::size_t i = 0; i < n; ++i) {
450 double exit = 0.0;
451 for (std::size_t j = 0; j < n; ++j) {
452 const double d0 = num_traits<T>::to_double(m.D0(i, j));
453 A[i][j] = (i == j ? s : std::complex<double>(0.0, 0.0)) - d0;
454 exit += d0;
455 }
456 x[i] = std::complex<double>(-exit, 0.0);
457 }
458 for (std::size_t col = 0; col < n; ++col) {
459 std::size_t piv = col;
460 double bv = std::abs(A[col][col]);
461 for (std::size_t r = col + 1; r < n; ++r) {
462 if (std::abs(A[r][col]) > bv) { bv = std::abs(A[r][col]); piv = r; }
463 }
464 if (piv != col) { std::swap(A[piv], A[col]); std::swap(x[piv], x[col]); }
465 if (std::abs(A[col][col]) == 0.0)
466 throw NumericError(ErrorKind{"Singular"}, "dist_lst: singular transform matrix");
467 for (std::size_t r = col + 1; r < n; ++r) {
468 const std::complex<double> f = A[r][col] / A[col][col];
469 for (std::size_t c = col; c < n; ++c) A[r][c] -= f * A[col][c];
470 x[r] -= f * x[col];
471 }
472 }
473 for (std::size_t row = n; row-- > 0;) {
474 std::complex<double> acc = x[row];
475 for (std::size_t c = row + 1; c < n; ++c) acc -= A[row][c] * x[c];
476 x[row] = acc / A[row][row];
477 }
478 std::complex<double> out(0.0, 0.0);
479 for (std::size_t i = 0; i < n; ++i) out += num_traits<T>::to_double(pie[i]) * x[i];
480 return out;
481 }
482
483 // Riemann-Stieltjes sum with true CDF increments, renormalized for the cut
484 // tail, so the result is still a transform.
485 const std::size_t n_grid = 2400;
486 const double mean = num_traits<T>::to_double(dist_moment(d, 1u));
487 double hi = mean * 60.0;
488 const double m2 = num_traits<T>::to_double(dist_moment(d, 2u));
489 const double var = m2 - mean * mean;
490 if (std::isfinite(var) && var > 0.0) hi = std::max(hi, mean + 12.0 * std::sqrt(var));
491 if (!std::isfinite(hi) || hi <= 0.0) return std::complex<double>(1.0, 0.0);
492 const double step = hi / static_cast<double>(n_grid);
493 std::complex<double> acc(0.0, 0.0);
494 double mass = 0.0;
496 for (std::size_t i = 0; i < n_grid; ++i) {
497 const double right = static_cast<double>(i + 1) * step;
498 const double cur = num_traits<T>::to_double(dist_cdf(d, num_traits<T>::from_double(right)));
499 const double w = cur - prev;
500 prev = cur;
501 if (w == 0.0) continue;
502 mass += w;
503 acc += w * std::exp(-s * ((static_cast<double>(i) + 0.5) * step));
504 }
505 return mass > 0.0 ? acc / mass : std::complex<double>(1.0, 0.0);
506}
507
508/**
509 * The k-th raw moment.
510 *
511 * The closed-form families are evaluated from their parameters, as the MATLAB
512 * classes do, rather than from the Erlang approximation of `sn.proc`: the
513 * approximation matches only the mean once the SCV exceeds 1.
514 */
515template <class T>
516T dist_moment(const Distrib<T>& d, unsigned k) {
517 const T one = num_traits<T>::from_int(1);
518 if (k == 0) return one;
519 if (d.is_prior()) reject_prior("dist_moment");
520 if (d.disabled) throw InputError("dist_moment: the distribution is disabled");
521 switch (d.type) {
523 return num_traits<T>::from_int(0);
524 case ProcessType::DET: {
525 T v = one;
526 for (unsigned i = 0; i < k; ++i) v = T(v * d.mean);
527 return v;
528 }
530 // (b^(k+1) - a^(k+1)) / ((k+1)(b-a))
531 const T a = d.params[0], b = d.params[1];
532 T pa = one, pb = one;
533 for (unsigned i = 0; i <= k; ++i) {
534 pa = T(pa * a);
535 pb = T(pb * b);
536 }
537 return T((pb - pa) / (num_traits<T>::from_int(static_cast<long>(k) + 1) * (b - a)));
538 }
539 case ProcessType::PARETO: {
540 // alpha k^m / (alpha - m), finite only for m < alpha
541 const T alpha = d.params[0], scale = d.params[1];
542 const T m = num_traits<T>::from_int(static_cast<long>(k));
543 if (!(alpha > m))
544 throw NumericError(ErrorKind{"Overflow"}, "dist_moment: the Pareto moment of this order is infinite");
545 T ps = one;
546 for (unsigned i = 0; i < k; ++i) ps = T(ps * scale);
547 return T(alpha * ps / (alpha - m));
548 }
550 const T n = num_traits<T>::from_int(static_cast<long>(d.trace.size()));
551 T acc = num_traits<T>::from_int(0);
552 for (const T& x : d.trace) {
553 T v = one;
554 for (unsigned i = 0; i < k; ++i) v = T(v * x);
555 acc += v;
556 }
557 return T(acc / n);
558 }
559 case ProcessType::GAMMA: {
560 if constexpr (!num_traits<T>::has_transcendental) {
561 throw UnsupportedError(ErrorKind{"Arithmetic"},
562 "dist_moment: the moments of a Gamma are values of the gamma function, which "
563 "exact arithmetic has no representation for");
564 } else {
565 const double shape = num_traits<T>::to_double(d.params[0]);
566 const double scale = num_traits<T>::to_double(d.params[1]);
567 return num_traits<T>::from_double(std::tgamma(shape + k) / std::tgamma(shape) *
568 std::pow(scale, static_cast<double>(k)));
569 }
570 }
572 if constexpr (!num_traits<T>::has_transcendental) {
573 throw UnsupportedError(ErrorKind{"Arithmetic"},
574 "dist_moment: the moments of a Weibull are values of the gamma function, "
575 "which exact arithmetic has no representation for");
576 } else {
577 const double a = num_traits<T>::to_double(d.params[0]);
578 const double r = num_traits<T>::to_double(d.params[1]);
579 return num_traits<T>::from_double(std::pow(a, static_cast<double>(k)) *
580 std::tgamma(1.0 + k / r));
581 }
582 }
584 if constexpr (!num_traits<T>::has_transcendental) {
585 throw UnsupportedError(ErrorKind{"Arithmetic"},
586 "dist_moment: the moments of a Lognormal are values of exp, which exact "
587 "arithmetic has no representation for");
588 } else {
589 const double mu = num_traits<T>::to_double(d.params[0]);
590 const double sg = num_traits<T>::to_double(d.params[1]);
591 return num_traits<T>::from_double(std::exp(k * mu + k * k * sg * sg / 2.0));
592 }
593 }
594 case ProcessType::NORMAL: {
595 // The raw moments from the recurrence m_k = mu m_{k-1} + (k-1)
596 // sigma^2 m_{k-2}, which is exact in any arithmetic and needs no
597 // double factorial: m_1 = mu and m_2 = mu^2 + sigma^2 seed it.
598 const T mu = d.params[0], sg = d.params[1];
599 T prev2 = one, prev1 = mu;
600 if (k == 1) return prev1;
601 for (unsigned i = 2; i <= k; ++i) {
602 const T next = T(mu * prev1 +
603 num_traits<T>::from_int(static_cast<long>(i) - 1) * sg * sg *
604 prev2);
605 prev2 = prev1;
606 prev1 = next;
607 }
608 return prev1;
609 }
610 default:
611 break;
612 }
613 return mam::map_moment(dist_to_map(d), k);
614}
615
616/**
617 * F(x) = P{X <= x}, MATLAB's `Distribution.evalCDF`.
618 *
619 * WHY IT EXISTS AT ALL in a port whose solvers read moments and transforms: it
620 * is the only thing `Prior.discretize` needs. The quadrature design of SolverUQ
621 * places its nodes at the conditional medians of equal-mass strata of the
622 * parameter density, which is an inverse CDF and nothing else, so a parameter
623 * law of any family can be discretized with no per-family quantile.
624 *
625 * PER FAMILY, from the closed form the reference's own class uses -- the Erlang
626 * from its Poisson sum, the Gamma from the regularized incomplete gamma, the
627 * Pareto from `gpcdf` reduced to 1 - (k/x)^alpha -- rather than from the Erlang
628 * approximation of `dist_to_map`, for the same reason `dist_moment` does: the
629 * approximation matches only the mean once the SCV exceeds one. The phase-type
630 * and MAP families fall through to 1 - pie exp(D0 x) e, which is `map_cdf`.
631 *
632 * ONE DELIBERATE DIVERGENCE FROM THE REFERENCE, and it is a defect on the other
633 * side: `Uniform.evalCDF` in MATLAB returns the constant DENSITY 1/(b-a) inside
634 * the support and 0 above it, so it is neither a CDF nor monotone. Reproducing
635 * that would make the bisection below fail to bracket rather than return a
636 * matching wrong number, and no ported quantity reads it, so the correct
637 * (x-a)/(b-a) is computed here. Recorded in BUGS.md.
638 */
639template <class T>
640T dist_cdf(const Distrib<T>& d, const T& x) {
641 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
642 if (d.is_prior())
643 throw UnsupportedError(ErrorKind{"Prior"},
644 "dist_cdf: a Prior's law is the mixture over its alternatives, which lives with the "
645 "Prior; call prior_cdf (lang/prior.h)");
646 if (d.disabled) throw InputError("dist_cdf: the distribution is disabled");
647 switch (d.type) {
649 // Immediate.evalCDF returns 1 everywhere, the point mass at zero.
650 return one;
651 case ProcessType::DET:
652 return x < d.params[0] ? zero : one;
654 const T a = d.params[0], b = d.params[1];
655 if (x <= a) return zero;
656 if (x >= b) return one;
657 return T((x - a) / (b - a));
658 }
659 case ProcessType::NORMAL: {
660 if constexpr (!num_traits<T>::has_transcendental) {
661 throw UnsupportedError(ErrorKind{"Arithmetic"},
662 "dist_cdf: the Gaussian CDF is a value of erf, which exact arithmetic has no "
663 "representation for");
664 } else {
665 // 0.5 (1 + erf((x - mu) / (sigma sqrt 2))), `Normal.m:87-95`.
666 const double mu = num_traits<T>::to_double(d.params[0]);
667 const double sg = num_traits<T>::to_double(d.params[1]);
669 0.5 * (1.0 + std::erf((num_traits<T>::to_double(x) - mu) /
670 (sg * std::sqrt(2.0)))));
671 }
672 }
674 // The empirical CDF of the trace: the fraction of samples <= x.
675 if (d.trace.empty()) throw InputError("dist_cdf: the Replayer carries no samples");
676 std::size_t below = 0;
677 for (const T& v : d.trace)
678 if (!(v > x)) ++below;
679 return T(num_traits<T>::from_int(static_cast<long>(below)) /
680 num_traits<T>::from_int(static_cast<long>(d.trace.size())));
681 }
682 default:
683 break;
684 }
685 if constexpr (!num_traits<T>::has_transcendental) {
686 throw UnsupportedError(ErrorKind{"Arithmetic"},
687 "dist_cdf: the law of this family is an exponential, which exact arithmetic has no "
688 "representation for; use the double or real backend");
689 } else {
690 if (!(x > zero)) return zero;
691 const double xv = num_traits<T>::to_double(x);
692 switch (d.type) {
693 case ProcessType::EXP: {
694 const double lam = num_traits<T>::to_double(d.params[0]);
695 return num_traits<T>::from_double(1.0 - std::exp(-lam * xv));
696 }
697 case ProcessType::ERLANG: {
698 // 1 - sum_{j<r} exp(-alpha x) (alpha x)^j / j!, the reference's form.
699 const double alpha = num_traits<T>::to_double(d.params[0]);
700 const long r = std::lround(num_traits<T>::to_double(d.params[1]));
701 const double z = alpha * xv;
702 double term = std::exp(-z), acc = term;
703 for (long j = 1; j < r; ++j) {
704 term *= z / static_cast<double>(j);
705 acc += term;
706 }
707 return num_traits<T>::from_double(1.0 - acc);
708 }
710 const double p = num_traits<T>::to_double(d.params[0]);
711 const double m1 = num_traits<T>::to_double(d.params[1]);
712 const double m2 = num_traits<T>::to_double(d.params[2]);
713 return num_traits<T>::from_double(p * (1.0 - std::exp(-m1 * xv)) +
714 (1.0 - p) * (1.0 - std::exp(-m2 * xv)));
715 }
716 case ProcessType::PARETO: {
717 const double alpha = num_traits<T>::to_double(d.params[0]);
718 const double scale = num_traits<T>::to_double(d.params[1]);
719 if (xv <= scale) return zero;
720 return num_traits<T>::from_double(1.0 - std::pow(scale / xv, alpha));
721 }
722 case ProcessType::GAMMA: {
723 const double shape = num_traits<T>::to_double(d.params[0]);
724 const double scale = num_traits<T>::to_double(d.params[1]);
725 return num_traits<T>::from_double(mam::gammainc_lower(shape, xv / scale));
726 }
728 const double a = num_traits<T>::to_double(d.params[0]);
729 const double r = num_traits<T>::to_double(d.params[1]);
730 return num_traits<T>::from_double(1.0 - std::exp(-std::pow(xv / a, r)));
731 }
733 const double mu = num_traits<T>::to_double(d.params[0]);
734 const double sg = num_traits<T>::to_double(d.params[1]);
736 0.5 * std::erfc(-(std::log(xv) - mu) / (sg * std::sqrt(2.0))));
737 }
738 default:
739 break;
740 }
741 // The phase-type and MAP families: map_cdf, 1 - pie exp(D0 x) e.
742 const mam::Map<T> m = dist_to_map(d);
743 const std::vector<T> pie = mam::map_pie(m);
744 Matrix<T> A(m.D0.rows(), m.D0.cols(), zero);
745 for (std::size_t i = 0; i < A.rows(); ++i)
746 for (std::size_t j = 0; j < A.cols(); ++j) A(i, j) = T(m.D0(i, j) * x);
747 const Matrix<T> E = expm(A);
748 T acc = zero;
749 for (std::size_t i = 0; i < E.rows(); ++i)
750 for (std::size_t j = 0; j < E.cols(); ++j) acc += T(pie[i] * E(i, j));
751 return T(one - acc);
752 }
753}
754
755/**
756 * The p-quantile, by bisection on `dist_cdf`.
757 *
758 * Port of `Prior.quantile`: bracketing starts at the mean and doubles outward,
759 * which terminates for any law with a finite mean, and the search then halves
760 * 200 times or until the bracket is within FineTol of its own width. Using only
761 * the CDF is what makes it applicable to every family at once, which is the
762 * reason `Prior.discretize` is written in probability space rather than in
763 * parameter space.
764 */
765template <class T>
766T dist_quantile(const Distrib<T>& d, const T& p) {
767 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
768 const T two = num_traits<T>::from_int(2);
769 if (!(p > zero) || !(p < one))
770 throw InputError("dist_quantile: p must lie strictly between 0 and 1");
771 T lo = zero;
773 ? d.mean
775 const int max_expand = 200;
776 // A law with mass BELOW ZERO needs the lower end walked down as well. Every
777 // ProcessType family but one is a duration and starts at 0, which is why the
778 // bracket did; a Normal parameter density does not, and leaving lo at 0
779 // would have returned a non-negative "quantile" for any p under F(0) -- a
780 // wrong stratum median for `Prior.discretize`, with no diagnostic.
781 if (d.type == ProcessType::NORMAL) {
782 // Walk both ends out from the mean in doubling multiples of sigma.
783 T step = d.params.size() > 1 ? d.params[1] : one;
784 lo = T(d.mean - step);
785 hi = T(d.mean + step);
786 int j = 0;
787 for (; j < max_expand; ++j) {
788 const bool low_ok = dist_cdf(d, lo) <= p, high_ok = dist_cdf(d, hi) >= p;
789 if (low_ok && high_ok) break;
790 step = T(step * two);
791 if (!low_ok) lo = T(d.mean - step);
792 if (!high_ok) hi = T(d.mean + step);
793 }
794 if (j == max_expand) throw NumericError(ErrorKind{"NonConvergence"}, "dist_quantile: failed to bracket the quantile");
795 } else {
796 int i = 0;
797 for (; i < max_expand; ++i) {
798 if (dist_cdf(d, hi) >= p) break;
799 hi = T(hi * two);
800 }
801 if (i == max_expand) throw NumericError(ErrorKind{"NonConvergence"}, "dist_quantile: failed to bracket the quantile");
802 }
804 for (int k = 0; k < 200; ++k) {
805 const T mid = T((lo + hi) / two);
806 if (dist_cdf(d, mid) < p)
807 lo = mid;
808 else
809 hi = mid;
810 const T scale = hi > one ? hi : one;
811 if (T(hi - lo) <= T(tol * scale)) break;
812 }
813 return T((lo + hi) / two);
814}
815
816/**
817 * Port of `Replayer.isNHPP`: test whether a trace is a sample path of a
818 * NON-HOMOGENEOUS POISSON process, by the conditional-uniform KS test with the
819 * Lewis refinement (`infer_nhpp_ks`).
820 *
821 * WHY THE QUESTION IS WORTH ASKING. A Replayer is used wherever a measured
822 * stream is fed to a solver, and every analytical method that consumes it as an
823 * arrival process assumes SOMETHING about its dependence structure. This test
824 * says whether the Poisson assumption -- independent increments, whatever the
825 * rate does with time -- survives contact with the data, which is the
826 * assumption a time-varying analysis (`mtginf`, `mol`, `tvms`) rests on. A
827 * small p-value says the stream is not Poisson at any rate function, so those
828 * methods are answering a different process.
829 *
830 * The trace holds INTER-ARRIVAL times, so the arrival epochs are their
831 * cumulative sum and the horizon is the last of them.
832 */
833template <class T>
835 if (d.trace.size() < 2)
836 throw InputError("dist_is_nhpp: the trace needs at least two inter-arrival times to test");
837 std::vector<T> epochs;
838 epochs.reserve(d.trace.size());
839 T acc = num_traits<T>::from_int(0);
840 for (std::size_t i = 0; i < d.trace.size(); ++i) {
841 acc = T(acc + d.trace[i]);
842 epochs.push_back(acc);
843 }
844 return infer::infer_nhpp_ks<T>(epochs, epochs.back());
845}
846
847} // namespace lang
848} // namespace line
849
850#endif // LINE_LANG_DISTRIBUTION_H
Minimal-order acyclic phase-type fit of the first three moments (matlab/lib/kpctoolbox/aph/aph_fit....
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
Kolmogorov-Smirnov tests for a non-homogeneous Poisson arrival process.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Transient distribution of a level-independent-in-the-tail QBD by an adaptive Taylor series (libQBD QB...
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
MAP constructors and structural transformations.
Dense matrix and non-owning view.
NhppKsResult< Tv > infer_nhpp_ks(const std::vector< Tv > &times, const Tv &T, const std::function< Tv(const Tv &)> &cumRate=std::function< Tv(const Tv &)>(), NhppKsMethod method=NhppKsMethod::Lewis, const Tv &T0=num_traits< Tv >::from_int(0))
Kolmogorov-Smirnov tests for a non-homogeneous Poisson arrival process.
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
T dist_quantile(const Distrib< T > &d, const T &p)
The p-quantile, by bisection on dist_cdf.
infer::NhppKsResult< T > dist_is_nhpp(const Distrib< T > &d)
Port of Replayer.isNHPP: test whether a trace is a sample path of a NON-HOMOGENEOUS POISSON process,...
T dist_lst(const Distrib< T > &d, const T &s)
sn.lst: the Laplace-Stieltjes transform E[exp(-sX)].
unsigned convert_to_map_phases(double scv)
The number of Erlang phases convertToMAP picks for a non-Markovian distribution: 20 when the SCV is b...
T dist_cdf(const Distrib< T > &d, const T &x)
F(x) = P{X <= x}, MATLAB's Distribution.evalCDF.
std::vector< T > dist_pie(const Distrib< T > &d)
sn.pie: the phase distribution seen by an arriving job.
@ NORMAL
A Gaussian, and the ONE family whose value is not MATLAB's, because MATLAB has none to copy: ProcessT...
Definition lang_types.h:596
void dmap_refresh_moments(Distrib< T > &d)
The first two moments of a DISCRETE-time MAP, from its own law.
bool process_is_markovian(ProcessType p)
ProcessType.isMarkovian: true when sn.proc carries an exact matrix representation of the law,...
Definition lang_types.h:655
void dist_refresh_moments(Distrib< T > &d)
Fill in the first two moments of a distribution given by its matrices.
void reject_prior(const char *who)
The refusal every lowering of a Prior shares.
T dist_moment(const Distrib< T > &d, unsigned k)
The k-th raw moment.
double gammainc_lower(double a, double x)
Regularized lower incomplete gamma P(a, x), MATLAB's gammainc(x, a, 'lower').
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
AphFitResult< T > aph_fit(const T &e1, const T &e2, const T &e3, unsigned nmax, const T &tol)
Fit an APH(n) with n <= nmax to the raw moments e1, e2, e3.
Definition aph_fit.h:176
Map< T > map_erlang(const T &mean, unsigned k)
Erlang-k renewal MAP with the given mean (map_erlang.m).
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
T map_scv(const Map< T > &m)
Squared coefficient of variation.
Definition map_moment.h:140
T map_moment(const Map< T > &m, unsigned k)
Raw moment of order k of the inter-arrival time: k!
Definition map_moment.h:118
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Number-type abstraction for the templated API port.
Adaptive quadrature for the qsys functions whose MATLAB originals call integral(),...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Outcome of the KS test.
bool has_map() const
True when the type carries a (D0,D1) pair of its own.
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:863
std::vector< T > params
Constructor arguments, in MATLAB getParam order.
Definition lang_types.h:836
std::vector< T > trace
Replayer / Trace samples; empty for every other type.
Definition lang_types.h:838
bool is_prior() const
Definition lang_types.h:888
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double CoarseTol
Definition lang_types.h:766
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54