LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mmapgk1.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_MMAPGK1_H
6#define LINE_API_QSYS_QSYS_MMAPGK1_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * The MMAP[K]/G[K]/1 FCFS queue: K customer types with class-dependent GENERAL
12 * service, fed by a marked Markovian arrival process.
13 *
14 * WHY THIS IS NOT MMAPPH1FCFS. That routine needs every type's service to be
15 * PHASE TYPE, because it builds a QBD whose phase carries the service phase.
16 * Here the service laws are arbitrary and may differ in family across types --
17 * deterministic for one, uniform for another -- so no finite phase carries
18 * them, and the analysis has to run through transforms instead.
19 *
20 * THE METHOD, which is He's, theorem for theorem. FCFS makes the actual waiting
21 * time of a customer the WORKLOAD it finds on arrival, so everything follows
22 * from the joint transform of workload and arrival phase,
23 * f(s)_j = E[exp(-s V) 1{phase = j}], which by He's Theorem 4.1 (eq. 4.6)
24 * satisfies
25 *
26 * f(s) [ s I + D0 + sum_k Dk gk(s) ] = s v0, (*)
27 *
28 * with v0 the idle-phase vector, his y0. v0 needs NO root search: the matrix U
29 * solving U = D0 + sum_k Dk Fk(U), Fk(U) = int exp(U t) dFk(t), is his
30 * eq. (4.4), the generator of the underlying Markov process obtained by
31 * EXCISING the busy periods, and eq. (4.5) with Theorem 4.2 give y0 Q = 0 and
32 * y0 e = 1 - rho, i.e. v0 = (1 - rho) pi_U. The same vector is what the
33 * analyticity of (*) forces, since for every left eigenpair (w, u) of U one has
34 * w [D0 + sum_k Dk gk(-u) - u I] = 0, so the roots of the bracket in the closed
35 * right half plane are exactly s = -u over the spectrum of U; the two agree to
36 * 2.5e-13, and the stationary route is taken because it needs no complex
37 * eigenvector.
38 *
39 * The per-type actual waiting time is the workload seen by a type-k arrival,
40 * biased by that type's own arrival block, his Theorem 5.1 eq. (5.1) summed
41 * over the post-arrival phase:
42 *
43 * E[exp(-s Wk)] = f(s) Dk e / lambda_k.
44 *
45 * SCOPE. He allows an arrival to be a BATCH carrying a sequence of types, and
46 * his Theorem 5.3 then multiplies the transform by prod_{i<n} f*_{h_i}(s), the
47 * service of the customers ahead of the tagged one WITHIN its own batch. This
48 * header covers the single-customer-per-arrival case, his Special case 3.3,
49 * where that product is empty, which is exactly the MMAP convention LINE
50 * carries.
51 *
52 * MOMENTS WITHOUT INVERSION. Differentiating (*) at s = 0 gives
53 * sum_i C(j,i) f_i M_{j-i} = [j = 1] v0. M_0 = D is SINGULAR with right null
54 * vector e, so each order fixes f_j only up to a multiple of theta, and that
55 * multiple is what the NEXT order's solvability condition supplies. At j = 0
56 * the same condition reads theta M_1 e = v0 e, i.e. 1 - rho = 1 - rho, which is
57 * the identity that validates the whole setup.
58 *
59 * ARITHMETIC. Gated on num_traits<T>::has_transcendental: the U iteration runs
60 * to a tolerance, and both the deterministic transform and the inversion need
61 * exp. The CDF is the Abate-Whitt Euler sum, which evaluates the transform OFF
62 * the real axis, so a small complex layer is carried here rather than in the
63 * distribution interface.
64 *
65 * Reference:
66 * Qi-Ming He, "The versatility of MMAP[K] and the MMAP[K]/G[K]/1 queue",
67 * Queueing Systems 38(4):397-418, 2001.
68 */
69
70#include <algorithm>
71#include <cmath>
72#include <complex>
73#include <functional>
74#include <limits>
75#include <string>
76#include <cstddef>
77#include <vector>
78
82#include "line/num/number.h"
83#include "line/util/error.h"
84#include "line/util/expm.h"
85#include "line/util/linalg.h"
86#include "line/util/lu.h"
87#include "line/util/matrix.h"
88
89namespace line {
90namespace qsys {
91
92/** Return value of qsys_mmapgk1, mirroring the MATLAB struct. */
93template <class T>
95 std::vector<T> lambdas; ///< per-type arrival rates
96 T arrivalRate; ///< sum of the per-type rates
97 T utilization; ///< rho = sum_k lambda_k E[S_k]
98 std::vector<T> idleVector; ///< v0, summing to 1 - rho
99 std::vector<std::vector<T> > waitMoments; ///< [type][order], E[Wk^j]
100 std::vector<T> meanWaitingTime; ///< per-type E[Wq]
101 std::vector<T> meanSojournTime; ///< per-type E[Wq] + E[S]
102 T meanQueueLength; ///< E[N] by Little over all types
103 std::vector<std::vector<T> > waitCDF; ///< [type][point], P(Wk <= t)
104 std::vector<T> waitPoints; ///< the requested points
105};
106
107/**
108 * A service law given by its transform, the reference's struct form
109 * `struct('lst', @(s) ..., 'moments', [m1 m2 ...])`: the LST at a complex
110 * argument and its raw moments, of which at least numWMoms + 1 are needed.
111 */
112template <class T>
114 std::function<std::complex<double>(std::complex<double>)> lst;
115 std::vector<T> moments;
116};
117
118/**
119 * One marked type's service law, in any of the three forms the reference
120 * accepts: a Distribution object, a (D0, D1) MAP pair, or a transform struct.
121 */
122template <class T>
134
135namespace mmapgk1detail {
136
137typedef std::complex<double> cdbl;
138
139/** Binomial coefficient; declared ahead of the complex block that uses it. */
140inline double binom(std::size_t n, std::size_t k);
141
142/** Left null vector of G normalized to sum one. */
143template <class T>
144std::vector<T> stat_left_null(const Matrix<T>& G) {
145 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
146 const std::size_t n = G.rows();
147 Matrix<T> A(n, n, zero);
148 for (std::size_t i = 0; i < n; ++i)
149 for (std::size_t j = 0; j < n; ++j) A(j, i) = G(i, j);
150 for (std::size_t j = 0; j < n; ++j) A(n - 1, j) = one;
151 std::vector<T> b(n, zero);
152 b[n - 1] = one;
153 return line::solve(A, b);
154}
155
156/** Midpoint nodes and true CDF increments over the support of the law. */
157template <class T>
158void stieltjes_nodes(const lang::Distrib<T>& law, std::vector<T>& x, std::vector<T>& w,
159 std::size_t quad_nodes = 120) {
160 // nGrid = max(quadNodes, 20) * 20 in the reference
161 const std::size_t n_grid = (quad_nodes > 20 ? quad_nodes : 20) * 20;
162 const T zero = num_traits<T>::from_int(0);
163 const double mean = num_traits<T>::to_double(lang::dist_moment(law, 1u));
164 double hi = mean * 60.0;
165 const double var = num_traits<T>::to_double((lang::dist_moment(law, 2u) - lang::dist_moment(law, 1u) * lang::dist_moment(law, 1u)));
166 if (std::isfinite(var) && var > 0.0) hi = std::max(hi, mean + 12.0 * std::sqrt(var));
167 const double step = hi / static_cast<double>(n_grid);
168 x.assign(n_grid, zero);
169 w.assign(n_grid, zero);
170 T mass = zero;
171 T prev = lang::dist_cdf(law, num_traits<T>::from_double(0.0));
172 for (std::size_t i = 0; i < n_grid; ++i) {
173 const T right = num_traits<T>::from_double((i + 1) * step);
174 const T cur = lang::dist_cdf(law, right);
175 x[i] = num_traits<T>::from_double((i + 0.5) * step);
176 w[i] = cur - prev;
177 mass += w[i];
178 prev = cur;
179 }
180 if (mass > zero)
181 for (std::size_t i = 0; i < n_grid; ++i) w[i] = w[i] / mass;
182}
183
184/**
185 * Raw moment E[S^j]; dist_moment already evaluates every family exactly.
186 *
187 * A HEAVY TAIL HAS NO MOMENT of high enough order, and dist_moment says so by
188 * THROWING (a Pareto of shape <= j). Here that is not an error but an answer:
189 * the moment recursion consumes M_j only from order j onwards, so an infinite
190 * M_3 leaves E[Wq] finite and makes E[Wq^2] infinite, which is the truth about
191 * such a queue. Letting the throw escape would lose the finite moments too.
192 */
193template <class T>
194T raw_moment(const lang::Distrib<T>& law, std::size_t j) {
195 try {
196 return lang::dist_moment(law, static_cast<unsigned>(j));
197 } catch (const NumericError&) {
198 return num_traits<T>::from_double(std::numeric_limits<double>::infinity());
199 }
200}
201
202/** Raw moment E[S^j] of a service law in any of its three forms. */
203template <class T>
204T raw_moment(const MmapGk1Service<T>& svc, std::size_t j) {
205 switch (svc.kind) {
207 return mam::map_moment(svc.map, static_cast<unsigned>(j));
209 if (j > svc.transform.moments.size())
210 throw InputError("qsys_mmapgk1: The service law was given as a transform with " +
211 std::to_string(svc.transform.moments.size()) +
212 " moments, but order " + std::to_string(j) +
213 " is needed. Supply at least numWMoms+1 moments.");
214 return svc.transform.moments[j - 1];
215 default:
216 return raw_moment(svc.dist, j);
217 }
218}
219
220/** Kronecker product; C++ has no shared templated kron. */
221template <class T>
222Matrix<T> gk_kron(const Matrix<T>& A, const Matrix<T>& B) {
223 Matrix<T> C(A.rows() * B.rows(), A.cols() * B.cols(), num_traits<T>::from_int(0));
224 for (std::size_t i = 0; i < A.rows(); ++i)
225 for (std::size_t j = 0; j < A.cols(); ++j)
226 for (std::size_t p = 0; p < B.rows(); ++p)
227 for (std::size_t q = 0; q < B.cols(); ++q)
228 C(i * B.rows() + p, j * B.cols() + q) = A(i, j) * B(p, q);
229 return C;
230}
231
232/** Gaussian elimination with partial pivoting over the complex field. */
233inline std::vector<cdbl> complex_solve(std::vector<std::vector<cdbl> > A, std::vector<cdbl> b);
234
235/**
236 * The matrix transform int_0^inf exp(U t) dF(t) for a Markovian (D0, D1)
237 * service law. The density is the SCALAR beta exp(St) s0, so the integral is
238 * exact on the Kronecker sum: int exp(Ut) x exp(St) dt = -(U (+) S)^-1.
239 */
240template <class T>
241Matrix<T> matrix_lst_map(const mam::Map<T>& rep, const Matrix<T>& U) {
242 const T zero = num_traits<T>::from_int(0);
243 const std::size_t n = U.rows();
244 const Matrix<T>& S = rep.D0;
245 const std::size_t ms = S.rows();
246 const std::vector<T> beta = mam::map_pie(rep);
247 std::vector<T> s0(ms, zero);
248 for (std::size_t i = 0; i < ms; ++i) {
249 T r = zero;
250 for (std::size_t j = 0; j < ms; ++j) r += S(i, j);
251 s0[i] = -r;
252 }
253 Matrix<T> Ims = eye<T>(ms);
254 Matrix<T> KS = gk_kron(U, Ims);
255 const Matrix<T> In = eye<T>(n);
256 const Matrix<T> IkS = gk_kron(In, S);
257 for (std::size_t i = 0; i < KS.rows(); ++i)
258 for (std::size_t j = 0; j < KS.cols(); ++j) KS(i, j) += IkS(i, j);
259 Matrix<T> betaM(1, ms, zero);
260 for (std::size_t i = 0; i < ms; ++i) betaM(0, i) = beta[i];
261 Matrix<T> s0M(ms, 1, zero);
262 for (std::size_t i = 0; i < ms; ++i) s0M(i, 0) = s0[i];
263 const Matrix<T> L = gk_kron(In, betaM);
264 Matrix<T> R = gk_kron(In, s0M);
265 Matrix<T> X = matmul(inverse(KS), R);
266 for (std::size_t i = 0; i < X.rows(); ++i)
267 for (std::size_t j = 0; j < X.cols(); ++j) X(i, j) = -X(i, j);
268 return matmul(L, X);
269}
270
271/**
272 * Complex eigen-decomposition U = V diag(ev) V^-1 of a small real matrix, the
273 * reference's `[Vd, Dd] = eig(U)`. LAPACK-free: the eigenvalues are the roots
274 * of the characteristic polynomial (Faddeev-LeVerrier, then Durand-Kerner),
275 * the eigenvectors come from inverse iteration. `rcond` is the reciprocal
276 * 1-norm condition number of V, which is what MATLAB's rcond(Vd) estimates;
277 * it is 0 when V could not be inverted.
278 */
279inline void eig_decomp(const std::vector<std::vector<double> >& U, std::vector<cdbl>& ev,
280 std::vector<std::vector<cdbl> >& V, std::vector<std::vector<cdbl> >& Vinv,
281 double& rcond) {
282 const std::size_t n = U.size();
283 ev.assign(n, cdbl(0.0, 0.0));
284 V.assign(n, std::vector<cdbl>(n, cdbl(0.0, 0.0)));
285 Vinv.assign(n, std::vector<cdbl>(n, cdbl(0.0, 0.0)));
286 rcond = 0.0;
287 if (n == 0) return;
288 // characteristic polynomial det(tau I - U) = tau^n + c1 tau^(n-1) + ... + cn
289 std::vector<double> c(n + 1, 0.0);
290 c[0] = 1.0;
291 std::vector<std::vector<double> > Mk(n, std::vector<double>(n, 0.0));
292 for (std::size_t i = 0; i < n; ++i) Mk[i][i] = 1.0;
293 for (std::size_t k = 1; k <= n; ++k) {
294 std::vector<std::vector<double> > TM(n, std::vector<double>(n, 0.0));
295 double tr = 0.0;
296 for (std::size_t i = 0; i < n; ++i)
297 for (std::size_t j = 0; j < n; ++j) {
298 double acc = 0.0;
299 for (std::size_t l = 0; l < n; ++l) acc += U[i][l] * Mk[l][j];
300 TM[i][j] = acc;
301 if (i == j) tr += acc;
302 }
303 c[k] = -tr / static_cast<double>(k);
304 Mk = TM;
305 for (std::size_t i = 0; i < n; ++i) Mk[i][i] += c[k];
306 }
307 // Durand-Kerner
308 {
309 const cdbl seed(0.4, 0.9);
310 cdbl pw(1.0, 0.0);
311 for (std::size_t i = 0; i < n; ++i) {
312 ev[i] = pw;
313 pw *= seed;
314 }
315 double scale = 0.0;
316 for (std::size_t i = 0; i < n; ++i)
317 for (std::size_t j = 0; j < n; ++j) scale = std::max(scale, std::fabs(U[i][j]));
318 if (scale > 0.0)
319 for (std::size_t i = 0; i < n; ++i) ev[i] *= scale;
320 for (int it = 0; it < 2000; ++it) {
321 double move = 0.0;
322 for (std::size_t i = 0; i < n; ++i) {
323 cdbl den(1.0, 0.0);
324 for (std::size_t j = 0; j < n; ++j)
325 if (j != i) den *= (ev[i] - ev[j]);
326 if (std::abs(den) < 1e-300) continue;
327 cdbl pv(0.0, 0.0);
328 for (std::size_t q = 0; q <= n; ++q) pv = pv * ev[i] + c[q];
329 const cdbl step = pv / den;
330 ev[i] -= step;
331 move = std::max(move, std::abs(step));
332 }
333 if (move < 1e-15 * std::max(1.0, scale)) break;
334 }
335 }
336 // eigenvectors by inverse iteration on the slightly shifted eigenvalue
337 double umax = 0.0;
338 for (std::size_t i = 0; i < n; ++i) umax = std::max(umax, std::abs(ev[i]));
339 for (std::size_t i = 0; i < n; ++i) {
340 const cdbl shift = ev[i] + cdbl(1e-10 * std::max(1.0, umax), 1e-10 * std::max(1.0, umax));
341 std::vector<cdbl> v(n);
342 for (std::size_t q = 0; q < n; ++q) v[q] = cdbl(1.0 / std::sqrt(static_cast<double>(n)), 0.0);
343 for (int it = 0; it < 3; ++it) {
344 std::vector<std::vector<cdbl> > A(n, std::vector<cdbl>(n, cdbl(0.0, 0.0)));
345 for (std::size_t p = 0; p < n; ++p) {
346 for (std::size_t q = 0; q < n; ++q) A[p][q] = cdbl(U[p][q], 0.0);
347 A[p][p] -= shift;
348 }
349 std::vector<cdbl> w = complex_solve(A, v);
350 double nrm = 0.0;
351 for (std::size_t q = 0; q < n; ++q) nrm += std::norm(w[q]);
352 nrm = std::sqrt(nrm);
353 if (!(nrm > 0.0) || !std::isfinite(nrm)) break;
354 for (std::size_t q = 0; q < n; ++q) v[q] = w[q] / nrm;
355 }
356 for (std::size_t q = 0; q < n; ++q) V[q][i] = v[q];
357 }
358 // V^-1 column by column, and rcond = 1 / (||V||_1 ||V^-1||_1)
359 double normV = 0.0, normW = 0.0;
360 for (std::size_t j = 0; j < n; ++j) {
361 std::vector<cdbl> ej(n, cdbl(0.0, 0.0));
362 ej[j] = cdbl(1.0, 0.0);
363 const std::vector<cdbl> col = complex_solve(V, ej);
364 double cs = 0.0, vs = 0.0;
365 for (std::size_t i = 0; i < n; ++i) {
366 Vinv[i][j] = col[i];
367 cs += std::abs(col[i]);
368 vs += std::abs(V[i][j]);
369 }
370 normW = std::max(normW, cs);
371 normV = std::max(normV, vs);
372 }
373 if (std::isfinite(normW) && normW > 0.0 && normV > 0.0) rcond = 1.0 / (normV * normW);
374}
375
376/** real(V diag(g) V^-1), the eigen route of the matrix transform. */
377template <class T>
378Matrix<T> matrix_lst_eig(std::size_t n, const std::vector<std::vector<cdbl> >& V,
379 const std::vector<std::vector<cdbl> >& Vinv, const std::vector<cdbl>& g) {
380 Matrix<T> F(n, n, num_traits<T>::from_int(0));
381 for (std::size_t i = 0; i < n; ++i)
382 for (std::size_t j = 0; j < n; ++j) {
383 cdbl acc(0.0, 0.0);
384 for (std::size_t q = 0; q < n; ++q) acc += V[i][q] * g[q] * Vinv[q][j];
385 F(i, j) = num_traits<T>::from_double(acc.real());
386 }
387 return F;
388}
389
390/** The matrix transform int_0^inf exp(U t) dF(t), for a Distribution object. */
391template <class T>
392Matrix<T> matrix_lst(const lang::Distrib<T>& law, const Matrix<T>& U, std::size_t quad_nodes = 120) {
393 const T zero = num_traits<T>::from_int(0);
394 const std::size_t n = U.rows();
395 if (lang::process_is_markovian(law.type)) return matrix_lst_map(lang::dist_to_map(law), U);
396 if (law.type == lang::ProcessType::DET) {
397 Matrix<T> Ud = U;
398 const T d = lang::dist_moment(law, 1u);
399 for (std::size_t i = 0; i < n; ++i)
400 for (std::size_t j = 0; j < n; ++j) Ud(i, j) = Ud(i, j) * d;
401 return line::expm(Ud);
402 }
403 // The reference's eigen-decomposition route: F = real(V diag(lst(-u)) V^-1)
404 // when V is well conditioned and every transform value is finite, else the
405 // Stieltjes sum over the CDF increments.
406 {
407 std::vector<std::vector<double> > Ud(n, std::vector<double>(n, 0.0));
408 for (std::size_t i = 0; i < n; ++i)
409 for (std::size_t j = 0; j < n; ++j) Ud[i][j] = num_traits<T>::to_double(U(i, j));
410 std::vector<cdbl> ev;
411 std::vector<std::vector<cdbl> > V, Vinv;
412 double rcond = 0.0;
413 eig_decomp(Ud, ev, V, Vinv, rcond);
414 bool ok = rcond > 1e-12;
415 std::vector<cdbl> g(n, cdbl(0.0, 0.0));
416 if (ok) {
417 for (std::size_t i = 0; i < n; ++i) {
418 try {
419 g[i] = lang::dist_lst(law, -ev[i]);
420 } catch (const Error&) {
421 ok = false;
422 break;
423 }
424 if (!std::isfinite(g[i].real()) || !std::isfinite(g[i].imag())) {
425 ok = false;
426 break;
427 }
428 }
429 }
430 if (ok) return matrix_lst_eig<T>(n, V, Vinv, g);
431 }
432 std::vector<T> x, w;
433 stieltjes_nodes(law, x, w, quad_nodes);
434 Matrix<T> F(n, n, zero);
435 for (std::size_t i = 0; i < x.size(); ++i) {
436 Matrix<T> Ut = U;
437 for (std::size_t p = 0; p < n; ++p)
438 for (std::size_t q = 0; q < n; ++q) Ut(p, q) = Ut(p, q) * x[i];
439 const Matrix<T> E = line::expm(Ut);
440 for (std::size_t p = 0; p < n; ++p)
441 for (std::size_t q = 0; q < n; ++q) F(p, q) += w[i] * E(p, q);
442 }
443 return F;
444}
445
446/** The matrix transform for a service law in any of its three forms. */
447template <class T>
448Matrix<T> matrix_lst(const MmapGk1Service<T>& svc, const Matrix<T>& U, std::size_t quad_nodes) {
449 switch (svc.kind) {
451 return matrix_lst_map(svc.map, U);
453 // F = real(V diag(lst(-u)) V^-1), as the reference does for a struct law
454 const std::size_t n = U.rows();
455 std::vector<std::vector<double> > Ud(n, std::vector<double>(n, 0.0));
456 for (std::size_t i = 0; i < n; ++i)
457 for (std::size_t j = 0; j < n; ++j) Ud[i][j] = num_traits<T>::to_double(U(i, j));
458 std::vector<cdbl> ev;
459 std::vector<std::vector<cdbl> > V, Vinv;
460 double rcond = 0.0;
461 eig_decomp(Ud, ev, V, Vinv, rcond);
462 std::vector<cdbl> g(n);
463 for (std::size_t i = 0; i < n; ++i) g[i] = svc.transform.lst(-ev[i]);
464 return matrix_lst_eig<T>(n, V, Vinv, g);
465 }
466 default:
467 return matrix_lst(svc.dist, U, quad_nodes);
468 }
469}
470
471/** Gaussian elimination with partial pivoting over the complex field. */
472inline std::vector<cdbl> complex_solve(std::vector<std::vector<cdbl> > A, std::vector<cdbl> b) {
473 const std::size_t n = b.size();
474 for (std::size_t col = 0; col < n; ++col) {
475 std::size_t piv = col;
476 double best = std::abs(A[col][col]);
477 for (std::size_t r = col + 1; r < n; ++r) {
478 if (std::abs(A[r][col]) > best) { best = std::abs(A[r][col]); piv = r; }
479 }
480 if (piv != col) { std::swap(A[piv], A[col]); std::swap(b[piv], b[col]); }
481 for (std::size_t r = col + 1; r < n; ++r) {
482 const cdbl f = A[r][col] / A[col][col];
483 for (std::size_t c = col; c < n; ++c) A[r][c] -= f * A[col][c];
484 b[r] -= f * b[col];
485 }
486 }
487 std::vector<cdbl> x(n);
488 for (std::size_t row = n; row-- > 0;) {
489 cdbl s = b[row];
490 for (std::size_t c = row + 1; c < n; ++c) s -= A[row][c] * x[c];
491 x[row] = s / A[row][row];
492 }
493 return x;
494}
495
496/**
497 * Scalar Laplace-Stieltjes transform of a service law at a complex argument.
498 *
499 * One line, because the LANGUAGE LAYER owns this: dist_lst carries a complex
500 * overload beside the real one, with the closed forms, the phase-type solve and
501 * the CDF-increment fallback already tiered. A second implementation here would
502 * be a second thing to keep true.
503 */
504template <class T>
505cdbl scalar_lst(const lang::Distrib<T>& law, cdbl s) {
506 return lang::dist_lst(law, s);
507}
508
509/** Scalar LST of a service law in any of its three forms. */
510template <class T>
511cdbl scalar_lst(const MmapGk1Service<T>& svc, cdbl s) {
512 switch (svc.kind) {
514 // alpha (s I - D0)^-1 (-D0 e), with alpha = map_pie
515 const Matrix<T>& D0s = svc.map.D0;
516 const std::size_t n = D0s.rows();
517 const std::vector<T> alpha = mam::map_pie(svc.map);
518 std::vector<std::vector<cdbl> > A(n, std::vector<cdbl>(n, cdbl(0.0, 0.0)));
519 std::vector<cdbl> b(n, cdbl(0.0, 0.0));
520 for (std::size_t i = 0; i < n; ++i) {
521 double rs = 0.0;
522 for (std::size_t j = 0; j < n; ++j) {
523 A[i][j] = cdbl(-num_traits<T>::to_double(D0s(i, j)), 0.0);
524 rs += num_traits<T>::to_double(D0s(i, j));
525 }
526 A[i][i] += s;
527 b[i] = cdbl(-rs, 0.0);
528 }
529 const std::vector<cdbl> x = complex_solve(A, b);
530 cdbl g(0.0, 0.0);
531 for (std::size_t i = 0; i < n; ++i) g += num_traits<T>::to_double(alpha[i]) * x[i];
532 return g;
533 }
535 return svc.transform.lst(s);
536 default:
537 return lang::dist_lst(svc.dist, s);
538 }
539}
540
541
542/** E[exp(-s Wk)] at a complex argument. */
543template <class T>
544cdbl wait_lst(const Matrix<T>& D0, const std::vector<Matrix<T> >& Dk,
545 const std::vector<MmapGk1Service<T> >& svc, const std::vector<T>& v0,
546 const std::vector<T>& lambdas, std::size_t k, cdbl s) {
547 const std::size_t ma = D0.rows();
548 std::vector<std::vector<cdbl> > M(ma, std::vector<cdbl>(ma, cdbl(0.0, 0.0)));
549 for (std::size_t i = 0; i < ma; ++i)
550 for (std::size_t j = 0; j < ma; ++j) M[i][j] = cdbl(num_traits<T>::to_double(D0(i, j)), 0.0);
551 for (std::size_t i = 0; i < ma; ++i) M[i][i] += s;
552 for (std::size_t q = 0; q < Dk.size(); ++q) {
553 const cdbl g = scalar_lst(svc[q], s);
554 for (std::size_t i = 0; i < ma; ++i)
555 for (std::size_t j = 0; j < ma; ++j)
556 M[i][j] += num_traits<T>::to_double(Dk[q](i, j)) * g;
557 }
558 // solve f M = s v0, i.e. M' f' = (s v0)'
559 std::vector<std::vector<cdbl> > A(ma, std::vector<cdbl>(ma, cdbl(0.0, 0.0)));
560 std::vector<cdbl> b(ma, cdbl(0.0, 0.0));
561 for (std::size_t i = 0; i < ma; ++i) {
562 for (std::size_t j = 0; j < ma; ++j) A[i][j] = M[j][i];
563 b[i] = s * num_traits<T>::to_double(v0[i]);
564 }
565 const std::vector<cdbl> f = complex_solve(A, b);
566 cdbl out(0.0, 0.0);
567 for (std::size_t i = 0; i < ma; ++i) {
568 double rowsum = 0.0;
569 for (std::size_t j = 0; j < ma; ++j) rowsum += num_traits<T>::to_double(Dk[k](i, j));
570 out += f[i] * rowsum;
571 }
572 return out / num_traits<T>::to_double(lambdas[k]);
573}
574
575/** Abate-Whitt Euler inversion of the type-k waiting time CDF. */
576template <class T>
577double euler_invert(const Matrix<T>& D0, const std::vector<Matrix<T> >& Dk,
578 const std::vector<MmapGk1Service<T> >& svc, const std::vector<T>& v0,
579 const std::vector<T>& lambdas, std::size_t k, double t) {
580 if (t <= 0.0) return wait_lst(D0, Dk, svc, v0, lambdas, k, cdbl(1e12, 0.0)).real();
581 const double A = 18.4;
582 const std::size_t nE = 15, mE = 11;
583 const double u = std::exp(A / 2) / t;
584 const double x = A / (2 * t);
585 std::vector<double> terms(nE + mE + 1, 0.0);
586 terms[0] = wait_lst(D0, Dk, svc, v0, lambdas, k, cdbl(x, 0.0)).real() / x / 2.0;
587 for (std::size_t j = 1; j <= nE + mE; ++j) {
588 const cdbl s(x, M_PI * static_cast<double>(j) / t);
589 terms[j] = ((j % 2 == 0) ? 1.0 : -1.0) * (wait_lst(D0, Dk, svc, v0, lambdas, k, s) / s).real();
590 }
591 std::vector<double> partial(terms.size(), 0.0);
592 double run = 0.0;
593 for (std::size_t j = 0; j < terms.size(); ++j) { run += terms[j]; partial[j] = run; }
594 double F = 0.0;
595 for (std::size_t j = 0; j <= mE; ++j) F += binom(mE, j) / std::pow(2.0, static_cast<double>(mE)) * partial[nE + j];
596 F = u * F;
597 return std::min(std::max(F, 0.0), 1.0);
598}
599
600
601inline double binom(std::size_t n, std::size_t k) {
602 double r = 1.0;
603 for (std::size_t i = 1; i <= k; ++i) r = r * static_cast<double>(n - k + i) / static_cast<double>(i);
604 return std::floor(r + 0.5);
605}
606
607} // namespace mmapgk1detail
608
609/**
610 * MMAP[K]/G[K]/1 FCFS, per type.
611 *
612 * @param MMAP LINE convention {D0, D1, D^(1), ..., D^(K)}, D1 = sum_k D^(k)
613 * @param svc K service laws, one per marked type, each a Distribution
614 * object, a (D0, D1) MAP pair or a transform struct
615 * @param w_points times at which to evaluate the per-type waiting time CDF
616 * @param num_w_moms how many per-type waiting time moments to return
617 * @param tol fixed point tolerance on U
618 * @param iter_max fixed point iteration cap (the reference's 10000)
619 * @param quad_nodes the reference's quadNodes (default 120): the Stieltjes
620 * fallback uses max(quadNodes, 20) * 20 CDF increments
621 */
622template <class T>
623MmapGk1Result<T> qsys_mmapgk1(const std::vector<Matrix<T> >& MMAP,
624 const std::vector<MmapGk1Service<T> >& svc,
625 const std::vector<T>& w_points, std::size_t num_w_moms,
626 double tol, std::size_t iter_max, std::size_t quad_nodes) {
628 "qsys_mmapgk1 requires transcendental arithmetic");
629 using namespace mmapgk1detail;
630 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
631 if (MMAP.size() < 3) throw InputError("qsys_mmapgk1: the MMAP must carry a marked block");
632 const std::size_t K = MMAP.size() - 2;
633 if (svc.size() != K) throw InputError("qsys_mmapgk1: one service law per marked type");
634 const Matrix<T>& D0 = MMAP[0];
635 const std::size_t ma = D0.rows();
636 std::vector<Matrix<T> > Dk;
637 Matrix<T> Dsum = D0;
638 for (std::size_t k = 0; k < K; ++k) {
639 Dk.push_back(MMAP[k + 2]);
640 for (std::size_t i = 0; i < ma; ++i)
641 for (std::size_t j = 0; j < ma; ++j) Dsum(i, j) += Dk[k](i, j);
642 }
643
644 const std::vector<T> theta = stat_left_null(Dsum);
645 std::vector<T> lambdas(K, zero), mean_s(K, zero);
646 T rho = zero;
647 for (std::size_t k = 0; k < K; ++k) {
648 T lam = zero;
649 for (std::size_t i = 0; i < ma; ++i)
650 for (std::size_t j = 0; j < ma; ++j) lam += theta[i] * Dk[k](i, j);
651 lambdas[k] = lam;
652 mean_s[k] = raw_moment(svc[k], 1);
653 rho += lam * mean_s[k];
654 }
655 if (rho >= one) throw InputError("qsys_mmapgk1: load rho must be strictly less than 1");
656
657 Matrix<T> U = D0;
658 for (std::size_t it = 0; it < iter_max; ++it) {
659 Matrix<T> Unew = D0;
660 for (std::size_t k = 0; k < K; ++k) {
661 const Matrix<T> Fk = matrix_lst(svc[k], U, quad_nodes);
662 const Matrix<T> P = matmul(Dk[k], Fk);
663 for (std::size_t i = 0; i < ma; ++i)
664 for (std::size_t j = 0; j < ma; ++j) Unew(i, j) += P(i, j);
665 }
666 double diff = 0.0;
667 for (std::size_t i = 0; i < ma; ++i)
668 for (std::size_t j = 0; j < ma; ++j)
669 diff = std::max(diff, std::fabs(num_traits<T>::to_double(Unew(i, j) - U(i, j))));
670 U = Unew;
671 if (diff <= tol) break;
672 }
673 // U e = 0 EXACTLY: a property of the fixed point, not of the iterate, and
674 // the residue at the tolerance above would move the stationary solve.
675 for (std::size_t i = 0; i < ma; ++i) {
676 T rowsum = zero;
677 for (std::size_t j = 0; j < ma; ++j) rowsum += U(i, j);
678 U(i, i) -= rowsum;
679 }
680 std::vector<T> v0 = stat_left_null(U);
681 for (std::size_t i = 0; i < ma; ++i) v0[i] = v0[i] * (one - rho);
682
683 // Moment recursion, see the header comment
684 std::vector<Matrix<T> > Mder;
685 for (std::size_t j = 0; j <= num_w_moms + 1; ++j) {
686 Matrix<T> Mj(ma, ma, zero);
687 if (j == 0) {
688 Mj = D0;
689 for (std::size_t k = 0; k < K; ++k)
690 for (std::size_t i = 0; i < ma; ++i)
691 for (std::size_t q = 0; q < ma; ++q) Mj(i, q) += Dk[k](i, q);
692 } else {
693 for (std::size_t k = 0; k < K; ++k) {
694 const T mom = raw_moment(svc[k], j);
695 const double sign = (j % 2 == 0) ? 1.0 : -1.0;
696 for (std::size_t i = 0; i < ma; ++i)
697 for (std::size_t q = 0; q < ma; ++q)
698 Mj(i, q) += num_traits<T>::from_double(sign) * Dk[k](i, q) * mom;
699 }
700 if (j == 1)
701 for (std::size_t i = 0; i < ma; ++i) Mj(i, i) += one;
702 }
703 Mder.push_back(Mj);
704 }
705 const std::vector<T> e(ma, one);
706 T denom = zero;
707 {
708 const std::vector<T> v = mulvec(Mder[1], e);
709 for (std::size_t i = 0; i < ma; ++i) denom += theta[i] * v[i];
710 }
711 std::vector<std::vector<T> > fder;
712 fder.push_back(theta);
713 // [M_0, e] with f_j^p e = 0 pins the particular solution; solved in the
714 // least squares sense, since the system is one equation over-determined.
715 Matrix<T> Abase(ma + 1, ma, zero);
716 for (std::size_t i = 0; i < ma; ++i)
717 for (std::size_t j = 0; j < ma; ++j) Abase(j, i) = Mder[0](i, j);
718 for (std::size_t i = 0; i < ma; ++i) Abase(ma, i) = one;
719 Matrix<T> AtA(ma, ma, zero);
720 for (std::size_t i = 0; i < ma; ++i)
721 for (std::size_t j = 0; j < ma; ++j) {
722 T s = zero;
723 for (std::size_t q = 0; q < ma + 1; ++q) s += Abase(q, i) * Abase(q, j);
724 AtA(i, j) = s;
725 }
726 const Matrix<T> AtAinv = inverse(AtA);
727 for (std::size_t j = 1; j <= num_w_moms; ++j) {
728 std::vector<T> rhs(ma, zero);
729 if (j == 1) for (std::size_t i = 0; i < ma; ++i) rhs[i] = v0[i];
730 for (std::size_t i = 0; i < j; ++i) {
731 const double c = binom(j, i);
732 for (std::size_t q = 0; q < ma; ++q) {
733 T acc = zero;
734 for (std::size_t p = 0; p < ma; ++p) acc += fder[i][p] * Mder[j - i](p, q);
735 rhs[q] -= num_traits<T>::from_double(c) * acc;
736 }
737 }
738 std::vector<T> rhsAug(ma + 1, zero);
739 for (std::size_t q = 0; q < ma; ++q) rhsAug[q] = rhs[q];
740 std::vector<T> Atb(ma, zero);
741 for (std::size_t i = 0; i < ma; ++i) {
742 T s = zero;
743 for (std::size_t q = 0; q < ma + 1; ++q) s += Abase(q, i) * rhsAug[q];
744 Atb[i] = s;
745 }
746 const std::vector<T> fp = mulvec(AtAinv, Atb);
747 T acc2 = zero;
748 for (std::size_t i = 0; i < j; ++i) {
749 const std::vector<T> v = mulvec(Mder[j + 1 - i], e);
750 T inner = zero;
751 for (std::size_t p = 0; p < ma; ++p) inner += fder[i][p] * v[p];
752 acc2 += num_traits<T>::from_double(binom(j + 1, i)) * inner;
753 }
754 T fpM1e = zero;
755 {
756 const std::vector<T> v = mulvec(Mder[1], e);
757 for (std::size_t p = 0; p < ma; ++p) fpM1e += fp[p] * v[p];
758 }
759 const T cfree = (-acc2 / num_traits<T>::from_int(static_cast<long>(j + 1)) - fpM1e) / denom;
760 std::vector<T> fj(ma, zero);
761 for (std::size_t p = 0; p < ma; ++p) fj[p] = fp[p] + cfree * theta[p];
762 fder.push_back(fj);
763 }
764
766 r.lambdas = lambdas;
767 r.arrivalRate = zero;
768 for (std::size_t k = 0; k < K; ++k) r.arrivalRate += lambdas[k];
769 r.utilization = rho;
770 r.idleVector = v0;
771 r.waitMoments.assign(K, std::vector<T>(num_w_moms, zero));
772 r.meanWaitingTime.assign(K, zero);
773 r.meanSojournTime.assign(K, zero);
774 r.meanQueueLength = zero;
775 for (std::size_t k = 0; k < K; ++k) {
776 for (std::size_t j = 1; j <= num_w_moms; ++j) {
777 const std::vector<T> v = mulvec(Dk[k], e);
778 T inner = zero;
779 for (std::size_t p = 0; p < ma; ++p) inner += fder[j][p] * v[p];
780 const double sign = (j % 2 == 0) ? 1.0 : -1.0;
781 r.waitMoments[k][j - 1] = num_traits<T>::from_double(sign) * inner / lambdas[k];
782 }
783 r.meanWaitingTime[k] = num_w_moms >= 1 ? r.waitMoments[k][0] : zero;
784 r.meanSojournTime[k] = r.meanWaitingTime[k] + mean_s[k];
785 r.meanQueueLength += lambdas[k] * r.meanSojournTime[k];
786 }
787
788 r.waitPoints = w_points;
789 if (!w_points.empty()) {
790 r.waitCDF.assign(K, std::vector<T>(w_points.size(), zero));
791 for (std::size_t k = 0; k < K; ++k) {
792 for (std::size_t it = 0; it < w_points.size(); ++it) {
793 const double t = num_traits<T>::to_double(w_points[it]);
795 euler_invert(D0, Dk, svc, v0, lambdas, k, t));
796 }
797 }
798 }
799 return r;
800}
801
802
803/** qsys_mmapgk1 on service variants with the reference's quadNodes = 120. */
804template <class T>
805MmapGk1Result<T> qsys_mmapgk1(const std::vector<Matrix<T> >& MMAP,
806 const std::vector<MmapGk1Service<T> >& svc,
807 const std::vector<T>& w_points, std::size_t num_w_moms,
808 double tol, std::size_t iter_max) {
809 return qsys_mmapgk1(MMAP, svc, w_points, num_w_moms, tol, iter_max,
810 static_cast<std::size_t>(120));
811}
812
813/** qsys_mmapgk1 on Distribution objects. */
814template <class T>
815MmapGk1Result<T> qsys_mmapgk1(const std::vector<Matrix<T> >& MMAP,
816 const std::vector<lang::Distrib<T> >& svc,
817 const std::vector<T>& w_points, std::size_t num_w_moms,
818 double tol, std::size_t iter_max) {
819 std::vector<MmapGk1Service<T> > laws;
820 laws.reserve(svc.size());
821 for (std::size_t k = 0; k < svc.size(); ++k) laws.push_back(MmapGk1Service<T>(svc[k]));
822 return qsys_mmapgk1(MMAP, laws, w_points, num_w_moms, tol, iter_max,
823 static_cast<std::size_t>(120));
824}
825
826/** qsys_mmapgk1 on Distribution objects with the reference's defaults. */
827template <class T>
828MmapGk1Result<T> qsys_mmapgk1(const std::vector<Matrix<T> >& MMAP,
829 const std::vector<lang::Distrib<T> >& svc) {
830 return qsys_mmapgk1(MMAP, svc, std::vector<T>(), static_cast<std::size_t>(3), 1e-12,
831 static_cast<std::size_t>(10000));
832}
833
834/** qsys_mmapgk1 on service variants with the reference's defaults. */
835template <class T>
836MmapGk1Result<T> qsys_mmapgk1(const std::vector<Matrix<T> >& MMAP,
837 const std::vector<MmapGk1Service<T> >& svc) {
838 return qsys_mmapgk1(MMAP, svc, std::vector<T>(), static_cast<std::size_t>(3), 1e-12,
839 static_cast<std::size_t>(10000), static_cast<std::size_t>(120));
840}
841
842} // namespace qsys
843} // namespace line
844
845#endif // LINE_API_QSYS_QSYS_MMAPGK1_H
InputError(const std::string &what)
Definition error.h:105
std::size_t rows() const
Definition matrix.h:89
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LU factorization with partial pivoting, templated on the number type.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
T dist_lst(const Distrib< T > &d, const T &s)
sn.lst: the Laplace-Stieltjes transform E[exp(-sX)].
T dist_cdf(const Distrib< T > &d, const T &x)
F(x) = P{X <= x}, MATLAB's Distribution.evalCDF.
bool process_is_markovian(ProcessType p)
ProcessType.isMarkovian: true when sn.proc carries an exact matrix representation of the law,...
Definition lang_types.h:655
T dist_moment(const Distrib< T > &d, unsigned k)
The k-th raw moment.
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_moment(const Map< T > &m, unsigned k)
Raw moment of order k of the inter-arrival time: k!
Definition map_moment.h:118
MmapGk1Result< T > qsys_mmapgk1(const std::vector< Matrix< T > > &MMAP, const std::vector< MmapGk1Service< T > > &svc, const std::vector< T > &w_points, std::size_t num_w_moms, double tol, std::size_t iter_max, std::size_t quad_nodes)
MMAP[K]/G[K]/1 FCFS, per type.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
std::vector< T > mulvec(const Matrix< T > &A, const std::vector< T > &v)
Matrix times column vector, A v.
Definition linalg.h:62
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
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Number-type abstraction for the templated API port.
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Return value of qsys_mmapgk1, mirroring the MATLAB struct.
T utilization
rho = sum_k lambda_k E[S_k]
std::vector< T > lambdas
per-type arrival rates
std::vector< T > waitPoints
the requested points
std::vector< T > meanWaitingTime
per-type E[Wq]
std::vector< T > meanSojournTime
per-type E[Wq] + E[S]
std::vector< std::vector< T > > waitCDF
[type][point], P(Wk <= t)
T arrivalRate
sum of the per-type rates
std::vector< T > idleVector
v0, summing to 1 - rho
T meanQueueLength
E[N] by Little over all types.
std::vector< std::vector< T > > waitMoments
[type][order], E[Wk^j]
One marked type's service law, in any of the three forms the reference accepts: a Distribution object...
lang::Distrib< T > dist
MmapGk1Service(const MmapGk1Transform< T > &t)
MmapGk1Service(const mam::Map< T > &m)
MmapGk1Transform< T > transform
MmapGk1Service(const lang::Distrib< T > &d)
A service law given by its transform, the reference's struct form struct('lst', @(s) ....
std::function< std::complex< double >(std::complex< double >)> lst