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 * REFERENCE DEFECTS.
94 * - FIXED IN MATLAB, and this port matches the fixed behaviour.
95 * qsys_mapdc.m used to return meanWaitingTime as a one-point left-rectangle
96 * quadrature of the Q-MAM waiting-time survival function (numSteps
97 * defaulting to 1). On the M/D/1 instance D0 = [-2], D1 = [2], s = 1/3 it
98 * returned 0.4444444443 against the exact Pollaczek-Khinchin value 1/3, an
99 * error of +33%. It now uses Little's law, meanQueueLength/lambda - s. This
100 * port reproduces the FIXED behaviour and is asserted against 1/3, never
101 * against the superseded number.
102 * - STILL OPEN. qsys_mapdc.m computes the utilization as rho = lambda s / c
103 * and returns the Q-MAM queue-length vector unchanged, so meanQueueLength is
104 * the mean number in SYSTEM; the Little's law line is consistent with that
105 * reading. No defect is claimed here, but note that the field is documented
106 * as "Mean number of customers in system" while the inline comment on the
107 * Q-MAM output says "ql(i) = Prob[(i-1) customers in the queue]". The two
108 * readings differ by rho c, and only the system reading makes
109 * meanWaitingTime non-negative at high load, so the system reading is the
110 * one implemented on both sides.
111 */
112
113#include <cmath>
114#include <cstddef>
115#include <vector>
116
118#include "line/api/mam/qbd_r.h"
120#include "line/num/number.h"
121#include "line/util/error.h"
122#include "line/util/expm.h"
123#include "line/util/linalg.h"
124#include "line/util/lu.h"
125#include "line/util/matrix.h"
126
127namespace line {
128namespace qsys {
129
130/** Return value of qsys_mapdc, mirroring the MATLAB result struct. */
131template <class T>
133 T meanQueueLength; ///< E[N], number in system, time-stationary
134 T meanWaitingTime; ///< Wq = E[N]/lambda - s
135 T meanSojournTime; ///< W = Wq + s = E[N]/lambda
136 T utilization; ///< rho = lambda s / c, per server
137 std::vector<T> queueLengthDist; ///< P(N = n), n = 0, 1, ...
138};
139
140/**
141 * MAP/D/c by Crommelin's exact embedded lattice chain.
142 *
143 * @param arrival arrival MAP (D0, D1)
144 * @param s deterministic service time, s > 0
145 * @param c number of servers, c >= 1
146 * @param dist_size how many entries of queueLengthDist to materialize
147 * @param max_arrivals cap on K, the number of arrivals per interval tracked
148 * @param max_levels cap on the number of super-levels generated
149 * @param tol tolerance on the counting-mass truncation, on the G
150 * iteration and on the level-tail truncation
151 */
152template <class T>
153MapDcResult<T> qsys_mapdc(const mam::Map<T>& arrival, const T& s, unsigned c,
154 std::size_t dist_size, unsigned max_arrivals, std::size_t max_levels,
155 const T& tol) {
157 "qsys_mapdc requires transcendental arithmetic");
158 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
159 if (s <= zero) throw InputError("qsys_mapdc: service time s must be positive");
160 if (c == 0) throw InputError("qsys_mapdc: at least one server is required");
161 if (dist_size == 0) throw InputError("qsys_mapdc: dist_size must be positive");
162 if (max_levels == 0) throw InputError("qsys_mapdc: max_levels must be positive");
163 const std::size_t m = arrival.D0.rows();
164 if (arrival.D0.cols() != m || arrival.D1.rows() != m || arrival.D1.cols() != m)
165 throw InputError("qsys_mapdc: D0 and D1 must be square and of equal order");
166
167 const Matrix<T>& D0 = arrival.D0;
168 const Matrix<T>& D1 = arrival.D1;
169 const T lambda = mam::map_lambda(arrival);
170 if (lambda <= zero) throw InputError("qsys_mapdc: non-positive arrival rate");
171 const T cT = num_traits<T>::from_int(static_cast<long>(c));
172 const T rho = lambda * s / cT;
173 if (rho >= one) throw InputError("qsys_mapdc: load rho must be strictly less than 1");
174
175 // ---- step 1: MAP counting probabilities over one interval of length s ----
176 T numax = zero;
177 for (std::size_t i = 0; i < m; ++i) {
178 const T d = -D0(i, i);
179 if (d > numax) numax = d;
180 }
181 const T est = numax * s;
182 unsigned K = 8u;
183 {
184 const double e = num_traits<T>::to_double(est);
185 const unsigned guess = static_cast<unsigned>(2.0 * e + 10.0 * std::sqrt(e + 1.0) + 8.0);
186 if (guess > K) K = guess;
187 }
188 if (K > max_arrivals) K = max_arrivals;
189 std::vector<Matrix<T>> A = detail::map_counting_at(D0, D1, s, K);
190 while (true) {
191 T missing = zero;
192 for (std::size_t i = 0; i < m; ++i) {
193 T row = zero;
194 for (std::size_t k = 0; k < A.size(); ++k)
195 for (std::size_t j = 0; j < m; ++j) row += A[k](i, j);
196 const T lack = one - row;
197 if (lack > missing) missing = lack;
198 }
199 if (missing <= tol || K >= max_arrivals) break;
200 K = (2u * K < max_arrivals) ? 2u * K : max_arrivals;
201 A = detail::map_counting_at(D0, D1, s, K);
202 }
203 const std::size_t Kmax = A.size() - 1;
204
205 // ---- step 2: group c consecutive levels into one super-level ----
206 // Ag[i][(u,j),(v,j')] = A_{i c + v - u}, Bg[i][(u,j),(v,j')] = A_{i c + v}.
207 const std::size_t cs = static_cast<std::size_t>(c);
208 const std::size_t nb = cs * m; // super-level block order
209 std::size_t Kg = (Kmax + cs - 1) / cs + 1; // top super-level index used
210 if (Kg < 2) Kg = 2; // the Horner sweeps below need at least A^(0), A^(1), A^(2)
211 std::vector<Matrix<T>> Ag(Kg + 1, Matrix<T>(nb, nb, zero));
212 std::vector<Matrix<T>> Bg(Kg + 1, Matrix<T>(nb, nb, zero));
213 for (std::size_t i = 0; i <= Kg; ++i) {
214 for (std::size_t u = 0; u < cs; ++u) {
215 for (std::size_t v = 0; v < cs; ++v) {
216 const long ka = static_cast<long>(i * cs + v) - static_cast<long>(u);
217 if (ka >= 0 && static_cast<std::size_t>(ka) <= Kmax) {
218 const Matrix<T>& blk = A[static_cast<std::size_t>(ka)];
219 for (std::size_t p = 0; p < m; ++p)
220 for (std::size_t q = 0; q < m; ++q)
221 Ag[i](u * m + p, v * m + q) = blk(p, q);
222 }
223 const std::size_t kb = i * cs + v;
224 if (kb <= Kmax) {
225 const Matrix<T>& blk = A[kb];
226 for (std::size_t p = 0; p < m; ++p)
227 for (std::size_t q = 0; q < m; ++q)
228 Bg[i](u * m + p, v * m + q) = blk(p, q);
229 }
230 }
231 }
232 }
233
234 // ---- step 3: Ramaswami's recursion on the grouped chain ----
235 Matrix<T> G(nb, nb, zero);
236 const unsigned gmax = 100000u;
237 for (unsigned it = 0; it < gmax; ++it) {
238 // S = sum_{i=2}^{Kg} A^(i) G^(i-2) by Horner, so sum_{i>=2} A^(i) G^(i-1) = S G.
239 Matrix<T> S = Ag[Kg];
240 for (std::size_t i = Kg; i-- > 2;) S = mam::qbd_detail::madd(Ag[i], matmul(S, G));
241 const Matrix<T> U = mam::qbd_detail::madd(Ag[1], matmul(S, G));
242 const Matrix<T> ImU = mam::qbd_detail::msub(eye<T>(nb), U);
243 const Matrix<T> Gn = matmul(inverse(ImU), Ag[0]);
244 T gap = zero;
245 for (std::size_t i = 0; i < nb; ++i)
246 for (std::size_t j = 0; j < nb; ++j) {
247 const T d = num_abs(T(Gn(i, j) - G(i, j)));
248 if (d > gap) gap = d;
249 }
250 G = Gn;
251 if (gap <= tol) break;
252 }
253
254 std::vector<Matrix<T>> Ahat(Kg + 2), Bhat(Kg + 2);
255 Ahat[Kg + 1] = Matrix<T>(nb, nb, zero);
256 Bhat[Kg + 1] = Matrix<T>(nb, nb, zero);
257 for (std::size_t i = Kg + 1; i-- > 0;) {
258 Ahat[i] = mam::qbd_detail::madd(Ag[i], matmul(Ahat[i + 1], G));
259 Bhat[i] = mam::qbd_detail::madd(Bg[i], matmul(Bhat[i + 1], G));
260 }
261
262 const Matrix<T> W = inverse(mam::qbd_detail::msub(eye<T>(nb), Ahat[1]));
263 Matrix<T> M0 = mam::qbd_detail::madd(Bg[0], matmul(matmul(Bhat[1], W), Ag[0]));
264 for (std::size_t i = 0; i < nb; ++i) M0(i, i) -= one;
265 std::vector<std::vector<T>> x;
266 x.push_back(mam::qbd_detail::statvec(M0));
267
268 T mass = zero;
269 for (const T& v : x[0]) mass += v;
270 for (std::size_t lvl = 1; lvl <= max_levels; ++lvl) {
271 std::vector<T> acc(nb, zero);
272 if (lvl <= Kg) {
273 const std::vector<T> t = vecmul(x[0], Bhat[lvl]);
274 for (std::size_t j = 0; j < nb; ++j) acc[j] += t[j];
275 }
276 const std::size_t kmin = (lvl + 1 > Kg) ? (lvl + 1 - Kg) : 1;
277 for (std::size_t k = kmin; k + 1 <= lvl; ++k) {
278 const std::vector<T> t = vecmul(x[k], Ahat[lvl - k + 1]);
279 for (std::size_t j = 0; j < nb; ++j) acc[j] += t[j];
280 }
281 const std::vector<T> xn = vecmul(acc, W);
282 T inc = zero;
283 for (const T& v : xn) inc += v;
284 x.push_back(xn);
285 mass += inc;
286 if (inc <= tol * mass) break;
287 }
288 for (std::vector<T>& row : x)
289 for (T& v : row) v /= mass;
290
291 // ---- step 4: unfold the super-levels into the level distribution ----
293 r.queueLengthDist.assign(dist_size, zero);
294 T L = zero;
295 for (std::size_t lvl = 0; lvl < x.size(); ++lvl) {
296 for (std::size_t u = 0; u < cs; ++u) {
297 T pn = zero;
298 for (std::size_t j = 0; j < m; ++j) pn += x[lvl][u * m + j];
299 const std::size_t n = lvl * cs + u;
300 L += num_traits<T>::from_int(static_cast<long>(n)) * pn;
301 if (n < dist_size) r.queueLengthDist[n] = pn;
302 }
303 }
304
305 r.meanQueueLength = L;
306 r.meanSojournTime = L / lambda;
307 // waiting-time clamp rationale: see _kb/03-api-layer.md (cpp port notes: qsys)
308 const T wq = r.meanSojournTime - s;
309 r.meanWaitingTime = (wq > zero) ? wq : zero;
310 r.utilization = rho;
311 return r;
312}
313
314/**
315 * qsys_mapdc with 100 materialized levels, an arrival-count cap of 4096, a
316 * super-level cap of 20000 and tolerance 1e-14, matching the qsys_mapd1
317 * defaults.
318 */
319template <class T>
320MapDcResult<T> qsys_mapdc(const mam::Map<T>& arrival, const T& s, unsigned c) {
321 return qsys_mapdc(arrival, s, c, static_cast<std::size_t>(100), 4096u,
322 static_cast<std::size_t>(20000), T(num_traits<T>::from_double(1e-14)));
323}
324
325} // namespace qsys
326} // namespace line
327
328#endif // LINE_API_QSYS_QSYS_MAPDC_H
InputError(const std::string &what)
Definition error.h:39
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
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)
MAP/D/c by Crommelin's exact embedded lattice chain.
Definition qsys_mapdc.h:153
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
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:132
T meanWaitingTime
Wq = E[N]/lambda - s.
Definition qsys_mapdc.h:134
T meanQueueLength
E[N], number in system, time-stationary.
Definition qsys_mapdc.h:133
T meanSojournTime
W = Wq + s = E[N]/lambda.
Definition qsys_mapdc.h:135
std::vector< T > queueLengthDist
P(N = n), n = 0, 1, ...
Definition qsys_mapdc.h:137
T utilization
rho = lambda s / c, per server
Definition qsys_mapdc.h:136