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 (D0,D1) pair that reaches `sn.proc`.
71 *
72 * A type that carries its own representation returns it unchanged. Det,
73 * Uniform, Pareto, Gamma, Weibull and Lognormal are replaced by the Erlang
74 * approximation of `convertToMAP`; a Replayer is fitted by `aph_fit`, which is
75 * what MATLAB's `Replayer.fitAPH` does before taking getProcess.
76 */
77/**
78 * The refusal every lowering of a `Prior` shares.
79 *
80 * A Prior is a set of models, not one law, so there is no (D0,D1), no transform
81 * and no moment of it that a solver could integrate: substituting any single
82 * alternative would answer for a model the caller did not describe, and
83 * collapsing the set to its mixture would answer for a model nobody described.
84 * SolverUQ is the one consumer, and it replaces the Prior before the design
85 * point is solved.
86 */
87inline void reject_prior(const char* who) {
88 throw UnsupportedError(std::string(who) +
89 ": the distribution is a Prior, which is a weighted set of alternative "
90 "MODELS rather than one law; solve the model with SolverUQ, which "
91 "replaces each Prior by one alternative per design point");
92}
93
94/**
95 * The first two moments of a DISCRETE-time MAP, from its own law.
96 *
97 * With alpha the arrival-epoch stationary vector -- `dmap_pie`, the stationary
98 * vector of (I - D0)^-1 D1 -- the interarrival count has
99 * P(N = k) = alpha D0^(k-1) D1 e, so
100 *
101 * E[N] = alpha (I - D0)^-1 e (MATLAB `DMAP.getMean`)
102 * E[N(N-1)] = 2 alpha D0 (I - D0)^-2 e
103 *
104 * THE SCV IS NOT MATLAB'S INHERITED ONE. `DMAP` declares no getSCV, so it falls
105 * through to `Markovian.getSCV` = map_scv({D0,D1}), a CONTINUOUS-time formula
106 * that reads D0 + D1 as a generator; for a DMAP that matrix is stochastic, so
107 * the stationary solve behind it is singular and the number it returns is not
108 * the SCV of anything. Reproducing it would propagate an undefined value into
109 * every AMVA path, so the discrete second moment is computed here and the
110 * reference defect is recorded in BUGS.md.
111 */
112template <class T>
114 const std::size_t n = d.D0.rows();
115 const T one = num_traits<T>::from_int(1), two = num_traits<T>::from_int(2);
116 Matrix<T> ImD0(n, n);
117 for (std::size_t i = 0; i < n; ++i)
118 for (std::size_t j = 0; j < n; ++j) ImD0(i, j) = T((i == j ? one : T(0)) - d.D0(i, j));
119 // alpha: the stationary vector of P = (I - D0)^-1 D1, formed column by
120 // column through one factorization rather than by inverting ImD0.
121 Matrix<T> LU = ImD0;
122 const std::vector<std::size_t> piv = lu_factor(LU);
123 Matrix<T> P(n, n);
124 for (std::size_t j = 0; j < n; ++j) {
125 std::vector<T> col(n);
126 for (std::size_t i = 0; i < n; ++i) col[i] = d.D1(i, j);
127 lu_solve(LU, piv, col);
128 for (std::size_t i = 0; i < n; ++i) P(i, j) = col[i];
129 }
130 const std::vector<T> alpha = mc::dtmc_solve(P);
131 std::vector<T> y(n, one);
132 lu_solve(LU, piv, y); // (I - D0)^-1 e
133 std::vector<T> z = y;
134 lu_solve(LU, piv, z); // (I - D0)^-2 e
136 for (std::size_t i = 0; i < n; ++i) {
137 m1 += T(alpha[i] * y[i]);
138 T dz = num_traits<T>::from_int(0);
139 for (std::size_t j = 0; j < n; ++j) dz += T(d.D0(i, j) * z[j]);
140 fac2 += T(alpha[i] * dz);
141 }
142 fac2 = T(two * fac2);
143 const T m2 = T(fac2 + m1);
144 d.mean = m1;
145 d.scv = T((m2 - m1 * m1) / (m1 * m1));
146}
147
148template <class T>
150 if (d.is_prior()) reject_prior("dist_to_map");
151 if (d.disabled) throw InputError("dist_to_map: the distribution is disabled");
152 if (d.type == ProcessType::NORMAL)
153 throw UnsupportedError(
154 "dist_to_map: a Normal puts mass below zero, so it is not the law of any duration and "
155 "has no Markovian representation. The default arm of this function would hand back the "
156 "Erlang fit of its mean, which is a positive law with the same mean and nothing else "
157 "in common; refusing instead. A Normal reaches this port only as the parameter density "
158 "of a continuous Prior");
159 if (d.has_map()) {
160 mam::Map<T> m;
161 m.D0 = d.D0;
162 m.D1 = d.D1;
163 return m;
164 }
165 if (d.type == ProcessType::REPLAYER) {
166 // MATLAB fits an APH to the trace's first three moments. The fit is
167 // Bobbio-Horvath-Telek, which needs roots and exponentials, so it is
168 // gated: without the guard the static_assert inside aph_fit fires for
169 // EVERY exact-arithmetic caller of dist_to_map, trace or not, because
170 // the branch is instantiated whether or not it is taken.
171 if constexpr (num_traits<T>::has_transcendental) {
172 const T n = num_traits<T>::from_int(static_cast<long>(d.trace.size()));
175 for (const T& x : d.trace) {
176 m1 += x;
177 m2 += T(x * x);
178 m3 += T(x * x * x);
179 }
180 return mam::aph_fit(T(m1 / n), T(m2 / n), T(m3 / n)).aph;
181 } else {
182 throw UnsupportedError(
183 "dist_to_map: fitting a Replayer trace to an acyclic phase-type needs "
184 "transcendental arithmetic, which exact rational arithmetic does not provide; "
185 "solve in double or Real<n>, or replace the trace by a fitted distribution");
186 }
187 }
189}
190
191/** `sn.pie`: the phase distribution seen by an arriving job. */
192template <class T>
193std::vector<T> dist_pie(const Distrib<T>& d) {
194 return mam::map_pie(dist_to_map(d));
195}
196
197/**
198 * Fill in the first two moments of a distribution given by its matrices.
199 *
200 * `Distrib::map_dist` cannot compute them -- they need the stationary vector --
201 * so a MAP built directly from (D0,D1) leaves mean and scv at their defaults
202 * until this runs. Every builder call that installs such a distribution passes
203 * through here.
204 */
205template <class T>
207 if (d.disabled || !d.has_map()) return;
208 if (d.type == ProcessType::DMAP) {
210 return;
211 }
212 mam::Map<T> m;
213 m.D0 = d.D0;
214 m.D1 = d.D1;
215 d.mean = mam::map_mean(m);
216 d.scv = mam::map_scv(m);
217}
218
219/**
220 * `sn.lst`: the Laplace-Stieltjes transform E[exp(-sX)].
221 *
222 * The phase-type families evaluate the closed form pie (sI - D0)^-1 (-D0) e;
223 * for a MAP that is the transform of its stationary interarrival time, which is
224 * the quantity the M/G/1 analyzers want.
225 */
226template <class T>
227T dist_lst(const Distrib<T>& d, const T& s) {
228 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
229 if (d.is_prior()) reject_prior("dist_lst");
230 if (d.disabled) throw InputError("dist_lst: the distribution is disabled");
231 if (s == zero) return one;
232
233 switch (d.type) {
235 return one;
237 // The EMPIRICAL transform mean(exp(-s x)) over the trace, which is
238 // what MATLAB `Replayer.evalLST` and the JAR/python twins return.
239 // Falling through to the phase-type arm would transform the APH fit
240 // of the first three moments instead, i.e. a different law: on
241 // gallery_replayerm1 that moves the G/M/1 caudal root from 0.332835
242 // to 0.332887 and QLen by 1e-4 relative.
243 if constexpr (!num_traits<T>::has_transcendental) {
244 throw UnsupportedError(
245 "dist_lst: the transform of a Replayer trace is a mean of exp, which exact "
246 "arithmetic has no representation for; use the double or real backend");
247 } else {
248 const double sv = num_traits<T>::to_double(s);
249 double acc = 0.0;
250 for (const T& x : d.trace) acc += std::exp(-sv * num_traits<T>::to_double(x));
251 return num_traits<T>::from_double(acc / static_cast<double>(d.trace.size()));
252 }
253 }
254 case ProcessType::DET: {
255 if constexpr (!num_traits<T>::has_transcendental) {
256 throw UnsupportedError(
257 "dist_lst: the transform of a Det is exp(-s m), which exact arithmetic has "
258 "no representation for; use the double or real backend");
259 } else {
262 }
263 }
265 if constexpr (!num_traits<T>::has_transcendental) {
266 throw UnsupportedError(
267 "dist_lst: the transform of a Uniform evaluates exp, which exact arithmetic "
268 "has no representation for; use the double or real backend");
269 } else {
270 const double a = num_traits<T>::to_double(d.params[0]);
271 const double b = num_traits<T>::to_double(d.params[1]);
272 const double sv = num_traits<T>::to_double(s);
273 return num_traits<T>::from_double((std::exp(-sv * a) - std::exp(-sv * b)) /
274 (sv * (b - a)));
275 }
276 }
277 case ProcessType::NORMAL: {
278 if constexpr (!num_traits<T>::has_transcendental) {
279 throw UnsupportedError(
280 "dist_lst: the transform of a Normal is a value of exp, which exact arithmetic "
281 "has no representation for; use the double or real backend");
282 } else {
283 // exp(-mu s + sigma^2 s^2 / 2), the Laplace-Stieltjes transform.
284 //
285 // ONE DELIBERATE DIVERGENCE, and it is a defect on the other
286 // side: `Normal.m:96-106` returns exp(+mu s + sigma^2 s^2 / 2),
287 // which is the MGF at +s and not E[exp(-sX)] at all -- its own
288 // comment says "the moment-generating function evaluated at -s",
289 // which that expression is also not. Nothing reads it: a Normal
290 // is never a service process, so no analyzer reaches this arm,
291 // and reproducing the sign would put a wrong transform in the
292 // one place a future caller would trust. Recorded in BUGS.md.
293 const double mu = num_traits<T>::to_double(d.params[0]);
294 const double sg = num_traits<T>::to_double(d.params[1]);
295 const double sv = num_traits<T>::to_double(s);
296 return num_traits<T>::from_double(std::exp(-mu * sv + sg * sg * sv * sv / 2.0));
297 }
298 }
299 case ProcessType::PARETO: {
300 if constexpr (!num_traits<T>::has_transcendental) {
301 throw UnsupportedError(
302 "dist_lst: the transform of a Pareto is an integral of exp, which exact "
303 "arithmetic has no representation for; use the double or real backend");
304 } else {
305 // alpha * int_0^1 u^(alpha-1) exp(-s k / u) du, the substitution
306 // x = k/u the reference uses to stay accurate as s -> 0.
307 const T alpha = d.params[0], k = d.params[1];
308 const T tiny = num_traits<T>::from_double(1e-300);
309 auto g = [&alpha, &k, &s, &tiny, &one](const T& u) -> T {
310 if (!(u > num_traits<T>::from_int(0))) return num_traits<T>::from_int(0);
311 const T uu = u > tiny ? u : tiny;
312 const double e = std::exp(-num_traits<T>::to_double(T(s * k / uu)));
313 const double p = std::pow(num_traits<T>::to_double(u),
314 num_traits<T>::to_double(T(alpha - one)));
315 return num_traits<T>::from_double(p * e);
316 };
317 const T val = qsys::detail::num_integral<T>(g, zero, one,
319 num_traits<T>::from_double(1e-300), 50);
320 return T(alpha * val);
321 }
322 }
324 if constexpr (!num_traits<T>::has_transcendental) {
325 throw UnsupportedError(
326 "dist_lst: the transform of a Weibull is a numerical integral, which exact "
327 "arithmetic has no representation for; use the double or real backend");
328 } else {
329 // The reference's 1000-panel right-endpoint sum, reproduced.
330 const double a = num_traits<T>::to_double(d.params[0]);
331 const double r = num_traits<T>::to_double(d.params[1]);
332 const double sv = num_traits<T>::to_double(s);
333 const double upper = a * std::pow(-std::log(1e-10), 1.0 / r);
334 const int n = 1000;
335 const double dx = upper / n;
336 double acc = 0.0;
337 for (int i = 1; i <= n; ++i) {
338 const double x = i * dx;
339 const double pdf =
340 (r / a) * std::pow(x / a, r - 1.0) * std::exp(-std::pow(x / a, r));
341 acc += std::exp(-sv * x) * pdf;
342 }
343 return num_traits<T>::from_double(acc * dx);
344 }
345 }
347 if constexpr (!num_traits<T>::has_transcendental) {
348 throw UnsupportedError(
349 "dist_lst: the transform of a Lognormal is a numerical integral, which exact "
350 "arithmetic has no representation for; use the double or real backend");
351 } else {
352 const double mu = num_traits<T>::to_double(d.params[0]);
353 const double sg = num_traits<T>::to_double(d.params[1]);
354 const double sv = num_traits<T>::to_double(s);
355 const double upper = std::exp(mu + 5.0 * sg);
356 const int n = 1000;
357 const double dx = upper / n;
358 double acc = 0.0;
359 for (int i = 1; i <= n; ++i) {
360 const double x = i * dx;
361 const double lx = std::log(x);
362 const double pdf = std::exp(-(lx - mu) * (lx - mu) / (2.0 * sg * sg)) /
363 (x * sg * std::sqrt(2.0 * M_PI));
364 acc += std::exp(-sv * x) * pdf;
365 }
366 return num_traits<T>::from_double(acc * dx);
367 }
368 }
369 case ProcessType::GAMMA: {
370 if constexpr (!num_traits<T>::has_transcendental) {
371 throw UnsupportedError(
372 "dist_lst: the transform of a Gamma is (1 + s theta)^-k, which exact "
373 "arithmetic has no representation for; use the double or real backend");
374 } else {
375 const double shape = num_traits<T>::to_double(d.params[0]);
376 const double scale = num_traits<T>::to_double(d.params[1]);
378 std::pow(1.0 + num_traits<T>::to_double(s) * scale, -shape));
379 }
380 }
381 default:
382 break;
383 }
384
385 // Phase-type / MAP families: pie (sI - D0)^-1 (-D0) e, a rational function
386 // of s and therefore exact wherever the arithmetic is.
387 const mam::Map<T> m = dist_to_map(d);
388 const std::vector<T> pie = mam::map_pie(m);
389 const std::size_t n = m.D0.rows();
390 Matrix<T> A(n, n, zero);
391 for (std::size_t i = 0; i < n; ++i)
392 for (std::size_t j = 0; j < n; ++j) A(i, j) = T((i == j ? s : zero) - m.D0(i, j));
393 // rhs = (-D0) e, the exit-rate vector
394 std::vector<T> rhs(n, zero);
395 for (std::size_t i = 0; i < n; ++i) {
396 T acc = zero;
397 for (std::size_t j = 0; j < n; ++j) acc += m.D0(i, j);
398 rhs[i] = T(-acc);
399 }
400 // Solve A x = rhs by Gaussian elimination with partial pivoting.
401 std::vector<T> x = rhs;
402 for (std::size_t col = 0; col < n; ++col) {
403 std::size_t best = col;
404 double bv = std::fabs(num_traits<T>::to_double(A(col, col)));
405 for (std::size_t r = col + 1; r < n; ++r) {
406 const double v = std::fabs(num_traits<T>::to_double(A(r, col)));
407 if (v > bv) {
408 bv = v;
409 best = r;
410 }
411 }
412 if (best != col) {
413 for (std::size_t j = 0; j < n; ++j) std::swap(A(col, j), A(best, j));
414 std::swap(x[col], x[best]);
415 }
416 if (A(col, col) == zero) throw NumericError("dist_lst: singular transform matrix");
417 for (std::size_t r = 0; r < n; ++r) {
418 if (r == col) continue;
419 const T f = T(A(r, col) / A(col, col));
420 if (f == zero) continue;
421 for (std::size_t j = 0; j < n; ++j) A(r, j) = T(A(r, j) - f * A(col, j));
422 x[r] = T(x[r] - f * x[col]);
423 }
424 }
425 T out = zero;
426 for (std::size_t i = 0; i < n; ++i) out += pie[i] * T(x[i] / A(i, i));
427 return out;
428}
429
430// Forward declarations: the complex transform below reaches the CDF and the raw
431// moments, both defined further down, and a dependent call would resolve only by
432// ADL at instantiation.
433template <class T>
434T dist_cdf(const Distrib<T>& d, const T& x);
435template <class T>
436T dist_moment(const Distrib<T>& d, unsigned k);
437
438/**
439 * `sn.lst` at a COMPLEX argument, E[exp(-sX)] with s off the real axis.
440 *
441 * WHY A SECOND OVERLOAD. A transform is evaluated off the real axis by anything
442 * that inverts it or locates its roots: the Abate-Whitt Euler sum walks the line
443 * Re(s) = A/(2t), and a matrix transform int exp(Ut) dF(t) is read off the
444 * spectrum of U, which is complex in general. `dist_lst(d, s)` above is
445 * templated on the arithmetic type T and returns T, so it cannot answer either;
446 * this twin fixes the argument and the result at std::complex<double>, since a
447 * complex transform is meaningless without transcendental arithmetic anyway.
448 * Parity note: the JAR carries the same capability by widening `sn.lst` to
449 * SerializableFunction<Complex, Complex>, MATLAB by its own closed forms, and
450 * python by Distribution.evalLST accepting a complex argument.
451 *
452 * THE TIERS mirror the real overload exactly: a closed form where the family has
453 * one, the phase-type solve where the law is Markovian, and the CDF-increment
454 * sum otherwise -- the last being a proper measure for ANY law, including one
455 * with an atom and one with no density.
456 */
457template <class T>
458std::complex<double> dist_lst(const Distrib<T>& d, const std::complex<double>& s) {
460 "dist_lst at a complex argument requires transcendental arithmetic");
461 if (d.is_prior()) reject_prior("dist_lst");
462 if (d.disabled) throw InputError("dist_lst: the distribution is disabled");
463 if (std::abs(s) == 0.0) return std::complex<double>(1.0, 0.0);
464
465 switch (d.type) {
467 return std::complex<double>(1.0, 0.0);
469 std::complex<double> acc(0.0, 0.0);
470 for (const T& x : d.trace) acc += std::exp(-s * num_traits<T>::to_double(x));
471 return acc / static_cast<double>(d.trace.size());
472 }
473 case ProcessType::DET:
474 return std::exp(-s * num_traits<T>::to_double(d.mean));
476 const double a = num_traits<T>::to_double(d.params[0]);
477 const double b = num_traits<T>::to_double(d.params[1]);
478 return (std::exp(-s * a) - std::exp(-s * b)) / (s * (b - a));
479 }
480 case ProcessType::NORMAL: {
481 const double mu = num_traits<T>::to_double(d.params[0]);
482 const double sg = num_traits<T>::to_double(d.params[1]);
483 return std::exp(-mu * s + sg * sg * s * s / 2.0);
484 }
485 case ProcessType::GAMMA: {
486 const double shape = num_traits<T>::to_double(d.params[0]);
487 const double scale = num_traits<T>::to_double(d.params[1]);
488 return std::pow(std::complex<double>(1.0, 0.0) + s * scale, -shape);
489 }
490 default:
491 break;
492 }
493
494 if (process_is_markovian(d.type)) {
495 // pie (sI - D0)^-1 (-D0) e, the same rational function as the real
496 // overload, continued to the complex plane.
497 const mam::Map<T> m = dist_to_map(d);
498 const std::vector<T> pie = mam::map_pie(m);
499 const std::size_t n = m.D0.rows();
500 std::vector<std::vector<std::complex<double> > > A(
501 n, std::vector<std::complex<double> >(n, std::complex<double>(0.0, 0.0)));
502 std::vector<std::complex<double> > x(n, std::complex<double>(0.0, 0.0));
503 for (std::size_t i = 0; i < n; ++i) {
504 double exit = 0.0;
505 for (std::size_t j = 0; j < n; ++j) {
506 const double d0 = num_traits<T>::to_double(m.D0(i, j));
507 A[i][j] = (i == j ? s : std::complex<double>(0.0, 0.0)) - d0;
508 exit += d0;
509 }
510 x[i] = std::complex<double>(-exit, 0.0);
511 }
512 for (std::size_t col = 0; col < n; ++col) {
513 std::size_t piv = col;
514 double bv = std::abs(A[col][col]);
515 for (std::size_t r = col + 1; r < n; ++r) {
516 if (std::abs(A[r][col]) > bv) { bv = std::abs(A[r][col]); piv = r; }
517 }
518 if (piv != col) { std::swap(A[piv], A[col]); std::swap(x[piv], x[col]); }
519 if (std::abs(A[col][col]) == 0.0)
520 throw NumericError("dist_lst: singular transform matrix");
521 for (std::size_t r = col + 1; r < n; ++r) {
522 const std::complex<double> f = A[r][col] / A[col][col];
523 for (std::size_t c = col; c < n; ++c) A[r][c] -= f * A[col][c];
524 x[r] -= f * x[col];
525 }
526 }
527 for (std::size_t row = n; row-- > 0;) {
528 std::complex<double> acc = x[row];
529 for (std::size_t c = row + 1; c < n; ++c) acc -= A[row][c] * x[c];
530 x[row] = acc / A[row][row];
531 }
532 std::complex<double> out(0.0, 0.0);
533 for (std::size_t i = 0; i < n; ++i) out += num_traits<T>::to_double(pie[i]) * x[i];
534 return out;
535 }
536
537 // Riemann-Stieltjes sum with true CDF increments, renormalized for the cut
538 // tail, so the result is still a transform.
539 const std::size_t n_grid = 2400;
540 const double mean = num_traits<T>::to_double(dist_moment(d, 1u));
541 double hi = mean * 60.0;
542 const double m2 = num_traits<T>::to_double(dist_moment(d, 2u));
543 const double var = m2 - mean * mean;
544 if (std::isfinite(var) && var > 0.0) hi = std::max(hi, mean + 12.0 * std::sqrt(var));
545 if (!std::isfinite(hi) || hi <= 0.0) return std::complex<double>(1.0, 0.0);
546 const double step = hi / static_cast<double>(n_grid);
547 std::complex<double> acc(0.0, 0.0);
548 double mass = 0.0;
550 for (std::size_t i = 0; i < n_grid; ++i) {
551 const double right = static_cast<double>(i + 1) * step;
552 const double cur = num_traits<T>::to_double(dist_cdf(d, num_traits<T>::from_double(right)));
553 const double w = cur - prev;
554 prev = cur;
555 if (w == 0.0) continue;
556 mass += w;
557 acc += w * std::exp(-s * ((static_cast<double>(i) + 0.5) * step));
558 }
559 return mass > 0.0 ? acc / mass : std::complex<double>(1.0, 0.0);
560}
561
562/**
563 * The k-th raw moment.
564 *
565 * The closed-form families are evaluated from their parameters, as the MATLAB
566 * classes do, rather than from the Erlang approximation of `sn.proc`: the
567 * approximation matches only the mean once the SCV exceeds 1.
568 */
569template <class T>
570T dist_moment(const Distrib<T>& d, unsigned k) {
571 const T one = num_traits<T>::from_int(1);
572 if (k == 0) return one;
573 if (d.is_prior()) reject_prior("dist_moment");
574 if (d.disabled) throw InputError("dist_moment: the distribution is disabled");
575 switch (d.type) {
577 return num_traits<T>::from_int(0);
578 case ProcessType::DET: {
579 T v = one;
580 for (unsigned i = 0; i < k; ++i) v = T(v * d.mean);
581 return v;
582 }
584 // (b^(k+1) - a^(k+1)) / ((k+1)(b-a))
585 const T a = d.params[0], b = d.params[1];
586 T pa = one, pb = one;
587 for (unsigned i = 0; i <= k; ++i) {
588 pa = T(pa * a);
589 pb = T(pb * b);
590 }
591 return T((pb - pa) / (num_traits<T>::from_int(static_cast<long>(k) + 1) * (b - a)));
592 }
593 case ProcessType::PARETO: {
594 // alpha k^m / (alpha - m), finite only for m < alpha
595 const T alpha = d.params[0], scale = d.params[1];
596 const T m = num_traits<T>::from_int(static_cast<long>(k));
597 if (!(alpha > m))
598 throw NumericError("dist_moment: the Pareto moment of this order is infinite");
599 T ps = one;
600 for (unsigned i = 0; i < k; ++i) ps = T(ps * scale);
601 return T(alpha * ps / (alpha - m));
602 }
604 const T n = num_traits<T>::from_int(static_cast<long>(d.trace.size()));
605 T acc = num_traits<T>::from_int(0);
606 for (const T& x : d.trace) {
607 T v = one;
608 for (unsigned i = 0; i < k; ++i) v = T(v * x);
609 acc += v;
610 }
611 return T(acc / n);
612 }
613 case ProcessType::GAMMA: {
614 if constexpr (!num_traits<T>::has_transcendental) {
615 throw UnsupportedError(
616 "dist_moment: the moments of a Gamma are values of the gamma function, which "
617 "exact arithmetic has no representation for");
618 } else {
619 const double shape = num_traits<T>::to_double(d.params[0]);
620 const double scale = num_traits<T>::to_double(d.params[1]);
621 return num_traits<T>::from_double(std::tgamma(shape + k) / std::tgamma(shape) *
622 std::pow(scale, static_cast<double>(k)));
623 }
624 }
626 if constexpr (!num_traits<T>::has_transcendental) {
627 throw UnsupportedError(
628 "dist_moment: the moments of a Weibull are values of the gamma function, "
629 "which exact arithmetic has no representation for");
630 } else {
631 const double a = num_traits<T>::to_double(d.params[0]);
632 const double r = num_traits<T>::to_double(d.params[1]);
633 return num_traits<T>::from_double(std::pow(a, static_cast<double>(k)) *
634 std::tgamma(1.0 + k / r));
635 }
636 }
638 if constexpr (!num_traits<T>::has_transcendental) {
639 throw UnsupportedError(
640 "dist_moment: the moments of a Lognormal are values of exp, which exact "
641 "arithmetic has no representation for");
642 } else {
643 const double mu = num_traits<T>::to_double(d.params[0]);
644 const double sg = num_traits<T>::to_double(d.params[1]);
645 return num_traits<T>::from_double(std::exp(k * mu + k * k * sg * sg / 2.0));
646 }
647 }
648 case ProcessType::NORMAL: {
649 // The raw moments from the recurrence m_k = mu m_{k-1} + (k-1)
650 // sigma^2 m_{k-2}, which is exact in any arithmetic and needs no
651 // double factorial: m_1 = mu and m_2 = mu^2 + sigma^2 seed it.
652 const T mu = d.params[0], sg = d.params[1];
653 T prev2 = one, prev1 = mu;
654 if (k == 1) return prev1;
655 for (unsigned i = 2; i <= k; ++i) {
656 const T next = T(mu * prev1 +
657 num_traits<T>::from_int(static_cast<long>(i) - 1) * sg * sg *
658 prev2);
659 prev2 = prev1;
660 prev1 = next;
661 }
662 return prev1;
663 }
664 default:
665 break;
666 }
667 return mam::map_moment(dist_to_map(d), k);
668}
669
670/**
671 * F(x) = P{X <= x}, MATLAB's `Distribution.evalCDF`.
672 *
673 * WHY IT EXISTS AT ALL in a port whose solvers read moments and transforms: it
674 * is the only thing `Prior.discretize` needs. The quadrature design of SolverUQ
675 * places its nodes at the conditional medians of equal-mass strata of the
676 * parameter density, which is an inverse CDF and nothing else, so a parameter
677 * law of any family can be discretized with no per-family quantile.
678 *
679 * PER FAMILY, from the closed form the reference's own class uses -- the Erlang
680 * from its Poisson sum, the Gamma from the regularized incomplete gamma, the
681 * Pareto from `gpcdf` reduced to 1 - (k/x)^alpha -- rather than from the Erlang
682 * approximation of `dist_to_map`, for the same reason `dist_moment` does: the
683 * approximation matches only the mean once the SCV exceeds one. The phase-type
684 * and MAP families fall through to 1 - pie exp(D0 x) e, which is `map_cdf`.
685 *
686 * ONE DELIBERATE DIVERGENCE FROM THE REFERENCE, and it is a defect on the other
687 * side: `Uniform.evalCDF` in MATLAB returns the constant DENSITY 1/(b-a) inside
688 * the support and 0 above it, so it is neither a CDF nor monotone. Reproducing
689 * that would make the bisection below fail to bracket rather than return a
690 * matching wrong number, and no ported quantity reads it, so the correct
691 * (x-a)/(b-a) is computed here. Recorded in BUGS.md.
692 */
693template <class T>
694T dist_cdf(const Distrib<T>& d, const T& x) {
695 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
696 if (d.is_prior())
697 throw UnsupportedError(
698 "dist_cdf: a Prior's law is the mixture over its alternatives, which lives with the "
699 "Prior; call prior_cdf (lang/prior.h)");
700 if (d.disabled) throw InputError("dist_cdf: the distribution is disabled");
701 switch (d.type) {
703 // Immediate.evalCDF returns 1 everywhere, the point mass at zero.
704 return one;
705 case ProcessType::DET:
706 return x < d.params[0] ? zero : one;
708 const T a = d.params[0], b = d.params[1];
709 if (x <= a) return zero;
710 if (x >= b) return one;
711 return T((x - a) / (b - a));
712 }
713 case ProcessType::NORMAL: {
714 if constexpr (!num_traits<T>::has_transcendental) {
715 throw UnsupportedError(
716 "dist_cdf: the Gaussian CDF is a value of erf, which exact arithmetic has no "
717 "representation for");
718 } else {
719 // 0.5 (1 + erf((x - mu) / (sigma sqrt 2))), `Normal.m:87-95`.
720 const double mu = num_traits<T>::to_double(d.params[0]);
721 const double sg = num_traits<T>::to_double(d.params[1]);
723 0.5 * (1.0 + std::erf((num_traits<T>::to_double(x) - mu) /
724 (sg * std::sqrt(2.0)))));
725 }
726 }
728 // The empirical CDF of the trace: the fraction of samples <= x.
729 if (d.trace.empty()) throw InputError("dist_cdf: the Replayer carries no samples");
730 std::size_t below = 0;
731 for (const T& v : d.trace)
732 if (!(v > x)) ++below;
733 return T(num_traits<T>::from_int(static_cast<long>(below)) /
734 num_traits<T>::from_int(static_cast<long>(d.trace.size())));
735 }
736 default:
737 break;
738 }
739 if constexpr (!num_traits<T>::has_transcendental) {
740 throw UnsupportedError(
741 "dist_cdf: the law of this family is an exponential, which exact arithmetic has no "
742 "representation for; use the double or real backend");
743 } else {
744 if (!(x > zero)) return zero;
745 const double xv = num_traits<T>::to_double(x);
746 switch (d.type) {
747 case ProcessType::EXP: {
748 const double lam = num_traits<T>::to_double(d.params[0]);
749 return num_traits<T>::from_double(1.0 - std::exp(-lam * xv));
750 }
751 case ProcessType::ERLANG: {
752 // 1 - sum_{j<r} exp(-alpha x) (alpha x)^j / j!, the reference's form.
753 const double alpha = num_traits<T>::to_double(d.params[0]);
754 const long r = std::lround(num_traits<T>::to_double(d.params[1]));
755 const double z = alpha * xv;
756 double term = std::exp(-z), acc = term;
757 for (long j = 1; j < r; ++j) {
758 term *= z / static_cast<double>(j);
759 acc += term;
760 }
761 return num_traits<T>::from_double(1.0 - acc);
762 }
764 const double p = num_traits<T>::to_double(d.params[0]);
765 const double m1 = num_traits<T>::to_double(d.params[1]);
766 const double m2 = num_traits<T>::to_double(d.params[2]);
767 return num_traits<T>::from_double(p * (1.0 - std::exp(-m1 * xv)) +
768 (1.0 - p) * (1.0 - std::exp(-m2 * xv)));
769 }
770 case ProcessType::PARETO: {
771 const double alpha = num_traits<T>::to_double(d.params[0]);
772 const double scale = num_traits<T>::to_double(d.params[1]);
773 if (xv <= scale) return zero;
774 return num_traits<T>::from_double(1.0 - std::pow(scale / xv, alpha));
775 }
776 case ProcessType::GAMMA: {
777 const double shape = num_traits<T>::to_double(d.params[0]);
778 const double scale = num_traits<T>::to_double(d.params[1]);
779 return num_traits<T>::from_double(mam::gammainc_lower(shape, xv / scale));
780 }
782 const double a = num_traits<T>::to_double(d.params[0]);
783 const double r = num_traits<T>::to_double(d.params[1]);
784 return num_traits<T>::from_double(1.0 - std::exp(-std::pow(xv / a, r)));
785 }
787 const double mu = num_traits<T>::to_double(d.params[0]);
788 const double sg = num_traits<T>::to_double(d.params[1]);
790 0.5 * std::erfc(-(std::log(xv) - mu) / (sg * std::sqrt(2.0))));
791 }
792 default:
793 break;
794 }
795 // The phase-type and MAP families: map_cdf, 1 - pie exp(D0 x) e.
796 const mam::Map<T> m = dist_to_map(d);
797 const std::vector<T> pie = mam::map_pie(m);
798 Matrix<T> A(m.D0.rows(), m.D0.cols(), zero);
799 for (std::size_t i = 0; i < A.rows(); ++i)
800 for (std::size_t j = 0; j < A.cols(); ++j) A(i, j) = T(m.D0(i, j) * x);
801 const Matrix<T> E = expm(A);
802 T acc = zero;
803 for (std::size_t i = 0; i < E.rows(); ++i)
804 for (std::size_t j = 0; j < E.cols(); ++j) acc += T(pie[i] * E(i, j));
805 return T(one - acc);
806 }
807}
808
809/**
810 * The p-quantile, by bisection on `dist_cdf`.
811 *
812 * Port of `Prior.quantile`: bracketing starts at the mean and doubles outward,
813 * which terminates for any law with a finite mean, and the search then halves
814 * 200 times or until the bracket is within FineTol of its own width. Using only
815 * the CDF is what makes it applicable to every family at once, which is the
816 * reason `Prior.discretize` is written in probability space rather than in
817 * parameter space.
818 */
819template <class T>
820T dist_quantile(const Distrib<T>& d, const T& p) {
821 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
822 const T two = num_traits<T>::from_int(2);
823 if (!(p > zero) || !(p < one))
824 throw InputError("dist_quantile: p must lie strictly between 0 and 1");
825 T lo = zero;
827 ? d.mean
829 const int max_expand = 200;
830 // A law with mass BELOW ZERO needs the lower end walked down as well. Every
831 // ProcessType family but one is a duration and starts at 0, which is why the
832 // bracket did; a Normal parameter density does not, and leaving lo at 0
833 // would have returned a non-negative "quantile" for any p under F(0) -- a
834 // wrong stratum median for `Prior.discretize`, with no diagnostic.
835 if (d.type == ProcessType::NORMAL) {
836 // Walk both ends out from the mean in doubling multiples of sigma.
837 T step = d.params.size() > 1 ? d.params[1] : one;
838 lo = T(d.mean - step);
839 hi = T(d.mean + step);
840 int j = 0;
841 for (; j < max_expand; ++j) {
842 const bool low_ok = dist_cdf(d, lo) <= p, high_ok = dist_cdf(d, hi) >= p;
843 if (low_ok && high_ok) break;
844 step = T(step * two);
845 if (!low_ok) lo = T(d.mean - step);
846 if (!high_ok) hi = T(d.mean + step);
847 }
848 if (j == max_expand) throw NumericError("dist_quantile: failed to bracket the quantile");
849 } else {
850 int i = 0;
851 for (; i < max_expand; ++i) {
852 if (dist_cdf(d, hi) >= p) break;
853 hi = T(hi * two);
854 }
855 if (i == max_expand) throw NumericError("dist_quantile: failed to bracket the quantile");
856 }
858 for (int k = 0; k < 200; ++k) {
859 const T mid = T((lo + hi) / two);
860 if (dist_cdf(d, mid) < p)
861 lo = mid;
862 else
863 hi = mid;
864 const T scale = hi > one ? hi : one;
865 if (T(hi - lo) <= T(tol * scale)) break;
866 }
867 return T((lo + hi) / two);
868}
869
870/**
871 * Port of `Replayer.isNHPP`: test whether a trace is a sample path of a
872 * NON-HOMOGENEOUS POISSON process, by the conditional-uniform KS test with the
873 * Lewis refinement (`infer_nhpp_ks`).
874 *
875 * WHY THE QUESTION IS WORTH ASKING. A Replayer is used wherever a measured
876 * stream is fed to a solver, and every analytical method that consumes it as an
877 * arrival process assumes SOMETHING about its dependence structure. This test
878 * says whether the Poisson assumption -- independent increments, whatever the
879 * rate does with time -- survives contact with the data, which is the
880 * assumption a time-varying analysis (`mtginf`, `mol`, `tvms`) rests on. A
881 * small p-value says the stream is not Poisson at any rate function, so those
882 * methods are answering a different process.
883 *
884 * The trace holds INTER-ARRIVAL times, so the arrival epochs are their
885 * cumulative sum and the horizon is the last of them.
886 */
887template <class T>
889 if (d.trace.size() < 2)
890 throw InputError("dist_is_nhpp: the trace needs at least two inter-arrival times to test");
891 std::vector<T> epochs;
892 epochs.reserve(d.trace.size());
893 T acc = num_traits<T>::from_int(0);
894 for (std::size_t i = 0; i < d.trace.size(); ++i) {
895 acc = T(acc + d.trace[i]);
896 epochs.push_back(acc);
897 }
898 return infer::infer_nhpp_ks<T>(epochs, epochs.back());
899}
900
901} // namespace lang
902} // namespace line
903
904#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:39
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:45
UnsupportedError(const std::string &what)
Definition error.h:51
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)
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:555
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:610
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 (D0,D1) pair that reaches sn.proc.
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
std::vector< T > dtmc_solve(const Matrix< T > &P)
Stationary distribution of a stochastic matrix P.
Definition dtmc_solve.h:106
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
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(),...
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:759
std::vector< T > params
Constructor arguments, in MATLAB getParam order.
Definition lang_types.h:734
std::vector< T > trace
Replayer / Trace samples; empty for every other type.
Definition lang_types.h:736
bool is_prior() const
Definition lang_types.h:776
static constexpr double FineTol
Definition lang_types.h:668
static constexpr double CoarseTol
Definition lang_types.h:669
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