LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mapdc.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_MAPDC_H
6#define LINE_API_QSYS_QSYS_MAPDC_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * The MAP/D/c FCFS queue: c servers, deterministic service of length s, fed by
12 * a Markovian arrival process. The multiserver generalization of qsys_mapd1.
13 *
14 * ALGORITHM, AND HOW IT DIFFERS FROM THE MATLAB REFERENCE.
15 * matlab/src/api/qsys/qsys_mapdc.m calls Q-MAM's Q_CT_MAP_D_C, which is not
16 * transcribed here. This port computes the same quantities by Crommelin's
17 * embedded lattice chain, which is exact for deterministic service and is NOT
18 * an Erlang-k or a heavy-traffic approximation.
19 *
20 * THE LATTICE CHAIN IS EXACT, AND WHY. Sample the system at the epochs
21 * t_n = n s and let N_n be the number in system at t_n, J_n the arrival phase.
22 * Every job in service at t_n started somewhere in (t_n - s, t_n], so it
23 * departs inside (t_n, t_{n+1}]; every job that starts service inside that
24 * interval departs after t_{n+1}. Hence exactly min(N_n, c) departures occur
25 * per interval -- all of them when the system is below capacity, since then no
26 * job waits -- and
27 *
28 * N_{n+1} = max(N_n - c, 0) + A_n,
29 *
30 * with A_n the number of MAP arrivals in the interval. (N_n, J_n) is therefore
31 * a Markov chain with transition blocks A_k = P_k(s), the MAP counting
32 * probabilities over an interval of exact length s. No residual service time
33 * has to be carried, which is what makes the deterministic case tractable where
34 * a general one would not be.
35 *
36 * The stationary law of that chain IS the time-stationary law of N. N(t)
37 * converges in distribution as t -> infinity (the system is non-lattice: the
38 * MAP has continuous interarrival times), so N(n s) converges to the same
39 * limit, and a positive recurrent Markov chain converges only to its own
40 * stationary law. No PASTA argument and no time-averaging step are involved --
41 * unlike qsys_mapd1, whose chain is embedded at DEPARTURE epochs and therefore
42 * needs the explicit inter-departure averaging of its step 4.
43 *
44 * FOUR STEPS.
45 * 1. A_k = P_k(s), k = 0..K, as the top block row of exp(M s) with M block
46 * bidiagonal carrying D0 on the diagonal and D1 above it. Shared with
47 * qsys_mapd1 (line::qsys::detail::map_counting_at). K grows from a Poisson
48 * tail estimate until the missing mass max_i (1 - sum_k (A_k e)_i) is under
49 * the tolerance, so the truncation is a measured quantity.
50 * 2. Grouping. The chain is skip-free to the left by c, not by 1, so c
51 * consecutive levels are grouped into one super-level: level n = L c + u
52 * becomes super-level L, sub-level u. Skip-freeness by one super-level then
53 * holds and the blocks are c m x c m with m the MAP order:
54 * repeating A^(i)[(u,v)] = A_{i c + v - u}, for i c + v - u >= 0
55 * boundary B^(i)[(u,v)] = A_{i c + v}, independent of u,
56 * the boundary row being the levels 0..c-1, from which max(n-c,0) = 0.
57 * 3. Ramaswami's recursion, exactly as in qsys_mapd1: G the minimal solution of
58 * G = sum_i A^(i) G^i, the partial sums Ahat_i = sum_{k>=i} A^(k) G^(k-i)
59 * and Bhat_i likewise, then
60 * x_0 (B^(0) + Bhat_1 (I - Ahat_1)^-1 A^(0)) = x_0,
61 * x_L = [x_0 Bhat_L + sum_{k=1}^{L-1} x_k Ahat_{L-k+1}] (I - Ahat_1)^-1.
62 * 4. Unfolding. Super-level L, sub-level u, phase j is the level L c + u, so
63 * P(N = n) is read off directly and E[N] = sum_n n P(N = n).
64 *
65 * meanWaitingTime is E[N]/lambda - s by Little's law and meanSojournTime is
66 * E[N]/lambda. This is exactly the definition the MATLAB reference adopted when
67 * its one-point quadrature was replaced; see the reference-defect note below.
68 *
69 * INVARIANTS THE TESTS ASSERT (both are identities of the construction, not
70 * fitted quantities):
71 * - sum_n P(N = n) = 1 up to the level-tail truncation;
72 * - sum_n min(n, c) P(N = n) = lambda s. Taking expectations in the recursion
73 * gives E[min(N,c)] = E[A] = lambda s, i.e. the mean number of departures
74 * per interval equals the mean number of arrivals. Equivalently the
75 * utilization is rho = lambda s / c, which is what the result reports.
76 *
77 * ORACLES.
78 * - c = 1 collapses to MAP/D/1 and is cross-checked against qsys_mapd1, which
79 * reaches the same numbers by the departure-epoch chain, a genuinely
80 * different construction.
81 * - Poisson arrivals and c = 1 collapse to M/D/1, where Pollaczek-Khinchin
82 * gives L = rho + rho^2/(2(1-rho)) and Wq = rho s/(2(1-rho)) exactly.
83 * - Poisson arrivals and c > 1 are checked against the MATLAB reference and
84 * against the M/D/c heavy-traffic ordering Wq(M/D/c) < Wq(M/M/c).
85 *
86 * ARITHMETIC. Gated on num_traits<T>::has_transcendental for two independent
87 * reasons, both inherited from qsys_mapd1: step 1 calls expm, which is a
88 * tolerance-controlled Pade approximation and cannot be exact in any
89 * arithmetic, and step 3 obtains G by a fixed-point iteration that does not
90 * terminate in a finite number of field operations. Steps 2 and 4 are finite
91 * exact matrix algebra and add no error of their own.
92 *
93 * meanWaitingTime is Little's law, meanQueueLength/lambda - s, as in MATLAB;
94 * the test asserts the M/D/1 instance D0 = [-2], D1 = [2], s = 1/3 against the
95 * Pollaczek-Khinchin value 1/3. Do not return to a one-point quadrature of the
96 * Q-MAM waiting-time survival function, which gives 0.444 there (+33%).
97 *
98 * meanQueueLength is the mean number in SYSTEM: qsys_mapdc.m computes
99 * rho = lambda s / c and returns the Q-MAM queue-length vector unchanged. The
100 * .m header documents the field as "Mean number of customers in system" while
101 * the inline comment on the Q-MAM output says "ql(i) = Prob[(i-1) customers in
102 * the queue]". The two readings differ by rho c, and only the system reading
103 * makes meanWaitingTime non-negative at high load, so it is the one implemented
104 * on both sides.
105 */
106
107#include <cmath>
108#include <cstddef>
109#include <string>
110#include <vector>
111
113#include "line/api/mam/qbd_r.h"
115#include "line/num/number.h"
116#include "line/util/error.h"
117#include "line/util/expm.h"
118#include "line/util/linalg.h"
119#include "line/util/lu.h"
120#include "line/util/matrix.h"
121
122namespace line {
123namespace qsys {
124
125/** Return value of qsys_mapdc, mirroring the MATLAB result struct. */
126template <class T>
128 T meanQueueLength; ///< E[N], number in system, time-stationary
129 T meanWaitingTime; ///< Wq = E[N]/lambda - s
130 T meanSojournTime; ///< W = Wq + s = E[N]/lambda
131 T utilization; ///< rho = lambda s / c, per server
132 std::vector<T> queueLengthDist; ///< P(N = n), n = 0, 1, ...
133 /**
134 * Waiting time CDF at the points {0, s/numSteps, 2 s/numSteps, ...}, the
135 * reference's Q-MAM `w` output (Q_CT_MAP_D_C), laid out as MATLAB's
136 * reshape(w, 1, []) of its numSteps x nw table.
137 */
138 std::vector<T> waitingTimeDist;
139 std::string analyzer; ///< the reference's 'Q-MAM:MAP/D/c'
140};
141
142/**
143 * MAP/D/c by Crommelin's exact embedded lattice chain.
144 *
145 * @param arrival arrival MAP (D0, D1)
146 * @param s deterministic service time, s > 0
147 * @param c number of servers, c >= 1
148 * @param dist_size how many entries of queueLengthDist to materialize
149 * @param max_arrivals cap on K, the number of arrivals per interval tracked
150 * @param max_levels cap on the number of super-levels generated
151 * @param tol tolerance on the counting-mass truncation, on the G
152 * iteration and on the level-tail truncation
153 * @param num_steps the reference's numSteps: waitingTimeDist is evaluated
154 * at {0, s/numSteps, 2 s/numSteps, ...} (default 1)
155 */
156template <class T>
157MapDcResult<T> qsys_mapdc(const mam::Map<T>& arrival, const T& s, unsigned c,
158 std::size_t dist_size, unsigned max_arrivals, std::size_t max_levels,
159 const T& tol, std::size_t num_steps) {
161 "qsys_mapdc requires transcendental arithmetic");
162 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
163 if (s <= zero) throw InputError("qsys_mapdc: service time s must be positive");
164 if (c == 0) throw InputError("qsys_mapdc: at least one server is required");
165 if (dist_size == 0) throw InputError("qsys_mapdc: dist_size must be positive");
166 if (max_levels == 0) throw InputError("qsys_mapdc: max_levels must be positive");
167 const std::size_t m = arrival.D0.rows();
168 if (arrival.D0.cols() != m || arrival.D1.rows() != m || arrival.D1.cols() != m)
169 throw InputError("qsys_mapdc: D0 and D1 must be square and of equal order");
170
171 const Matrix<T>& D0 = arrival.D0;
172 const Matrix<T>& D1 = arrival.D1;
173 const T lambda = mam::map_lambda(arrival);
174 if (lambda <= zero) throw InputError("qsys_mapdc: non-positive arrival rate");
175 const T cT = num_traits<T>::from_int(static_cast<long>(c));
176 const T rho = lambda * s / cT;
177 if (rho >= one) throw InputError("qsys_mapdc: load rho must be strictly less than 1");
178
179 // ---- step 1: MAP counting probabilities over one interval of length s ----
180 T numax = zero;
181 for (std::size_t i = 0; i < m; ++i) {
182 const T d = -D0(i, i);
183 if (d > numax) numax = d;
184 }
185 const T est = numax * s;
186 unsigned K = 8u;
187 {
188 const double e = num_traits<T>::to_double(est);
189 const unsigned guess = static_cast<unsigned>(2.0 * e + 10.0 * std::sqrt(e + 1.0) + 8.0);
190 if (guess > K) K = guess;
191 }
192 if (K > max_arrivals) K = max_arrivals;
193 std::vector<Matrix<T>> A = detail::map_counting_at(D0, D1, s, K);
194 while (true) {
195 T missing = zero;
196 for (std::size_t i = 0; i < m; ++i) {
197 T row = zero;
198 for (std::size_t k = 0; k < A.size(); ++k)
199 for (std::size_t j = 0; j < m; ++j) row += A[k](i, j);
200 const T lack = one - row;
201 if (lack > missing) missing = lack;
202 }
203 if (missing <= tol || K >= max_arrivals) break;
204 K = (2u * K < max_arrivals) ? 2u * K : max_arrivals;
205 A = detail::map_counting_at(D0, D1, s, K);
206 }
207 const std::size_t Kmax = A.size() - 1;
208
209 // ---- step 2: group c consecutive levels into one super-level ----
210 // Ag[i][(u,j),(v,j')] = A_{i c + v - u}, Bg[i][(u,j),(v,j')] = A_{i c + v}.
211 const std::size_t cs = static_cast<std::size_t>(c);
212 const std::size_t nb = cs * m; // super-level block order
213 std::size_t Kg = (Kmax + cs - 1) / cs + 1; // top super-level index used
214 if (Kg < 2) Kg = 2; // the Horner sweeps below need at least A^(0), A^(1), A^(2)
215 std::vector<Matrix<T>> Ag(Kg + 1, Matrix<T>(nb, nb, zero));
216 std::vector<Matrix<T>> Bg(Kg + 1, Matrix<T>(nb, nb, zero));
217 for (std::size_t i = 0; i <= Kg; ++i) {
218 for (std::size_t u = 0; u < cs; ++u) {
219 for (std::size_t v = 0; v < cs; ++v) {
220 const long ka = static_cast<long>(i * cs + v) - static_cast<long>(u);
221 if (ka >= 0 && static_cast<std::size_t>(ka) <= Kmax) {
222 const Matrix<T>& blk = A[static_cast<std::size_t>(ka)];
223 for (std::size_t p = 0; p < m; ++p)
224 for (std::size_t q = 0; q < m; ++q)
225 Ag[i](u * m + p, v * m + q) = blk(p, q);
226 }
227 const std::size_t kb = i * cs + v;
228 if (kb <= Kmax) {
229 const Matrix<T>& blk = A[kb];
230 for (std::size_t p = 0; p < m; ++p)
231 for (std::size_t q = 0; q < m; ++q)
232 Bg[i](u * m + p, v * m + q) = blk(p, q);
233 }
234 }
235 }
236 }
237
238 // ---- step 3: Ramaswami's recursion on the grouped chain ----
239 Matrix<T> G(nb, nb, zero);
240 const unsigned gmax = 100000u;
241 for (unsigned it = 0; it < gmax; ++it) {
242 // S = sum_{i=2}^{Kg} A^(i) G^(i-2) by Horner, so sum_{i>=2} A^(i) G^(i-1) = S G.
243 Matrix<T> S = Ag[Kg];
244 for (std::size_t i = Kg; i-- > 2;) S = mam::qbd_detail::madd(Ag[i], matmul(S, G));
245 const Matrix<T> U = mam::qbd_detail::madd(Ag[1], matmul(S, G));
246 const Matrix<T> ImU = mam::qbd_detail::msub(eye<T>(nb), U);
247 const Matrix<T> Gn = matmul(inverse(ImU), Ag[0]);
248 T gap = zero;
249 for (std::size_t i = 0; i < nb; ++i)
250 for (std::size_t j = 0; j < nb; ++j) {
251 const T d = num_abs(T(Gn(i, j) - G(i, j)));
252 if (d > gap) gap = d;
253 }
254 G = Gn;
255 if (gap <= tol) break;
256 }
257
258 std::vector<Matrix<T>> Ahat(Kg + 2), Bhat(Kg + 2);
259 Ahat[Kg + 1] = Matrix<T>(nb, nb, zero);
260 Bhat[Kg + 1] = Matrix<T>(nb, nb, zero);
261 for (std::size_t i = Kg + 1; i-- > 0;) {
262 Ahat[i] = mam::qbd_detail::madd(Ag[i], matmul(Ahat[i + 1], G));
263 Bhat[i] = mam::qbd_detail::madd(Bg[i], matmul(Bhat[i + 1], G));
264 }
265
266 const Matrix<T> W = inverse(mam::qbd_detail::msub(eye<T>(nb), Ahat[1]));
267 Matrix<T> M0 = mam::qbd_detail::madd(Bg[0], matmul(matmul(Bhat[1], W), Ag[0]));
268 for (std::size_t i = 0; i < nb; ++i) M0(i, i) -= one;
269 std::vector<std::vector<T>> x;
270 x.push_back(mam::qbd_detail::statvec(M0));
271
272 T mass = zero;
273 for (const T& v : x[0]) mass += v;
274 for (std::size_t lvl = 1; lvl <= max_levels; ++lvl) {
275 std::vector<T> acc(nb, zero);
276 if (lvl <= Kg) {
277 const std::vector<T> t = vecmul(x[0], Bhat[lvl]);
278 for (std::size_t j = 0; j < nb; ++j) acc[j] += t[j];
279 }
280 const std::size_t kmin = (lvl + 1 > Kg) ? (lvl + 1 - Kg) : 1;
281 for (std::size_t k = kmin; k + 1 <= lvl; ++k) {
282 const std::vector<T> t = vecmul(x[k], Ahat[lvl - k + 1]);
283 for (std::size_t j = 0; j < nb; ++j) acc[j] += t[j];
284 }
285 const std::vector<T> xn = vecmul(acc, W);
286 T inc = zero;
287 for (const T& v : xn) inc += v;
288 x.push_back(xn);
289 mass += inc;
290 if (inc <= tol * mass) break;
291 }
292 for (std::vector<T>& row : x)
293 for (T& v : row) v /= mass;
294
295 // ---- step 4: unfold the super-levels into the level distribution ----
297 r.queueLengthDist.assign(dist_size, zero);
298 T L = zero;
299 for (std::size_t lvl = 0; lvl < x.size(); ++lvl) {
300 for (std::size_t u = 0; u < cs; ++u) {
301 T pn = zero;
302 for (std::size_t j = 0; j < m; ++j) pn += x[lvl][u * m + j];
303 const std::size_t n = lvl * cs + u;
304 L += num_traits<T>::from_int(static_cast<long>(n)) * pn;
305 if (n < dist_size) r.queueLengthDist[n] = pn;
306 }
307 }
308
309 r.meanQueueLength = L;
310 r.meanSojournTime = L / lambda;
311 // waiting-time clamp rationale: see _kb/03-api-layer.md (cpp port notes: qsys)
312 const T wq = r.meanSojournTime - s;
313 r.meanWaitingTime = (wq > zero) ? wq : zero;
314 r.utilization = rho;
315
316 // ---- waitingTimeDist: the reference's Q-MAM w from the lattice chain's
317 // stationary law, level n = L c + u, phase j, read off the super-levels.
318 {
319 std::vector<std::vector<T>> pi(x.size() * cs, std::vector<T>(m, zero));
320 for (std::size_t lvl = 0; lvl < x.size(); ++lvl)
321 for (std::size_t u = 0; u < cs; ++u)
322 for (std::size_t j = 0; j < m; ++j) pi[lvl * cs + u][j] = x[lvl][u * m + j];
323 const std::vector<T> thetaA = mam::map_prob(arrival);
324 r.waitingTimeDist = detail::mapd_waiting_time(pi, m, cs, D0, D1, s,
325 num_steps == 0 ? 1 : num_steps, thetaA,
326 max_arrivals, tol);
327 }
328 r.analyzer = "Q-MAM:MAP/D/" + std::to_string(c);
329 return r;
330}
331
332/** qsys_mapdc with the reference's numSteps = 1. */
333template <class T>
334MapDcResult<T> qsys_mapdc(const mam::Map<T>& arrival, const T& s, unsigned c,
335 std::size_t dist_size, unsigned max_arrivals, std::size_t max_levels,
336 const T& tol) {
337 return qsys_mapdc(arrival, s, c, dist_size, max_arrivals, max_levels, tol,
338 static_cast<std::size_t>(1));
339}
340
341/**
342 * qsys_mapdc with 100 materialized levels, an arrival-count cap of 4096, a
343 * super-level cap of 20000 and tolerance 1e-14, matching the qsys_mapd1
344 * defaults.
345 */
346template <class T>
347MapDcResult<T> qsys_mapdc(const mam::Map<T>& arrival, const T& s, unsigned c) {
348 return qsys_mapdc(arrival, s, c, static_cast<std::size_t>(100), 4096u,
349 static_cast<std::size_t>(20000), T(num_traits<T>::from_double(1e-14)));
350}
351
352} // namespace qsys
353} // namespace line
354
355#endif // LINE_API_QSYS_QSYS_MAPDC_H
InputError(const std::string &what)
Definition error.h:105
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.
std::vector< T > map_prob(const Map< T > &m)
Stationary distribution of the phase process, pi (D0 + D1) = 0.
Definition map_moment.h:73
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
MapDcResult< T > qsys_mapdc(const mam::Map< T > &arrival, const T &s, unsigned c, std::size_t dist_size, unsigned max_arrivals, std::size_t max_levels, const T &tol, std::size_t num_steps)
MAP/D/c by Crommelin's exact embedded lattice chain.
Definition qsys_mapdc.h:157
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_abs(const T &v)
Definition number.h:184
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
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,...
The MAP/D/1 FCFS queue: deterministic service of length s fed by a Markovian arrival process.
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_mapdc, mirroring the MATLAB result struct.
Definition qsys_mapdc.h:127
T meanWaitingTime
Wq = E[N]/lambda - s.
Definition qsys_mapdc.h:129
T meanQueueLength
E[N], number in system, time-stationary.
Definition qsys_mapdc.h:128
std::string analyzer
the reference's 'Q-MAM:MAP/D/c'
Definition qsys_mapdc.h:139
T meanSojournTime
W = Wq + s = E[N]/lambda.
Definition qsys_mapdc.h:130
std::vector< T > queueLengthDist
P(N = n), n = 0, 1, ...
Definition qsys_mapdc.h:132
std::vector< T > waitingTimeDist
Waiting time CDF at the points {0, s/numSteps, 2 s/numSteps, ...}, the reference's Q-MAM w output (Q_...
Definition qsys_mapdc.h:138
T utilization
rho = lambda s / c, per server
Definition qsys_mapdc.h:131