LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_bmapphnn_retrial.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_BMAPPHNN_RETRIAL_H
6#define LINE_API_QSYS_QSYS_BMAPPHNN_RETRIAL_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * The BMAP/PH/N/N bufferless retrial queue with flexible retrial admission
12 * control.
13 *
14 * Templated port of matlab/src/api/qsys/qsys_bmapphnn_retrial.m, which
15 * implements Dudin et al., "Analysis of BMAP/PH/N-Type Queueing System with
16 * Flexible Retrials Admission Control", Mathematics 2025, 13(9), 1434.
17 *
18 * THE MODEL. N servers and no waiting room. Arrivals come in batches from a
19 * BMAP (D0, D1, ..., DK) on V states; a batch that finds fewer than its size
20 * free servers fills what it can and the excess either joins the orbit, with
21 * probability 1 - p, or is lost, with probability p. Service is PH (beta, S)
22 * on M phases, so the server-side state with n busy servers is the multiset of
23 * their phases, of which there are T_n = C(n+M-1, M-1); the level of the
24 * generator is the orbit population and each level carries V * sum_n T_n
25 * states. Each orbiting customer retries at rate alpha and abandons at rate
26 * gamma, and a retrial SUCCEEDS only while n <= R(nu), which is the admission
27 * control: above the threshold the retrial finds the system closed and the
28 * customer stays in orbit.
29 *
30 * THE STATE SPACE IS A LEVEL-DEPENDENT QBD and the reference does not solve it
31 * as one: it truncates the orbit at a finite level, assembles the whole
32 * generator densely, and solves pi Q = 0 with the last column replaced by the
33 * normalization. This port does the same, so that the two agree, including on
34 * the truncation. See the two reference defects below, which are both about
35 * that truncation.
36 *
37 * WHAT THE PORT CHANGES, AND WHY IT DOES NOT CHANGE THE ANSWER. The reference
38 * rebuilds B, B_bar, Gamma and B_tilde inside buildGeneratorLevel, i.e. once
39 * per (i, j) block pair, although none of them depends on i or j; on the
40 * default truncation that is over a hundred rebuilds of the same matrices.
41 * They are hoisted here. The generator entries are identical.
42 *
43 * REFERENCE DEFECTS in qsys_bmapphnn_retrial.m:
44 *
45 * 1. AN UNSTABLE MODEL RETURNS A FINITE, PLAUSIBLE-LOOKING ANSWER. The
46 * truncated chain is always positive recurrent, and line 202 renormalizes
47 * it (pi = pi / sum(pi)), so divergence appears as a number rather than as
48 * an error. MATLAB reproduction, from matlab/:
49 * for L = [100 200 400 800 1600]
50 * r = qsys_bmapphnn_retrial({-2,2}, 1, -1, 3, 0.5, 0, 0, 0, 'MaxLevel', L);
51 * fprintf('%d %g %g\n', L, r.L_orbit, sum(r.pi(end,:)));
52 * end
53 * gives L_orbit 94.5, 194.0, 393.7, 793.6, 1593.5 -- growing linearly with
54 * the truncation level -- while the mass sitting at the TOP level stays at
55 * about 0.18 instead of decaying, and the reported throughput drifts from
56 * 1.8607 to 1.8741. With R = 0 a retrial succeeds only when the system is
57 * completely empty, so the orbit is not stable even though the offered
58 * load rho = 2/3 is well below one. Nothing warns.
59 * 2. THE DEFAULT TRUNCATION LEVEL IGNORES EVERY ORBIT PARAMETER. Line 127
60 * sets truncLevel = max(100, ceil(50/(1 - min(rho, 0.99)))) with
61 * rho = lambda b1 / N, so it depends on neither alpha nor gamma nor the
62 * blocking probability, which are exactly what govern the decay of the
63 * orbit tail. MATLAB reproduction: with alpha = 0.02 the default level is
64 * 150, and L_orbit converges to 57.73566168 (reached by MaxLevel 400),
65 * while MaxLevel 100 gives 57.5935191, an error of 0.25 per cent with no
66 * indication that anything was truncated.
67 *
68 * Neither is worked around here. The port returns truncLevel and, as the
69 * diagnostic the reference lacks, topLevelMass: the probability mass at
70 * the highest retained level. A caller can test it (it should be
71 * negligible, of the order of 1e-16 on a converged instance) and raise
72 * maxLevel when it is not. The default level and the returned means are
73 * the reference's, unchanged.
74 *
75 * 3. THE DOCUMENTED MEANING OF R DOES NOT MATCH THE CODE. The header of the
76 * .m file says "When n > R(nu), arriving customers go to orbit", but R
77 * enters the generator only through computeGamma and computeBbar, both of
78 * which act on RETRIALS from the orbit. A fresh arrival always takes a
79 * free server if there is one, whatever R is. The code is consistent with
80 * the paper's "flexible retrials admission control"; the docstring is not.
81 *
82 * 4. computeC's first branch builds a (T_n x 1) zero column, a shape that
83 * cannot be a valid block, and the caller silently discards it through a
84 * size test (size(C_nk, 2) == T(N+1)). It also assigns the same zero
85 * matrix twice under an if that cannot change it. Harmless, and the port
86 * simply does not create the malformed shape.
87 *
88 * ARITHMETIC. Nothing here needs a transcendental function: the generator is a
89 * rational expression in the inputs and the solution is one linear solve, so
90 * the header is instantiable at T = Rational and the exact instantiation
91 * returns the stationary law of the TRUNCATED chain exactly. That is worth
92 * having for a small truncation, where it makes the generator itself
93 * checkable, and impractical for the default one, where the dense solve is
94 * hundreds of dimensions wide.
95 */
96
97#include <cmath>
98#include <cstddef>
99#include <vector>
100
101#include "line/num/number.h"
102#include "line/util/error.h"
103#include "line/util/linalg.h"
104#include "line/util/lu.h"
105#include "line/util/matrix.h"
106
107namespace line {
108namespace qsys {
109
110/** Options of qsys_bmapphnn_retrial. */
112 /** Orbit truncation level; 0 selects the reference's automatic choice. */
113 std::size_t maxLevel = 0;
114};
115
116/** Result of qsys_bmapphnn_retrial. */
117template <class T>
119 T L_orbit; ///< mean number of customers in orbit
120 T N_server; ///< mean number of busy servers
121 T L_system; ///< L_orbit + N_server
122 T utilization; ///< N_server / N
123 T throughput; ///< N_server / b1
124 T P_idle; ///< probability that every server is idle
125 T P_empty_orbit; ///< probability that the orbit is empty
126 T P_empty_system; ///< probability that the system is empty
127 Matrix<T> pi; ///< stationary law, (truncLevel + 1) x (V d)
128 std::size_t truncLevel; ///< truncation level used
129 T topLevelMass; ///< mass at the highest retained level; see defect 2
130 bool clipped; ///< a negative probability had to be clipped to zero
131};
132
133namespace detail {
134
135/**
136 * Weak compositions of n into M parts in the reference's reverse
137 * lexicographic order (generateCompositions in the .m file). The order matters:
138 * it fixes the index of every server-phase multiset.
139 */
140inline std::vector<std::vector<int>> retrial_compositions(int n, int M) {
141 std::vector<std::vector<int>> out;
142 if (M <= 0) return out;
143 if (M == 1) {
144 out.push_back(std::vector<int>(1, n));
145 return out;
146 }
147 for (int m1 = n; m1 >= 0; --m1) {
148 const std::vector<std::vector<int>> sub = retrial_compositions(n - m1, M - 1);
149 for (std::size_t s = 0; s < sub.size(); ++s) {
150 std::vector<int> row;
151 row.reserve(static_cast<std::size_t>(M));
152 row.push_back(m1);
153 row.insert(row.end(), sub[s].begin(), sub[s].end());
154 out.push_back(row);
155 }
156 }
157 return out;
158}
159
160/** Index of a composition in the reverse-lexicographic list, or -1. */
161inline int retrial_find(const std::vector<std::vector<int>>& comps, const std::vector<int>& key) {
162 for (std::size_t j = 0; j < comps.size(); ++j)
163 if (comps[j] == key) return static_cast<int>(j);
164 return -1;
165}
166
167/** Everything the block builders need, assembled once. */
168template <class T>
169struct RetrialCtx {
170 std::vector<Matrix<T>> D; ///< D0 ... DK
171 std::vector<T> beta;
172 Matrix<T> S;
173 std::vector<T> S0;
174 int M = 0, N = 0, V = 0, K = 0;
175 std::size_t d = 0;
176 std::vector<std::size_t> Tn; ///< T_n, n = 0..N
177 std::vector<long> R; ///< admission threshold per BMAP state
178 T alpha, gamma, p;
179 std::vector<std::vector<std::vector<int>>> comps; ///< comps[n]
180};
181
182/** Starting index of the states with n busy servers, 0-based. */
183template <class T>
184std::size_t retrial_offset(const RetrialCtx<T>& c, int n) {
185 std::size_t off = 0;
186 for (int i = 0; i < n; ++i) off += c.Tn[static_cast<std::size_t>(i)];
187 return off;
188}
189
190/** L_n: service completions, T_n x T_{n-1}. */
191template <class T>
192Matrix<T> retrial_L(const RetrialCtx<T>& c, int n) {
193 const T zero = num_traits<T>::from_int(0);
194 if (n == 0) return Matrix<T>();
195 Matrix<T> L(c.Tn[static_cast<std::size_t>(n)], c.Tn[static_cast<std::size_t>(n - 1)], zero);
196 const std::vector<std::vector<int>>& cn = c.comps[static_cast<std::size_t>(n)];
197 const std::vector<std::vector<int>>& cm = c.comps[static_cast<std::size_t>(n - 1)];
198 for (std::size_t i = 0; i < cn.size(); ++i)
199 for (int l = 0; l < c.M; ++l) {
200 if (cn[i][static_cast<std::size_t>(l)] <= 0) continue;
201 std::vector<int> mp = cn[i];
202 --mp[static_cast<std::size_t>(l)];
203 const int j = retrial_find(cm, mp);
204 if (j >= 0)
205 L(i, static_cast<std::size_t>(j)) +=
206 num_traits<T>::from_int(cn[i][static_cast<std::size_t>(l)]) *
207 c.S0[static_cast<std::size_t>(l)];
208 }
209 return L;
210}
211
212/** A_n: service phase changes, T_n x T_n. */
213template <class T>
214Matrix<T> retrial_A(const RetrialCtx<T>& c, int n) {
215 const T zero = num_traits<T>::from_int(0);
216 if (n == 0) return Matrix<T>(1, 1, zero);
217 Matrix<T> A(c.Tn[static_cast<std::size_t>(n)], c.Tn[static_cast<std::size_t>(n)], zero);
218 const std::vector<std::vector<int>>& cn = c.comps[static_cast<std::size_t>(n)];
219 for (std::size_t i = 0; i < cn.size(); ++i)
220 for (int l = 0; l < c.M; ++l) {
221 if (cn[i][static_cast<std::size_t>(l)] <= 0) continue;
222 for (int lp = 0; lp < c.M; ++lp) {
223 if (lp == l) continue;
224 if (!(c.S(static_cast<std::size_t>(l), static_cast<std::size_t>(lp)) > zero))
225 continue;
226 std::vector<int> mp = cn[i];
227 --mp[static_cast<std::size_t>(l)];
228 ++mp[static_cast<std::size_t>(lp)];
229 const int j = retrial_find(cn, mp);
230 if (j >= 0)
231 A(i, static_cast<std::size_t>(j)) +=
232 num_traits<T>::from_int(cn[i][static_cast<std::size_t>(l)]) *
233 c.S(static_cast<std::size_t>(l), static_cast<std::size_t>(lp));
234 }
235 }
236 return A;
237}
238
239/** P_n: a new customer enters service, T_n x T_{n+1}. */
240template <class T>
241Matrix<T> retrial_P(const RetrialCtx<T>& c, int n) {
242 const T zero = num_traits<T>::from_int(0);
243 if (n >= c.N) return Matrix<T>();
244 Matrix<T> P(c.Tn[static_cast<std::size_t>(n)], c.Tn[static_cast<std::size_t>(n + 1)], zero);
245 const std::vector<std::vector<int>>& cn = c.comps[static_cast<std::size_t>(n)];
246 const std::vector<std::vector<int>>& cp = c.comps[static_cast<std::size_t>(n + 1)];
247 for (std::size_t i = 0; i < cn.size(); ++i)
248 for (int l = 0; l < c.M; ++l) {
249 if (!(c.beta[static_cast<std::size_t>(l)] > zero)) continue;
250 std::vector<int> mp = cn[i];
251 ++mp[static_cast<std::size_t>(l)];
252 const int j = retrial_find(cp, mp);
253 if (j >= 0)
254 P(i, static_cast<std::size_t>(j)) += c.beta[static_cast<std::size_t>(l)];
255 }
256 return P;
257}
258
259/** Delta_n: total exit rate of each server-phase multiset, diagonal T_n. */
260template <class T>
261Matrix<T> retrial_Delta(const RetrialCtx<T>& c, int n) {
262 const T zero = num_traits<T>::from_int(0);
263 if (n == 0) return Matrix<T>(1, 1, zero);
264 Matrix<T> D(c.Tn[static_cast<std::size_t>(n)], c.Tn[static_cast<std::size_t>(n)], zero);
265 const std::vector<std::vector<int>>& cn = c.comps[static_cast<std::size_t>(n)];
266 for (std::size_t i = 0; i < cn.size(); ++i) {
267 T total = zero;
268 for (int l = 0; l < c.M; ++l)
269 total += num_traits<T>::from_int(cn[i][static_cast<std::size_t>(l)]) *
270 T(-c.S(static_cast<std::size_t>(l), static_cast<std::size_t>(l)));
271 D(i, i) = total;
272 }
273 return D;
274}
275
276/** Gamma^(nu): the indicator of n > R(nu), diagonal d. */
277template <class T>
278Matrix<T> retrial_Gamma(const RetrialCtx<T>& c, int nu) {
279 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
280 Matrix<T> G(c.d, c.d, zero);
281 std::size_t off = 0;
282 for (int n = 0; n <= c.N; ++n) {
283 const std::size_t width = c.Tn[static_cast<std::size_t>(n)];
284 if (static_cast<long>(n) > c.R[static_cast<std::size_t>(nu)])
285 for (std::size_t t = 0; t < width; ++t) G(off + t, off + t) = one;
286 off += width;
287 }
288 return G;
289}
290
291/** The scalar of G_{n,n}^{(nu,nu')}: batch losses when the batch cannot fit. */
292template <class T>
293T retrial_G_scalar(const RetrialCtx<T>& c, int n, int nu, int nuPrime) {
294 const T zero = num_traits<T>::from_int(0);
295 if (n <= c.N - c.K) return zero;
296 T total = zero;
297 for (int k = c.N - n + 1; k <= c.K; ++k)
298 if (k >= 1)
299 total += c.D[static_cast<std::size_t>(k)](static_cast<std::size_t>(nu),
300 static_cast<std::size_t>(nuPrime));
301 return T(c.p * total);
302}
303
304/**
305 * The product P_n P_{n+1} ... P_{n+k-1}, which places k arriving customers
306 * into service starting from n busy servers.
307 */
308template <class T>
309Matrix<T> retrial_Pprod(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& P, int n, int upto) {
310 Matrix<T> prod = eye<T>(c.Tn[static_cast<std::size_t>(n)]);
311 for (int j = n; j < upto; ++j)
312 if (j < c.N) prod = matmul(prod, P[static_cast<std::size_t>(j)]);
313 return prod;
314}
315
316/** B^(nu): the within-level block for a BMAP state that does not change. */
317template <class T>
318Matrix<T> retrial_B(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& L,
319 const std::vector<Matrix<T>>& A, const std::vector<Matrix<T>>& P,
320 const std::vector<Matrix<T>>& Delta, int nu) {
321 const T zero = num_traits<T>::from_int(0);
322 Matrix<T> B(c.d, c.d, zero);
323 for (int n = 0; n <= c.N; ++n) {
324 const std::size_t rs = retrial_offset(c, n);
325 const std::size_t w = c.Tn[static_cast<std::size_t>(n)];
326 const T g = retrial_G_scalar(c, n, nu, nu);
327 if (n == 0) {
328 B(rs, rs) = g;
329 } else {
330 for (std::size_t a = 0; a < w; ++a)
331 for (std::size_t b = 0; b < w; ++b)
332 B(rs + a, rs + b) = A[static_cast<std::size_t>(n)](a, b) +
333 Delta[static_cast<std::size_t>(n)](a, b) +
334 (a == b ? g : zero);
335 }
336 if (n >= 1) {
337 const std::size_t cs = retrial_offset(c, n - 1);
338 for (std::size_t a = 0; a < w; ++a)
339 for (std::size_t b = 0; b < c.Tn[static_cast<std::size_t>(n - 1)]; ++b)
340 B(rs + a, cs + b) = L[static_cast<std::size_t>(n)](a, b);
341 }
342 for (int k = 1; k <= c.K; ++k) {
343 if (n + k > c.N) continue;
344 const std::size_t cs = retrial_offset(c, n + k);
345 const T dk = c.D[static_cast<std::size_t>(k)](static_cast<std::size_t>(nu),
346 static_cast<std::size_t>(nu));
347 const Matrix<T> pp = retrial_Pprod(c, P, n, n + k);
348 for (std::size_t a = 0; a < w; ++a)
349 for (std::size_t b = 0; b < c.Tn[static_cast<std::size_t>(n + k)]; ++b)
350 B(rs + a, cs + b) = dk * pp(a, b);
351 }
352 }
353 return B;
354}
355
356/** B_bar^(nu): a successful retrial, valid only while n <= R(nu). */
357template <class T>
358Matrix<T> retrial_Bbar(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& P, int nu) {
359 const T zero = num_traits<T>::from_int(0);
360 Matrix<T> B(c.d, c.d, zero);
361 const long cap = c.R[static_cast<std::size_t>(nu)] < static_cast<long>(c.N - 1)
362 ? c.R[static_cast<std::size_t>(nu)]
363 : static_cast<long>(c.N - 1);
364 for (long n = 0; n <= cap; ++n) {
365 const std::size_t rs = retrial_offset(c, static_cast<int>(n));
366 const std::size_t cs = retrial_offset(c, static_cast<int>(n) + 1);
367 const Matrix<T>& Pn = P[static_cast<std::size_t>(n)];
368 for (std::size_t a = 0; a < Pn.rows(); ++a)
369 for (std::size_t b = 0; b < Pn.cols(); ++b) B(rs + a, cs + b) = Pn(a, b);
370 }
371 return B;
372}
373
374/** B_tilde^(nu,nu'): the within-level block when the BMAP state changes. */
375template <class T>
376Matrix<T> retrial_Btilde(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& P, int nu,
377 int nuPrime) {
378 const T zero = num_traits<T>::from_int(0);
379 Matrix<T> B(c.d, c.d, zero);
380 for (int n = 0; n <= c.N; ++n) {
381 const std::size_t rs = retrial_offset(c, n);
382 const std::size_t w = c.Tn[static_cast<std::size_t>(n)];
383 const T g = retrial_G_scalar(c, n, nu, nuPrime);
384 for (std::size_t a = 0; a < w; ++a) B(rs + a, rs + a) = g;
385 for (int k = 1; k <= c.K; ++k) {
386 if (n + k > c.N) continue;
387 const std::size_t cs = retrial_offset(c, n + k);
388 const T dk = c.D[static_cast<std::size_t>(k)](static_cast<std::size_t>(nu),
389 static_cast<std::size_t>(nuPrime));
390 const Matrix<T> pp = retrial_Pprod(c, P, n, n + k);
391 for (std::size_t a = 0; a < w; ++a)
392 for (std::size_t b = 0; b < c.Tn[static_cast<std::size_t>(n + k)]; ++b)
393 B(rs + a, cs + b) = dk * pp(a, b);
394 }
395 }
396 return B;
397}
398
399/**
400 * C_{n,k}^(nu,nu'): a batch that overflows the free servers sends k customers
401 * to the orbit and leaves all N servers busy. Returns an empty matrix when the
402 * block does not exist, in place of the reference's malformed zero column.
403 */
404template <class T>
405Matrix<T> retrial_C(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& P, int n, int k, int nu,
406 int nuPrime) {
407 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
408 if (n < c.N - c.K + k) return Matrix<T>();
409 if (n < c.N) {
410 const int batch = c.N - n + k;
411 if (batch < 1 || batch > c.K) return Matrix<T>();
412 const T db = c.D[static_cast<std::size_t>(batch)](static_cast<std::size_t>(nu),
413 static_cast<std::size_t>(nuPrime));
414 Matrix<T> pp = retrial_Pprod(c, P, n, c.N);
415 for (std::size_t a = 0; a < pp.rows(); ++a)
416 for (std::size_t b = 0; b < pp.cols(); ++b) pp(a, b) = T(one - c.p) * db * pp(a, b);
417 return pp;
418 }
419 if (k < 1 || k > c.K) return Matrix<T>();
420 const T dk = c.D[static_cast<std::size_t>(k)](static_cast<std::size_t>(nu),
421 static_cast<std::size_t>(nuPrime));
422 Matrix<T> out(c.Tn[static_cast<std::size_t>(c.N)], c.Tn[static_cast<std::size_t>(c.N)], zero);
423 for (std::size_t a = 0; a < out.rows(); ++a) out(a, a) = T(one - c.p) * dk;
424 return out;
425}
426
427} // namespace detail
428
429/**
430 * The BMAP/PH/N/N bufferless retrial queue.
431 *
432 * @param D the BMAP as {D0, D1, ..., DK}, each V x V
433 * @param beta PH service entry vector, length M
434 * @param S PH service sub-generator, M x M
435 * @param N number of servers, which is also the capacity
436 * @param alpha retrial rate per orbiting customer
437 * @param gamma abandonment rate per orbiting customer
438 * @param p probability that an overflowing batch is lost rather than
439 * joining the orbit
440 * @param R admission threshold per BMAP state; a single entry is broadcast
441 * @param opt truncation level
442 */
443template <class T>
445 const std::vector<T>& beta, const Matrix<T>& S,
446 int N, const T& alpha, const T& gamma, const T& p,
447 const std::vector<long>& R,
449 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
450 if (D.size() < 2) throw InputError("qsys_bmapphnn_retrial: the BMAP needs D0 and at least D1");
451 if (N < 1) throw InputError("qsys_bmapphnn_retrial: at least one server is required");
452 const int V = static_cast<int>(D[0].rows());
453 if (V < 1) throw InputError("qsys_bmapphnn_retrial: empty BMAP");
454 for (std::size_t k = 0; k < D.size(); ++k)
455 if (D[k].rows() != static_cast<std::size_t>(V) || D[k].cols() != static_cast<std::size_t>(V))
456 throw InputError("qsys_bmapphnn_retrial: every BMAP matrix must be V x V");
457 const int M = static_cast<int>(S.rows());
458 if (M < 1 || S.cols() != static_cast<std::size_t>(M) ||
459 beta.size() != static_cast<std::size_t>(M))
460 throw InputError("qsys_bmapphnn_retrial: the PH service is malformed");
461 if (R.empty()) throw InputError("qsys_bmapphnn_retrial: no admission threshold given");
462
463 detail::RetrialCtx<T> c;
464 c.D = D;
465 c.beta = beta;
466 c.S = S;
467 c.M = M;
468 c.N = N;
469 c.V = V;
470 c.K = static_cast<int>(D.size()) - 1;
471 c.alpha = alpha;
472 c.gamma = gamma;
473 c.p = p;
474 c.R.assign(static_cast<std::size_t>(V), R[0]);
475 if (R.size() > 1) {
476 if (R.size() != static_cast<std::size_t>(V))
477 throw InputError("qsys_bmapphnn_retrial: R must be a scalar or one entry per BMAP "
478 "state");
479 c.R = R;
480 }
481
482 // S0 = -S e, the absorption rates
483 c.S0.assign(static_cast<std::size_t>(M), zero);
484 for (int i = 0; i < M; ++i) {
485 T s = zero;
486 for (int j = 0; j < M; ++j)
487 s += S(static_cast<std::size_t>(i), static_cast<std::size_t>(j));
488 c.S0[static_cast<std::size_t>(i)] = -s;
489 }
490
491 // T_n = C(n + M - 1, M - 1), and the compositions that index those states
492 c.Tn.assign(static_cast<std::size_t>(N) + 1, 0);
493 c.comps.resize(static_cast<std::size_t>(N) + 1);
494 c.d = 0;
495 for (int n = 0; n <= N; ++n) {
496 c.comps[static_cast<std::size_t>(n)] = detail::retrial_compositions(n, M);
497 c.Tn[static_cast<std::size_t>(n)] = c.comps[static_cast<std::size_t>(n)].size();
498 c.d += c.Tn[static_cast<std::size_t>(n)];
499 }
500
501 // mean arrival rate and mean service time, for rho and the throughput
502 Matrix<T> Dsum(static_cast<std::size_t>(V), static_cast<std::size_t>(V), zero);
503 Matrix<T> DkSum(static_cast<std::size_t>(V), static_cast<std::size_t>(V), zero);
504 for (std::size_t k = 0; k < D.size(); ++k)
505 for (std::size_t i = 0; i < Dsum.rows(); ++i)
506 for (std::size_t j = 0; j < Dsum.cols(); ++j) {
507 Dsum(i, j) += D[k](i, j);
508 if (k >= 1)
509 DkSum(i, j) += num_traits<T>::from_int(static_cast<long>(k)) * D[k](i, j);
510 }
511 // theta Dsum = 0, sum theta = 1, by replacing the last equation
512 Matrix<T> A = Dsum.transpose();
513 for (std::size_t j = 0; j < A.cols(); ++j) A(A.rows() - 1, j) = one;
514 std::vector<T> rhs(A.rows(), zero);
515 rhs[A.rows() - 1] = one;
516 const std::vector<T> theta = solve(A, rhs);
517 T lambda = zero;
518 for (std::size_t i = 0; i < theta.size(); ++i)
519 for (std::size_t j = 0; j < DkSum.cols(); ++j) lambda += theta[i] * DkSum(i, j);
520
521 Matrix<T> negS = S;
522 for (std::size_t i = 0; i < negS.rows(); ++i)
523 for (std::size_t j = 0; j < negS.cols(); ++j) negS(i, j) = -negS(i, j);
524 const std::vector<T> tau = mulvec(inverse(negS), ones<T>(static_cast<std::size_t>(M)));
525 T b1 = zero;
526 for (int i = 0; i < M; ++i) b1 += beta[static_cast<std::size_t>(i)] * tau[static_cast<std::size_t>(i)];
527 if (!(b1 > zero)) throw InputError("qsys_bmapphnn_retrial: the mean service time must be positive");
528
529 // truncation level, the reference's formula
530 std::size_t truncLevel = opt.maxLevel;
531 if (truncLevel == 0) {
532 const double rho = num_traits<T>::to_double(T(lambda * b1)) / static_cast<double>(N);
533 const double capped = rho < 0.99 ? rho : 0.99;
534 const double lv = std::ceil(50.0 / (1.0 - capped));
535 truncLevel = lv > 100.0 ? static_cast<std::size_t>(lv) : static_cast<std::size_t>(100);
536 }
537
538 // Blocks that do not depend on the level, built once (the reference
539 // rebuilds them per block pair).
540 std::vector<Matrix<T>> L(static_cast<std::size_t>(N) + 1), Am(static_cast<std::size_t>(N) + 1),
541 P(static_cast<std::size_t>(N) + 1), Delta(static_cast<std::size_t>(N) + 1);
542 for (int n = 0; n <= N; ++n) {
543 L[static_cast<std::size_t>(n)] = detail::retrial_L(c, n);
544 Am[static_cast<std::size_t>(n)] = detail::retrial_A(c, n);
545 P[static_cast<std::size_t>(n)] = detail::retrial_P(c, n);
546 Delta[static_cast<std::size_t>(n)] = detail::retrial_Delta(c, n);
547 }
548 std::vector<Matrix<T>> B(static_cast<std::size_t>(V)), Bbar(static_cast<std::size_t>(V)),
549 Gam(static_cast<std::size_t>(V));
550 for (int nu = 0; nu < V; ++nu) {
551 B[static_cast<std::size_t>(nu)] = detail::retrial_B(c, L, Am, P, Delta, nu);
552 Bbar[static_cast<std::size_t>(nu)] = detail::retrial_Bbar(c, P, nu);
553 Gam[static_cast<std::size_t>(nu)] = detail::retrial_Gamma(c, nu);
554 }
555 std::vector<std::vector<Matrix<T>>> Btilde(
556 static_cast<std::size_t>(V), std::vector<Matrix<T>>(static_cast<std::size_t>(V)));
557 for (int nu = 0; nu < V; ++nu)
558 for (int nup = 0; nup < V; ++nup)
559 if (nu != nup) Btilde[static_cast<std::size_t>(nu)][static_cast<std::size_t>(nup)] =
560 detail::retrial_Btilde(c, P, nu, nup);
561
562 const std::size_t Vd = static_cast<std::size_t>(V) * c.d;
563 const std::size_t total = (truncLevel + 1) * Vd;
564 Matrix<T> Q(total, total, zero);
565
566 for (std::size_t i = 0; i <= truncLevel; ++i) {
567 const T iT = num_traits<T>::from_int(static_cast<long>(i));
568 // diagonal block
569 for (int nu = 0; nu < V; ++nu) {
570 const std::size_t rs = i * Vd + static_cast<std::size_t>(nu) * c.d;
571 for (int nup = 0; nup < V; ++nup) {
572 const std::size_t cs = i * Vd + static_cast<std::size_t>(nup) * c.d;
573 const T d0 = D[0](static_cast<std::size_t>(nu), static_cast<std::size_t>(nup));
574 if (nu == nup) {
575 for (std::size_t a = 0; a < c.d; ++a)
576 for (std::size_t b = 0; b < c.d; ++b) {
577 T v = B[static_cast<std::size_t>(nu)](a, b) +
578 iT * alpha * Gam[static_cast<std::size_t>(nu)](a, b);
579 if (a == b) v += d0 - iT * T(gamma + alpha);
580 Q(rs + a, cs + b) = v;
581 }
582 } else {
583 const Matrix<T>& bt =
584 Btilde[static_cast<std::size_t>(nu)][static_cast<std::size_t>(nup)];
585 for (std::size_t a = 0; a < c.d; ++a)
586 for (std::size_t b = 0; b < c.d; ++b)
587 Q(rs + a, cs + b) = bt(a, b) + (a == b ? d0 : zero);
588 }
589 }
590 }
591 // subdiagonal block: abandonment and successful retrials
592 if (i >= 1) {
593 for (int nu = 0; nu < V; ++nu) {
594 const std::size_t rs = i * Vd + static_cast<std::size_t>(nu) * c.d;
595 const std::size_t cs = (i - 1) * Vd + static_cast<std::size_t>(nu) * c.d;
596 for (std::size_t a = 0; a < c.d; ++a)
597 for (std::size_t b = 0; b < c.d; ++b) {
598 T v = iT * alpha * Bbar[static_cast<std::size_t>(nu)](a, b);
599 if (a == b) v += iT * gamma;
600 Q(rs + a, cs + b) = v;
601 }
602 }
603 }
604 // superdiagonal blocks: batches that overflow into the orbit
605 for (int k = 1; k <= c.K; ++k) {
606 const std::size_t j = i + static_cast<std::size_t>(k);
607 if (j > truncLevel) break;
608 for (int nu = 0; nu < V; ++nu) {
609 const std::size_t rs = i * Vd + static_cast<std::size_t>(nu) * c.d;
610 for (int nup = 0; nup < V; ++nup) {
611 const std::size_t cs = j * Vd + static_cast<std::size_t>(nup) * c.d;
612 const std::size_t ncol = detail::retrial_offset(c, N);
613 for (int n = 0; n <= N; ++n) {
614 const Matrix<T> Cnk = detail::retrial_C(c, P, n, k, nu, nup);
615 if (Cnk.rows() == 0) continue;
616 if (Cnk.cols() != c.Tn[static_cast<std::size_t>(N)]) continue;
617 const std::size_t nrow = detail::retrial_offset(c, n);
618 for (std::size_t a = 0; a < Cnk.rows(); ++a)
619 for (std::size_t b = 0; b < Cnk.cols(); ++b)
620 Q(rs + nrow + a, cs + ncol + b) += Cnk(a, b);
621 }
622 }
623 }
624 }
625 }
626
627 // conservative diagonal, as the reference does after assembly
628 for (std::size_t i = 0; i < total; ++i) {
629 T s = zero;
630 for (std::size_t j = 0; j < total; ++j) s += Q(i, j);
631 Q(i, i) = Q(i, i) - s;
632 }
633
634 // pi Q = 0 with the last column carrying the normalization
635 for (std::size_t i = 0; i < total; ++i) Q(i, total - 1) = one;
636 Matrix<T> QT = Q.transpose();
637 std::vector<T> rhs2(total, zero);
638 rhs2[total - 1] = one;
639 std::vector<T> pi = solve(QT, rhs2);
640
642 r.clipped = false;
643 T mass = zero;
644 for (std::size_t i = 0; i < total; ++i) {
645 if (pi[i] < zero) {
646 pi[i] = zero;
647 r.clipped = true;
648 }
649 mass += pi[i];
650 }
651 if (!(mass > zero)) throw NumericError("qsys_bmapphnn_retrial: the solution has no mass");
652 for (std::size_t i = 0; i < total; ++i) pi[i] = pi[i] / mass;
653
654 r.pi = Matrix<T>(truncLevel + 1, Vd);
655 for (std::size_t i = 0; i <= truncLevel; ++i)
656 for (std::size_t j = 0; j < Vd; ++j) r.pi(i, j) = pi[i * Vd + j];
657
658 r.L_orbit = zero;
659 for (std::size_t i = 1; i <= truncLevel; ++i) {
660 T lv = zero;
661 for (std::size_t j = 0; j < Vd; ++j) lv += r.pi(i, j);
662 r.L_orbit += num_traits<T>::from_int(static_cast<long>(i)) * lv;
663 }
664 r.N_server = zero;
665 r.P_idle = zero;
666 for (std::size_t i = 0; i <= truncLevel; ++i)
667 for (int nu = 0; nu < V; ++nu) {
668 const std::size_t base = static_cast<std::size_t>(nu) * c.d;
669 for (int n = 0; n <= N; ++n) {
670 const std::size_t off = base + detail::retrial_offset(c, n);
671 for (std::size_t t = 0; t < c.Tn[static_cast<std::size_t>(n)]; ++t)
672 r.N_server += num_traits<T>::from_int(n) * r.pi(i, off + t);
673 }
674 r.P_idle += r.pi(i, base);
675 }
676 r.P_empty_orbit = zero;
677 for (std::size_t j = 0; j < Vd; ++j) r.P_empty_orbit += r.pi(0, j);
678 r.P_empty_system = zero;
679 for (int nu = 0; nu < V; ++nu) r.P_empty_system += r.pi(0, static_cast<std::size_t>(nu) * c.d);
680 r.topLevelMass = zero;
681 for (std::size_t j = 0; j < Vd; ++j) r.topLevelMass += r.pi(truncLevel, j);
682
683 r.L_system = T(r.L_orbit + r.N_server);
685 r.throughput = T(r.N_server / b1);
686 r.truncLevel = truncLevel;
687 return r;
688}
689
690/** qsys_bmapphnn_retrial with the reference's automatic truncation level. */
691template <class T>
693 const std::vector<T>& beta, const Matrix<T>& S,
694 int N, const T& alpha, const T& gamma, const T& p,
695 long R) {
696 return qsys_bmapphnn_retrial(D, beta, S, N, alpha, gamma, p, std::vector<long>(1, R),
698}
699
700} // namespace qsys
701} // namespace line
702
703#endif // LINE_API_QSYS_QSYS_BMAPPHNN_RETRIAL_H
InputError(const std::string &what)
Definition error.h:39
std::size_t size() const
Definition matrix.h:91
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
Matrix transpose() const
Definition matrix.h:110
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.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
BmapPhNnRetrialResult< T > qsys_bmapphnn_retrial(const std::vector< Matrix< T > > &D, const std::vector< T > &beta, const Matrix< T > &S, int N, const T &alpha, const T &gamma, const T &p, const std::vector< long > &R, const BmapPhNnRetrialOptions &opt)
The BMAP/PH/N/N bufferless retrial queue.
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
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.
Options of qsys_bmapphnn_retrial.
std::size_t maxLevel
Orbit truncation level; 0 selects the reference's automatic choice.
Result of qsys_bmapphnn_retrial.
T P_empty_orbit
probability that the orbit is empty
T L_orbit
mean number of customers in orbit
std::size_t truncLevel
truncation level used
T P_idle
probability that every server is idle
Matrix< T > pi
stationary law, (truncLevel + 1) x (V d)
T topLevelMass
mass at the highest retained level; see defect 2
T P_empty_system
probability that the system is empty
T N_server
mean number of busy servers
bool clipped
a negative probability had to be clipped to zero