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.
57 * 2. The Golub-Welsch call behind the panelled quadrature is the tree's own
58 * `gauss_legendre`, which computes the same Legendre nodes by Newton. The
59 * reference notes that the rule is fixed so that every codebase shares it;
60 * the nodes agree to machine precision, so they do.
61 *
62 * ARITHMETIC: double. The inversion, the root finding and the contour are all
63 * inherently floating point, and the reference's own tolerances are absolute in
64 * double.
65 */
66
67#include <algorithm>
68#include <cmath>
69#include <complex>
70#include <cstddef>
71#include <functional>
72#include <limits>
73#include <vector>
74
76#include "line/num/number.h"
77#include "line/util/error.h"
78#include "line/util/expm.h"
79#include "line/util/lu.h"
80#include "line/util/matrix.h"
81
82namespace line {
83namespace qsys {
84
85using Cplx = std::complex<double>;
86
87/** Options of `qsys_mg1_ps`, mirroring the reference's name-value pairs. */
89 std::vector<double> x; ///< service requirements to condition on
90 std::vector<double> s; ///< transform arguments to tabulate the LST at
91 std::vector<double> t; ///< times to evaluate the sojourn distribution at
92 std::size_t nterms = 41; ///< function evaluations per numerical inversion, ODD
93 /** Service density, needed to remove the conditioning on a callback path. */
94 std::function<double(double)> pdf;
95};
96
97/** Everything `qsys_mg1_ps` returns. */
99 double rho = 0.0;
100 double m1 = 0.0;
101 double m2 = std::numeric_limits<double>::quiet_NaN();
102 std::function<Cplx(Cplx, double)> lstCond; ///< (s,x) -> E[exp(-s V(x))]
103 std::function<Cplx(Cplx, double)> lstExcess; ///< (s,x) -> E[exp(-s (V(x)-x))]
104 std::function<Cplx(Cplx)> lstUncond; ///< s -> E[exp(-s V)]
105 std::function<Cplx(Cplx)> dominantRoot; ///< s -> tau*(s)
106 std::vector<double> x, s, t;
107 Matrix<double> lstCondVal; ///< (|x| x |s|)
108 std::vector<double> lstUncondVal;
109 std::vector<double> atomCond; ///< (1-rho) exp(-lambda x), at t = x
110 double atomUncond = 0.0; ///< (1-rho) bhat(lambda)
111 std::vector<double> meanCond, m2Cond, varCond;
112 double meanUncond = 0.0;
113 double m2Uncond = std::numeric_limits<double>::quiet_NaN();
114 double varUncond = std::numeric_limits<double>::quiet_NaN();
115 Matrix<double> pdfCond, cdfCond; ///< (|x| x |t|)
116 std::vector<double> pdfUncond, cdfUncond;
117};
118
119namespace mg1psdetail {
120
121/** Polynomial product, highest degree first, as MATLAB's `conv`. */
122inline std::vector<double> conv(const std::vector<double>& a, const std::vector<double>& b) {
123 if (a.empty() || b.empty()) return std::vector<double>();
124 std::vector<double> c(a.size() + b.size() - 1, 0.0);
125 for (std::size_t i = 0; i < a.size(); ++i)
126 for (std::size_t j = 0; j < b.size(); ++j) c[i + j] += a[i] * b[j];
127 return c;
128}
129
130/** Horner evaluation of a polynomial given highest degree first. */
131template <class C, class U>
132U polyval(const std::vector<C>& p, const U& z) {
133 U v = U(0.0);
134 for (std::size_t i = 0; i < p.size(); ++i) v = v * z + U(p[i]);
135 return v;
136}
137
138/** Derivative of a polynomial given highest degree first. */
139inline std::vector<double> polyder(const std::vector<double>& p) {
140 if (p.size() <= 1) return std::vector<double>(1, 0.0);
141 const std::size_t n = p.size() - 1;
142 std::vector<double> d(n, 0.0);
143 for (std::size_t i = 0; i < n; ++i) d[i] = p[i] * static_cast<double>(n - i);
144 return d;
145}
146
147/**
148 * Roots of a polynomial by Durand-Kerner.
149 *
150 * MATLAB uses the companion-matrix eigenvalues; this is LAPACK-free and exact
151 * to machine precision at the degrees involved (one more than the phase count).
152 * The roots are a SET and every consumer here is symmetric in them.
153 */
154inline std::vector<Cplx> poly_roots(const std::vector<double>& p_in) {
155 std::vector<double> p = p_in;
156 // Strip leading zeros: they are not roots, they are a lower degree.
157 std::size_t lead = 0;
158 while (lead + 1 < p.size() && p[lead] == 0.0) ++lead;
159 p.erase(p.begin(), p.begin() + static_cast<long>(lead));
160 if (p.size() <= 1) return std::vector<Cplx>();
161 const std::size_t n = p.size() - 1;
162 std::vector<double> mon(p.size());
163 for (std::size_t i = 0; i < p.size(); ++i) mon[i] = p[i] / p[0];
164
165 // The classic spiral start, off the real axis so a real polynomial's
166 // iterates do not collapse onto it.
167 std::vector<Cplx> r(n);
168 const Cplx seed(0.4, 0.9);
169 Cplx pw(1.0, 0.0);
170 for (std::size_t i = 0; i < n; ++i) {
171 r[i] = pw;
172 pw *= seed;
173 }
174 for (int it = 0; it < 1000; ++it) {
175 double move = 0.0;
176 for (std::size_t i = 0; i < n; ++i) {
177 Cplx den(1.0, 0.0);
178 for (std::size_t j = 0; j < n; ++j)
179 if (j != i) den *= (r[i] - r[j]);
180 if (std::abs(den) < 1e-300) continue;
181 const Cplx step = polyval(mon, r[i]) / den;
182 r[i] -= step;
183 move = std::max(move, std::abs(step));
184 }
185 if (move < 1e-14) break;
186 }
187 return r;
188}
189
190/**
191 * Matrix exponential of a COMPLEX matrix, by scaling and squaring with a
192 * Taylor series.
193 *
194 * `util/expm.h` is templated on `num_traits`, which has no complex
195 * instantiation, so the repeated-pole branch below needs its own. Scaling to a
196 * norm under 1/2 makes the truncated series converge to machine precision in
197 * around twenty terms, and the squaring recovers the full argument.
198 */
199inline Matrix<Cplx> expm_cplx(const Matrix<Cplx>& A) {
200 const std::size_t n = A.rows();
201 double nrm = 0.0;
202 for (std::size_t i = 0; i < n; ++i) {
203 double row = 0.0;
204 for (std::size_t j = 0; j < n; ++j) row += std::abs(A(i, j));
205 nrm = std::max(nrm, row);
206 }
207 int sq = 0;
208 while (nrm > 0.5) {
209 nrm /= 2.0;
210 ++sq;
211 }
212 const double sc = std::pow(2.0, -static_cast<double>(sq));
213 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));
214 for (std::size_t i = 0; i < n; ++i) {
215 E(i, i) = Cplx(1.0, 0.0);
216 Tk(i, i) = Cplx(1.0, 0.0);
217 for (std::size_t j = 0; j < n; ++j) B(i, j) = A(i, j) * sc;
218 }
219 for (int k = 1; k <= 30; ++k) {
220 Matrix<Cplx> P(n, n, Cplx(0.0, 0.0));
221 for (std::size_t i = 0; i < n; ++i)
222 for (std::size_t j = 0; j < n; ++j) {
223 Cplx acc(0.0, 0.0);
224 for (std::size_t l = 0; l < n; ++l) acc += Tk(i, l) * B(l, j);
225 P(i, j) = acc / static_cast<double>(k);
226 }
227 Tk = P;
228 for (std::size_t i = 0; i < n; ++i)
229 for (std::size_t j = 0; j < n; ++j) E(i, j) += Tk(i, j);
230 }
231 for (int t = 0; t < sq; ++t) {
232 Matrix<Cplx> P(n, n, Cplx(0.0, 0.0));
233 for (std::size_t i = 0; i < n; ++i)
234 for (std::size_t j = 0; j < n; ++j) {
235 Cplx acc(0.0, 0.0);
236 for (std::size_t l = 0; l < n; ++l) acc += E(i, l) * E(l, j);
237 P(i, j) = acc;
238 }
239 E = P;
240 }
241 return E;
242}
243
244/**
245 * Abate-Whitt Euler inversion, symmetrized so that a complex-valued time
246 * function is handled as well as a real-valued one.
247 */
248template <class Fn>
249Cplx ilt(Fn fun, double t, std::size_t nterms) {
250 const long ne = static_cast<long>((nterms - 1) / 2);
251 std::vector<double> eta(static_cast<std::size_t>(2 * ne + 1), 0.0);
252 eta[0] = 0.5;
253 for (long i = 1; i <= ne; ++i) eta[static_cast<std::size_t>(i)] = 1.0;
254 eta[static_cast<std::size_t>(2 * ne)] = std::pow(2.0, -static_cast<double>(ne));
255 for (long k = 1; k <= ne - 1; ++k)
256 eta[static_cast<std::size_t>(2 * ne - k)] =
257 eta[static_cast<std::size_t>(2 * ne - k + 1)] +
258 std::exp(std::lgamma(static_cast<double>(ne) + 1.0) -
259 static_cast<double>(ne) * std::log(2.0) -
260 std::lgamma(static_cast<double>(k) + 1.0) -
261 std::lgamma(static_cast<double>(ne - k) + 1.0));
262 Cplx g(0.0, 0.0);
263 const double pref = std::pow(10.0, static_cast<double>(ne) / 3.0);
264 for (long k = 0; k <= 2 * ne; ++k) {
265 const Cplx beta(static_cast<double>(ne) * std::log(10.0) / 3.0,
266 M_PI * static_cast<double>(k));
267 const double sgn = (k % 2 == 0) ? 1.0 : -1.0;
268 const double e = pref * sgn * eta[static_cast<std::size_t>(k)];
269 const Cplx bj = beta / t;
270 g += 0.5 * e * (fun(bj) + fun(std::conj(bj)));
271 }
272 return g / t;
273}
274
275/**
276 * The panelled Gauss-Legendre rule the reference uses to remove the
277 * conditioning: eight panels growing geometrically toward ymax, so that both
278 * ends of an exponentially decaying density are resolved, times a 32-point rule.
279 * The rule is FIXED, so it is the same in every codebase.
280 */
281inline void quad_nodes(double ymax, std::vector<double>* y, std::vector<double>* w,
282 std::size_t npanel = 8, std::size_t ng = 32) {
283 y->clear();
284 w->clear();
285 std::vector<double> edges;
286 edges.push_back(0.0);
287 for (long k = -static_cast<long>(npanel); k <= 0; ++k)
288 edges.push_back(ymax * std::pow(2.0, static_cast<double>(k)));
289 for (std::size_t k = 0; k + 1 < edges.size(); ++k) {
290 std::vector<double> xg, wg;
291 pfqn::detail::gauss_legendre<double>(ng, edges[k], edges[k + 1], xg, wg);
292 for (std::size_t i = 0; i < xg.size(); ++i) {
293 y->push_back(xg[i]);
294 w->push_back(wg[i]);
295 }
296 }
297}
298
299/**
300 * Second moment from the transform's curvature at the origin.
301 *
302 * The stencil is ONE-SIDED so the transform is never sampled at a negative
303 * argument, where it need not converge; the step is scaled by the conditional
304 * mean but capped by the unconditional one so it stays finite as x -> 0; and
305 * Richardson extrapolation over h and h/2 removes the leading truncation.
306 */
307template <class Fn>
308double second_moment(Fn lst, double meanref, double meanscale) {
309 if (!(meanref > 0.0)) return 0.0;
310 const double h = std::min(1e-2 / meanref, 1.0 / meanscale);
311 auto d2 = [&lst](double hh) {
312 double f[6];
313 for (int j = 0; j < 6; ++j) f[j] = std::real(lst(Cplx(static_cast<double>(j) * hh, 0.0)));
314 return (45.0 * f[0] - 154.0 * f[1] + 214.0 * f[2] - 156.0 * f[3] + 61.0 * f[4] -
315 10.0 * f[5]) /
316 (12.0 * hh * hh);
317 };
318 return (16.0 * d2(h / 2.0) - d2(h)) / 15.0;
319}
320
321} // namespace mg1psdetail
322
323/**
324 * @brief Sojourn-time distribution of the M/G/1 processor-sharing queue.
325 *
326 * @param lambda arrival rate, finite and positive
327 * @param alpha phase-type initial probability vector
328 * @param Tmat phase-type subgenerator
329 * @param opt grids and tuning
330 */
331inline Mg1PsResult qsys_mg1_ps(double lambda, const std::vector<double>& alpha,
332 const Matrix<double>& Tmat, const Mg1PsOptions& opt) {
333 using namespace mg1psdetail;
334 if (!(lambda > 0.0) || !std::isfinite(lambda))
335 throw InputError("qsys_mg1_ps: lambda must be a finite positive scalar");
336 if (opt.nterms % 2 == 0 || opt.nterms < 11)
337 throw InputError("qsys_mg1_ps: nterms must be an odd integer of at least 11");
338
339 const std::size_t n = alpha.size();
340 if (n == 0 || Tmat.rows() != n || Tmat.cols() != n)
341 throw InputError("qsys_mg1_ps: T must be n x n to match alpha");
342 double asum = 0.0;
343 for (std::size_t i = 0; i < n; ++i) {
344 if (alpha[i] < -1e-12) throw InputError("qsys_mg1_ps: alpha must be a probability vector");
345 asum += alpha[i];
346 }
347 if (std::fabs(asum - 1.0) > 1e-8)
348 throw InputError("qsys_mg1_ps: alpha must be a probability vector");
349 std::vector<double> exitrate(n, 0.0);
350 for (std::size_t i = 0; i < n; ++i) {
351 double row = 0.0;
352 for (std::size_t j = 0; j < n; ++j) row += Tmat(i, j);
353 exitrate[i] = -row;
354 if (exitrate[i] < -1e-10 || Tmat(i, i) >= 0.0)
355 throw InputError("qsys_mg1_ps: T must be a proper phase-type subgenerator");
356 }
357
358 // Moments, by solving with (-T) rather than forming its inverse.
359 Matrix<double> negT(n, n, 0.0);
360 for (std::size_t i = 0; i < n; ++i)
361 for (std::size_t j = 0; j < n; ++j) negT(i, j) = -Tmat(i, j);
362 const std::vector<double> ones(n, 1.0);
363 const std::vector<double> u1 = solve(negT, ones);
364 const std::vector<double> u2 = solve(negT, u1);
365 double m1 = 0.0, m2 = 0.0;
366 for (std::size_t i = 0; i < n; ++i) {
367 m1 += alpha[i] * u1[i];
368 m2 += 2.0 * alpha[i] * u2[i];
369 }
370 const double rho = lambda * m1;
371 if (rho >= 1.0)
372 throw InputError("qsys_mg1_ps: the system is unstable, the utilization is at least one");
373
374 // Faddeev-LeVerrier gives det(tau I - T) and the adjugate in one sweep, so
375 // bhat(tau) = nb(tau)/db(tau) as polynomials of degree n-1 and n.
376 std::vector<double> db(n + 1, 0.0), nb(n, 0.0);
377 db[0] = 1.0;
378 Matrix<double> Mk(n, n, 0.0);
379 for (std::size_t i = 0; i < n; ++i) Mk(i, i) = 1.0;
380 for (std::size_t k = 1; k <= n; ++k) {
381 double v = 0.0;
382 for (std::size_t i = 0; i < n; ++i) {
383 double row = 0.0;
384 for (std::size_t j = 0; j < n; ++j) row += Mk(i, j) * exitrate[j];
385 v += alpha[i] * row;
386 }
387 nb[k - 1] = v;
388 Matrix<double> TM(n, n, 0.0);
389 for (std::size_t i = 0; i < n; ++i)
390 for (std::size_t j = 0; j < n; ++j) {
391 double acc = 0.0;
392 for (std::size_t l = 0; l < n; ++l) acc += Tmat(i, l) * Mk(l, j);
393 TM(i, j) = acc;
394 }
395 double tr = 0.0;
396 for (std::size_t i = 0; i < n; ++i) tr += TM(i, i);
397 db[k] = -tr / static_cast<double>(k);
398 Mk = TM;
399 for (std::size_t i = 0; i < n; ++i) Mk(i, i) += db[k];
400 }
401
402 auto bhat = [db, nb](const Cplx& tau) { return polyval(nb, tau) / polyval(db, tau); };
403 auto bpdf = [&Tmat, &alpha, &exitrate, n](double y) {
404 Matrix<double> E = Tmat;
405 for (std::size_t i = 0; i < n; ++i)
406 for (std::size_t j = 0; j < n; ++j) E(i, j) = Tmat(i, j) * y;
407 const Matrix<double> Ey = expm(E);
408 double v = 0.0;
409 for (std::size_t i = 0; i < n; ++i) {
410 double row = 0.0;
411 for (std::size_t j = 0; j < n; ++j) row += Ey(i, j) * exitrate[j];
412 v += alpha[i] * row;
413 }
414 return v;
415 };
416
417 // D(s,x) = exp(scale x) val, exactly, from the residues of f(tau;s), whose
418 // double pole at the origin cancels. The scale is carried separately so
419 // that neither factor overflows at large s.
420 auto denom = [db, nb, n, lambda, rho](const Cplx& s, double x, double* scale_out) {
421 std::vector<double> dm(db.size(), 0.0);
422 dm[0] = db[0];
423 for (std::size_t i = 1; i < db.size(); ++i) dm[i] = db[i] - nb[i - 1];
424
425 // These carry s, so they are complex; the polynomial helpers are
426 // written over a scalar type for exactly that reason.
427 std::vector<Cplx> Pc, Ac;
428 {
429 std::vector<Cplx> lin;
430 lin.push_back(Cplx(1.0, 0.0));
431 lin.push_back(-(s + Cplx(lambda, 0.0)));
432 std::vector<Cplx> dbz(db.size());
433 for (std::size_t i = 0; i < db.size(); ++i) dbz[i] = Cplx(db[i], 0.0);
434 Pc.assign(lin.size() + dbz.size() - 1, Cplx(0.0, 0.0));
435 for (std::size_t i = 0; i < lin.size(); ++i)
436 for (std::size_t j = 0; j < dbz.size(); ++j) Pc[i + j] += lin[i] * dbz[j];
437 // + [0, 0, lambda*nb]
438 for (std::size_t i = 0; i < nb.size(); ++i)
439 Pc[Pc.size() - nb.size() + i] += Cplx(lambda * nb[i], 0.0);
440
441 std::vector<Cplx> q1;
442 q1.push_back(Cplx(1.0 - rho, 0.0));
443 q1.push_back(s * Cplx(rho, 0.0));
444 q1.push_back(Cplx(0.0, 0.0));
445 std::vector<Cplx> t1(q1.size() + dbz.size() - 1, Cplx(0.0, 0.0));
446 for (std::size_t i = 0; i < q1.size(); ++i)
447 for (std::size_t j = 0; j < dbz.size(); ++j) t1[i + j] += q1[i] * dbz[j];
448
449 std::vector<Cplx> q2;
450 q2.push_back(Cplx(1.0 - rho, 0.0));
451 q2.push_back(s);
452 std::vector<Cplx> ldm(dm.size());
453 for (std::size_t i = 0; i < dm.size(); ++i) ldm[i] = Cplx(lambda * dm[i], 0.0);
454 std::vector<Cplx> t2(q2.size() + ldm.size() - 1, Cplx(0.0, 0.0));
455 for (std::size_t i = 0; i < q2.size(); ++i)
456 for (std::size_t j = 0; j < ldm.size(); ++j) t2[i + j] += q2[i] * ldm[j];
457
458 Ac.assign(t1.size(), Cplx(0.0, 0.0));
459 for (std::size_t i = 0; i < t1.size(); ++i) Ac[i] = t1[i];
460 for (std::size_t i = 0; i < t2.size(); ++i) Ac[Ac.size() - t2.size() + i] -= t2[i];
461 }
462 // The double pole at the origin must cancel: the two lowest
463 // coefficients of A are what would survive it.
464 double nrmA = 0.0;
465 for (std::size_t i = 0; i < Ac.size(); ++i) nrmA = std::max(nrmA, std::abs(Ac[i]));
466 const double tailA = std::abs(Ac[Ac.size() - 2]) + std::abs(Ac[Ac.size() - 1]);
467 if (tailA > 1e-6 * std::max(1.0, nrmA))
468 throw InputError("qsys_mg1_ps: the double pole at the origin did not cancel");
469 // `A(1:n+1)` in the reference: the first n+1 of the n+3 coefficients.
470 // The two dropped ones are the cancelled double pole, checked just
471 // above; taking n+2 instead silently keeps half of it and the LST comes
472 // back above one.
473 std::vector<Cplx> Ahat(Ac.begin(), Ac.begin() + static_cast<long>(n + 1));
474
475 // Durand-Kerner over the complex coefficients of P.
476 std::vector<Cplx> r;
477 {
478 std::vector<Cplx> mon(Pc.size());
479 for (std::size_t i = 0; i < Pc.size(); ++i) mon[i] = Pc[i] / Pc[0];
480 const std::size_t deg = mon.size() - 1;
481 r.assign(deg, Cplx(0.0, 0.0));
482 const Cplx seed(0.4, 0.9);
483 Cplx pw(1.0, 0.0);
484 for (std::size_t i = 0; i < deg; ++i) {
485 r[i] = pw;
486 pw *= seed;
487 }
488 for (int it = 0; it < 2000; ++it) {
489 double move = 0.0;
490 for (std::size_t i = 0; i < deg; ++i) {
491 Cplx den(1.0, 0.0);
492 for (std::size_t j = 0; j < deg; ++j)
493 if (j != i) den *= (r[i] - r[j]);
494 if (std::abs(den) < 1e-300) continue;
495 const Cplx step = polyval(mon, r[i]) / den;
496 r[i] -= step;
497 move = std::max(move, std::abs(step));
498 }
499 if (move < 1e-14) break;
500 }
501 }
502 double scale = -std::numeric_limits<double>::infinity();
503 for (std::size_t i = 0; i < r.size(); ++i) scale = std::max(scale, r[i].real());
504 *scale_out = scale;
505
506 double maxr = 0.0, minsep = std::numeric_limits<double>::infinity();
507 for (std::size_t i = 0; i < r.size(); ++i) maxr = std::max(maxr, std::abs(r[i]));
508 for (std::size_t i = 0; i < r.size(); ++i)
509 for (std::size_t j = 0; j < r.size(); ++j)
510 if (i != j) minsep = std::min(minsep, std::abs(r[i] - r[j]));
511
512 if (r.size() >= 2 && minsep > 1e-7 * std::max(1.0, maxr)) {
513 std::vector<Cplx> dP(Pc.size() - 1, Cplx(0.0, 0.0));
514 for (std::size_t i = 0; i + 1 < Pc.size(); ++i)
515 dP[i] = Pc[i] * static_cast<double>(Pc.size() - 1 - i);
516 Cplx val(0.0, 0.0);
517 for (std::size_t i = 0; i < r.size(); ++i) {
518 const Cplx coef = polyval(Ahat, r[i]) / polyval(dP, r[i]);
519 val += std::exp((r[i] - Cplx(scale, 0.0)) * x) * coef;
520 }
521 return val;
522 }
523 // Repeated poles: the companion realization of Ahat/P, exactly as the
524 // reference falls back to. A residue sum is undefined there; the
525 // matrix exponential is not.
526 const std::size_t d = Pc.size() - 1;
527 Matrix<Cplx> Acomp(d, d, Cplx(0.0, 0.0));
528 for (std::size_t j = 0; j < d; ++j) Acomp(0, j) = -Pc[j + 1] / Pc[0];
529 for (std::size_t i = 1; i < d; ++i) Acomp(i, i - 1) = Cplx(1.0, 0.0);
530 for (std::size_t i = 0; i < d; ++i) Acomp(i, i) -= Cplx(scale, 0.0);
531 Matrix<Cplx> Ax(d, d, Cplx(0.0, 0.0));
532 for (std::size_t i = 0; i < d; ++i)
533 for (std::size_t j = 0; j < d; ++j) Ax(i, j) = Acomp(i, j) * x;
534 const Matrix<Cplx> E = expm_cplx(Ax);
535 Cplx val(0.0, 0.0);
536 for (std::size_t j = 0; j < d && j < Ahat.size(); ++j) val += (Ahat[j] / Pc[0]) * E(j, 0);
537 return val;
538 };
539
540 Mg1PsResult res;
541 res.rho = rho;
542 res.m1 = m1;
543 res.m2 = m2;
544 res.meanUncond = m1 / (1.0 - rho);
545 res.atomUncond = (1.0 - rho) * bhat(Cplx(lambda, 0.0)).real();
546
547 auto lst_cond = [denom, rho](const Cplx& s, double x, bool excess) {
548 if (x == 0.0) return Cplx(1.0, 0.0);
549 double scale = 0.0;
550 const Cplx val = denom(s, x, &scale);
551 const Cplx expo = (excess ? s : Cplx(0.0, 0.0)) - Cplx(scale, 0.0);
552 return Cplx(1.0 - rho, 0.0) * std::exp(expo * x) / val;
553 };
554 res.lstCond = [lst_cond](Cplx s, double x) { return lst_cond(s, x, false); };
555 res.lstExcess = [lst_cond](Cplx s, double x) { return lst_cond(s, x, true); };
556 res.dominantRoot = [lambda, bhat, rho](Cplx s) {
557 Cplx tau = s;
558 const int maxit = std::max(200, static_cast<int>(std::ceil(
559 3.0 * std::log(1e-15) / std::log(std::max(rho, 1e-3)))));
560 for (int it = 0; it < maxit; ++it) {
561 const Cplx nx = s + Cplx(lambda, 0.0) * (Cplx(1.0, 0.0) - bhat(tau));
562 if (std::abs(nx - tau) <= 1e-14 * std::max(1.0, std::abs(nx))) return nx;
563 tau = nx;
564 }
565 throw InputError("qsys_mg1_ps: the dominant root iteration did not converge");
566 };
567
568 // The quadrature that removes the conditioning. The nodes do not move with
569 // s, so the service density is sampled once.
570 double mineig = std::numeric_limits<double>::infinity();
571 for (std::size_t i = 0; i < n; ++i) mineig = std::min(mineig, -Tmat(i, i));
572 const double ymax = std::max(40.0 * m1, 40.0 / std::max(mineig, 1e-12));
573 std::vector<double> yq, wq, bq;
574 quad_nodes(ymax, &yq, &wq);
575 bq.resize(yq.size());
576 for (std::size_t k = 0; k < yq.size(); ++k) bq[k] = opt.pdf ? opt.pdf(yq[k]) : bpdf(yq[k]);
577 res.lstUncond = [lst_cond, yq, wq, bq](Cplx s) {
578 Cplx v(0.0, 0.0);
579 for (std::size_t k = 0; k < yq.size(); ++k)
580 v += wq[k] * bq[k] * lst_cond(s, yq[k], false);
581 return v;
582 };
583
584 res.x = opt.x;
585 res.s = opt.s;
586 res.t = opt.t;
587 res.lstCondVal = Matrix<double>(res.x.size(), res.s.size(), 0.0);
588 for (std::size_t i = 0; i < res.x.size(); ++i)
589 for (std::size_t j = 0; j < res.s.size(); ++j)
590 res.lstCondVal(i, j) = res.lstCond(Cplx(res.s[j], 0.0), res.x[i]).real();
591 res.lstUncondVal.assign(res.s.size(), 0.0);
592 for (std::size_t j = 0; j < res.s.size(); ++j)
593 res.lstUncondVal[j] = res.lstUncond(Cplx(res.s[j], 0.0)).real();
594
595 res.atomCond.assign(res.x.size(), 0.0);
596 res.meanCond.assign(res.x.size(), 0.0);
597 res.m2Cond.assign(res.x.size(), 0.0);
598 res.varCond.assign(res.x.size(), 0.0);
599 for (std::size_t i = 0; i < res.x.size(); ++i) {
600 res.atomCond[i] = (1.0 - rho) * std::exp(-lambda * res.x[i]);
601 res.meanCond[i] = res.x[i] / (1.0 - rho);
602 const double xi = res.x[i];
603 res.m2Cond[i] = second_moment([&res, xi](Cplx u) { return res.lstCond(u, xi); },
604 res.meanCond[i], res.meanUncond);
605 res.varCond[i] = res.m2Cond[i] - res.meanCond[i] * res.meanCond[i];
606 }
607
608 // The unconditional second moment is integrated from the conditional one,
609 // which is far better conditioned than differentiating the quadrature that
610 // removed the conditioning.
611 double m2u = 0.0;
612 for (std::size_t k = 0; k < yq.size(); ++k) {
613 const double yk = yq[k];
614 m2u += wq[k] * bq[k] *
615 second_moment([&res, yk](Cplx u) { return res.lstCond(u, yk); },
616 yk / (1.0 - rho), res.meanUncond);
617 }
618 res.m2Uncond = m2u;
619 res.varUncond = m2u - res.meanUncond * res.meanUncond;
620
621 res.pdfCond = Matrix<double>(res.x.size(), res.t.size(), 0.0);
622 res.cdfCond = Matrix<double>(res.x.size(), res.t.size(), 0.0);
623 for (std::size_t i = 0; i < res.x.size(); ++i) {
624 const double atom = res.atomCond[i], xi = res.x[i];
625 // V(x) >= x with an atom at x, so what is inverted is the EXCESS
626 // V(x)-x net of its atom.
627 auto gpdf = [&res, xi, atom](Cplx u) { return res.lstExcess(u, xi) - Cplx(atom, 0.0); };
628 auto gcdf = [gpdf](Cplx u) { return gpdf(u) / u; };
629 for (std::size_t j = 0; j < res.t.size(); ++j) {
630 const double tj = res.t[j];
631 if (tj < xi) continue;
632 if (tj == xi) {
633 res.cdfCond(i, j) = atom;
634 continue;
635 }
636 res.cdfCond(i, j) = ilt(gcdf, tj - xi, opt.nterms).real() + atom;
637 const double ratio = tj / xi;
638 if (std::fabs(ratio - std::floor(ratio + 0.5)) < 1e-9)
639 res.pdfCond(i, j) = std::numeric_limits<double>::quiet_NaN();
640 else
641 res.pdfCond(i, j) = ilt(gpdf, tj - xi, opt.nterms).real();
642 }
643 }
644
645 res.pdfUncond.assign(res.t.size(), 0.0);
646 res.cdfUncond.assign(res.t.size(), 0.0);
647 for (std::size_t j = 0; j < res.t.size(); ++j) {
648 if (res.t[j] <= 0.0) continue;
649 res.pdfUncond[j] = ilt(res.lstUncond, res.t[j], opt.nterms).real();
650 res.cdfUncond[j] =
651 ilt([&res](Cplx u) { return res.lstUncond(u) / u; }, res.t[j], opt.nterms).real();
652 }
653 return res;
654}
655
656} // namespace qsys
657} // namespace line
658
659#endif // LINE_API_QSYS_QSYS_MG1_PS_H
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
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:85
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:88
std::function< double(double)> pdf
Service density, needed to remove the conditioning on a callback path.
Definition qsys_mg1_ps.h:94
std::vector< double > s
transform arguments to tabulate the LST at
Definition qsys_mg1_ps.h:90
std::vector< double > t
times to evaluate the sojourn distribution at
Definition qsys_mg1_ps.h:91
std::size_t nterms
function evaluations per numerical inversion, ODD
Definition qsys_mg1_ps.h:92
std::vector< double > x
service requirements to condition on
Definition qsys_mg1_ps.h:89
Everything qsys_mg1_ps returns.
Definition qsys_mg1_ps.h:98
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