LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mapd1.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_MAPD1_H
6#define LINE_API_QSYS_QSYS_MAPD1_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * The MAP/D/1 FCFS queue: deterministic service of length s fed by a Markovian
12 * arrival process.
13 *
14 * ALGORITHM, AND HOW IT DIFFERS FROM THE MATLAB REFERENCE.
15 * matlab/src/api/qsys/qsys_mapd1.m delegates to qsys_mapdc, which calls
16 * Q-MAM's Q_CT_MAP_D_C, which is not transcribed here. This port computes the
17 * SAME quantities exactly, by the embedded-chain route, and NOT by an
18 * Erlang-k approximation:
19 * a deterministic service time has no phase-type representation, so no QBD can
20 * carry it, but an M/G/1-type Markov chain can.
21 *
22 * The construction is in four exact steps.
23 *
24 * 1. MAP counting probabilities over one service. P_k(u) is the matrix whose
25 * (i,j) entry is P(k arrivals in [0,u], phase j at u | phase i at 0). They
26 * satisfy dP_k/du = P_k D0 + P_{k-1} D1, so the row (P_0(s) ... P_K(s)) is
27 * the top block row of exp(M s) with M the (K+1)-block bidiagonal matrix
28 * carrying D0 on the diagonal and D1 on the superdiagonal. One expm gives
29 * all of them. K is grown until the missing mass max_i (1 - sum_k P_k(s)e)
30 * is below the tolerance, so the truncation is a controlled quantity and not
31 * a modelling choice.
32 *
33 * 2. Time-in-level integrals. I_k = integral_0^s P_k(u) du follows from the
34 * same differential equation without a second expm: integrating gives
35 * P_k(s) - P_k(0) = I_k D0 + I_{k-1} D1, hence I_0 = (P_0(s) - I) D0^-1 and
36 * I_k = (P_k(s) - I_{k-1} D1) D0^-1. D0 is a non-singular sub-generator.
37 *
38 * 3. The embedded chain at departure epochs. With the level equal to the number
39 * left behind by a departure and the phase equal to the arrival phase, the
40 * chain is skip-free to the left with blocks A_k = P_k(s) from a busy level
41 * and B_k = (-D0)^-1 D1 A_k from the empty level (wait for the next arrival,
42 * then serve). Its stationary vector comes from Ramaswami's recursion: with
43 * G the minimal solution of G = sum_k A_k G^k, and the partial sums
44 * Ahat_i = sum_{k>=i} A_k G^(k-i), Bhat_i = sum_{k>=i} B_k G^(k-i),
45 *
46 * x_0 (B_0 + Bhat_1 (I - Ahat_1)^-1 A_0) = x_0,
47 * x_n = [ x_0 Bhat_n + sum_{k=1}^{n-1} x_k Ahat_{n-k+1} ] (I - Ahat_1)^-1.
48 *
49 * 4. The time-stationary distribution. The departure-epoch vector x is NOT the
50 * time-stationary one unless the arrivals are Poisson, so the two are not
51 * interchanged here. Averaging over an inter-departure cycle with the
52 * integrals of step 2,
53 *
54 * p_0 = lambda x_0 (-D0)^-1 e,
55 * p_n = lambda [ y_0 I_{n-1} e + sum_{m=1}^{n} x_m I_{n-m} e ], n >= 1,
56 *
57 * with y_0 = x_0 (-D0)^-1 D1 the phase at the arrival that ends an idle
58 * period. Since I_k e sums to s e over k, this construction satisfies
59 * sum_n p_n = lambda (E[idle] + s) = 1 and p_0 = 1 - lambda s identically,
60 * which the test file asserts. The mean follows in closed form from
61 * sum_k I_k and sum_k k I_k without materializing the levels.
62 *
63 * meanWaitingTime is (L - rho)/lambda by Little's law and meanSojournTime is
64 * that plus s.
65 *
66 * MEASURED AGREEMENT (MATLAB R2025a, T = double). meanQueueLength
67 * agrees with the reference; meanWaitingTime does not, and the reference is
68 * the one that is wrong -- see the defect note below.
69 * - M/D/1 collapse, qsys_mapd1(D0 = [-2], D1 = [2], s = 1/3), rho = 2/3.
70 * Textbook L = rho + rho^2/(2(1-rho)) = 4/3 and Wq = rho s/(2(1-rho)) = 1/3.
71 * The port returns 1.33333333333307 and 0.333333333333203, i.e. the
72 * textbook values to 2.0e-13 and 4.0e-13. MATLAB's meanQueueLength is
73 * 1.333333330345585, itself 2.2e-9 below the textbook value (its own
74 * maxNumComp truncation), so the port and MATLAB differ by 2.24e-9.
75 * - Correlated MMPP2 arrivals D0 = [-2.5 0.2; 0.1 -0.7], D1 = diag(2.3, 0.6)
76 * (lambda = 7/6), s = 0.4, rho = 7/15: MATLAB meanQueueLength
77 * 1.118300079597477, port 1.11830008358428, relative difference 3.57e-9.
78 * - Erlang-2 arrivals D0 = [-4 4; 0 -4], D1 = [0 0; 4 0] (lambda = 2),
79 * s = 0.3, rho = 0.6: MATLAB meanQueueLength 0.7758216464528508, port
80 * 0.775821646757718, relative difference 3.93e-10.
81 *
82 * Both non-Poisson values were confirmed independently of Q-MAM by the
83 * Erlang-k limit through LINE's own MATLAB qbd_mapmap1: replacing the
84 * deterministic service by an Erlang-k of the same mean and Richardson-
85 * extrapolating the O(1/k) convergence from k = 40 and k = 80 gives
86 * 1.11832240806 and 0.775808057317, which match the port to 2.0e-5 and 1.7e-5,
87 * the residual of the extrapolation itself. The port additionally satisfies
88 * sum_n p_n = 1 and p_0 = 1 - rho to 1e-15 on all three instances, both being
89 * identities of the construction rather than fitted quantities.
90 *
91 * At T = Real50 the port reproduces its own double results to 2e-14, so the
92 * double values above are not precision-limited.
93 *
94 * DEFECT IN THE MATLAB REFERENCE (reported, not fixed here). qsys_mapdc, and
95 * therefore qsys_mapd1, computes meanWaitingTime as a left-rectangle sum of
96 * the survival function of the Q-MAM waiting-time CDF with step s/numSteps and
97 * numSteps defaulting to 1. Two consequences:
98 * (a) even for Poisson arrivals the default is a one-point quadrature. On
99 * the M/D/1 instance above it returns 0.4444444443040794 against the
100 * exact 1/3, a +33% error, converging as O(1/numSteps): 0.3350694 at
101 * numSteps = 64 and 0.3334418 at numSteps = 1024.
102 * (b) for non-Poisson arrivals it does not converge to the right value at
103 * all. On the correlated instance it converges to 0.3540778458263887
104 * while Little's law applied to its own (correct) meanQueueLength gives
105 * 0.5585429253692660; on the Erlang-2 instance it converges to
106 * 0.1428343846295046 against 0.0879108232264254. Errors of -37% and
107 * +62%, in opposite directions, so this is not a quadrature artefact.
108 * The reference's meanQueueLength is correct in all three cases -- that is what
109 * the Erlang-k cross-check above establishes -- so this port takes Little's law
110 * as the definition of meanWaitingTime and does not reproduce the reference's
111 * waiting-time numbers. Reproduction: run qsys_mapd1([-2],[2],1/3,'numSteps',N)
112 * for N = 1, 64, 1024 and compare meanWaitingTime against 1/3.
113 *
114 * ARITHMETIC. Gated on num_traits<T>::has_transcendental for two independent
115 * reasons: step 1 calls expm, a scaling-and-squaring Pade approximation that is
116 * tolerance-controlled and cannot be exact in any arithmetic, and step 3
117 * computes G by a fixed-point iteration that does not terminate in a finite
118 * number of field operations. Steps 2 and 4 are finite exact matrix algebra
119 * given P_k(s) and G, and add no error of their own.
120 */
121
122#include <cmath>
123#include <cstddef>
124#include <vector>
125
127#include "line/api/mam/qbd_r.h"
128#include "line/num/number.h"
129#include "line/util/error.h"
130#include "line/util/expm.h"
131#include "line/util/linalg.h"
132#include "line/util/lu.h"
133#include "line/util/matrix.h"
134
135namespace line {
136namespace qsys {
137
138/** Return value of qsys_mapd1, mirroring the MATLAB result struct. */
139template <class T>
141 T meanQueueLength; ///< L, number in system, time-stationary
142 T meanWaitingTime; ///< Wq = (L - rho)/lambda
143 T meanSojournTime; ///< W = Wq + s
144 T utilization; ///< rho = lambda s
145 std::vector<T> queueLengthDist; ///< P(N = n), n = 0, 1, ...
146};
147
148namespace detail {
149
150/** Right division X = C D^-1, formed from an explicit inverse of D. */
151template <class T>
152Matrix<T> rdivide(const Matrix<T>& C, const Matrix<T>& Dinv) {
153 return matmul(C, Dinv);
154}
155
156/**
157 * Counting probabilities P_k(s), k = 0..K, of a MAP over an interval of exact
158 * length s, as the top block row of exp(M s) with M block bidiagonal. One expm
159 * of order (K+1)n.
160 */
161template <class T>
162std::vector<Matrix<T>> map_counting_at(const Matrix<T>& D0, const Matrix<T>& D1, const T& s,
163 unsigned K) {
164 const std::size_t n = D0.rows();
165 const std::size_t dim = (static_cast<std::size_t>(K) + 1) * n;
166 Matrix<T> M(dim, dim, num_traits<T>::from_int(0));
167 for (unsigned b = 0; b <= K; ++b) {
168 for (std::size_t i = 0; i < n; ++i)
169 for (std::size_t j = 0; j < n; ++j) M(b * n + i, b * n + j) = D0(i, j);
170 if (b < K)
171 for (std::size_t i = 0; i < n; ++i)
172 for (std::size_t j = 0; j < n; ++j) M(b * n + i, (b + 1) * n + j) = D1(i, j);
173 }
174 const Matrix<T> E = expm(M, s);
175 std::vector<Matrix<T>> P(static_cast<std::size_t>(K) + 1);
176 for (unsigned b = 0; b <= K; ++b) {
177 P[b] = Matrix<T>(n, n);
178 for (std::size_t i = 0; i < n; ++i)
179 for (std::size_t j = 0; j < n; ++j) P[b](i, j) = E(i, b * n + j);
180 }
181 return P;
182}
183
184} // namespace detail
185
186/**
187 * MAP/D/1 by the exact embedded M/G/1-type chain.
188 *
189 * @param arrival arrival MAP (D0, D1)
190 * @param s deterministic service time, s > 0
191 * @param dist_size how many entries of queueLengthDist to materialize
192 * @param max_arrivals cap on K, the number of arrivals per service that is
193 * tracked; K is grown from a Poisson-tail estimate until
194 * the missing counting mass falls below tol
195 * @param max_levels cap on the number of embedded levels generated by
196 * Ramaswami's recursion
197 * @param tol tolerance on the counting-mass truncation, on the G
198 * iteration and on the level-tail truncation
199 */
200template <class T>
201MapD1Result<T> qsys_mapd1(const mam::Map<T>& arrival, const T& s, std::size_t dist_size,
202 unsigned max_arrivals, std::size_t max_levels, const T& tol) {
204 "qsys_mapd1 requires transcendental arithmetic");
205 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
206 if (s <= zero) throw InputError("qsys_mapd1: service time s must be positive");
207 if (dist_size == 0) throw InputError("qsys_mapd1: dist_size must be positive");
208 if (max_levels == 0) throw InputError("qsys_mapd1: max_levels must be positive");
209 const std::size_t n = arrival.D0.rows();
210 if (arrival.D0.cols() != n || arrival.D1.rows() != n || arrival.D1.cols() != n)
211 throw InputError("qsys_mapd1: D0 and D1 must be square and of equal order");
212
213 const Matrix<T>& D0 = arrival.D0;
214 const Matrix<T>& D1 = arrival.D1;
215 const T lambda = mam::map_lambda(arrival);
216 if (lambda <= zero) throw InputError("qsys_mapd1: non-positive arrival rate");
217 const T rho = lambda * s;
218 if (rho >= one) throw InputError("qsys_mapd1: load rho must be strictly less than 1");
219
220 // counting-probability start rationale: see _kb/03-api-layer.md (cpp port notes: qsys)
221 T numax = zero;
222 for (std::size_t i = 0; i < n; ++i) {
223 const T d = -D0(i, i);
224 if (d > numax) numax = d;
225 }
226 const T est = numax * s;
227 unsigned K = 8u;
228 {
229 const double e = num_traits<T>::to_double(est);
230 const unsigned guess = static_cast<unsigned>(2.0 * e + 10.0 * std::sqrt(e + 1.0) + 8.0);
231 if (guess > K) K = guess;
232 }
233 if (K > max_arrivals) K = max_arrivals;
234 std::vector<Matrix<T>> A = detail::map_counting_at(D0, D1, s, K);
235 while (true) {
236 T missing = zero;
237 for (std::size_t i = 0; i < n; ++i) {
238 T row = zero;
239 for (std::size_t k = 0; k < A.size(); ++k)
240 for (std::size_t j = 0; j < n; ++j) row += A[k](i, j);
241 const T lack = one - row;
242 if (lack > missing) missing = lack;
243 }
244 if (missing <= tol || K >= max_arrivals) break;
245 K = (2u * K < max_arrivals) ? 2u * K : max_arrivals;
246 A = detail::map_counting_at(D0, D1, s, K);
247 }
248
249 // ---- step 2: time-in-level integrals over one service ----
250 const Matrix<T> invD0 = inverse(D0);
251 std::vector<Matrix<T>> I(A.size());
252 {
253 Matrix<T> C = A[0];
254 for (std::size_t i = 0; i < n; ++i) C(i, i) -= one;
255 I[0] = detail::rdivide(C, invD0);
256 for (std::size_t k = 1; k < A.size(); ++k) {
257 Matrix<T> Ck = A[k];
258 const Matrix<T> prev = matmul(I[k - 1], D1);
259 for (std::size_t i = 0; i < n; ++i)
260 for (std::size_t j = 0; j < n; ++j) Ck(i, j) -= prev(i, j);
261 I[k] = detail::rdivide(Ck, invD0);
262 }
263 }
264
265 // ---- step 3: embedded chain at departures, Ramaswami's recursion ----
266 Matrix<T> negD0inv(n, n);
267 for (std::size_t i = 0; i < n; ++i)
268 for (std::size_t j = 0; j < n; ++j) negD0inv(i, j) = -invD0(i, j);
269 const Matrix<T> Pa = matmul(negD0inv, D1); // phase at the arrival ending an idle period
270 std::vector<Matrix<T>> B(A.size());
271 for (std::size_t k = 0; k < A.size(); ++k) B[k] = matmul(Pa, A[k]);
272
273 // G by the U-based natural iteration G <- (I - A_1 - sum_{k>=2} A_k G^(k-1))^-1 A_0.
274 Matrix<T> G(n, n, zero);
275 const unsigned gmax = 100000u;
276 const std::size_t ktop = A.size() - 1; // = K, and K >= 8 by construction
277 for (unsigned it = 0; it < gmax; ++it) {
278 // S = sum_{k=2}^{K} A_k G^(k-2) by Horner, so sum_{k>=2} A_k G^(k-1) = S G.
279 Matrix<T> S = A[ktop];
280 for (std::size_t k = ktop; k-- > 2;) S = mam::qbd_detail::madd(A[k], matmul(S, G));
281 const Matrix<T> U = mam::qbd_detail::madd(A[1], matmul(S, G));
282 const Matrix<T> ImU = mam::qbd_detail::msub(eye<T>(n), U);
283 const Matrix<T> Gn = matmul(inverse(ImU), A[0]);
284 T gap = zero;
285 for (std::size_t i = 0; i < n; ++i)
286 for (std::size_t j = 0; j < n; ++j) {
287 const T d = num_abs(T(Gn(i, j) - G(i, j)));
288 if (d > gap) gap = d;
289 }
290 G = Gn;
291 if (gap <= tol) break;
292 }
293
294 // Ahat_i = sum_{k>=i} A_k G^(k-i), Bhat_i likewise, by Horner from the top.
295 const std::size_t K1 = A.size() - 1;
296 std::vector<Matrix<T>> Ahat(K1 + 2), Bhat(K1 + 2);
297 Ahat[K1 + 1] = Matrix<T>(n, n, zero);
298 Bhat[K1 + 1] = Matrix<T>(n, n, zero);
299 for (std::size_t i = K1 + 1; i-- > 0;) {
300 Ahat[i] = mam::qbd_detail::madd(A[i], matmul(Ahat[i + 1], G));
301 Bhat[i] = mam::qbd_detail::madd(B[i], matmul(Bhat[i + 1], G));
302 }
303
304 const Matrix<T> W = inverse(mam::qbd_detail::msub(eye<T>(n), Ahat[1]));
305 // x_0 is the stationary vector of the level-0 censored chain.
306 Matrix<T> M0 = mam::qbd_detail::madd(B[0], matmul(matmul(Bhat[1], W), A[0]));
307 for (std::size_t i = 0; i < n; ++i) M0(i, i) -= one;
308 std::vector<std::vector<T>> x;
309 x.push_back(mam::qbd_detail::statvec(M0));
310
311 T mass = zero;
312 for (const T& v : x[0]) mass += v;
313 for (std::size_t lvl = 1; lvl <= max_levels; ++lvl) {
314 std::vector<T> acc(n, zero);
315 if (lvl <= K1) {
316 const std::vector<T> t = vecmul(x[0], Bhat[lvl]);
317 for (std::size_t j = 0; j < n; ++j) acc[j] += t[j];
318 }
319 const std::size_t kmin = (lvl + 1 > K1) ? (lvl + 1 - K1) : 1;
320 for (std::size_t k = kmin; k + 1 <= lvl; ++k) {
321 const std::vector<T> t = vecmul(x[k], Ahat[lvl - k + 1]);
322 for (std::size_t j = 0; j < n; ++j) acc[j] += t[j];
323 }
324 const std::vector<T> xn = vecmul(acc, W);
325 T inc = zero;
326 for (const T& v : xn) inc += v;
327 x.push_back(xn);
328 mass += inc;
329 if (inc <= tol * mass) break;
330 }
331 for (std::vector<T>& row : x)
332 for (T& v : row) v /= mass;
333
334 // ---- step 4: time-stationary distribution and its mean ----
335 const std::vector<T> e = ones<T>(n);
336 const std::vector<T> y0 = vecmul(x[0], Pa);
337 std::vector<std::vector<T>> Ie(I.size());
338 for (std::size_t k = 0; k < I.size(); ++k) Ie[k] = mulvec(I[k], e);
339 std::vector<T> Isum_e(n, zero), Iw_e(n, zero);
340 for (std::size_t k = 0; k < I.size(); ++k)
341 for (std::size_t j = 0; j < n; ++j) {
342 Isum_e[j] += Ie[k][j];
343 Iw_e[j] += num_traits<T>::from_int(static_cast<long>(k)) * Ie[k][j];
344 }
345
346 T L = zero;
347 for (std::size_t m = 1; m < x.size(); ++m) {
348 T a = zero, b = zero;
349 for (std::size_t j = 0; j < n; ++j) {
350 a += x[m][j] * Isum_e[j];
351 b += x[m][j] * Iw_e[j];
352 }
353 L += num_traits<T>::from_int(static_cast<long>(m)) * a + b;
354 }
355 for (std::size_t j = 0; j < n; ++j) L += y0[j] * (Isum_e[j] + Iw_e[j]);
356 L *= lambda;
357
359 r.meanQueueLength = L;
360 r.meanWaitingTime = (L - rho) / lambda;
362 r.utilization = rho;
363
364 r.queueLengthDist.assign(dist_size, zero);
365 {
366 const std::vector<T> idle = mulvec(negD0inv, e);
367 T p0 = zero;
368 for (std::size_t j = 0; j < n; ++j) p0 += x[0][j] * idle[j];
369 r.queueLengthDist[0] = lambda * p0;
370 for (std::size_t lvl = 1; lvl < dist_size; ++lvl) {
371 T acc = zero;
372 if (lvl - 1 < Ie.size())
373 for (std::size_t j = 0; j < n; ++j) acc += y0[j] * Ie[lvl - 1][j];
374 for (std::size_t m = 1; m <= lvl && m < x.size(); ++m) {
375 const std::size_t kk = lvl - m;
376 if (kk >= Ie.size()) continue;
377 for (std::size_t j = 0; j < n; ++j) acc += x[m][j] * Ie[kk][j];
378 }
379 r.queueLengthDist[lvl] = lambda * acc;
380 }
381 }
382 return r;
383}
384
385/**
386 * qsys_mapd1 with 100 materialized levels, an arrival-count cap of 4096, a
387 * level cap of 20000 and tolerance 1e-14.
388 */
389template <class T>
390MapD1Result<T> qsys_mapd1(const mam::Map<T>& arrival, const T& s) {
391 return qsys_mapd1(arrival, s, static_cast<std::size_t>(100), 4096u,
392 static_cast<std::size_t>(20000), T(num_traits<T>::from_double(1e-14)));
393}
394
395} // namespace qsys
396} // namespace line
397
398#endif // LINE_API_QSYS_QSYS_MAPD1_H
InputError(const std::string &what)
Definition error.h:39
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.
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.
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
MapD1Result< T > qsys_mapd1(const mam::Map< T > &arrival, const T &s, std::size_t dist_size, unsigned max_arrivals, std::size_t max_levels, const T &tol)
MAP/D/1 by the exact embedded M/G/1-type chain.
Definition qsys_mapd1.h:201
T num_abs(const T &v)
Definition number.h:172
std::vector< T > vecmul(const std::vector< T > &v, const Matrix< T > &A)
Row vector times matrix, v A.
Definition linalg.h:50
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 > ones(std::size_t n)
Column vector of ones, the ubiquitous e in MAP algebra.
Definition linalg.h:104
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
Number-type abstraction for the templated API port.
Quasi-birth-death processes: the rate matrix R, the fundamental matrix G, the caudal characteristic,...
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
Return value of qsys_mapd1, mirroring the MATLAB result struct.
Definition qsys_mapd1.h:140
T meanSojournTime
W = Wq + s.
Definition qsys_mapd1.h:143
T meanWaitingTime
Wq = (L - rho)/lambda.
Definition qsys_mapd1.h:142
T meanQueueLength
L, number in system, time-stationary.
Definition qsys_mapd1.h:141
std::vector< T > queueLengthDist
P(N = n), n = 0, 1, ...
Definition qsys_mapd1.h:145
T utilization
rho = lambda s
Definition qsys_mapd1.h:144