LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_bmapm1.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_BMAPM1_H
6#define LINE_API_QSYS_QSYS_BMAPM1_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * BMAP/M/1 by the matrix-analytic (M/G/1-type) method.
12 *
13 * Port of `matlab/src/api/qsys/qsys_bmapm1.m`. What makes this function
14 * unusual, and what `getMAMResult` exists to expose, is that it returns the
15 * INTERMEDIATE OBJECTS rather than only the mean measures: the randomized
16 * blocks, the matrix G, the drift, the measured decay rate and the level
17 * probabilities. Mean values alone hide the objects the method is built on, so
18 * a matrix-analytic result cannot otherwise be checked against a published
19 * derivation.
20 *
21 * THE RANDOMIZATION. The continuous-time chain is uniformized by q, chosen as
22 * `max_i(-D0(i,i)) + mu` unless supplied, into a discrete-time M/G/1-type chain
23 * with blocks A0 = (mu/q)I (a service completion, level down), A1 = (1/q)(D0 -
24 * mu I) + I (level unchanged), Bk = (1/q)D_k (level up by k) and the boundary
25 * local block B0 = (1/q)D0 + I, which differs from A1 because no service can
26 * complete at level zero. A q that does not dominate the outflow would give the
27 * randomized chain negative entries, and is refused by name rather than
28 * silently clamped.
29 *
30 * THE DECAY RATE IS MEASURED, NOT DERIVED, and that is deliberate in the
31 * reference: it is read as the ratio pi_(n+1)/pi_n at a level where the mass is
32 * still numerically meaningful (half way up the usable range), rather than
33 * taken from a spectral convention that would have to pick a branch. That is
34 * also why this function needs NO eigen-decomposition and is therefore
35 * instantiable at `Real`, not double-only. Every returned quantity is a linear
36 * solve, a functional iteration or a ratio.
37 *
38 * ARITHMETIC. Gated on `num_traits<T>::has_transcendental` because the G
39 * iteration and the level truncation both terminate on a tolerance. The
40 * algebra itself is rational.
41 */
42
43#include <algorithm>
44#include <cmath>
45#include <cstddef>
46#include <string>
47#include <vector>
48
51#include "line/num/number.h"
52#include "line/util/error.h"
53#include "line/util/linalg.h"
54#include "line/util/lu.h"
55#include "line/util/matrix.h"
56
57namespace line {
58namespace qsys {
59
60/** Everything `qsys_bmapm1` returns, mirroring the MATLAB result struct. */
61template <class T>
63 std::vector<T> theta; ///< stationary vector of the BMAP phase process
64 T lambda; ///< mean arrival rate, theta (sum_k k D_k) e
65 T rho; ///< offered load lambda/mu
66 T q; ///< uniformization constant actually used
67 Matrix<T> A0, A1, B0; ///< randomized blocks: down, local, boundary local
68 std::vector<Matrix<T>> Bk; ///< level up by k, k = 1..K
69 Matrix<T> A; ///< A0 + A1 + sum_k Bk, the phase process
70 std::vector<T> alpha; ///< stationary vector of A
71 Matrix<T> G; ///< minimal non-negative solution of the M/G/1-type equation
72 T drift; ///< stable iff strictly negative
73 double decayRate; ///< measured pi_(n+1)/pi_n; NaN when unmeasurable
74 Matrix<T> levelProb; ///< level probabilities, row n = pi_n
75 T pi0; ///< probability the system is empty
79 std::size_t truncLevel;
80 double truncError;
81 bool gConverged = true;
82};
83
84namespace bmapm1_detail {
85
86/**
87 * Stationary distribution of the level-truncated CTMC.
88 *
89 * The reference replaces the last column of Q by ones and solves b/Q with
90 * b = e_last, which is the standard normalized stationary solve; reproduced
91 * here on the transpose because `line::solve` takes a column right-hand side.
92 */
93template <class T>
94Matrix<T> solve_levels(const std::vector<Matrix<T>>& D, const T& mu, std::size_t V, std::size_t K,
95 std::size_t levelMax) {
96 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
97 const std::size_t dim = (levelMax + 1) * V;
98 Matrix<T> Q(dim, dim, zero);
99 for (std::size_t n = 0; n <= levelMax; ++n) {
100 const std::size_t base = n * V;
101 // D0 on every level
102 for (std::size_t i = 0; i < V; ++i)
103 for (std::size_t j = 0; j < V; ++j) Q(base + i, base + j) += D[0](i, j);
104 // service: level n -> n-1 for n >= 1
105 if (n >= 1)
106 for (std::size_t i = 0; i < V; ++i) Q(base + i, base - V + i) += mu;
107 // batch arrivals: level n -> n+k
108 for (std::size_t k = 1; k <= K; ++k) {
109 if (n + k > levelMax) continue;
110 for (std::size_t i = 0; i < V; ++i)
111 for (std::size_t j = 0; j < V; ++j)
112 Q(base + i, (n + k) * V + j) += D[k](i, j);
113 }
114 }
115 // Row-sum correction: the reference subtracts the row sums from the
116 // diagonal AFTER assembling, so D0's own negative diagonal is included in
117 // the sum and the result is a proper generator of the truncated chain.
118 for (std::size_t i = 0; i < dim; ++i) {
119 T s = zero;
120 for (std::size_t j = 0; j < dim; ++j) s += Q(i, j);
121 Q(i, i) -= s;
122 }
123 // Replace the last column by ones and solve pi Q = e_last^T.
124 for (std::size_t i = 0; i < dim; ++i) Q(i, dim - 1) = one;
125 Matrix<T> Qt(dim, dim, zero);
126 for (std::size_t i = 0; i < dim; ++i)
127 for (std::size_t j = 0; j < dim; ++j) Qt(i, j) = Q(j, i);
128 std::vector<T> b(dim, zero);
129 b[dim - 1] = one;
130 const std::vector<T> pi = solve(Qt, b);
131
132 Matrix<T> levelProb(levelMax + 1, V, zero);
133 T tot = zero;
134 for (std::size_t n = 0; n <= levelMax; ++n)
135 for (std::size_t i = 0; i < V; ++i) {
136 const T v = pi[n * V + i];
137 levelProb(n, i) = (v < zero) ? zero : v;
138 tot += levelProb(n, i);
139 }
140 if (tot > zero)
141 for (std::size_t n = 0; n <= levelMax; ++n)
142 for (std::size_t i = 0; i < V; ++i) levelProb(n, i) /= tot;
143 return levelProb;
144}
145
146/** Relative contribution the truncated tail would add to the mean level. */
147template <class T>
148double level_tail_error(const Matrix<T>& levelProb) {
149 const std::size_t L = levelProb.rows();
150 double meanLevel = 0.0, last = 0.0;
151 for (std::size_t n = 0; n < L; ++n) {
152 double m = 0.0;
153 for (std::size_t i = 0; i < levelProb.cols(); ++i)
154 m += num_traits<T>::to_double(levelProb(n, i));
155 meanLevel += static_cast<double>(n) * m;
156 if (n + 1 == L) last = m;
157 }
158 const double denom = std::max(meanLevel, std::numeric_limits<double>::min());
159 return static_cast<double>(L - 1) * last / denom;
160}
161
162} // namespace bmapm1_detail
163
164/**
165 * @brief BMAP/M/1 by the matrix-analytic (M/G/1-type) method.
166 *
167 * @param D the BMAP {D0, D1, ..., DK}; D0 carries hidden transitions, Dk the
168 * transitions releasing a batch of k
169 * @param mu exponential service rate
170 * @param qParam uniformization constant; <= 0 selects the reference's default
171 * @param maxLevelParam explicit level truncation; 0 selects the adaptive search
172 * @param maxIter iteration cap for the G matrix
173 * @param tol convergence tolerance for the G matrix
174 * @param tailTol relative truncation target for the level distribution
175 */
176template <class T>
177BmapM1Result<T> qsys_bmapm1(const std::vector<Matrix<T>>& D, const T& mu, const T& qParam,
178 std::size_t maxLevelParam, unsigned maxIter, const T& tol,
179 double tailTol) {
181 "qsys_bmapm1 runs tolerance-terminated iterations (G, and the level truncation)");
183 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
184 if (D.size() < 2)
185 throw InputError(
186 "qsys_bmapm1: the BMAP must be given as {D0,D1,...,DK} with at least D0 and D1");
187 const std::size_t V = D[0].rows();
188 for (std::size_t k = 0; k < D.size(); ++k) {
189 if (D[k].rows() != V || D[k].cols() != V)
190 throw InputError("qsys_bmapm1: BMAP matrix D{" + std::to_string(k) +
191 "} is not conformable");
192 if (k >= 1)
193 for (std::size_t i = 0; i < V; ++i)
194 for (std::size_t j = 0; j < V; ++j)
195 if (num_traits<T>::to_double(D[k](i, j)) < -GlobalConstants::FineTol)
196 throw InputError("qsys_bmapm1: BMAP arrival matrix D{" +
197 std::to_string(k) + "} must be non-negative");
198 }
199 Matrix<T> Dsum(V, V, zero);
200 for (const Matrix<T>& Dk : D)
201 for (std::size_t i = 0; i < V; ++i)
202 for (std::size_t j = 0; j < V; ++j) Dsum(i, j) += Dk(i, j);
203 for (std::size_t i = 0; i < V; ++i) {
204 T s = zero;
205 for (std::size_t j = 0; j < V; ++j) s += Dsum(i, j);
206 if (std::fabs(num_traits<T>::to_double(s)) > std::sqrt(GlobalConstants::FineTol))
207 throw InputError(
208 "qsys_bmapm1: BMAP matrices are inconsistent, sum_k D_k must have zero row sums");
209 }
210 if (!(num_traits<T>::to_double(mu) > 0.0) || !std::isfinite(num_traits<T>::to_double(mu)))
211 throw InputError("qsys_bmapm1: the service rate mu must be a finite positive scalar");
212
213 const std::size_t K = D.size() - 1;
215
216 r.theta = mc::ctmc_solve(Dsum);
217 Matrix<T> sumKDk(V, V, zero);
218 for (std::size_t k = 1; k <= K; ++k)
219 for (std::size_t i = 0; i < V; ++i)
220 for (std::size_t j = 0; j < V; ++j)
221 sumKDk(i, j) += num_traits<T>::from_int(static_cast<int>(k)) * D[k](i, j);
222 r.lambda = zero;
223 {
224 const std::vector<T> t = vecmul(r.theta, sumKDk);
225 for (const T& v : t) r.lambda += v;
226 }
227 r.rho = T(r.lambda / mu);
228
229 T outflow = T(-D[0](0, 0));
230 for (std::size_t i = 1; i < V; ++i)
231 if (T(-D[0](i, i)) > outflow) outflow = T(-D[0](i, i));
232 const T qmin = T(outflow + mu);
233 r.q = (num_traits<T>::to_double(qParam) > 0.0) ? qParam : qmin;
235 num_traits<T>::to_double(qmin) - GlobalConstants::FineTol)
236 throw InputError(
237 "qsys_bmapm1: the uniformization constant does not dominate the total outflow rate; "
238 "the randomized chain would have negative entries");
239
240 r.A0 = Matrix<T>(V, V, zero);
241 r.A1 = Matrix<T>(V, V, zero);
242 r.B0 = Matrix<T>(V, V, zero);
243 for (std::size_t i = 0; i < V; ++i) {
244 r.A0(i, i) = T(mu / r.q);
245 for (std::size_t j = 0; j < V; ++j) {
246 r.A1(i, j) = T(D[0](i, j) / r.q);
247 r.B0(i, j) = T(D[0](i, j) / r.q);
248 }
249 r.A1(i, i) = T(r.A1(i, i) - mu / r.q + one);
250 r.B0(i, i) = T(r.B0(i, i) + one);
251 }
252 r.Bk.resize(K);
253 for (std::size_t k = 1; k <= K; ++k) {
254 r.Bk[k - 1] = Matrix<T>(V, V, zero);
255 for (std::size_t i = 0; i < V; ++i)
256 for (std::size_t j = 0; j < V; ++j) r.Bk[k - 1](i, j) = T(D[k](i, j) / r.q);
257 }
258 r.A = Matrix<T>(V, V, zero);
259 for (std::size_t i = 0; i < V; ++i)
260 for (std::size_t j = 0; j < V; ++j) {
261 r.A(i, j) = r.A0(i, j) + r.A1(i, j);
262 for (std::size_t k = 0; k < K; ++k) r.A(i, j) += r.Bk[k](i, j);
263 }
264 r.alpha = mc::dtmc_solve(r.A);
265
266 // G = A0 + A1 G + sum_k Bk G^(k+1), by functional iteration from zero.
267 r.G = Matrix<T>(V, V, zero);
268 r.gConverged = false;
269 for (unsigned it = 0; it < maxIter; ++it) {
270 Matrix<T> Gpow = r.G;
271 Matrix<T> Gnew = matmul(r.A1, r.G);
272 for (std::size_t i = 0; i < V; ++i)
273 for (std::size_t j = 0; j < V; ++j) Gnew(i, j) += r.A0(i, j);
274 for (std::size_t k = 0; k < K; ++k) {
275 Gpow = matmul(Gpow, r.G);
276 const Matrix<T> add = matmul(r.Bk[k], Gpow);
277 for (std::size_t i = 0; i < V; ++i)
278 for (std::size_t j = 0; j < V; ++j) Gnew(i, j) += add(i, j);
279 }
280 double diff = 0.0;
281 for (std::size_t i = 0; i < V; ++i)
282 for (std::size_t j = 0; j < V; ++j)
283 diff = std::max(diff, std::fabs(num_traits<T>::to_double(Gnew(i, j)) -
284 num_traits<T>::to_double(r.G(i, j))));
285 r.G = Gnew;
286 if (diff < num_traits<T>::to_double(tol)) {
287 r.gConverged = true;
288 break;
289 }
290 }
291
292 Matrix<T> upDrift(V, V, zero);
293 for (std::size_t k = 0; k < K; ++k)
294 for (std::size_t i = 0; i < V; ++i)
295 for (std::size_t j = 0; j < V; ++j)
296 upDrift(i, j) +=
297 num_traits<T>::from_int(static_cast<int>(k + 1)) * r.Bk[k](i, j);
298 r.drift = zero;
299 {
300 const std::vector<T> up = vecmul(r.alpha, upDrift);
301 const std::vector<T> dn = vecmul(r.alpha, r.A0);
302 for (std::size_t i = 0; i < V; ++i) r.drift += up[i] - dn[i];
303 }
304
305 // Level probabilities, adaptively refined until the tail is negligible.
306 std::size_t levelMax;
307 if (maxLevelParam > 0) {
308 levelMax = maxLevelParam;
309 r.levelProb = bmapm1_detail::solve_levels(D, mu, V, K, levelMax);
310 r.truncError = bmapm1_detail::level_tail_error(r.levelProb);
311 } else {
312 const double rd = num_traits<T>::to_double(r.rho);
313 const double slack = std::max(1.0 - std::min(rd, 0.999),
314 std::numeric_limits<double>::epsilon());
315 levelMax = std::max<std::size_t>(50, static_cast<std::size_t>(std::ceil(20.0 / slack)));
316 for (;;) {
317 r.levelProb = bmapm1_detail::solve_levels(D, mu, V, K, levelMax);
318 r.truncError = bmapm1_detail::level_tail_error(r.levelProb);
319 if (r.truncError <= tailTol || (2 * levelMax + 1) * V > 200000) break;
320 levelMax *= 2;
321 }
322 }
323
324 std::vector<double> levelMass(r.levelProb.rows(), 0.0);
325 for (std::size_t n = 0; n < r.levelProb.rows(); ++n)
326 for (std::size_t i = 0; i < V; ++i)
327 levelMass[n] += num_traits<T>::to_double(r.levelProb(n, i));
328 // Read the ratio where the mass is still meaningful, not at the boundary.
329 // THE INDEX IS THE REFERENCE'S AND IT IS 1-BASED: `usable` is
330 // find(levelMass > 1e-12, 1, 'last'), `ref = max(2, floor(usable/2))`, and
331 // the ratio is levelMass(ref+1)/levelMass(ref). Reading it one level higher
332 // samples the ratio before it has settled: on the Bolch fixture that moves
333 // the answer from 0.413155865252739 to 0.413243742742746, a 2.1e-4 relative
334 // shift that no other returned quantity shows.
335 std::size_t usable1 = 0; // 1-based, 0 = none
336 for (std::size_t n = 0; n < levelMass.size(); ++n)
337 if (levelMass[n] > 1e-12) usable1 = n + 1;
338 if (usable1 < 3) {
339 r.decayRate = std::numeric_limits<double>::quiet_NaN();
340 } else {
341 const std::size_t ref0 = std::max<std::size_t>(2, usable1 / 2) - 1;
342 r.decayRate = (ref0 + 1 < levelMass.size() && levelMass[ref0] > 0.0)
343 ? levelMass[ref0 + 1] / levelMass[ref0]
344 : std::numeric_limits<double>::quiet_NaN();
345 }
346
347 r.meanQueueLength = zero;
348 for (std::size_t n = 0; n < levelMass.size(); ++n)
349 r.meanQueueLength +=
350 num_traits<T>::from_int(static_cast<int>(n)) * num_traits<T>::from_double(levelMass[n]);
351 r.pi0 = num_traits<T>::from_double(levelMass[0]);
352 r.utilization = r.rho;
353 r.throughput = r.lambda;
354 r.truncLevel = r.levelProb.rows() - 1;
355 return r;
356}
357
358/** The reference's defaults: adaptive truncation, 10000 iterations, tol 1e-12. */
359template <class T>
360BmapM1Result<T> qsys_bmapm1(const std::vector<Matrix<T>>& D, const T& mu) {
361 return qsys_bmapm1(D, mu, num_traits<T>::from_int(0), static_cast<std::size_t>(0), 10000u,
362 T(num_traits<T>::from_double(1e-12)), 1e-10);
363}
364
365} // namespace qsys
366} // namespace line
367
368#endif // LINE_API_QSYS_QSYS_BMAPM1_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
Steady-state distribution of a continuous-time Markov chain.
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
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.
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
std::vector< T > dtmc_solve(const Matrix< T > &P)
Stationary distribution of a stochastic matrix P.
Definition dtmc_solve.h:106
BmapM1Result< T > qsys_bmapm1(const std::vector< Matrix< T > > &D, const T &mu, const T &qParam, std::size_t maxLevelParam, unsigned maxIter, const T &tol, double tailTol)
BMAP/M/1 by the matrix-analytic (M/G/1-type) method.
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 > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
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
Number-type abstraction for the templated API port.
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:667
Everything qsys_bmapm1 returns, mirroring the MATLAB result struct.
Definition qsys_bmapm1.h:62
T q
uniformization constant actually used
Definition qsys_bmapm1.h:66
T drift
stable iff strictly negative
Definition qsys_bmapm1.h:72
T pi0
probability the system is empty
Definition qsys_bmapm1.h:75
std::vector< Matrix< T > > Bk
level up by k, k = 1..K
Definition qsys_bmapm1.h:68
Matrix< T > levelProb
level probabilities, row n = pi_n
Definition qsys_bmapm1.h:74
T lambda
mean arrival rate, theta (sum_k k D_k) e
Definition qsys_bmapm1.h:64
std::vector< T > theta
stationary vector of the BMAP phase process
Definition qsys_bmapm1.h:63
T rho
offered load lambda/mu
Definition qsys_bmapm1.h:65
Matrix< T > B0
randomized blocks: down, local, boundary local
Definition qsys_bmapm1.h:67
Matrix< T > A
A0 + A1 + sum_k Bk, the phase process.
Definition qsys_bmapm1.h:69
Matrix< T > G
minimal non-negative solution of the M/G/1-type equation
Definition qsys_bmapm1.h:71
double decayRate
measured pi_(n+1)/pi_n; NaN when unmeasurable
Definition qsys_bmapm1.h:73
std::vector< T > alpha
stationary vector of A
Definition qsys_bmapm1.h:70