LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mapmc.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_MAPMC_H
6#define LINE_API_QSYS_QSYS_MAPMC_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * The MAP/M/c FCFS queue: c identical exponential servers of rate mu fed by a
12 * Markovian arrival process.
13 *
14 * ALGORITHM, AND HOW IT DIFFERS FROM THE MATLAB REFERENCE.
15 * matlab/src/api/qsys/qsys_mapmc.m obtains these quantities by calling Q-MAM's
16 * Q_CT_MAP_M_C, which is not transcribed here. This port computes the SAME
17 * quantities from the port's own quasi-birth-death machinery, by building the
18 * QBD directly: the level is the
19 * number in system and the phase is the arrival phase, so for levels n >= c
20 * the process is level independent with
21 *
22 * A0 = D1 (an arrival), A1 = D0 - c mu I (local), A2 = c mu I (a
23 * completion),
24 *
25 * while levels 0..c-1 form the boundary, where level k has downward rate
26 * k mu instead of c mu. R is the minimal non-negative solution of
27 * R^2 A2 + R A1 + A0 = 0, reached by cyclic reduction (qbd_fundmat called with
28 * B = A2, L = A1, F = A0), and pi_{c+k} = pi_c R^k. The boundary vectors
29 * pi_0..pi_c come from the level balance equations with the last one replaced
30 * by the normalization
31 *
32 * sum_{n<c} pi_n e + pi_c (I - R)^-1 e = 1,
33 *
34 * after which the number in system is
35 *
36 * L = sum_{n<c} n pi_n e + pi_c ( c (I-R)^-1 + R (I-R)^-2 ) e
37 *
38 * and the queued jobs are Lq = pi_c R (I-R)^-2 e. This is the same
39 * construction as qsys_phmc.h with the PH arrival replaced by a general MAP:
40 * a PH arrival is the special case D0 = T, D1 = (-T e) alpha. Utilization is
41 * lambda/(c mu), the reference's per-server convention.
42 *
43 * meanWaitingTime is Lq/lambda by Little's law. Since 2026-10-18 MATLAB, the
44 * JAR and Python do the same: Q_CT_MAP_M_C returns the closed-form L and Lq, and
45 * the four codebases agree to about 1e-13 (e.g. 3.12185040294165 /
46 * 1.67587177394999 on the correlated MMPP2 instance, mu = 1, c = 2).
47 *
48 * THE REFERENCE USED TO BE THE INACCURATE ONE. It summed ql truncated at
49 * maxNumComp = 500 levels and took meanWaitingTime from the waiting-time PH,
50 * whose Sylvester fixed point stops at a 1e-10 step while converging at a rate
51 * near sp(R). At moderate load that cost 1e-9 (M/M/2: 1.874999996296936 /
52 * 0.5624999998489539 against Erlang-C 1.875 / 0.5625); at high load with
53 * slowly modulated arrivals it cost 8% (a 3-phase KPC fit at rho = 0.8 gave
54 * Wq 663.96 against 723.766), and the JAR's 1000-iteration cap on that fixed
55 * point made it worse.
56 *
57 * That the closed form is the accurate one was checked without MATLAB: at
58 * c = 1 the same instance is also a MAP/MAP/1 queue with an exponential
59 * service MAP, and qsys_mapmap1 -- a completely different route,
60 * cyclic reduction on the Kronecker-product QBD followed by the closed-form
61 * factorial moment -- reproduces this function to 5e-16 on both the correlated
62 * and the Erlang-2 arrival instances. The test file carries that cross-check.
63 *
64 * ARITHMETIC. Gated on num_traits<T>::has_transcendental: cyclic reduction
65 * drives R to a tolerance and never terminates in a finite number of field
66 * operations, exactly as in qbd_r.h and qsys_phmc.h. The boundary solve, the
67 * geometric-tail sums and the moment formulas that consume R are finite exact
68 * matrix algebra and add no error of their own.
69 */
70
71#include <cstddef>
72#include <vector>
73
75#include "line/api/mam/qbd_r.h"
76#include "line/num/number.h"
77#include "line/util/error.h"
78#include "line/util/linalg.h"
79#include "line/util/lu.h"
80#include "line/util/matrix.h"
81
82namespace line {
83namespace qsys {
84
85/** Return value of qsys_mapmc and qsys_mapm1, mirroring the MATLAB struct. */
86template <class T>
88 T meanQueueLength; ///< L, number in system
89 T meanWaitingTime; ///< Wq, time in queue
90 T meanSojournTime; ///< W = Wq + 1/mu
91 T utilization; ///< rho = lambda / (c mu), per server
92 std::vector<T> queueLengthDist; ///< P(N = n), n = 0, 1, ...
93};
94
95/**
96 * MAP/M/c by the matrix-geometric solution.
97 *
98 * @param arrival arrival MAP (D0, D1)
99 * @param mu exponential service rate of one server
100 * @param c number of servers, c >= 1
101 * @param dist_size how many entries of queueLengthDist to materialize
102 */
103template <class T>
104MapMcResult<T> qsys_mapmc(const mam::Map<T>& arrival, const T& mu, unsigned c,
105 std::size_t dist_size) {
107 "qsys_mapmc requires transcendental arithmetic");
108 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
109 if (mu <= zero) throw InputError("qsys_mapmc: service rate mu must be positive");
110 if (c < 1) throw InputError("qsys_mapmc: c must be a positive integer");
111 if (dist_size == 0) throw InputError("qsys_mapmc: dist_size must be positive");
112 const std::size_t k = arrival.D0.rows();
113 if (arrival.D0.cols() != k || arrival.D1.rows() != k || arrival.D1.cols() != k)
114 throw InputError("qsys_mapmc: D0 and D1 must be square and of equal order");
115 const T ct = num_traits<T>::from_int(static_cast<long>(c));
116
117 const Matrix<T>& D0 = arrival.D0;
118 const Matrix<T>& D1 = arrival.D1;
119 const T lambda = mam::map_lambda(arrival);
120 if (lambda <= zero) throw InputError("qsys_mapmc: non-positive arrival rate");
121 const T rho = lambda / (ct * mu);
122 if (rho >= one) throw InputError("qsys_mapmc: load rho must be strictly less than 1");
123
124 // cyclic-reduction R rationale: see _kb/03-api-layer.md (cpp port notes: qsys)
125 const Matrix<T>& A0 = D1;
126 Matrix<T> A1(k, k), A2(k, k, zero);
127 for (std::size_t i = 0; i < k; ++i) {
128 for (std::size_t j = 0; j < k; ++j) A1(i, j) = D0(i, j);
129 A1(i, i) -= ct * mu;
130 A2(i, i) = ct * mu;
131 }
132 const Matrix<T> R = mam::qbd_fundmat(A2, A1, A0).R;
133
134 // Boundary system on the stacked unknowns [pi_0 ... pi_c].
135 const std::size_t nvar = (c + 1) * k;
136 Matrix<T> M(nvar, nvar, zero);
137 // Level 0: pi_0 D0 + pi_1 (mu I) = 0.
138 for (std::size_t j = 0; j < k; ++j) {
139 for (std::size_t i = 0; i < k; ++i) M(j, i) += D0(i, j);
140 M(j, k + j) += mu;
141 }
142 // Levels 1..c-1: pi_{n-1} D1 + pi_n (D0 - n mu I) + pi_{n+1} ((n+1) mu I) = 0.
143 for (unsigned n = 1; n < c; ++n) {
144 const T nt = num_traits<T>::from_int(static_cast<long>(n));
145 for (std::size_t j = 0; j < k; ++j) {
146 const std::size_t row = n * k + j;
147 for (std::size_t i = 0; i < k; ++i) {
148 M(row, (n - 1) * k + i) += D1(i, j);
149 M(row, n * k + i) += D0(i, j);
150 }
151 M(row, n * k + j) -= nt * mu;
152 M(row, (n + 1) * k + j) += (nt + one) * mu;
153 }
154 }
155 // Level c: pi_{c-1} D1 + pi_c (D0 - c mu I + R (c mu I)) = 0.
156 const Matrix<T> RA2c = matmul(R, A2);
157 for (std::size_t j = 0; j < k; ++j) {
158 const std::size_t row = c * k + j;
159 for (std::size_t i = 0; i < k; ++i) {
160 M(row, (c - 1) * k + i) += D1(i, j);
161 M(row, c * k + i) += D0(i, j) + RA2c(i, j);
162 }
163 M(row, c * k + j) -= ct * mu;
164 }
165 // Replace the last equation with the normalization.
166 Matrix<T> IR(k, k);
167 for (std::size_t i = 0; i < k; ++i)
168 for (std::size_t j = 0; j < k; ++j) IR(i, j) = (i == j ? one : zero) - R(i, j);
169 const std::vector<T> sum_geom = line::solve(IR, ones<T>(k));
170 for (std::size_t col = 0; col < nvar; ++col) M(nvar - 1, col) = zero;
171 for (unsigned n = 0; n < c; ++n)
172 for (std::size_t i = 0; i < k; ++i) M(nvar - 1, n * k + i) = one;
173 for (std::size_t i = 0; i < k; ++i) M(nvar - 1, c * k + i) = sum_geom[i];
174 std::vector<T> b(nvar, zero);
175 b[nvar - 1] = one;
176 const std::vector<T> x = line::solve(M, b);
177
178 std::vector<T> pi_c(k);
179 for (std::size_t i = 0; i < k; ++i) pi_c[i] = x[c * k + i];
180
181 const Matrix<T> IRinv = inverse(IR);
182 const Matrix<T> IRinv2 = matmul(IRinv, IRinv);
183 const Matrix<T> R_IRinv2 = matmul(R, IRinv2);
184 const std::vector<T> e = ones<T>(k);
185 const std::vector<T> v1 = mulvec(R_IRinv2, e);
186 T Lq = zero;
187 for (std::size_t i = 0; i < k; ++i) Lq += pi_c[i] * v1[i];
188 Matrix<T> bulk(k, k);
189 for (std::size_t i = 0; i < k; ++i)
190 for (std::size_t j = 0; j < k; ++j) bulk(i, j) = ct * IRinv(i, j) + R_IRinv2(i, j);
191 const std::vector<T> v2 = mulvec(bulk, e);
192 T L = zero;
193 for (std::size_t i = 0; i < k; ++i) L += pi_c[i] * v2[i];
194 for (unsigned n = 0; n < c; ++n) {
195 T s = zero;
196 for (std::size_t i = 0; i < k; ++i) s += x[n * k + i];
197 L += num_traits<T>::from_int(static_cast<long>(n)) * s;
198 }
199
201 r.meanQueueLength = L;
202 r.meanWaitingTime = Lq / lambda;
203 r.meanSojournTime = r.meanWaitingTime + one / mu;
204 r.utilization = rho;
205
206 // Level distribution: pi_n from the boundary for n <= c, pi_c R^(n-c) above.
207 r.queueLengthDist.assign(dist_size, zero);
208 std::vector<T> cur(k, zero);
209 for (std::size_t n = 0; n < dist_size; ++n) {
210 if (n <= c) {
211 for (std::size_t i = 0; i < k; ++i) cur[i] = x[n * k + i];
212 } else {
213 cur = vecmul(cur, R);
214 }
215 T s = zero;
216 for (std::size_t i = 0; i < k; ++i) s += cur[i];
217 r.queueLengthDist[n] = s;
218 }
219 return r;
220}
221
222/** qsys_mapmc with 500 materialized levels, the reference's maxNumComp default. */
223template <class T>
224MapMcResult<T> qsys_mapmc(const mam::Map<T>& arrival, const T& mu, unsigned c) {
225 return qsys_mapmc(arrival, mu, c, static_cast<std::size_t>(500));
226}
227
228} // namespace qsys
229} // namespace line
230
231#endif // LINE_API_QSYS_QSYS_MAPMC_H
InputError(const std::string &what)
Definition error.h:105
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.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
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:285
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
MapMcResult< T > qsys_mapmc(const mam::Map< T > &arrival, const T &mu, unsigned c, std::size_t dist_size)
MAP/M/c by the matrix-geometric solution.
Definition qsys_mapmc.h:104
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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
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
Number-type abstraction for the templated API port.
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
Return value of qsys_mapmc and qsys_mapm1, mirroring the MATLAB struct.
Definition qsys_mapmc.h:87
T utilization
rho = lambda / (c mu), per server
Definition qsys_mapmc.h:91
T meanWaitingTime
Wq, time in queue.
Definition qsys_mapmc.h:89
T meanSojournTime
W = Wq + 1/mu.
Definition qsys_mapmc.h:90
T meanQueueLength
L, number in system.
Definition qsys_mapmc.h:88
std::vector< T > queueLengthDist
P(N = n), n = 0, 1, ...
Definition qsys_mapmc.h:92