LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qbd_depproc.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_MAM_QBD_DEPPROC_H
6#define LINE_API_MAM_QBD_DEPPROC_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Departure process of a MAP/MAP/1 queue: the ETAQA-truncated MAP descriptor
12 * under FCFS and under PS, and the joint moments of consecutive
13 * inter-departure times.
14 *
15 * Templated port of matlab/src/api/mam/qbd_depproc_etaqa.m,
16 * qbd_depproc_etaqa_ps.m and qbd_depproc_jointmom.m.
17 *
18 * The queue is the usual MAP/MAP/1 QBD, level = number in system, phase =
19 * (arrival phase, service phase) with the arrival phase major:
20 *
21 * F = D1^a (x) I_ns L = D0^a (+) D0^s
22 * B = I_na (x) D1^s L0 = D0^a (x) I_ns
23 *
24 * ETAQA keeps levels 0..n-1 explicitly and lumps every level from n upwards
25 * into a single aggregate block, using G to describe how the aggregate returns
26 * to level n-1: Bbar = B + F G and Bhat = F G, with Lhat = F + L. The
27 * descriptor has (n+1) blocks of order na*ns; a transition that carries a
28 * departure goes into D1 and everything else into D0.
29 *
30 * ARITHMETIC. All three entry points call qbd_fundmat for R and G, so they are
31 * gated on num_traits<T>::has_transcendental; see qbd_r.h for why cyclic
32 * reduction cannot be exact. Everything downstream of R and G is a finite
33 * sequence of matrix products and inverses.
34 *
35 * REFERENCE DEFECTS (reproduced here verbatim, see the tests).
36 *
37 * 1. qbd_depproc_etaqa returns a pair (D0, D1) that is NOT a conservative
38 * generator: (D0 + D1) e is nonzero in the last two block rows, so the
39 * result is not a MAP and map_lambda / map_prob applied to it are
40 * meaningless. Two independent causes, both an off-by-one in the block
41 * index arithmetic:
42 * a) the line
43 * D0(((n-1)*lvlsz+1):n*lvlsz, ((n-1)*lvlsz+1):n*lvlsz) = Lhat
44 * addresses the FULL padded matrix, whose block rows are 0..n after
45 * the two zero paddings, so it writes Lhat = F + L into block
46 * (n-1, n-1), the last EXPLICIT level, and not into block (n, n), the
47 * aggregate. Block (n-1, n-1) already has the up-block F sitting at
48 * (n-1, n), so that row acquires F twice and its row sum becomes F e
49 * instead of 0, while the aggregate keeps a bare L.
50 * b) the aggregate row carries both Bbar = B + F G at (n, n-1) and
51 * Bhat = F G at (n, n), so F G is counted twice there; the row sum is
52 * F G e = F e rather than 0.
53 * Measured on the M/M/1 instance of the test (lambda = 0.6, mu = 1, n = 4):
54 * ||(D0 + D1) e||_inf = 0.6 = lambda, concentrated in exactly those two
55 * rows and zero everywhere else.
56 *
57 * 2. qbd_depproc_etaqa_ps additionally puts the FULL down-block Bbar at
58 * (n, n-1) into BOTH D0 and D1, instead of splitting it (1 - 1/n) / (1/n)
59 * the way it splits B at every explicit level and Bhat at the aggregate.
60 * The aggregate therefore fires that transition at twice its rate, once as
61 * a departure and once silently, and (D0 + D1) e picks up a further Bbar e.
62 * It also divides by j at level j starting from j = 1, so the level-1 row
63 * gets B * (1 - 1/1) = 0 for the non-departure part, which is right, but
64 * the loop stops at n-1 and never treats the boundary the same way.
65 *
66 * The port does NOT repair either: these functions are the reference for the
67 * JAR and Python ports and a silent divergence would be worse than a
68 * reproduced defect. qbd_depproc_etaqa_residual below measures ||(D0+D1)e||_inf
69 * so a caller can see it, and the tests pin the measured value.
70 */
71
72#include <cstddef>
73#include <utility>
74#include <vector>
75
79#include "line/api/mam/qbd_r.h"
80#include "line/num/number.h"
81#include "line/util/error.h"
82#include "line/util/linalg.h"
83#include "line/util/matrix.h"
84
85namespace line {
86namespace mam {
87
88namespace depproc_detail {
89
90/** Copy src into dest at block position (br, bc) of blocks of size sz. */
91template <class T>
92void put_block(Matrix<T>& dest, std::size_t br, std::size_t bc, const Matrix<T>& src) {
93 const std::size_t r0 = br * src.rows(), c0 = bc * src.cols();
94 if (r0 + src.rows() > dest.rows() || c0 + src.cols() > dest.cols())
95 throw InputError("qbd_depproc: block write out of range");
96 for (std::size_t i = 0; i < src.rows(); ++i)
97 for (std::size_t j = 0; j < src.cols(); ++j) dest(r0 + i, c0 + j) = src(i, j);
98}
99
100/** Add src into dest at block position (br, bc). */
101template <class T>
102void add_block(Matrix<T>& dest, std::size_t br, std::size_t bc, const Matrix<T>& src) {
103 const std::size_t r0 = br * src.rows(), c0 = bc * src.cols();
104 if (r0 + src.rows() > dest.rows() || c0 + src.cols() > dest.cols())
105 throw InputError("qbd_depproc: block write out of range");
106 for (std::size_t i = 0; i < src.rows(); ++i)
107 for (std::size_t j = 0; j < src.cols(); ++j) dest(r0 + i, c0 + j) += src(i, j);
108}
109
110/** The ETAQA pieces shared by the FCFS and the PS construction. */
111template <class T>
112struct EtaqaPieces {
113 Matrix<T> F, L, B, L0;
114 Matrix<T> R, G;
115 Matrix<T> Lhat; ///< F + L
116 Matrix<T> Bbar; ///< B + F G
117 Matrix<T> Bhat; ///< F G
118 std::size_t lvlsz;
119};
120
121template <class T>
122EtaqaPieces<T> etaqa_pieces(const Map<T>& arrival, const Map<T>& service) {
123 using namespace qbd_detail;
124 const QbdMapMap1Blocks<T> blk = qbd_mapmap1_blocks(arrival, service);
125 EtaqaPieces<T> p;
126 p.F = blk.F;
127 p.L = blk.L;
128 p.B = blk.B;
129 p.L0 = blk.Lbar;
130 p.lvlsz = blk.L.rows();
131 const QbdFundMat<T> fm = qbd_fundmat(p.B, p.L, p.F);
132 p.R = fm.R;
133 // G recomputation rationale: see _kb/03-api-layer.md (cpp port notes: mam)
134 p.G = matmul(inverse(msub(mscale(p.L, T(num_traits<T>::from_int(-1))), matmul(p.R, p.B))), p.B);
135 p.Lhat = madd(p.F, p.L);
136 p.Bbar = madd(p.B, matmul(p.F, p.G));
137 p.Bhat = matmul(p.F, p.G);
138 return p;
139}
140
141} // namespace depproc_detail
142
143/**
144 * MAP descriptor of the departure process of a MAP/MAP/1-FCFS queue,
145 * ETAQA-truncated at level n (qbd_depproc_etaqa.m).
146 *
147 * @param n number of explicitly represented levels; the descriptor has n+1
148 * blocks of order na*ns, the last one being the aggregate
149 * @param arrival the arrival MAP
150 * @param service the service MAP
151 */
152template <class T>
153Map<T> qbd_depproc_etaqa(const Map<T>& arrival, const Map<T>& service, std::size_t n) {
155 "qbd_depproc_etaqa requires transcendental arithmetic");
156 if (n < 2) throw InputError("qbd_depproc_etaqa: the truncation level must be at least 2");
157 using namespace depproc_detail;
158 const EtaqaPieces<T> p = etaqa_pieces(arrival, service);
159 const std::size_t m = p.lvlsz;
160 const std::size_t dim = (n + 1) * m;
161 const T zero = num_traits<T>::from_int(0);
162
163 Matrix<T> D0(dim, dim, zero);
164 // Diagonal L on blocks 1..n and superdiagonal F on blocks (1,2)..(n-1,n),
165 // which is the padded image of kron(I_n, L) + kron(superdiag, F).
166 for (std::size_t r = 1; r <= n; ++r) put_block(D0, r, r, p.L);
167 for (std::size_t r = 1; r + 1 <= n; ++r) put_block(D0, r, r + 1, p.F);
168 // Boundary row: [L0, F] over the first two block columns.
169 put_block(D0, 0, 0, p.L0);
170 put_block(D0, 0, 1, p.F);
171 // Reference defect 1a: Lhat lands on block (n-1, n-1), not on the aggregate.
172 put_block(D0, n - 1, n - 1, p.Lhat);
173
174 Matrix<T> D1(dim, dim, zero);
175 put_block(D1, n, n - 1, p.Bbar);
176 put_block(D1, n, n, p.Bhat);
177 // The leading n x n block region is overwritten with the plain subdiagonal
178 // B, which leaves the aggregate row and column untouched.
179 for (std::size_t r = 1; r + 1 <= n; ++r) put_block(D1, r, r - 1, p.B);
180
181 Map<T> out;
182 out.D0 = D0;
183 out.D1 = D1;
184 return out;
185}
186
187/**
188 * MAP descriptor of the departure process of a MAP/MAP/1-PS queue,
189 * ETAQA-truncated at level n (qbd_depproc_etaqa_ps.m).
190 *
191 * With j jobs sharing the server a completion is a departure of the tagged
192 * job with probability 1/j, so the down-block at level j splits into
193 * B * (1/j) into D1 and B * (1 - 1/j) into D0.
194 */
195template <class T>
196Map<T> qbd_depproc_etaqa_ps(const Map<T>& arrival, const Map<T>& service, std::size_t n) {
198 "qbd_depproc_etaqa_ps requires transcendental arithmetic");
199 if (n < 2) throw InputError("qbd_depproc_etaqa_ps: the truncation level must be at least 2");
200 using namespace depproc_detail;
201 using namespace qbd_detail;
202 const EtaqaPieces<T> p = etaqa_pieces(arrival, service);
203 const std::size_t m = p.lvlsz;
204 const std::size_t dim = (n + 1) * m;
205 const T zero = num_traits<T>::from_int(0);
206 const T one = num_traits<T>::from_int(1);
207 const T nT = num_traits<T>::from_int(static_cast<long>(n));
208
209 Matrix<T> D0(dim, dim, zero);
210 for (std::size_t r = 1; r <= n; ++r) put_block(D0, r, r, p.L);
211 for (std::size_t r = 1; r + 1 <= n; ++r) put_block(D0, r, r + 1, p.F);
212 put_block(D0, 0, 0, p.L0);
213 put_block(D0, 0, 1, p.F);
214 put_block(D0, n - 1, n - 1, p.Lhat);
215 // Reference defect 2: the full Bbar goes into D0 as well as into D1.
216 add_block(D0, n, n - 1, p.Bbar);
217 add_block(D0, n, n, mscale(p.Bhat, T((nT - one) / nT)));
218
219 Matrix<T> D1(dim, dim, zero);
220 put_block(D1, n, n - 1, p.Bbar);
221 put_block(D1, n, n, mscale(p.Bhat, T(one / nT)));
222
223 for (std::size_t j = 1; j + 1 <= n; ++j) {
224 const T jT = num_traits<T>::from_int(static_cast<long>(j));
225 put_block(D0, j, j - 1, mscale(p.B, T(one - one / jT)));
226 put_block(D1, j, j - 1, mscale(p.B, T(one / jT)));
227 }
228
229 Map<T> out;
230 out.D0 = D0;
231 out.D1 = D1;
232 return out;
233}
234
235/**
236 * ||(D0 + D1) e||_inf, zero for a genuine MAP. Exposed so a caller can see
237 * reference defects 1 and 2 rather than discovering them downstream.
238 */
239template <class T>
241 const std::size_t n = m.D0.rows();
242 T worst = num_traits<T>::from_int(0);
243 for (std::size_t i = 0; i < n; ++i) {
245 for (std::size_t j = 0; j < m.D0.cols(); ++j) s += m.D0(i, j) + m.D1(i, j);
246 const T a = num_abs(T(s));
247 if (a > worst) worst = a;
248 }
249 return worst;
250}
251
252/**
253 * Joint moments E[X_0^i X_1^j] of consecutive inter-departure times of a
254 * MAP/MAP/1-FCFS queue (qbd_depproc_jointmom.m).
255 *
256 * The initial vector is built from the level-0 vector of the QBD and the
257 * first three terms of the geometric tail,
258 *
259 * z = [v0 R F, v0 R^2 F, v0 R^3 (I-R)^-1 F] / lambda_s,
260 *
261 * normalized to a probability vector, and the moments follow from the
262 * three-level block operators
263 *
264 * M0 = [L0 F 0; 0 L F; 0 0 L+F], M1 = [0 0 0; B 0 0; 0 B 0]
265 *
266 * as JM = z i! (-M0)^{-i-1} M1 j! (-M0)^{-j} e.
267 *
268 * @param iset one (i, j) pair per requested moment
269 * @param arrival the arrival MAP
270 * @param service the service MAP
271 */
272template <class T>
273std::vector<T> qbd_depproc_jointmom(const Map<T>& arrival, const Map<T>& service,
274 const std::vector<std::pair<unsigned, unsigned>>& iset) {
276 "qbd_depproc_jointmom requires transcendental arithmetic");
277 using namespace depproc_detail;
278 using namespace qbd_detail;
279 const EtaqaPieces<T> p = etaqa_pieces(arrival, service);
280 const std::size_t m = p.lvlsz;
281 const T zero = num_traits<T>::from_int(0);
282
283 const Matrix<T> pi = qbd_pi(p.B, p.L0, p.R);
284 std::vector<T> v0(m);
285 for (std::size_t j = 0; j < m; ++j) v0[j] = pi(0, j);
286
287 const T lambdaS = map_lambda(service);
288 if (lambdaS == zero) throw NumericError("qbd_depproc_jointmom: zero service rate");
289 const T inv = num_traits<T>::from_int(1) / lambdaS;
290
291 const std::vector<T> v0R = vecmul(v0, p.R);
292 const std::vector<T> v0R2 = vecmul(v0R, p.R);
293 const std::vector<T> v0R3 = vecmul(v0R2, p.R);
294 const Matrix<T> ImRinv = inverse(msub(eye<T>(m), p.R));
295
296 // Departure epochs are the B transitions, so the embedded vector weighs the
297 // level probabilities by B and not by the arrival matrix F.
298 const std::vector<T> v0D = vecmul(v0R, p.B);
299 const std::vector<T> v1D = vecmul(v0R2, p.B);
300 const std::vector<T> v2Dp = vecmul(vecmul(v0R3, ImRinv), p.B);
301
302 std::vector<T> z(3 * m);
303 for (std::size_t j = 0; j < m; ++j) {
304 z[j] = inv * v0D[j];
305 z[m + j] = inv * v1D[j];
306 z[2 * m + j] = inv * v2Dp[j];
307 }
308 T zs = zero;
309 for (const T& v : z) zs += v;
310 if (zs == zero) throw NumericError("qbd_depproc_jointmom: degenerate initial vector");
311 for (T& v : z) v /= zs;
312
313 Matrix<T> M0(3 * m, 3 * m, zero), M1(3 * m, 3 * m, zero);
314 put_block(M0, 0, 0, p.L0);
315 put_block(M0, 0, 1, p.F);
316 put_block(M0, 1, 1, p.L);
317 put_block(M0, 1, 2, p.F);
318 put_block(M0, 2, 2, p.Lhat);
319 put_block(M1, 1, 0, p.B);
320 put_block(M1, 2, 1, p.B);
321
322 const Matrix<T> negM0inv = inverse(mscale(M0, T(num_traits<T>::from_int(-1))));
323 const std::vector<T> e = ones<T>(3 * m);
324
325 std::vector<T> out;
326 out.reserve(iset.size());
327 for (std::size_t k = 0; k < iset.size(); ++k) {
328 const unsigned i = iset[k].first, j = iset[k].second;
329 std::vector<T> row = vecmul(z, matpow(negM0inv, i + 1));
330 for (T& v : row) v *= num_factorial<T>(i);
331 row = vecmul(row, M1);
332 row = vecmul(row, matpow(negM0inv, j));
333 for (T& v : row) v *= num_factorial<T>(j);
334 T s = zero;
335 for (std::size_t q = 0; q < row.size(); ++q) s += row[q] * e[q];
336 out.push_back(s);
337 }
338 return out;
339}
340
341} // namespace mam
342} // namespace line
343
344#endif // LINE_API_MAM_QBD_DEPPROC_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
Marked MAP (MMAP) algebra: per-class rates, class probabilities, superposition, normalization and sca...
QbdMapMap1Blocks< T > qbd_mapmap1_blocks(const Map< T > &arrival, const Map< T > &service)
Level blocks of the MAP/MAP/1 QBD from the arrival and service MAPs.
Definition qbd_mapmap1.h:72
T qbd_depproc_residual(const Map< T > &m)
||(D0 + D1) e||_inf, zero for a genuine MAP.
Map< T > qbd_depproc_etaqa(const Map< T > &arrival, const Map< T > &service, std::size_t n)
MAP descriptor of the departure process of a MAP/MAP/1-FCFS queue, ETAQA-truncated at level n (qbd_de...
Map< T > qbd_depproc_etaqa_ps(const Map< T > &arrival, const Map< T > &service, std::size_t n)
MAP descriptor of the departure process of a MAP/MAP/1-PS queue, ETAQA-truncated at level n (qbd_depp...
QbdFundMat< T > qbd_fundmat(const Matrix< T > &B, const Matrix< T > &L, const Matrix< T > &F, unsigned iter_max, const T &tol)
G and R by cyclic reduction (qbd_fundmat.m, the Bini-Meini logarithmic reduction on the raw level blo...
Definition qbd_r.h:280
Matrix< T > qbd_pi(const Matrix< T > &B, const Matrix< T > &Lbar, const Matrix< T > &R, std::size_t max_levels, const T &mass_tol)
Stationary distribution of a QBD given R (QBD_pi.m, continuous-time branch, default boundary).
Definition qbd_r.h:412
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
std::vector< T > qbd_depproc_jointmom(const Map< T > &arrival, const Map< T > &service, const std::vector< std::pair< unsigned, unsigned > > &iset)
Joint moments E[X_0^i X_1^j] of consecutive inter-departure times of a MAP/MAP/1-FCFS queue (qbd_depp...
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
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 > matpow(const Matrix< T > &A, unsigned k)
Integer matrix power, by repeated squaring.
Definition linalg.h:89
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.
The MAP/MAP/1 queue solved as a quasi-birth-death process.
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