LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mg1_ps.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_API_QSYS_QSYS_MG1_PS_H
6#define LINE_API_QSYS_QSYS_MG1_PS_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * Sojourn-time distribution of the M/G/1 processor-sharing queue.
12 *
13 * Port of matlab/src/api/qsys/qsys_mg1_ps.m (twins `Qsys_mg1_ps.java`,
14 * `python/line_solver/api/qsys/mg1ps.py`). MATLAB is the reference.
15 *
16 * Jobs arrive Poisson at rate lambda at one egalitarian processor-sharing
17 * server whose service requirement has LST bhat(tau) and mean m1. Writing V(x)
18 * for the sojourn of a tagged job of requirement x and rho = lambda m1 < 1, Ott
19 * (1984) and Yashkov (1983) give
20 *
21 * E[exp(-s V(x))] = (1 - rho) / D(s,x),
22 *
23 * D(s,x) being the inverse Laplace transform, evaluated at x, of
24 *
25 * f(tau;s) = [ (1-rho) tau^2 - (1-rho) lambda (1-bhat(tau)) tau
26 * + s rho tau - s lambda (1-bhat(tau)) ]
27 * / [ tau^2 (tau - s - lambda (1-bhat(tau))) ].
28 *
29 * The transform is exact but IMPLICIT: f has to be inverted in tau. For
30 * phase-type service f is a proper rational function of tau, the double pole at
31 * the origin cancels, and D(s,x) comes out in closed form as a finite sum of
32 * residues -- so the M/PH/1-PS queue, hence every service law that can be
33 * fitted phase-type, is exactly solvable. For a transform supplied as a
34 * callback, f is inverted numerically on a Bromwich contour placed to the right
35 * of the dominant singularity tau*(s), the unique root of
36 * tau = s + lambda (1 - bhat(tau)) in the right half plane, which the fixed
37 * point of that equation reaches at geometric rate rho.
38 *
39 * THE CONDITIONAL SOJOURN IS ATOMIC ON THE LATTICE t = (k+1) x, and that is not
40 * a numerical artifact. Processor sharing gives every job in the system the
41 * same service rate, so if the k jobs present on arrival all outlive the tagged
42 * job and nothing arrives, the sojourn is exactly (k+1) x. The k = 0 atom,
43 * (1-rho) exp(-lambda x), is the probability of finding the system empty and
44 * sharing it with nobody, and it is the only one that stays exact for general
45 * service. It is removed before inverting in s; the remaining atoms make the
46 * conditional CDF JUMP and leave no density, so the density is reported as NaN
47 * on the lattice rather than as the finite garbage a smooth inversion returns
48 * there.
49 *
50 * TWO SUBSTITUTIONS FOR MATLAB, both self-contained rather than approximate:
51 *
52 * 1. `roots(P)` is computed by Durand-Kerner rather than by the eigenvalues of
53 * the companion matrix, so this header does not require LAPACK. The
54 * polynomial has degree n+1 in the phase count, which is small, and the
55 * iteration converges to machine precision on it; the roots are a SET, and
56 * the residue sum that consumes them does not depend on their order. The
57 * same routine supplies `eig(T)` for the quadrature range, as the roots of
58 * the characteristic polynomial det(tau I - T) that Faddeev-LeVerrier
59 * already produces.
60 * 2. The Golub-Welsch call behind the panelled quadrature is the tree's own
61 * `gauss_legendre`, which computes the same Legendre nodes by Newton. The
62 * reference notes that the rule is fixed so that every codebase shares it;
63 * the nodes agree to machine precision, so they do.
64 *
65 * ARITHMETIC: double. The inversion, the root finding and the contour are all
66 * inherently floating point, and the reference's own tolerances are absolute in
67 * double.
68 */
69
70#include <algorithm>
71#include <cmath>
72#include <complex>
73#include <cstddef>
74#include <functional>
75#include <limits>
76#include <vector>
77
79#include "line/num/number.h"
80#include "line/util/error.h"
81#include "line/util/expm.h"
82#include "line/util/lu.h"
83#include "line/util/matrix.h"
84
85namespace line {
86namespace qsys {
87
88using Cplx = std::complex<double>;
89
90/** Options of `qsys_mg1_ps`, mirroring the reference's name-value pairs. */
92 std::vector<double> x; ///< service requirements to condition on
93 std::vector<double> s; ///< transform arguments to tabulate the LST at
94 std::vector<double> t; ///< times to evaluate the sojourn distribution at
95 std::size_t nterms = 41; ///< function evaluations per numerical inversion, ODD
96 /** Service density, needed to remove the conditioning on a callback path. */
97 std::function<double(double)> pdf;
98};
99
100/** Everything `qsys_mg1_ps` returns. */
102 double rho = 0.0;
103 double m1 = 0.0;
104 double m2 = std::numeric_limits<double>::quiet_NaN();
105 std::function<Cplx(Cplx, double)> lstCond; ///< (s,x) -> E[exp(-s V(x))]
106 std::function<Cplx(Cplx, double)> lstExcess; ///< (s,x) -> E[exp(-s (V(x)-x))]
107 std::function<Cplx(Cplx)> lstUncond; ///< s -> E[exp(-s V)]
108 std::function<Cplx(Cplx)> dominantRoot; ///< s -> tau*(s)
109 std::vector<double> x, s, t;
110 Matrix<double> lstCondVal; ///< (|x| x |s|)
111 std::vector<double> lstUncondVal;
112 std::vector<double> atomCond; ///< (1-rho) exp(-lambda x), at t = x
113 double atomUncond = 0.0; ///< (1-rho) bhat(lambda)
114 std::vector<double> meanCond, m2Cond, varCond;
115 double meanUncond = 0.0;
116 double m2Uncond = std::numeric_limits<double>::quiet_NaN();
117 double varUncond = std::numeric_limits<double>::quiet_NaN();
118 Matrix<double> pdfCond, cdfCond; ///< (|x| x |t|)
119 std::vector<double> pdfUncond, cdfUncond;
120};
121
122namespace mg1psdetail {
123
124/** Polynomial product, highest degree first, as MATLAB's `conv`. */
125inline std::vector<double> conv(const std::vector<double>& a, const std::vector<double>& b) {
126 if (a.empty() || b.empty()) return std::vector<double>();
127 std::vector<double> c(a.size() + b.size() - 1, 0.0);
128 for (std::size_t i = 0; i < a.size(); ++i)
129 for (std::size_t j = 0; j < b.size(); ++j) c[i + j] += a[i] * b[j];
130 return c;
131}
132
133/** Horner evaluation of a polynomial given highest degree first. */
134template <class C, class U>
135U polyval(const std::vector<C>& p, const U& z) {
136 U v = U(0.0);
137 for (std::size_t i = 0; i < p.size(); ++i) v = v * z + U(p[i]);
138 return v;
139}
140
141/** Derivative of a polynomial given highest degree first. */
142inline std::vector<double> polyder(const std::vector<double>& p) {
143 if (p.size() <= 1) return std::vector<double>(1, 0.0);
144 const std::size_t n = p.size() - 1;
145 std::vector<double> d(n, 0.0);
146 for (std::size_t i = 0; i < n; ++i) d[i] = p[i] * static_cast<double>(n - i);
147 return d;
148}
149
150/**
151 * Roots of a polynomial by Durand-Kerner.
152 *
153 * MATLAB uses the companion-matrix eigenvalues; this is LAPACK-free and exact
154 * to machine precision at the degrees involved (one more than the phase count).
155 * The roots are a SET and every consumer here is symmetric in them.
156 */
157inline std::vector<Cplx> poly_roots(const std::vector<double>& p_in) {
158 std::vector<double> p = p_in;
159 // Strip leading zeros: they are not roots, they are a lower degree.
160 std::size_t lead = 0;
161 while (lead + 1 < p.size() && p[lead] == 0.0) ++lead;
162 p.erase(p.begin(), p.begin() + static_cast<long>(lead));
163 if (p.size() <= 1) return std::vector<Cplx>();
164 const std::size_t n = p.size() - 1;
165 std::vector<double> mon(p.size());
166 for (std::size_t i = 0; i < p.size(); ++i) mon[i] = p[i] / p[0];
167
168 // The classic spiral start, off the real axis so a real polynomial's
169 // iterates do not collapse onto it.
170 std::vector<Cplx> r(n);
171 const Cplx seed(0.4, 0.9);
172 Cplx pw(1.0, 0.0);
173 for (std::size_t i = 0; i < n; ++i) {
174 r[i] = pw;
175 pw *= seed;
176 }
177 for (int it = 0; it < 1000; ++it) {
178 double move = 0.0;
179 for (std::size_t i = 0; i < n; ++i) {
180 Cplx den(1.0, 0.0);
181 for (std::size_t j = 0; j < n; ++j)
182 if (j != i) den *= (r[i] - r[j]);
183 if (std::abs(den) < 1e-300) continue;
184 const Cplx step = polyval(mon, r[i]) / den;
185 r[i] -= step;
186 move = std::max(move, std::abs(step));
187 }
188 if (move < 1e-14) break;
189 }
190 return r;
191}
192
193/**
194 * Matrix exponential of a COMPLEX matrix, by scaling and squaring with a
195 * Taylor series.
196 *
197 * `util/expm.h` is templated on `num_traits`, which has no complex
198 * instantiation, so the repeated-pole branch below needs its own. Scaling to a
199 * norm under 1/2 makes the truncated series converge to machine precision in
200 * around twenty terms, and the squaring recovers the full argument.
201 */
202inline Matrix<Cplx> expm_cplx(const Matrix<Cplx>& A) {
203 const std::size_t n = A.rows();
204 double nrm = 0.0;
205 for (std::size_t i = 0; i < n; ++i) {
206 double row = 0.0;
207 for (std::size_t j = 0; j < n; ++j) row += std::abs(A(i, j));
208 nrm = std::max(nrm, row);
209 }
210 int sq = 0;
211 while (nrm > 0.5) {
212 nrm /= 2.0;
213 ++sq;
214 }
215 const double sc = std::pow(2.0, -static_cast<double>(sq));
216 Matrix<Cplx> B(n, n, Cplx(0.0, 0.0)), E(n, n, Cplx(0.0, 0.0)), Tk(n, n, Cplx(0.0, 0.0));
217 for (std::size_t i = 0; i < n; ++i) {
218 E(i, i) = Cplx(1.0, 0.0);
219 Tk(i, i) = Cplx(1.0, 0.0);
220 for (std::size_t j = 0; j < n; ++j) B(i, j) = A(i, j) * sc;
221 }
222 for (int k = 1; k <= 30; ++k) {
223 Matrix<Cplx> P(n, n, Cplx(0.0, 0.0));
224 for (std::size_t i = 0; i < n; ++i)
225 for (std::size_t j = 0; j < n; ++j) {
226 Cplx acc(0.0, 0.0);
227 for (std::size_t l = 0; l < n; ++l) acc += Tk(i, l) * B(l, j);
228 P(i, j) = acc / static_cast<double>(k);
229 }
230 Tk = P;
231 for (std::size_t i = 0; i < n; ++i)
232 for (std::size_t j = 0; j < n; ++j) E(i, j) += Tk(i, j);
233 }
234 for (int t = 0; t < sq; ++t) {
235 Matrix<Cplx> P(n, n, Cplx(0.0, 0.0));
236 for (std::size_t i = 0; i < n; ++i)
237 for (std::size_t j = 0; j < n; ++j) {
238 Cplx acc(0.0, 0.0);
239 for (std::size_t l = 0; l < n; ++l) acc += E(i, l) * E(l, j);
240 P(i, j) = acc;
241 }
242 E = P;
243 }
244 return E;
245}
246
247/**
248 * Abate-Whitt Euler inversion, symmetrized so that a complex-valued time
249 * function is handled as well as a real-valued one.
250 */
251template <class Fn>
252Cplx ilt(Fn fun, double t, std::size_t nterms) {
253 const long ne = static_cast<long>((nterms - 1) / 2);
254 std::vector<double> eta(static_cast<std::size_t>(2 * ne + 1), 0.0);
255 eta[0] = 0.5;
256 for (long i = 1; i <= ne; ++i) eta[static_cast<std::size_t>(i)] = 1.0;
257 eta[static_cast<std::size_t>(2 * ne)] = std::pow(2.0, -static_cast<double>(ne));
258 for (long k = 1; k <= ne - 1; ++k)
259 eta[static_cast<std::size_t>(2 * ne - k)] =
260 eta[static_cast<std::size_t>(2 * ne - k + 1)] +
261 std::exp(std::lgamma(static_cast<double>(ne) + 1.0) -
262 static_cast<double>(ne) * std::log(2.0) -
263 std::lgamma(static_cast<double>(k) + 1.0) -
264 std::lgamma(static_cast<double>(ne - k) + 1.0));
265 Cplx g(0.0, 0.0);
266 const double pref = std::pow(10.0, static_cast<double>(ne) / 3.0);
267 for (long k = 0; k <= 2 * ne; ++k) {
268 const Cplx beta(static_cast<double>(ne) * std::log(10.0) / 3.0,
269 M_PI * static_cast<double>(k));
270 const double sgn = (k % 2 == 0) ? 1.0 : -1.0;
271 const double e = pref * sgn * eta[static_cast<std::size_t>(k)];
272 const Cplx bj = beta / t;
273 g += 0.5 * e * (fun(bj) + fun(std::conj(bj)));
274 }
275 return g / t;
276}
277
278/**
279 * The panelled Gauss-Legendre rule the reference uses to remove the
280 * conditioning: eight panels growing geometrically toward ymax, so that both
281 * ends of an exponentially decaying density are resolved, times a 32-point rule.
282 * The rule is FIXED, so it is the same in every codebase.
283 */
284inline void quad_nodes(double ymax, std::vector<double>* y, std::vector<double>* w,
285 std::size_t npanel = 8, std::size_t ng = 32) {
286 y->clear();
287 w->clear();
288 std::vector<double> edges;
289 edges.push_back(0.0);
290 for (long k = -static_cast<long>(npanel); k <= 0; ++k)
291 edges.push_back(ymax * std::pow(2.0, static_cast<double>(k)));
292 for (std::size_t k = 0; k + 1 < edges.size(); ++k) {
293 std::vector<double> xg, wg;
294 pfqn::detail::gauss_legendre<double>(ng, edges[k], edges[k + 1], xg, wg);
295 for (std::size_t i = 0; i < xg.size(); ++i) {
296 y->push_back(xg[i]);
297 w->push_back(wg[i]);
298 }
299 }
300}
301
302/**
303 * Second moment from the transform's curvature at the origin.
304 *
305 * The stencil is ONE-SIDED so the transform is never sampled at a negative
306 * argument, where it need not converge; the step is scaled by the conditional
307 * mean but capped by the unconditional one so it stays finite as x -> 0; and
308 * Richardson extrapolation over h and h/2 removes the leading truncation.
309 */
310template <class Fn>
311double second_moment(Fn lst, double meanref, double meanscale) {
312 if (!(meanref > 0.0)) return 0.0;
313 const double h = std::min(1e-2 / meanref, 1.0 / meanscale);
314 auto d2 = [&lst](double hh) {
315 double f[6];
316 for (int j = 0; j < 6; ++j) f[j] = std::real(lst(Cplx(static_cast<double>(j) * hh, 0.0)));
317 return (45.0 * f[0] - 154.0 * f[1] + 214.0 * f[2] - 156.0 * f[3] + 61.0 * f[4] -
318 10.0 * f[5]) /
319 (12.0 * hh * hh);
320 };
321 return (16.0 * d2(h / 2.0) - d2(h)) / 15.0;
322}
323
324/**
325 * Everything downstream of D(s,x): the transforms, the grids, the moments and
326 * the inversions, shared by the phase-type and the transform-handle forms.
327 *
328 * @param bhat service LST
329 * @param denom (s, x, &scale) -> val with D(s,x) = exp(scale x) val
330 * @param bpdf service density, empty when unavailable (hasPdf false)
331 * @param ymax range of the panelled quadrature removing the conditioning
332 * @param isPH phase-type path: the time grid is admissible
333 */
334inline Mg1PsResult mg1ps_finish(double lambda, double rho, double m1, double m2,
335 const std::function<Cplx(const Cplx&)>& bhat,
336 const std::function<Cplx(const Cplx&, double, double*)>& denom,
337 const std::function<double(double)>& bpdf, double ymax, bool isPH,
338 const Mg1PsOptions& opt) {
339 const bool hasPdf = static_cast<bool>(bpdf);
340 const double nan = std::numeric_limits<double>::quiet_NaN();
341
342 Mg1PsResult res;
343 res.rho = rho;
344 res.m1 = m1;
345 res.m2 = m2;
346 res.meanUncond = m1 / (1.0 - rho);
347 res.atomUncond = (1.0 - rho) * bhat(Cplx(lambda, 0.0)).real();
348
349 auto lst_cond = [denom, rho](const Cplx& s, double x, bool excess) {
350 if (x == 0.0) return Cplx(1.0, 0.0);
351 double scale = 0.0;
352 const Cplx val = denom(s, x, &scale);
353 const Cplx expo = (excess ? s : Cplx(0.0, 0.0)) - Cplx(scale, 0.0);
354 return Cplx(1.0 - rho, 0.0) * std::exp(expo * x) / val;
355 };
356 res.lstCond = [lst_cond](Cplx s, double x) { return lst_cond(s, x, false); };
357 res.lstExcess = [lst_cond](Cplx s, double x) { return lst_cond(s, x, true); };
358 res.dominantRoot = [lambda, bhat, rho](Cplx s) {
359 Cplx tau = s;
360 const int maxit = std::max(200, static_cast<int>(std::ceil(
361 3.0 * std::log(1e-15) / std::log(std::max(rho, 1e-3)))));
362 for (int it = 0; it < maxit; ++it) {
363 const Cplx nx = s + Cplx(lambda, 0.0) * (Cplx(1.0, 0.0) - bhat(tau));
364 if (std::abs(nx - tau) <= 1e-14 * std::max(1.0, std::abs(nx))) return nx;
365 tau = nx;
366 }
367 throw InputError("qsys_mg1_ps: the dominant root iteration did not converge");
368 };
369
370 // The quadrature that removes the conditioning. The nodes do not move with
371 // s, so the service density is sampled once.
372 std::vector<double> yq, wq, bq;
373 quad_nodes(ymax, &yq, &wq);
374 if (hasPdf) {
375 bq.resize(yq.size());
376 for (std::size_t k = 0; k < yq.size(); ++k) bq[k] = bpdf(yq[k]);
377 }
378 res.lstUncond = [lst_cond, yq, wq, bq](Cplx s) {
379 if (bq.empty())
380 throw InputError("qsys_mg1_ps: the service density is required to remove the "
381 "conditioning, pass it as the pdf option");
382 Cplx v(0.0, 0.0);
383 for (std::size_t k = 0; k < yq.size(); ++k)
384 v += wq[k] * bq[k] * lst_cond(s, yq[k], false);
385 return v;
386 };
387
388 res.x = opt.x;
389 res.s = opt.s;
390 res.t = opt.t;
391 res.lstCondVal = Matrix<double>(res.x.size(), res.s.size(), 0.0);
392 for (std::size_t i = 0; i < res.x.size(); ++i)
393 for (std::size_t j = 0; j < res.s.size(); ++j)
394 res.lstCondVal(i, j) = res.lstCond(Cplx(res.s[j], 0.0), res.x[i]).real();
395 res.lstUncondVal.assign(res.s.size(), nan);
396 if (hasPdf)
397 for (std::size_t j = 0; j < res.s.size(); ++j)
398 res.lstUncondVal[j] = res.lstUncond(Cplx(res.s[j], 0.0)).real();
399
400 res.atomCond.assign(res.x.size(), 0.0);
401 res.meanCond.assign(res.x.size(), 0.0);
402 res.m2Cond.assign(res.x.size(), 0.0);
403 res.varCond.assign(res.x.size(), 0.0);
404 for (std::size_t i = 0; i < res.x.size(); ++i) {
405 res.atomCond[i] = (1.0 - rho) * std::exp(-lambda * res.x[i]);
406 res.meanCond[i] = res.x[i] / (1.0 - rho);
407 const double xi = res.x[i];
408 res.m2Cond[i] = second_moment([&res, xi](Cplx u) { return res.lstCond(u, xi); },
409 res.meanCond[i], res.meanUncond);
410 res.varCond[i] = res.m2Cond[i] - res.meanCond[i] * res.meanCond[i];
411 }
412
413 // The unconditional second moment is integrated from the conditional one,
414 // which is far better conditioned than differentiating the quadrature that
415 // removed the conditioning.
416 res.m2Uncond = nan;
417 res.varUncond = nan;
418 if (hasPdf) {
419 double m2u = 0.0;
420 for (std::size_t k = 0; k < yq.size(); ++k) {
421 const double yk = yq[k];
422 m2u += wq[k] * bq[k] *
423 second_moment([&res, yk](Cplx u) { return res.lstCond(u, yk); },
424 yk / (1.0 - rho), res.meanUncond);
425 }
426 res.m2Uncond = m2u;
427 res.varUncond = m2u - res.meanUncond * res.meanUncond;
428 }
429
430 if (!res.t.empty() && !isPH)
431 throw InputError("qsys_mg1_ps: the sojourn time distribution needs phase-type service, "
432 "since inverting a numerically inverted transform is unstable in double "
433 "precision; with a transform handle only the LST and its moments are "
434 "available");
435 res.pdfCond = Matrix<double>(res.x.size(), res.t.size(), 0.0);
436 res.cdfCond = Matrix<double>(res.x.size(), res.t.size(), 0.0);
437 for (std::size_t i = 0; i < res.x.size(); ++i) {
438 const double atom = res.atomCond[i], xi = res.x[i];
439 // V(x) >= x with an atom at x, so what is inverted is the EXCESS
440 // V(x)-x net of its atom.
441 auto gpdf = [&res, xi, atom](Cplx u) { return res.lstExcess(u, xi) - Cplx(atom, 0.0); };
442 auto gcdf = [gpdf](Cplx u) { return gpdf(u) / u; };
443 for (std::size_t j = 0; j < res.t.size(); ++j) {
444 const double tj = res.t[j];
445 if (tj < xi) continue;
446 if (tj == xi) {
447 res.cdfCond(i, j) = atom;
448 continue;
449 }
450 res.cdfCond(i, j) = ilt(gcdf, tj - xi, opt.nterms).real() + atom;
451 const double ratio = tj / xi;
452 if (std::fabs(ratio - std::floor(ratio + 0.5)) < 1e-9)
453 res.pdfCond(i, j) = std::numeric_limits<double>::quiet_NaN();
454 else
455 res.pdfCond(i, j) = ilt(gpdf, tj - xi, opt.nterms).real();
456 }
457 }
458
459 res.pdfUncond.assign(res.t.size(), nan);
460 res.cdfUncond.assign(res.t.size(), nan);
461 if (hasPdf)
462 for (std::size_t j = 0; j < res.t.size(); ++j) {
463 if (res.t[j] <= 0.0) {
464 res.pdfUncond[j] = 0.0;
465 res.cdfUncond[j] = 0.0;
466 continue;
467 }
468 res.pdfUncond[j] = ilt(res.lstUncond, res.t[j], opt.nterms).real();
469 res.cdfUncond[j] =
470 ilt([&res](Cplx u) { return res.lstUncond(u) / u; }, res.t[j], opt.nterms).real();
471 }
472 return res;
473}
474
475} // namespace mg1psdetail
476
477/**
478 * @brief Sojourn-time distribution of the M/G/1 processor-sharing queue.
479 *
480 * @param lambda arrival rate, finite and positive
481 * @param alpha phase-type initial probability vector
482 * @param Tmat phase-type subgenerator
483 * @param opt grids and tuning
484 */
485inline Mg1PsResult qsys_mg1_ps(double lambda, const std::vector<double>& alpha,
486 const Matrix<double>& Tmat, const Mg1PsOptions& opt) {
487 using namespace mg1psdetail;
488 if (!(lambda > 0.0) || !std::isfinite(lambda))
489 throw InputError("qsys_mg1_ps: lambda must be a finite positive scalar");
490 if (opt.nterms % 2 == 0 || opt.nterms < 11)
491 throw InputError("qsys_mg1_ps: nterms must be an odd integer of at least 11");
492
493 const std::size_t n = alpha.size();
494 if (n == 0 || Tmat.rows() != n || Tmat.cols() != n)
495 throw InputError("qsys_mg1_ps: T must be n x n to match alpha");
496 double asum = 0.0;
497 for (std::size_t i = 0; i < n; ++i) {
498 if (alpha[i] < -1e-12) throw InputError("qsys_mg1_ps: alpha must be a probability vector");
499 asum += alpha[i];
500 }
501 if (std::fabs(asum - 1.0) > 1e-8)
502 throw InputError("qsys_mg1_ps: alpha must be a probability vector");
503 std::vector<double> exitrate(n, 0.0);
504 for (std::size_t i = 0; i < n; ++i) {
505 double row = 0.0;
506 for (std::size_t j = 0; j < n; ++j) row += Tmat(i, j);
507 exitrate[i] = -row;
508 if (exitrate[i] < -1e-10 || Tmat(i, i) >= 0.0)
509 throw InputError("qsys_mg1_ps: T must be a proper phase-type subgenerator");
510 }
511
512 // Moments, by solving with (-T) rather than forming its inverse.
513 Matrix<double> negT(n, n, 0.0);
514 for (std::size_t i = 0; i < n; ++i)
515 for (std::size_t j = 0; j < n; ++j) negT(i, j) = -Tmat(i, j);
516 const std::vector<double> ones(n, 1.0);
517 const std::vector<double> u1 = solve(negT, ones);
518 const std::vector<double> u2 = solve(negT, u1);
519 double m1 = 0.0, m2 = 0.0;
520 for (std::size_t i = 0; i < n; ++i) {
521 m1 += alpha[i] * u1[i];
522 m2 += 2.0 * alpha[i] * u2[i];
523 }
524 const double rho = lambda * m1;
525 if (rho >= 1.0)
526 throw InputError("qsys_mg1_ps: the system is unstable, the utilization is at least one");
527
528 // Faddeev-LeVerrier gives det(tau I - T) and the adjugate in one sweep, so
529 // bhat(tau) = nb(tau)/db(tau) as polynomials of degree n-1 and n.
530 std::vector<double> db(n + 1, 0.0), nb(n, 0.0);
531 db[0] = 1.0;
532 Matrix<double> Mk(n, n, 0.0);
533 for (std::size_t i = 0; i < n; ++i) Mk(i, i) = 1.0;
534 for (std::size_t k = 1; k <= n; ++k) {
535 double v = 0.0;
536 for (std::size_t i = 0; i < n; ++i) {
537 double row = 0.0;
538 for (std::size_t j = 0; j < n; ++j) row += Mk(i, j) * exitrate[j];
539 v += alpha[i] * row;
540 }
541 nb[k - 1] = v;
542 Matrix<double> TM(n, n, 0.0);
543 for (std::size_t i = 0; i < n; ++i)
544 for (std::size_t j = 0; j < n; ++j) {
545 double acc = 0.0;
546 for (std::size_t l = 0; l < n; ++l) acc += Tmat(i, l) * Mk(l, j);
547 TM(i, j) = acc;
548 }
549 double tr = 0.0;
550 for (std::size_t i = 0; i < n; ++i) tr += TM(i, i);
551 db[k] = -tr / static_cast<double>(k);
552 Mk = TM;
553 for (std::size_t i = 0; i < n; ++i) Mk(i, i) += db[k];
554 }
555
556 auto bhat = [db, nb](const Cplx& tau) { return polyval(nb, tau) / polyval(db, tau); };
557 auto bpdf = [&Tmat, &alpha, &exitrate, n](double y) {
558 Matrix<double> E = Tmat;
559 for (std::size_t i = 0; i < n; ++i)
560 for (std::size_t j = 0; j < n; ++j) E(i, j) = Tmat(i, j) * y;
561 const Matrix<double> Ey = expm(E);
562 double v = 0.0;
563 for (std::size_t i = 0; i < n; ++i) {
564 double row = 0.0;
565 for (std::size_t j = 0; j < n; ++j) row += Ey(i, j) * exitrate[j];
566 v += alpha[i] * row;
567 }
568 return v;
569 };
570
571 // D(s,x) = exp(scale x) val, exactly, from the residues of f(tau;s), whose
572 // double pole at the origin cancels. The scale is carried separately so
573 // that neither factor overflows at large s.
574 auto denom = [db, nb, n, lambda, rho](const Cplx& s, double x, double* scale_out) {
575 std::vector<double> dm(db.size(), 0.0);
576 dm[0] = db[0];
577 for (std::size_t i = 1; i < db.size(); ++i) dm[i] = db[i] - nb[i - 1];
578
579 // These carry s, so they are complex; the polynomial helpers are
580 // written over a scalar type for exactly that reason.
581 std::vector<Cplx> Pc, Ac;
582 {
583 std::vector<Cplx> lin;
584 lin.push_back(Cplx(1.0, 0.0));
585 lin.push_back(-(s + Cplx(lambda, 0.0)));
586 std::vector<Cplx> dbz(db.size());
587 for (std::size_t i = 0; i < db.size(); ++i) dbz[i] = Cplx(db[i], 0.0);
588 Pc.assign(lin.size() + dbz.size() - 1, Cplx(0.0, 0.0));
589 for (std::size_t i = 0; i < lin.size(); ++i)
590 for (std::size_t j = 0; j < dbz.size(); ++j) Pc[i + j] += lin[i] * dbz[j];
591 // + [0, 0, lambda*nb]
592 for (std::size_t i = 0; i < nb.size(); ++i)
593 Pc[Pc.size() - nb.size() + i] += Cplx(lambda * nb[i], 0.0);
594
595 std::vector<Cplx> q1;
596 q1.push_back(Cplx(1.0 - rho, 0.0));
597 q1.push_back(s * Cplx(rho, 0.0));
598 q1.push_back(Cplx(0.0, 0.0));
599 std::vector<Cplx> t1(q1.size() + dbz.size() - 1, Cplx(0.0, 0.0));
600 for (std::size_t i = 0; i < q1.size(); ++i)
601 for (std::size_t j = 0; j < dbz.size(); ++j) t1[i + j] += q1[i] * dbz[j];
602
603 std::vector<Cplx> q2;
604 q2.push_back(Cplx(1.0 - rho, 0.0));
605 q2.push_back(s);
606 std::vector<Cplx> ldm(dm.size());
607 for (std::size_t i = 0; i < dm.size(); ++i) ldm[i] = Cplx(lambda * dm[i], 0.0);
608 std::vector<Cplx> t2(q2.size() + ldm.size() - 1, Cplx(0.0, 0.0));
609 for (std::size_t i = 0; i < q2.size(); ++i)
610 for (std::size_t j = 0; j < ldm.size(); ++j) t2[i + j] += q2[i] * ldm[j];
611
612 Ac.assign(t1.size(), Cplx(0.0, 0.0));
613 for (std::size_t i = 0; i < t1.size(); ++i) Ac[i] = t1[i];
614 for (std::size_t i = 0; i < t2.size(); ++i) Ac[Ac.size() - t2.size() + i] -= t2[i];
615 }
616 // The double pole at the origin must cancel: the two lowest
617 // coefficients of A are what would survive it.
618 double nrmA = 0.0;
619 for (std::size_t i = 0; i < Ac.size(); ++i) nrmA = std::max(nrmA, std::abs(Ac[i]));
620 const double tailA = std::abs(Ac[Ac.size() - 2]) + std::abs(Ac[Ac.size() - 1]);
621 if (tailA > 1e-6 * std::max(1.0, nrmA))
622 throw InputError("qsys_mg1_ps: the double pole at the origin did not cancel");
623 // `A(1:n+1)` in the reference: the first n+1 of the n+3 coefficients.
624 // The two dropped ones are the cancelled double pole, checked just
625 // above; taking n+2 instead silently keeps half of it and the LST comes
626 // back above one.
627 std::vector<Cplx> Ahat(Ac.begin(), Ac.begin() + static_cast<long>(n + 1));
628
629 // Durand-Kerner over the complex coefficients of P.
630 std::vector<Cplx> r;
631 {
632 std::vector<Cplx> mon(Pc.size());
633 for (std::size_t i = 0; i < Pc.size(); ++i) mon[i] = Pc[i] / Pc[0];
634 const std::size_t deg = mon.size() - 1;
635 r.assign(deg, Cplx(0.0, 0.0));
636 const Cplx seed(0.4, 0.9);
637 Cplx pw(1.0, 0.0);
638 for (std::size_t i = 0; i < deg; ++i) {
639 r[i] = pw;
640 pw *= seed;
641 }
642 for (int it = 0; it < 2000; ++it) {
643 double move = 0.0;
644 for (std::size_t i = 0; i < deg; ++i) {
645 Cplx den(1.0, 0.0);
646 for (std::size_t j = 0; j < deg; ++j)
647 if (j != i) den *= (r[i] - r[j]);
648 if (std::abs(den) < 1e-300) continue;
649 const Cplx step = polyval(mon, r[i]) / den;
650 r[i] -= step;
651 move = std::max(move, std::abs(step));
652 }
653 if (move < 1e-14) break;
654 }
655 }
656 double scale = -std::numeric_limits<double>::infinity();
657 for (std::size_t i = 0; i < r.size(); ++i) scale = std::max(scale, r[i].real());
658 *scale_out = scale;
659
660 double maxr = 0.0, minsep = std::numeric_limits<double>::infinity();
661 for (std::size_t i = 0; i < r.size(); ++i) maxr = std::max(maxr, std::abs(r[i]));
662 for (std::size_t i = 0; i < r.size(); ++i)
663 for (std::size_t j = 0; j < r.size(); ++j)
664 if (i != j) minsep = std::min(minsep, std::abs(r[i] - r[j]));
665
666 if (r.size() >= 2 && minsep > 1e-7 * std::max(1.0, maxr)) {
667 std::vector<Cplx> dP(Pc.size() - 1, Cplx(0.0, 0.0));
668 for (std::size_t i = 0; i + 1 < Pc.size(); ++i)
669 dP[i] = Pc[i] * static_cast<double>(Pc.size() - 1 - i);
670 Cplx val(0.0, 0.0);
671 for (std::size_t i = 0; i < r.size(); ++i) {
672 const Cplx coef = polyval(Ahat, r[i]) / polyval(dP, r[i]);
673 val += std::exp((r[i] - Cplx(scale, 0.0)) * x) * coef;
674 }
675 return val;
676 }
677 // Repeated poles: the companion realization of Ahat/P, exactly as the
678 // reference falls back to. A residue sum is undefined there; the
679 // matrix exponential is not.
680 const std::size_t d = Pc.size() - 1;
681 Matrix<Cplx> Acomp(d, d, Cplx(0.0, 0.0));
682 for (std::size_t j = 0; j < d; ++j) Acomp(0, j) = -Pc[j + 1] / Pc[0];
683 for (std::size_t i = 1; i < d; ++i) Acomp(i, i - 1) = Cplx(1.0, 0.0);
684 for (std::size_t i = 0; i < d; ++i) Acomp(i, i) -= Cplx(scale, 0.0);
685 Matrix<Cplx> Ax(d, d, Cplx(0.0, 0.0));
686 for (std::size_t i = 0; i < d; ++i)
687 for (std::size_t j = 0; j < d; ++j) Ax(i, j) = Acomp(i, j) * x;
688 const Matrix<Cplx> E = expm_cplx(Ax);
689 Cplx val(0.0, 0.0);
690 for (std::size_t j = 0; j < d && j < Ahat.size(); ++j) val += (Ahat[j] / Pc[0]) * E(j, 0);
691 return val;
692 };
693
694 // ymax for the quadrature that removes the conditioning: the reference's
695 // max(40 m1, 40 / min(-real(eig(T)))). The eigenvalues of T are the roots
696 // of det(tau I - T) = db(tau), which Faddeev-LeVerrier has just produced,
697 // so they come from the same Durand-Kerner the residue sum uses.
698 double mineig = std::numeric_limits<double>::infinity();
699 {
700 const std::vector<Cplx> ev = poly_roots(db);
701 for (std::size_t i = 0; i < ev.size(); ++i) mineig = std::min(mineig, -ev[i].real());
702 if (ev.empty() || !std::isfinite(mineig))
703 for (std::size_t i = 0; i < n; ++i) mineig = std::min(mineig, -Tmat(i, i));
704 }
705 const double ymax = std::max(40.0 * m1, 40.0 / std::max(mineig, 1e-12));
706
707 std::function<Cplx(const Cplx&, double, double*)> denom_fn = denom;
708 std::function<Cplx(const Cplx&)> bhat_fn = bhat;
709 std::function<double(double)> bpdf_fn = opt.pdf ? opt.pdf : std::function<double(double)>(bpdf);
710 return mg1psdetail::mg1ps_finish(lambda, rho, m1, m2, bhat_fn, denom_fn, bpdf_fn, ymax, true,
711 opt);
712}
713
714/**
715 * @brief Sojourn-time distribution of the M/G/1-PS queue for a service law
716 * given by its Laplace-Stieltjes transform, the reference's second form
717 * `qsys_mg1_ps(lambda, bhat, m1, ...)`.
718 *
719 * f(tau; s) is inverted in tau numerically on a Bromwich contour placed
720 * 0.25 (1-rho)/m1 to the right of the dominant root tau*(s). The service
721 * density `opt.pdf` is needed to remove the conditioning (lstUncond,
722 * lstUncondVal, m2Uncond, varUncond are NaN or unavailable without it), m2 is
723 * NaN, and a time grid `opt.t` is refused, as in the reference: inverting a
724 * numerically inverted transform is unstable in double precision.
725 *
726 * @param lambda arrival rate, finite and positive
727 * @param bhat service LST, accepting complex arguments
728 * @param m1 mean service time
729 * @param opt grids and tuning
730 */
731inline Mg1PsResult qsys_mg1_ps(double lambda, const std::function<Cplx(const Cplx&)>& bhat,
732 double m1, const Mg1PsOptions& opt) {
733 using namespace mg1psdetail;
734 if (!(lambda > 0.0) || !std::isfinite(lambda))
735 throw InputError("qsys_mg1_ps: lambda must be a finite positive scalar");
736 if (opt.nterms % 2 == 0 || opt.nterms < 11)
737 throw InputError("qsys_mg1_ps: nterms must be an odd integer of at least 11");
738 if (!bhat) throw InputError("qsys_mg1_ps: the service transform handle is empty");
739 if (!std::isfinite(m1) || m1 <= 0.0)
740 throw InputError("qsys_mg1_ps: the mean service time must be a finite positive scalar");
741 const double rho = lambda * m1;
742 if (rho >= 1.0)
743 throw InputError("qsys_mg1_ps: the system is unstable, the utilization is at least one");
744 if (!opt.t.empty())
745 throw InputError("qsys_mg1_ps: the sojourn time distribution needs phase-type service, "
746 "since inverting a numerically inverted transform is unstable in double "
747 "precision; with a transform handle only the LST and its moments are "
748 "available");
749
750 // the dominant root tau*(s), the reference's local_root
751 auto rootfun = [lambda, bhat, rho](const Cplx& s) {
752 Cplx tau = s;
753 const int maxit = std::max(200, static_cast<int>(std::ceil(
754 3.0 * std::log(1e-15) / std::log(std::max(rho, 1e-3)))));
755 for (int it = 0; it < maxit; ++it) {
756 const Cplx nx = s + Cplx(lambda, 0.0) * (Cplx(1.0, 0.0) - bhat(tau));
757 if (std::abs(nx - tau) <= 1e-14 * std::max(1.0, std::abs(nx))) return nx;
758 tau = nx;
759 }
760 throw InputError("qsys_mg1_ps: the dominant root iteration did not converge");
761 };
762 const double shift = 0.25 * (1.0 - rho) / m1;
763 const std::size_t nterms = opt.nterms;
764 // D(s,x) for a general service transform: f(tau;s) inverted in tau on a
765 // contour just to the right of the dominant singularity (local_denom_gen)
766 std::function<Cplx(const Cplx&, double, double*)> denom =
767 [lambda, rho, bhat, rootfun, shift, nterms](const Cplx& s, double x, double* scale_out) {
768 const double scale = rootfun(s).real() + shift;
769 *scale_out = scale;
770 auto f = [&](const Cplx& u) {
771 const Cplx tau = u + Cplx(scale, 0.0);
772 const Cplx bh = bhat(tau);
773 const Cplx one_m_bh = Cplx(1.0, 0.0) - bh;
774 const Cplx num = Cplx(1.0 - rho, 0.0) * tau * tau -
775 Cplx((1.0 - rho) * lambda, 0.0) * one_m_bh * tau +
776 s * Cplx(rho, 0.0) * tau - s * Cplx(lambda, 0.0) * one_m_bh;
777 return num / (tau * tau * (tau - s - Cplx(lambda, 0.0) * one_m_bh));
778 };
779 return ilt(f, x, nterms);
780 };
781 const double ymax = 60.0 * m1;
782 return mg1ps_finish(lambda, rho, m1, std::numeric_limits<double>::quiet_NaN(), bhat, denom,
783 opt.pdf, ymax, false, opt);
784}
785
786} // namespace qsys
787} // namespace line
788
789#endif // LINE_API_QSYS_QSYS_MG1_PS_H
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
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
Mg1PsResult qsys_mg1_ps(double lambda, const std::vector< double > &alpha, const Matrix< double > &Tmat, const Mg1PsOptions &opt)
Sojourn-time distribution of the M/G/1 processor-sharing queue.
std::complex< double > Cplx
Definition qsys_mg1_ps.h:88
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
std::vector< T > ones(std::size_t n)
Column vector of ones, the ubiquitous e in MAP algebra.
Definition linalg.h:104
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Options of qsys_mg1_ps, mirroring the reference's name-value pairs.
Definition qsys_mg1_ps.h:91
std::function< double(double)> pdf
Service density, needed to remove the conditioning on a callback path.
Definition qsys_mg1_ps.h:97
std::vector< double > s
transform arguments to tabulate the LST at
Definition qsys_mg1_ps.h:93
std::vector< double > t
times to evaluate the sojourn distribution at
Definition qsys_mg1_ps.h:94
std::size_t nterms
function evaluations per numerical inversion, ODD
Definition qsys_mg1_ps.h:95
std::vector< double > x
service requirements to condition on
Definition qsys_mg1_ps.h:92
Everything qsys_mg1_ps returns.
Matrix< double > lstCondVal
(|x| x |s|)
std::function< Cplx(Cplx)> lstUncond
s -> E[exp(-s V)]
std::vector< double > lstUncondVal
std::vector< double > meanCond
double atomUncond
(1-rho) bhat(lambda)
Matrix< double > cdfCond
(|x| x |t|)
std::function< Cplx(Cplx, double)> lstCond
(s,x) -> E[exp(-s V(x))]
std::function< Cplx(Cplx, double)> lstExcess
(s,x) -> E[exp(-s (V(x)-x))]
std::vector< double > cdfUncond
std::vector< double > m2Cond
std::vector< double > s
std::vector< double > varCond
std::vector< double > pdfUncond
std::vector< double > x
std::function< Cplx(Cplx)> dominantRoot
s -> tau*(s)
Matrix< double > pdfCond
std::vector< double > t
std::vector< double > atomCond
(1-rho) exp(-lambda x), at t = x