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 ORBIT CAN RETURN A FINITE, PLAUSIBLE-LOOKING ANSWER. The
46 * truncated chain is always positive recurrent and pi is renormalized, so
47 * divergence appears as a number. With R = 0 a retrial succeeds only when
48 * the system is empty, so the orbit is unstable even at rho = 2/3:
49 * qsys_bmapphnn_retrial({-2,2}, 1, -1, 3, 0.5, 0, 0, 0, 'MaxLevel', L)
50 * gives L_orbit 94.5, 194.0, ..., 1593.5 for L = 100, 200, ..., 1600, with
51 * the top-level mass stuck near 0.18. A fixed MaxLevel never warns; the
52 * adaptive default below fails to converge, so the reference warns and the
53 * port reports converged = false.
54 * 2. THE STARTING TRUNCATION LEVEL IGNORES EVERY ORBIT PARAMETER:
55 * max(100, ceil(50/(1 - min(rho, 0.99)))) with rho = lambda b1 / N depends
56 * on neither alpha nor gamma nor the blocking probability. With
57 * alpha = 0.02 it is 150, and MaxLevel 100 gives L_orbit 57.5935191 against
58 * the converged 57.73566168. The reference and this port therefore double
59 * the level until the relative orbit-truncation error (truncError, the
60 * top-level mass times the level over the mean orbit length) falls under
61 * TailTolerance, or the next generator would exceed MaxDim. A fixed maxLevel
62 * disables the refinement. The port also returns topLevelMass as a direct
63 * diagnostic, and supports RetrialPolicy.CONSTANT and the MaxBlockSize cap.
64 *
65 * 3. THE DOCUMENTED MEANING OF R DOES NOT MATCH THE CODE. The header of the
66 * .m file says "When n > R(nu), arriving customers go to orbit", but R
67 * enters the generator only through computeGamma and computeBbar, both of
68 * which act on RETRIALS from the orbit. A fresh arrival always takes a
69 * free server if there is one, whatever R is. The code is consistent with
70 * the paper's "flexible retrials admission control"; the docstring is not.
71 *
72 * 4. computeC's first branch builds a (T_n x 1) zero column, a shape that
73 * cannot be a valid block, and the caller silently discards it through a
74 * size test (size(C_nk, 2) == T(N+1)). It also assigns the same zero
75 * matrix twice under an if that cannot change it. Harmless, and the port
76 * simply does not create the malformed shape.
77 *
78 * ARITHMETIC. Nothing here needs a transcendental function: the generator is a
79 * rational expression in the inputs and the solution is one linear solve, so
80 * the header is instantiable at T = Rational and the exact instantiation
81 * returns the stationary law of the TRUNCATED chain exactly. That is worth
82 * having for a small truncation, where it makes the generator itself
83 * checkable, and impractical for the default one, where the dense solve is
84 * hundreds of dimensions wide.
85 */
86
87#include <cmath>
88#include <cstddef>
89#include <limits>
90#include <string>
91#include <vector>
92
94#include "line/num/number.h"
95#include "line/util/error.h"
96#include "line/util/linalg.h"
97#include "line/util/lu.h"
98#include "line/util/matrix.h"
99
100namespace line {
101namespace qsys {
102
103/** Retrial admission policy (RetrialPolicy in MATLAB). */
104enum class RetrialPolicy {
105 LINEAR, ///< each orbiting customer retries at rate alpha (orbit rate i*alpha)
106 CONSTANT ///< the orbit retries at rate alpha whenever it is non-empty
107};
108
109/** Options of qsys_bmapphnn_retrial (the name-value pairs of the .m file). */
111 /**
112 * Fixed orbit truncation level ('MaxLevel'); 0 selects the reference's
113 * adaptive choice, which doubles the level until the mass retained at the
114 * top level contributes less than tailTolerance of the mean orbit length.
115 */
116 std::size_t maxLevel = 0;
117 /** 'RetrialPolicy'. */
119 /** 'Tolerance' (parsed by the reference and, like there, not consumed). */
120 double tolerance = 1e-10;
121 /** 'TailTolerance': relative orbit-truncation error target. */
122 double tailTolerance = 1e-6;
123 /** 'MaxDim': cap on the total generator dimension of the refinement. */
124 double maxDim = 2e5;
125 /** 'MaxBlockSize': cap on the per-level block size V*d; exceeding it is an error. */
126 double maxBlockSize = 5000;
127};
128
129/** Result of qsys_bmapphnn_retrial. */
130template <class T>
132 T L_orbit; ///< mean number of customers in orbit
133 T N_server; ///< mean number of busy servers
134 T L_system; ///< L_orbit + N_server
135 T utilization; ///< N_server / N
136 T throughput; ///< N_server / b1
137 T P_idle; ///< probability that every server is idle
138 T P_empty_orbit; ///< probability that the orbit is empty
139 T P_empty_system; ///< probability that the system is empty
140 Matrix<T> pi; ///< stationary law, (truncLevel + 1) x (V d)
141 std::size_t truncLevel; ///< truncation level used
142 T truncError; ///< relative orbit-truncation error estimate at truncLevel
143 T topLevelMass; ///< mass at the highest retained level; see defect 2
144 bool clipped; ///< a negative probability had to be clipped to zero
145 /**
146 * The adaptive refinement reached tailTolerance (always true for a fixed
147 * maxLevel). False stands for the reference's line_warning that the orbit
148 * measures are underestimated at the dimension cap maxDim.
149 */
151};
152
153namespace detail {
154
155/**
156 * Weak compositions of n into M parts in the reference's reverse
157 * lexicographic order (generateCompositions in the .m file). The order matters:
158 * it fixes the index of every server-phase multiset.
159 */
160inline std::vector<std::vector<int>> retrial_compositions(int n, int M) {
161 std::vector<std::vector<int>> out;
162 if (M <= 0) return out;
163 if (M == 1) {
164 out.push_back(std::vector<int>(1, n));
165 return out;
166 }
167 for (int m1 = n; m1 >= 0; --m1) {
168 const std::vector<std::vector<int>> sub = retrial_compositions(n - m1, M - 1);
169 for (std::size_t s = 0; s < sub.size(); ++s) {
170 std::vector<int> row;
171 row.reserve(static_cast<std::size_t>(M));
172 row.push_back(m1);
173 row.insert(row.end(), sub[s].begin(), sub[s].end());
174 out.push_back(row);
175 }
176 }
177 return out;
178}
179
180/** Index of a composition in the reverse-lexicographic list, or -1. */
181inline int retrial_find(const std::vector<std::vector<int>>& comps, const std::vector<int>& key) {
182 for (std::size_t j = 0; j < comps.size(); ++j)
183 if (comps[j] == key) return static_cast<int>(j);
184 return -1;
185}
186
187/** Everything the block builders need, assembled once. */
188template <class T>
189struct RetrialCtx {
190 std::vector<Matrix<T>> D; ///< D0 ... DK
191 std::vector<T> beta;
192 Matrix<T> S;
193 std::vector<T> S0;
194 int M = 0, N = 0, V = 0, K = 0;
195 std::size_t d = 0;
196 std::vector<std::size_t> Tn; ///< T_n, n = 0..N
197 std::vector<long> R; ///< admission threshold per BMAP state
198 T alpha, gamma, p;
199 std::vector<std::vector<std::vector<int>>> comps; ///< comps[n]
200};
201
202/** Starting index of the states with n busy servers, 0-based. */
203template <class T>
204std::size_t retrial_offset(const RetrialCtx<T>& c, int n) {
205 std::size_t off = 0;
206 for (int i = 0; i < n; ++i) off += c.Tn[static_cast<std::size_t>(i)];
207 return off;
208}
209
210/** L_n: service completions, T_n x T_{n-1}. */
211template <class T>
212Matrix<T> retrial_L(const RetrialCtx<T>& c, int n) {
213 const T zero = num_traits<T>::from_int(0);
214 if (n == 0) return Matrix<T>();
215 Matrix<T> L(c.Tn[static_cast<std::size_t>(n)], c.Tn[static_cast<std::size_t>(n - 1)], zero);
216 const std::vector<std::vector<int>>& cn = c.comps[static_cast<std::size_t>(n)];
217 const std::vector<std::vector<int>>& cm = c.comps[static_cast<std::size_t>(n - 1)];
218 for (std::size_t i = 0; i < cn.size(); ++i)
219 for (int l = 0; l < c.M; ++l) {
220 if (cn[i][static_cast<std::size_t>(l)] <= 0) continue;
221 std::vector<int> mp = cn[i];
222 --mp[static_cast<std::size_t>(l)];
223 const int j = retrial_find(cm, mp);
224 if (j >= 0)
225 L(i, static_cast<std::size_t>(j)) +=
226 num_traits<T>::from_int(cn[i][static_cast<std::size_t>(l)]) *
227 c.S0[static_cast<std::size_t>(l)];
228 }
229 return L;
230}
231
232/** A_n: service phase changes, T_n x T_n. */
233template <class T>
234Matrix<T> retrial_A(const RetrialCtx<T>& c, int n) {
235 const T zero = num_traits<T>::from_int(0);
236 if (n == 0) return Matrix<T>(1, 1, zero);
237 Matrix<T> A(c.Tn[static_cast<std::size_t>(n)], c.Tn[static_cast<std::size_t>(n)], zero);
238 const std::vector<std::vector<int>>& cn = c.comps[static_cast<std::size_t>(n)];
239 for (std::size_t i = 0; i < cn.size(); ++i)
240 for (int l = 0; l < c.M; ++l) {
241 if (cn[i][static_cast<std::size_t>(l)] <= 0) continue;
242 for (int lp = 0; lp < c.M; ++lp) {
243 if (lp == l) continue;
244 if (!(c.S(static_cast<std::size_t>(l), static_cast<std::size_t>(lp)) > zero))
245 continue;
246 std::vector<int> mp = cn[i];
247 --mp[static_cast<std::size_t>(l)];
248 ++mp[static_cast<std::size_t>(lp)];
249 const int j = retrial_find(cn, mp);
250 if (j >= 0)
251 A(i, static_cast<std::size_t>(j)) +=
252 num_traits<T>::from_int(cn[i][static_cast<std::size_t>(l)]) *
253 c.S(static_cast<std::size_t>(l), static_cast<std::size_t>(lp));
254 }
255 }
256 return A;
257}
258
259/** P_n: a new customer enters service, T_n x T_{n+1}. */
260template <class T>
261Matrix<T> retrial_P(const RetrialCtx<T>& c, int n) {
262 const T zero = num_traits<T>::from_int(0);
263 if (n >= c.N) return Matrix<T>();
264 Matrix<T> P(c.Tn[static_cast<std::size_t>(n)], c.Tn[static_cast<std::size_t>(n + 1)], zero);
265 const std::vector<std::vector<int>>& cn = c.comps[static_cast<std::size_t>(n)];
266 const std::vector<std::vector<int>>& cp = c.comps[static_cast<std::size_t>(n + 1)];
267 for (std::size_t i = 0; i < cn.size(); ++i)
268 for (int l = 0; l < c.M; ++l) {
269 if (!(c.beta[static_cast<std::size_t>(l)] > zero)) continue;
270 std::vector<int> mp = cn[i];
271 ++mp[static_cast<std::size_t>(l)];
272 const int j = retrial_find(cp, mp);
273 if (j >= 0)
274 P(i, static_cast<std::size_t>(j)) += c.beta[static_cast<std::size_t>(l)];
275 }
276 return P;
277}
278
279/** Delta_n: total exit rate of each server-phase multiset, diagonal T_n. */
280template <class T>
281Matrix<T> retrial_Delta(const RetrialCtx<T>& c, int n) {
282 const T zero = num_traits<T>::from_int(0);
283 if (n == 0) return Matrix<T>(1, 1, zero);
284 Matrix<T> D(c.Tn[static_cast<std::size_t>(n)], c.Tn[static_cast<std::size_t>(n)], zero);
285 const std::vector<std::vector<int>>& cn = c.comps[static_cast<std::size_t>(n)];
286 for (std::size_t i = 0; i < cn.size(); ++i) {
287 T total = zero;
288 for (int l = 0; l < c.M; ++l)
289 total += num_traits<T>::from_int(cn[i][static_cast<std::size_t>(l)]) *
290 T(-c.S(static_cast<std::size_t>(l), static_cast<std::size_t>(l)));
291 D(i, i) = total;
292 }
293 return D;
294}
295
296/** Gamma^(nu): the indicator of n > R(nu), diagonal d. */
297template <class T>
298Matrix<T> retrial_Gamma(const RetrialCtx<T>& c, int nu) {
299 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
300 Matrix<T> G(c.d, c.d, zero);
301 std::size_t off = 0;
302 for (int n = 0; n <= c.N; ++n) {
303 const std::size_t width = c.Tn[static_cast<std::size_t>(n)];
304 if (static_cast<long>(n) > c.R[static_cast<std::size_t>(nu)])
305 for (std::size_t t = 0; t < width; ++t) G(off + t, off + t) = one;
306 off += width;
307 }
308 return G;
309}
310
311/** The scalar of G_{n,n}^{(nu,nu')}: batch losses when the batch cannot fit. */
312template <class T>
313T retrial_G_scalar(const RetrialCtx<T>& c, int n, int nu, int nuPrime) {
314 const T zero = num_traits<T>::from_int(0);
315 if (n <= c.N - c.K) return zero;
316 T total = zero;
317 for (int k = c.N - n + 1; k <= c.K; ++k)
318 if (k >= 1)
319 total += c.D[static_cast<std::size_t>(k)](static_cast<std::size_t>(nu),
320 static_cast<std::size_t>(nuPrime));
321 return T(c.p * total);
322}
323
324/**
325 * The product P_n P_{n+1} ... P_{n+k-1}, which places k arriving customers
326 * into service starting from n busy servers.
327 */
328template <class T>
329Matrix<T> retrial_Pprod(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& P, int n, int upto) {
330 Matrix<T> prod = eye<T>(c.Tn[static_cast<std::size_t>(n)]);
331 for (int j = n; j < upto; ++j)
332 if (j < c.N) prod = matmul(prod, P[static_cast<std::size_t>(j)]);
333 return prod;
334}
335
336/** B^(nu): the within-level block for a BMAP state that does not change. */
337template <class T>
338Matrix<T> retrial_B(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& L,
339 const std::vector<Matrix<T>>& A, const std::vector<Matrix<T>>& P,
340 const std::vector<Matrix<T>>& Delta, int nu) {
341 const T zero = num_traits<T>::from_int(0);
342 Matrix<T> B(c.d, c.d, zero);
343 for (int n = 0; n <= c.N; ++n) {
344 const std::size_t rs = retrial_offset(c, n);
345 const std::size_t w = c.Tn[static_cast<std::size_t>(n)];
346 const T g = retrial_G_scalar(c, n, nu, nu);
347 if (n == 0) {
348 B(rs, rs) = g;
349 } else {
350 for (std::size_t a = 0; a < w; ++a)
351 for (std::size_t b = 0; b < w; ++b)
352 B(rs + a, rs + b) = A[static_cast<std::size_t>(n)](a, b) +
353 Delta[static_cast<std::size_t>(n)](a, b) +
354 (a == b ? g : zero);
355 }
356 if (n >= 1) {
357 const std::size_t cs = retrial_offset(c, n - 1);
358 for (std::size_t a = 0; a < w; ++a)
359 for (std::size_t b = 0; b < c.Tn[static_cast<std::size_t>(n - 1)]; ++b)
360 B(rs + a, cs + b) = L[static_cast<std::size_t>(n)](a, b);
361 }
362 for (int k = 1; k <= c.K; ++k) {
363 if (n + k > c.N) continue;
364 const std::size_t cs = retrial_offset(c, n + k);
365 const T dk = c.D[static_cast<std::size_t>(k)](static_cast<std::size_t>(nu),
366 static_cast<std::size_t>(nu));
367 const Matrix<T> pp = retrial_Pprod(c, P, n, n + k);
368 for (std::size_t a = 0; a < w; ++a)
369 for (std::size_t b = 0; b < c.Tn[static_cast<std::size_t>(n + k)]; ++b)
370 B(rs + a, cs + b) = dk * pp(a, b);
371 }
372 }
373 return B;
374}
375
376/** B_bar^(nu): a successful retrial, valid only while n <= R(nu). */
377template <class T>
378Matrix<T> retrial_Bbar(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& P, int nu) {
379 const T zero = num_traits<T>::from_int(0);
380 Matrix<T> B(c.d, c.d, zero);
381 const long cap = c.R[static_cast<std::size_t>(nu)] < static_cast<long>(c.N - 1)
382 ? c.R[static_cast<std::size_t>(nu)]
383 : static_cast<long>(c.N - 1);
384 for (long n = 0; n <= cap; ++n) {
385 const std::size_t rs = retrial_offset(c, static_cast<int>(n));
386 const std::size_t cs = retrial_offset(c, static_cast<int>(n) + 1);
387 const Matrix<T>& Pn = P[static_cast<std::size_t>(n)];
388 for (std::size_t a = 0; a < Pn.rows(); ++a)
389 for (std::size_t b = 0; b < Pn.cols(); ++b) B(rs + a, cs + b) = Pn(a, b);
390 }
391 return B;
392}
393
394/** B_tilde^(nu,nu'): the within-level block when the BMAP state changes. */
395template <class T>
396Matrix<T> retrial_Btilde(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& P, int nu,
397 int nuPrime) {
398 const T zero = num_traits<T>::from_int(0);
399 Matrix<T> B(c.d, c.d, zero);
400 for (int n = 0; n <= c.N; ++n) {
401 const std::size_t rs = retrial_offset(c, n);
402 const std::size_t w = c.Tn[static_cast<std::size_t>(n)];
403 const T g = retrial_G_scalar(c, n, nu, nuPrime);
404 for (std::size_t a = 0; a < w; ++a) B(rs + a, rs + a) = g;
405 for (int k = 1; k <= c.K; ++k) {
406 if (n + k > c.N) continue;
407 const std::size_t cs = retrial_offset(c, n + k);
408 const T dk = c.D[static_cast<std::size_t>(k)](static_cast<std::size_t>(nu),
409 static_cast<std::size_t>(nuPrime));
410 const Matrix<T> pp = retrial_Pprod(c, P, n, n + k);
411 for (std::size_t a = 0; a < w; ++a)
412 for (std::size_t b = 0; b < c.Tn[static_cast<std::size_t>(n + k)]; ++b)
413 B(rs + a, cs + b) = dk * pp(a, b);
414 }
415 }
416 return B;
417}
418
419/**
420 * C_{n,k}^(nu,nu'): a batch that overflows the free servers sends k customers
421 * to the orbit and leaves all N servers busy. Returns an empty matrix when the
422 * block does not exist, in place of the reference's malformed zero column.
423 */
424template <class T>
425Matrix<T> retrial_C(const RetrialCtx<T>& c, const std::vector<Matrix<T>>& P, int n, int k, int nu,
426 int nuPrime) {
427 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
428 if (n < c.N - c.K + k) return Matrix<T>();
429 if (n < c.N) {
430 const int batch = c.N - n + k;
431 if (batch < 1 || batch > c.K) return Matrix<T>();
432 const T db = c.D[static_cast<std::size_t>(batch)](static_cast<std::size_t>(nu),
433 static_cast<std::size_t>(nuPrime));
434 Matrix<T> pp = retrial_Pprod(c, P, n, c.N);
435 for (std::size_t a = 0; a < pp.rows(); ++a)
436 for (std::size_t b = 0; b < pp.cols(); ++b) pp(a, b) = T(one - c.p) * db * pp(a, b);
437 return pp;
438 }
439 if (k < 1 || k > c.K) return Matrix<T>();
440 const T dk = c.D[static_cast<std::size_t>(k)](static_cast<std::size_t>(nu),
441 static_cast<std::size_t>(nuPrime));
442 Matrix<T> out(c.Tn[static_cast<std::size_t>(c.N)], c.Tn[static_cast<std::size_t>(c.N)], zero);
443 for (std::size_t a = 0; a < out.rows(); ++a) out(a, a) = T(one - c.p) * dk;
444 return out;
445}
446
447} // namespace detail
448
449/**
450 * The BMAP/PH/N/N bufferless retrial queue.
451 *
452 * @param D the BMAP as {D0, D1, ..., DK}, each V x V
453 * @param beta PH service entry vector, length M
454 * @param S PH service sub-generator, M x M
455 * @param N number of servers, which is also the capacity
456 * @param alpha retrial rate per orbiting customer
457 * @param gamma abandonment rate per orbiting customer
458 * @param p probability that an overflowing batch is lost rather than
459 * joining the orbit
460 * @param R admission threshold per BMAP state; a single entry is broadcast
461 * @param opt truncation level
462 */
463template <class T>
465 const std::vector<T>& beta, const Matrix<T>& S,
466 int N, const T& alpha, const T& gamma, const T& p,
467 const std::vector<long>& R,
469 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
470 if (D.size() < 2) throw InputError("qsys_bmapphnn_retrial: the BMAP needs D0 and at least D1");
471 if (N < 1) throw InputError("qsys_bmapphnn_retrial: at least one server is required");
472 const int V = static_cast<int>(D[0].rows());
473 if (V < 1) throw InputError("qsys_bmapphnn_retrial: empty BMAP");
474 for (std::size_t k = 0; k < D.size(); ++k)
475 if (D[k].rows() != static_cast<std::size_t>(V) || D[k].cols() != static_cast<std::size_t>(V))
476 throw InputError("qsys_bmapphnn_retrial: every BMAP matrix must be V x V");
477 const int M = static_cast<int>(S.rows());
478 if (M < 1 || S.cols() != static_cast<std::size_t>(M) ||
479 beta.size() != static_cast<std::size_t>(M))
480 throw InputError("qsys_bmapphnn_retrial: the PH service is malformed");
481 if (R.empty()) throw InputError("qsys_bmapphnn_retrial: no admission threshold given");
482
483 // validateRetrialInputs: reject anything that is not a well-formed BMAP/PH
484 // pair before any large allocation, as the reference does.
485 {
486 const double fineTol = lang::GlobalConstants::FineTol;
487 const double sqrtFineTol = std::sqrt(fineTol);
488 Matrix<T> DsumV(static_cast<std::size_t>(V), static_cast<std::size_t>(V), zero);
489 for (std::size_t k = 0; k < D.size(); ++k)
490 for (std::size_t i = 0; i < D[k].rows(); ++i)
491 for (std::size_t j = 0; j < D[k].cols(); ++j) {
492 const double v = num_traits<T>::to_double(D[k](i, j));
493 if (!std::isfinite(v))
494 throw InputError("qsys_bmapphnn_retrial: BMAP matrix D{" +
495 std::to_string(k + 1) +
496 "} contains NaN or Inf entries. The arrival process is "
497 "disabled or not phase-type representable.");
498 if (k >= 1 && v < -fineTol)
499 throw InputError("qsys_bmapphnn_retrial: BMAP arrival matrix D{" +
500 std::to_string(k + 1) + "} must be non-negative.");
501 DsumV(i, j) += D[k](i, j);
502 }
503 for (std::size_t i = 0; i < D[0].rows(); ++i) {
504 if (num_traits<T>::to_double(D[0](i, i)) > fineTol)
505 throw InputError("qsys_bmapphnn_retrial: BMAP matrix D0 must have non-positive "
506 "diagonal entries.");
507 T rs = zero;
508 for (std::size_t j = 0; j < DsumV.cols(); ++j) rs += DsumV(i, j);
509 if (std::fabs(num_traits<T>::to_double(rs)) > sqrtFineTol)
510 throw InputError("qsys_bmapphnn_retrial: BMAP matrices are inconsistent: sum_k D_k "
511 "must have zero row sums.");
512 }
513 T betaSum = zero;
514 for (std::size_t i = 0; i < beta.size(); ++i) {
515 const double v = num_traits<T>::to_double(beta[i]);
516 if (!std::isfinite(v))
517 throw InputError("qsys_bmapphnn_retrial: Phase-type service vector beta is empty or "
518 "contains NaN/Inf. The service distribution is disabled or not "
519 "phase-type representable.");
520 if (v < -fineTol)
521 throw InputError("qsys_bmapphnn_retrial: Phase-type service vector beta must be "
522 "non-negative and sum to one.");
523 betaSum += beta[i];
524 }
525 if (std::fabs(num_traits<T>::to_double(betaSum) - 1.0) > sqrtFineTol)
526 throw InputError("qsys_bmapphnn_retrial: Phase-type service vector beta must be "
527 "non-negative and sum to one.");
528 for (std::size_t i = 0; i < S.rows(); ++i) {
529 T rs = zero;
530 for (std::size_t j = 0; j < S.cols(); ++j) {
531 if (!std::isfinite(num_traits<T>::to_double(S(i, j))))
532 throw InputError("qsys_bmapphnn_retrial: Phase-type service subgenerator S "
533 "contains NaN or Inf entries. The service distribution is "
534 "disabled or not phase-type representable.");
535 rs += S(i, j);
536 }
537 if (!(S(i, i) < zero))
538 throw InputError("qsys_bmapphnn_retrial: Phase-type service subgenerator S must "
539 "have strictly negative diagonal entries.");
540 if (num_traits<T>::to_double(T(-rs)) < -fineTol)
541 throw InputError("qsys_bmapphnn_retrial: Phase-type service subgenerator S must "
542 "have non-negative exit rates.");
543 }
544 const double ad = num_traits<T>::to_double(alpha), gd = num_traits<T>::to_double(gamma),
546 if (!std::isfinite(ad) || ad < 0.0)
547 throw InputError("qsys_bmapphnn_retrial: Retrial rate alpha must be a finite "
548 "non-negative scalar.");
549 if (!std::isfinite(gd) || gd < 0.0)
550 throw InputError("qsys_bmapphnn_retrial: Orbit impatience rate gamma must be a finite "
551 "non-negative scalar.");
552 if (!std::isfinite(pd) || pd < 0.0 || pd > 1.0)
553 throw InputError("qsys_bmapphnn_retrial: Batch rejection probability p must lie in "
554 "[0,1].");
555 for (std::size_t i = 0; i < R.size(); ++i)
556 if (R[i] < 0 || R[i] > static_cast<long>(N))
557 throw InputError("qsys_bmapphnn_retrial: Admission threshold R must lie in [0," +
558 std::to_string(N) + "].");
559 }
560
561 detail::RetrialCtx<T> c;
562 c.D = D;
563 c.beta = beta;
564 c.S = S;
565 c.M = M;
566 c.N = N;
567 c.V = V;
568 c.K = static_cast<int>(D.size()) - 1;
569 c.alpha = alpha;
570 c.gamma = gamma;
571 c.p = p;
572 c.R.assign(static_cast<std::size_t>(V), R[0]);
573 if (R.size() > 1) {
574 if (R.size() != static_cast<std::size_t>(V))
575 throw InputError("qsys_bmapphnn_retrial: R must be a scalar or one entry per BMAP "
576 "state");
577 c.R = R;
578 }
579
580 // S0 = -S e, the absorption rates
581 c.S0.assign(static_cast<std::size_t>(M), zero);
582 for (int i = 0; i < M; ++i) {
583 T s = zero;
584 for (int j = 0; j < M; ++j)
585 s += S(static_cast<std::size_t>(i), static_cast<std::size_t>(j));
586 c.S0[static_cast<std::size_t>(i)] = -s;
587 }
588
589 // T_n = C(n + M - 1, M - 1), and the compositions that index those states
590 c.Tn.assign(static_cast<std::size_t>(N) + 1, 0);
591 c.comps.resize(static_cast<std::size_t>(N) + 1);
592 c.d = 0;
593 for (int n = 0; n <= N; ++n) {
594 c.comps[static_cast<std::size_t>(n)] = detail::retrial_compositions(n, M);
595 c.Tn[static_cast<std::size_t>(n)] = c.comps[static_cast<std::size_t>(n)].size();
596 c.d += c.Tn[static_cast<std::size_t>(n)];
597 }
598
599 // mean arrival rate and mean service time, for rho and the throughput
600 Matrix<T> Dsum(static_cast<std::size_t>(V), static_cast<std::size_t>(V), zero);
601 Matrix<T> DkSum(static_cast<std::size_t>(V), static_cast<std::size_t>(V), zero);
602 for (std::size_t k = 0; k < D.size(); ++k)
603 for (std::size_t i = 0; i < Dsum.rows(); ++i)
604 for (std::size_t j = 0; j < Dsum.cols(); ++j) {
605 Dsum(i, j) += D[k](i, j);
606 if (k >= 1)
607 DkSum(i, j) += num_traits<T>::from_int(static_cast<long>(k)) * D[k](i, j);
608 }
609 // theta Dsum = 0, sum theta = 1, by replacing the last equation
610 Matrix<T> A = Dsum.transpose();
611 for (std::size_t j = 0; j < A.cols(); ++j) A(A.rows() - 1, j) = one;
612 std::vector<T> rhs(A.rows(), zero);
613 rhs[A.rows() - 1] = one;
614 const std::vector<T> theta = solve(A, rhs);
615 T lambda = zero;
616 for (std::size_t i = 0; i < theta.size(); ++i)
617 for (std::size_t j = 0; j < DkSum.cols(); ++j) lambda += theta[i] * DkSum(i, j);
618
619 Matrix<T> negS = S;
620 for (std::size_t i = 0; i < negS.rows(); ++i)
621 for (std::size_t j = 0; j < negS.cols(); ++j) negS(i, j) = -negS(i, j);
622 const std::vector<T> tau = mulvec(inverse(negS), ones<T>(static_cast<std::size_t>(M)));
623 T b1 = zero;
624 for (int i = 0; i < M; ++i) b1 += beta[static_cast<std::size_t>(i)] * tau[static_cast<std::size_t>(i)];
625 if (!(b1 > zero)) throw InputError("qsys_bmapphnn_retrial: the mean service time must be positive");
626
627 const std::size_t Vd = static_cast<std::size_t>(V) * c.d;
628
629 // A level block of size Vd is dense and is built once per level, so an
630 // oversized block must be rejected rather than attempted ('MaxBlockSize').
631 if (static_cast<double>(Vd) > opt.maxBlockSize)
632 throw InputError("qsys_bmapphnn_retrial: Per-level block size V*d = " + std::to_string(Vd) +
633 " exceeds MaxBlockSize = " +
634 std::to_string(static_cast<long long>(std::llround(opt.maxBlockSize))) +
635 ". The service distribution has " + std::to_string(M) +
636 " phases and the station has " + std::to_string(N) +
637 " servers, which yields " + std::to_string(c.d) +
638 " service configurations. Reduce the phase-type order (e.g. fit the "
639 "service distribution with fewer phases), reduce the number of servers, "
640 "or raise maxBlockSize if the memory cost is acceptable.");
641
642 // Blocks that do not depend on the level, built once (the reference
643 // rebuilds them per block pair).
644 std::vector<Matrix<T>> L(static_cast<std::size_t>(N) + 1), Am(static_cast<std::size_t>(N) + 1),
645 P(static_cast<std::size_t>(N) + 1), Delta(static_cast<std::size_t>(N) + 1);
646 for (int n = 0; n <= N; ++n) {
647 L[static_cast<std::size_t>(n)] = detail::retrial_L(c, n);
648 Am[static_cast<std::size_t>(n)] = detail::retrial_A(c, n);
649 P[static_cast<std::size_t>(n)] = detail::retrial_P(c, n);
650 Delta[static_cast<std::size_t>(n)] = detail::retrial_Delta(c, n);
651 }
652 std::vector<Matrix<T>> B(static_cast<std::size_t>(V)), Bbar(static_cast<std::size_t>(V)),
653 Gam(static_cast<std::size_t>(V));
654 for (int nu = 0; nu < V; ++nu) {
655 B[static_cast<std::size_t>(nu)] = detail::retrial_B(c, L, Am, P, Delta, nu);
656 Bbar[static_cast<std::size_t>(nu)] = detail::retrial_Bbar(c, P, nu);
657 Gam[static_cast<std::size_t>(nu)] = detail::retrial_Gamma(c, nu);
658 }
659 std::vector<std::vector<Matrix<T>>> Btilde(
660 static_cast<std::size_t>(V), std::vector<Matrix<T>>(static_cast<std::size_t>(V)));
661 for (int nu = 0; nu < V; ++nu)
662 for (int nup = 0; nup < V; ++nup)
663 if (nu != nup) Btilde[static_cast<std::size_t>(nu)][static_cast<std::size_t>(nup)] =
664 detail::retrial_Btilde(c, P, nu, nup);
665
666 bool clipped = false;
667
668 // solveAtLevel: build the level-truncated generator and solve pi Q = 0,
669 // pi e = 1; returns pi reshaped to (truncLevel + 1) x Vd. The impatience
670 // terms scale with the orbit size i; the retrial terms with the policy
671 // weight: i under LINEAR, one whenever the orbit is non-empty under CONSTANT.
672 auto solveAtLevel = [&](std::size_t truncLevel) -> Matrix<T> {
673 const std::size_t total = (truncLevel + 1) * Vd;
674 Matrix<T> Q(total, total, zero);
675
676 for (std::size_t i = 0; i <= truncLevel; ++i) {
677 const T iT = num_traits<T>::from_int(static_cast<long>(i));
678 const T wT = opt.retrialPolicy == RetrialPolicy::CONSTANT ? (i >= 1 ? one : zero) : iT;
679 // diagonal block
680 for (int nu = 0; nu < V; ++nu) {
681 const std::size_t rs = i * Vd + static_cast<std::size_t>(nu) * c.d;
682 for (int nup = 0; nup < V; ++nup) {
683 const std::size_t cs = i * Vd + static_cast<std::size_t>(nup) * c.d;
684 const T d0 = D[0](static_cast<std::size_t>(nu), static_cast<std::size_t>(nup));
685 if (nu == nup) {
686 for (std::size_t a = 0; a < c.d; ++a)
687 for (std::size_t b = 0; b < c.d; ++b) {
688 T v = B[static_cast<std::size_t>(nu)](a, b) +
689 wT * alpha * Gam[static_cast<std::size_t>(nu)](a, b);
690 if (a == b) v += d0 - iT * gamma - wT * alpha;
691 Q(rs + a, cs + b) = v;
692 }
693 } else {
694 const Matrix<T>& bt =
695 Btilde[static_cast<std::size_t>(nu)][static_cast<std::size_t>(nup)];
696 for (std::size_t a = 0; a < c.d; ++a)
697 for (std::size_t b = 0; b < c.d; ++b)
698 Q(rs + a, cs + b) = bt(a, b) + (a == b ? d0 : zero);
699 }
700 }
701 }
702 // subdiagonal block: abandonment and successful retrials
703 if (i >= 1) {
704 for (int nu = 0; nu < V; ++nu) {
705 const std::size_t rs = i * Vd + static_cast<std::size_t>(nu) * c.d;
706 const std::size_t cs = (i - 1) * Vd + static_cast<std::size_t>(nu) * c.d;
707 for (std::size_t a = 0; a < c.d; ++a)
708 for (std::size_t b = 0; b < c.d; ++b) {
709 T v = wT * alpha * Bbar[static_cast<std::size_t>(nu)](a, b);
710 if (a == b) v += iT * gamma;
711 Q(rs + a, cs + b) = v;
712 }
713 }
714 }
715 // superdiagonal blocks: batches that overflow into the orbit
716 for (int k = 1; k <= c.K; ++k) {
717 const std::size_t j = i + static_cast<std::size_t>(k);
718 if (j > truncLevel) break;
719 for (int nu = 0; nu < V; ++nu) {
720 const std::size_t rs = i * Vd + static_cast<std::size_t>(nu) * c.d;
721 for (int nup = 0; nup < V; ++nup) {
722 const std::size_t cs = j * Vd + static_cast<std::size_t>(nup) * c.d;
723 const std::size_t ncol = detail::retrial_offset(c, N);
724 for (int n = 0; n <= N; ++n) {
725 const Matrix<T> Cnk = detail::retrial_C(c, P, n, k, nu, nup);
726 if (Cnk.rows() == 0) continue;
727 if (Cnk.cols() != c.Tn[static_cast<std::size_t>(N)]) continue;
728 const std::size_t nrow = detail::retrial_offset(c, n);
729 for (std::size_t a = 0; a < Cnk.rows(); ++a)
730 for (std::size_t b = 0; b < Cnk.cols(); ++b)
731 Q(rs + nrow + a, cs + ncol + b) += Cnk(a, b);
732 }
733 }
734 }
735 }
736 }
737
738 // conservative diagonal, as the reference does after assembly
739 for (std::size_t i = 0; i < total; ++i) {
740 T s = zero;
741 for (std::size_t j = 0; j < total; ++j) s += Q(i, j);
742 Q(i, i) = Q(i, i) - s;
743 }
744
745 // pi Q = 0 with the last column carrying the normalization
746 for (std::size_t i = 0; i < total; ++i) Q(i, total - 1) = one;
747 Matrix<T> QT = Q.transpose();
748 std::vector<T> rhs2(total, zero);
749 rhs2[total - 1] = one;
750 std::vector<T> pi = solve(QT, rhs2);
751
752 // Handle numerical issues: the reference warns below -1e-8, clips every
753 // negative entry and renormalizes.
754 const T warnTol = num_traits<T>::from_double(-1e-8);
755 T mass = zero;
756 for (std::size_t i = 0; i < total; ++i) {
757 if (pi[i] < warnTol) clipped = true;
758 if (pi[i] < zero) pi[i] = zero;
759 mass += pi[i];
760 }
761 if (!(mass > zero)) throw NumericError(ErrorKind{"Degenerate"}, "qsys_bmapphnn_retrial: the solution has no mass");
762 Matrix<T> pim(truncLevel + 1, Vd);
763 for (std::size_t i = 0; i <= truncLevel; ++i)
764 for (std::size_t j = 0; j < Vd; ++j) pim(i, j) = pi[i * Vd + j] / mass;
765 return pim;
766 };
767
768 // orbitTruncationError: relative contribution the truncated tail would add
769 // to the mean orbit length, bounded by the mass sitting at the top level.
770 auto orbitTruncationError = [&](const Matrix<T>& pim, std::size_t truncLevel) -> T {
771 T lorbit = zero, top = zero;
772 for (std::size_t i = 0; i <= truncLevel; ++i) {
773 T lv = zero;
774 for (std::size_t j = 0; j < Vd; ++j) lv += pim(i, j);
775 lorbit += num_traits<T>::from_int(static_cast<long>(i)) * lv;
776 if (i == truncLevel) top = lv;
777 }
778 const T realmin = num_traits<T>::from_double(std::numeric_limits<double>::min());
779 const T denom = lorbit > realmin ? lorbit : realmin;
780 return T(num_traits<T>::from_int(static_cast<long>(truncLevel)) * top / denom);
781 };
782
783 // Truncation level: fixed when given, else the reference's starting formula
784 // followed by the doubling refinement against tailTolerance under maxDim.
785 std::size_t truncLevel = opt.maxLevel;
786 Matrix<T> pim;
787 T truncError = zero;
788 bool converged = true;
789 if (truncLevel > 0) {
790 pim = solveAtLevel(truncLevel);
791 truncError = orbitTruncationError(pim, truncLevel);
792 } else {
793 const double rho = num_traits<T>::to_double(T(lambda * b1)) / static_cast<double>(N);
794 const double capped = rho < 0.99 ? rho : 0.99;
795 const double lv = std::ceil(50.0 / (1.0 - capped));
796 truncLevel = lv > 100.0 ? static_cast<std::size_t>(lv) : static_cast<std::size_t>(100);
797 converged = false;
798 while (true) {
799 pim = solveAtLevel(truncLevel);
800 truncError = orbitTruncationError(pim, truncLevel);
801 if (num_traits<T>::to_double(truncError) <= opt.tailTolerance) {
802 converged = true;
803 break;
804 }
805 const std::size_t nextLevel = 2 * truncLevel;
806 if (static_cast<double>(nextLevel + 1) * static_cast<double>(Vd) > opt.maxDim) break;
807 truncLevel = nextLevel;
808 }
809 // !converged stands for the reference's line_warning: orbit measures
810 // are underestimated; raise maxDim or set maxLevel explicitly.
811 }
812
814 r.clipped = clipped;
815 r.converged = converged;
816 r.pi = pim;
817
818 r.L_orbit = zero;
819 for (std::size_t i = 1; i <= truncLevel; ++i) {
820 T lv = zero;
821 for (std::size_t j = 0; j < Vd; ++j) lv += r.pi(i, j);
822 r.L_orbit += num_traits<T>::from_int(static_cast<long>(i)) * lv;
823 }
824 r.N_server = zero;
825 r.P_idle = zero;
826 for (std::size_t i = 0; i <= truncLevel; ++i)
827 for (int nu = 0; nu < V; ++nu) {
828 const std::size_t base = static_cast<std::size_t>(nu) * c.d;
829 for (int n = 0; n <= N; ++n) {
830 const std::size_t off = base + detail::retrial_offset(c, n);
831 for (std::size_t t = 0; t < c.Tn[static_cast<std::size_t>(n)]; ++t)
832 r.N_server += num_traits<T>::from_int(n) * r.pi(i, off + t);
833 }
834 r.P_idle += r.pi(i, base);
835 }
836 r.P_empty_orbit = zero;
837 for (std::size_t j = 0; j < Vd; ++j) r.P_empty_orbit += r.pi(0, j);
838 r.P_empty_system = zero;
839 for (int nu = 0; nu < V; ++nu) r.P_empty_system += r.pi(0, static_cast<std::size_t>(nu) * c.d);
840 r.topLevelMass = zero;
841 for (std::size_t j = 0; j < Vd; ++j) r.topLevelMass += r.pi(truncLevel, j);
842
843 r.L_system = T(r.L_orbit + r.N_server);
845 r.throughput = T(r.N_server / b1);
846 r.truncLevel = truncLevel;
847 r.truncError = truncError;
848 return r;
849}
850
851/** qsys_bmapphnn_retrial with the reference's automatic truncation level. */
852template <class T>
854 const std::vector<T>& beta, const Matrix<T>& S,
855 int N, const T& alpha, const T& gamma, const T& p,
856 long R) {
857 return qsys_bmapphnn_retrial(D, beta, S, N, alpha, gamma, p, std::vector<long>(1, R),
859}
860
861} // namespace qsys
862} // namespace line
863
864#endif // LINE_API_QSYS_QSYS_BMAPPHNN_RETRIAL_H
Malformed or inconsistent input (dimensions, negative populations, ...): LINE:input:<kind>.
Definition error.h:103
InputError(const std::string &what)
Definition error.h:105
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:112
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
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.
RetrialPolicy
Retrial admission policy (RetrialPolicy in MATLAB).
@ CONSTANT
the orbit retries at rate alpha whenever it is non-empty
@ LINEAR
each orbiting customer retries at rate alpha (orbit rate i*alpha)
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.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static constexpr double FineTol
Definition lang_types.h:765
Options of qsys_bmapphnn_retrial (the name-value pairs of the .m file).
double tolerance
'Tolerance' (parsed by the reference and, like there, not consumed).
std::size_t maxLevel
Fixed orbit truncation level ('MaxLevel'); 0 selects the reference's adaptive choice,...
double maxDim
'MaxDim': cap on the total generator dimension of the refinement.
double tailTolerance
'TailTolerance': relative orbit-truncation error target.
RetrialPolicy retrialPolicy
'RetrialPolicy'.
double maxBlockSize
'MaxBlockSize': cap on the per-level block size V*d; exceeding it is an error.
Result of qsys_bmapphnn_retrial.
bool converged
The adaptive refinement reached tailTolerance (always true for a fixed maxLevel).
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
T truncError
relative orbit-truncation error estimate at truncLevel
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