LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_mvac.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_PFQN_MVAC_H
6#define LINE_API_PFQN_MVAC_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * MVAC: exact mean value analysis BY CHAIN of a closed multichain product-form
12 * network (Conway, de Souza e Silva and Lavenberg, IEEE Trans. Computers
13 * 38(3):432-442, 1989).
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_mvac.m.
16 *
17 * Where the classic MVA recursion of pfqn_mva recurs on the POPULATION vector
18 * and costs O(prod(N+1)), MVAC recurs on the CHAINS: each chain is reduced to
19 * single-customer chains and the removed ones are replaced by self-looping
20 * single-customer (SCSL) chains pinned at a service center. The multiplicity
21 * vector v = (v_1,...,v_J), v_j the number of SCSL chains at center j, indexes
22 * the recursion in place of the population. Writing L^k_j(v) for the mean
23 * number at center j with the SCSL customers excluded,
24 *
25 * lambda^k_k(v) = 1 / ( a_k + sum_{j SSFR} a_jk (L^{k-1}_j(v) + v_j) ) (10)
26 * L^k_{jk}(v) = lambda^k_k(v) a_jk (1 + L^{k-1}_j(v) + v_j), j SSFR (9a)
27 * L^k_{jk}(v) = lambda^k_k(v) a_jk, j IS (9b)
28 * L^k_i(v) = sum_j L^k_{jk}(v) L^{k-1}_i(v + 1_j) + L^k_{ik}(v) (7)
29 * L^k_{il}(v) = sum_j L^k_{jk}(v) L^{k-1}_{il}(v + 1_j), l < k (6)
30 *
31 * with L^0 = 0, read off at k = K, v = 0. Part 1 evaluates (10), (9) and (7);
32 * part 2 evaluates (6) for the chains that visit at least one IS center, whose
33 * throughput then follows from Little's law there; the chains that visit only
34 * SSFR centers need a re-execution of part 1 with their label interchanged with
35 * K, which is cheap because the levels below the interchanged label are
36 * untouched and are reused.
37 *
38 * IDENTICAL CHAINS. Classes with N_r > 1, and classes with identical demand
39 * columns, collapse into one subset of identical single-customer chains: only
40 * the representative is analyzed and its per-chain measures are multiplied by
41 * the class population. The cost therefore depends on the number of DISTINCT
42 * chains, not on K. The subsets are found by MATLAB's
43 * unique(...,'rows','stable'), whose first-appearance order the port
44 * reproduces, because the chain labelling (representatives last, IS-visiting
45 * ones before the rest) is built from it and part 3 interchanges labels by
46 * position.
47 *
48 * NO NORMALIZING CONSTANT is formed anywhere, so MVAC does not suffer the
49 * underflow and overflow that complicate RECAL and convolution.
50 *
51 * Arithmetic: EXACT-CAPABLE. Additions, multiplications and divisions in the
52 * field of the inputs only, no logarithm and no tolerance. Instantiated at
53 * Rational it returns the same throughputs and queue lengths as pfqn_mva and
54 * pfqn_ca as exact fractions.
55 *
56 * REFERENCE DEFECTS: none found. Agreement with pfqn_mva is to the last ulp on
57 * every model tried, including models with a delay, without a delay, with
58 * repeated demand columns and with empty classes.
59 */
60
61#include <algorithm>
62#include <cstddef>
63#include <vector>
64
66#include "line/num/number.h"
67#include "line/util/error.h"
68#include "line/util/matrix.h"
69
70namespace line {
71namespace pfqn {
72
73/** Return value of pfqn_mvac, mirroring [XN, QN, UN, CN]. */
74template <class T>
75struct MvacResult {
76 std::vector<T> X; ///< (R) per-class throughput
77 Matrix<T> Q; ///< (M x R) per-class queue length at the SSFR queues
78 Matrix<T> U; ///< (M x R) per-class utilization
79 Matrix<T> C; ///< (M x R) per-class residence time
80};
81
82namespace detail {
83
84/**
85 * Chain subsets, labelling and multiplicity-vector lattice shared by pfqn_mvac
86 * and pfqn_mvacld. Both build exactly the same objects from the same A matrix;
87 * only the recursion that runs over them differs.
88 */
89template <class T>
90struct MvacSetup {
91 std::size_t J1 = 0, J = 0, D = 0, S = 0;
92 std::size_t K = 0;
93 Matrix<T> a; ///< (J x K) per-chain demands
94 std::vector<std::size_t> posr; ///< classes with N > 0
95 std::vector<std::size_t> grpOfClass; ///< subset of each populated class (0-based)
96 std::vector<std::size_t> gorder; ///< subset order: IS-visiting first
97 std::vector<std::vector<int>> Vlist; ///< multiplicity vectors, by increasing sum
98 std::vector<int> vsum;
99 std::vector<std::size_t> off, cnt; ///< block offset and size per component sum
100 Matrix<long> succ; ///< index of v + 1_j, or -1
101};
102
103template <class T>
104MvacSetup<T> mvac_setup(const Matrix<T>& A, const std::vector<int>& N, std::size_t J1,
105 std::size_t J, std::size_t K) {
106 const T zero = num_traits<T>::from_int(0);
107 MvacSetup<T> st;
108 st.J1 = J1;
109 st.J = J;
110 st.K = K;
111
112 for (std::size_t r = 0; r < N.size(); ++r)
113 if (N[r] > 0) st.posr.push_back(r);
114
115 // distinct demand columns, first-appearance order (MATLAB unique 'stable')
116 std::vector<std::vector<T>> Adist;
117 st.grpOfClass.assign(st.posr.size(), 0);
118 for (std::size_t p = 0; p < st.posr.size(); ++p) {
119 std::vector<T> colv(J, zero);
120 for (std::size_t j = 0; j < J; ++j) colv[j] = A(j, st.posr[p]);
121 std::size_t g = Adist.size();
122 for (std::size_t q = 0; q < Adist.size(); ++q)
123 if (Adist[q] == colv) {
124 g = q;
125 break;
126 }
127 if (g == Adist.size()) Adist.push_back(colv);
128 st.grpOfClass[p] = g;
129 }
130 const std::size_t Dall = Adist.size();
131
132 std::vector<bool> visitsIS(Dall, false);
133 for (std::size_t g = 0; g < Dall; ++g)
134 for (std::size_t j = J1; j < J; ++j)
135 if (Adist[g][j] > zero) {
136 visitsIS[g] = true;
137 break;
138 }
139 for (std::size_t g = 0; g < Dall; ++g)
140 if (visitsIS[g]) st.gorder.push_back(g);
141 for (std::size_t g = 0; g < Dall; ++g)
142 if (!visitsIS[g]) st.gorder.push_back(g);
143 st.D = Dall;
144 st.S = 0;
145 for (std::size_t g = 0; g < Dall; ++g)
146 if (!visitsIS[g]) ++st.S;
147
148 // chain labels: the D representatives take K-D+1..K, the rest fill 1..K-D
149 std::vector<std::size_t> mult(st.D, 0);
150 for (std::size_t g = 0; g < st.D; ++g)
151 for (std::size_t p = 0; p < st.posr.size(); ++p)
152 if (st.grpOfClass[p] == st.gorder[g])
153 mult[g] += static_cast<std::size_t>(N[st.posr[p]]);
154 std::vector<std::size_t> chainGroup(K, 0);
155 for (std::size_t g = 0; g < st.D; ++g) chainGroup[K - st.D + g] = g;
156 std::size_t p = 0;
157 for (std::size_t g = 0; g < st.D; ++g)
158 for (std::size_t cc = 0; cc + 1 < mult[g]; ++cc) chainGroup[p++] = g;
159
160 st.a = Matrix<T>(J, K, zero);
161 for (std::size_t k = 0; k < K; ++k)
162 for (std::size_t j = 0; j < J; ++j) st.a(j, k) = Adist[st.gorder[chainGroup[k]]][j];
163
164 // multiplicity vectors, enumerated by increasing component sum
165 st.off.assign(K + 1, 0);
166 st.cnt.assign(K + 1, 0);
167 for (std::size_t t = 0; t <= K; ++t) {
168 const std::vector<std::vector<int>> Vt =
169 multichoose_rows(static_cast<int>(J), static_cast<int>(t));
170 st.off[t] = st.Vlist.size();
171 st.cnt[t] = Vt.size();
172 for (std::size_t i = 0; i < Vt.size(); ++i) {
173 st.Vlist.push_back(Vt[i]);
174 st.vsum.push_back(static_cast<int>(t));
175 }
176 }
177 const std::size_t nv = st.Vlist.size();
178 st.succ = Matrix<long>(nv, J, -1);
179 for (std::size_t vi = 0; vi < nv; ++vi) {
180 const int t = st.vsum[vi];
181 if (static_cast<std::size_t>(t) + 1 > K) continue;
182 for (std::size_t j = 0; j < J; ++j) {
183 std::vector<int> w = st.Vlist[vi];
184 w[j] += 1;
185 for (std::size_t q = 0; q < st.cnt[t + 1]; ++q)
186 if (st.Vlist[st.off[t + 1] + q] == w) {
187 st.succ(vi, j) = static_cast<long>(st.off[t + 1] + q);
188 break;
189 }
190 }
191 }
192 return st;
193}
194
195/** Part 1 of the basic step: (10), (9a)-(9b) and (7) for k = k0..K over sum(v) <= K-k. */
196template <class T>
197void mvac_part1(std::size_t k0, const MvacSetup<T>& st, std::vector<Matrix<T>>& Lall,
198 std::vector<Matrix<T>>& Ljkall, std::vector<std::vector<T>>& lamall) {
199 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
200 const std::size_t J = st.J, J1 = st.J1, K = st.K, nv = st.Vlist.size();
201 for (std::size_t k = k0; k <= K; ++k) {
202 const Matrix<T>& Lp = Lall[k - 1];
203 Matrix<T> Lk(J, nv, zero), Ljk(J, nv, zero);
204 std::vector<T> lamv(nv, zero);
205 for (std::size_t vi = 0; vi < nv; ++vi) {
206 if (static_cast<std::size_t>(st.vsum[vi]) > K - k) continue;
207 T den = zero;
208 for (std::size_t j = 0; j < J; ++j) den += st.a(j, k - 1); // a_k over ALL centers
209 for (std::size_t j = 0; j < J1; ++j)
210 den += (Lp(j, vi) + num_traits<T>::from_int(st.Vlist[vi][j])) * st.a(j, k - 1);
211 if (den <= zero) throw NumericError("pfqn_mvac: a chain has zero total demand");
212 const T lam = one / den;
213 for (std::size_t j = 0; j < J1; ++j)
214 Ljk(j, vi) = lam * (one + Lp(j, vi) + num_traits<T>::from_int(st.Vlist[vi][j])) *
215 st.a(j, k - 1);
216 for (std::size_t j = J1; j < J; ++j) Ljk(j, vi) = lam * st.a(j, k - 1);
217 lamv[vi] = lam;
218 for (std::size_t i = 0; i < J; ++i) {
219 T s = Ljk(i, vi);
220 for (std::size_t j = 0; j < J; ++j) {
221 const long sj = st.succ(vi, j);
222 if (sj < 0) throw NumericError("pfqn_mvac: multiplicity vector out of range");
223 s += Ljk(j, vi) * Lp(i, static_cast<std::size_t>(sj));
224 }
225 Lk(i, vi) = s;
226 }
227 }
228 Lall[k] = Lk;
229 Ljkall[k] = Ljk;
230 lamall[k] = lamv;
231 }
232}
233
234} // namespace detail
235
236/**
237 * @brief MVAC: exact mean value analysis BY CHAIN of a closed multichain
238 * product-form network (Conway, de Souza e Silva and Lavenberg, IEEE
239 * Trans. Computers 38(3):432-442, 1989).
240 *
241 * @param L (M x R) demands of the single-server fixed-rate queues
242 * @param N (R) closed populations
243 * @param Z (Mz x R) demands of the infinite-server centers, one row per center
244 */
245template <class T>
246MvacResult<T> pfqn_mvac(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z) {
247 const std::size_t M = L.rows(), R = L.cols();
248 if (M == 0 || R == 0) throw InputError("pfqn_mvac: empty demand matrix");
249 if (N.size() != R) throw InputError("pfqn_mvac: L and N disagree on the class count");
250 if (!Z.empty() && Z.cols() != R)
251 throw InputError("pfqn_mvac: the think time matrix and the demand matrix disagree");
252 const T zero = num_traits<T>::from_int(0);
253
254 MvacResult<T> res;
255 res.X.assign(R, zero);
256 res.Q = Matrix<T>(M, R, zero);
257 res.U = Matrix<T>(M, R, zero);
258 res.C = Matrix<T>(M, R, zero);
259
260 std::size_t K = 0;
261 for (std::size_t r = 0; r < R; ++r) {
262 if (N[r] < 0) throw InputError("pfqn_mvac: the population vector must be nonnegative");
263 K += static_cast<std::size_t>(N[r]);
264 }
265 if (K == 0) return res;
266
267 // discard the centers no chain visits
268 std::vector<std::size_t> ssfrIdx, isIdx;
269 for (std::size_t i = 0; i < M; ++i)
270 for (std::size_t r = 0; r < R; ++r)
271 if (L(i, r) > zero) {
272 ssfrIdx.push_back(i);
273 break;
274 }
275 for (std::size_t i = 0; i < Z.rows(); ++i)
276 for (std::size_t r = 0; r < R; ++r)
277 if (Z(i, r) > zero) {
278 isIdx.push_back(i);
279 break;
280 }
281 const std::size_t J1 = ssfrIdx.size(), J = J1 + isIdx.size();
282 if (J == 0) throw InputError("pfqn_mvac: all service demands are zero, the throughput is unbounded");
283
284 Matrix<T> A(J, R, zero);
285 for (std::size_t j = 0; j < J1; ++j)
286 for (std::size_t r = 0; r < R; ++r) A(j, r) = L(ssfrIdx[j], r);
287 for (std::size_t j = J1; j < J; ++j)
288 for (std::size_t r = 0; r < R; ++r) A(j, r) = Z(isIdx[j - J1], r);
289
290 detail::MvacSetup<T> st = detail::mvac_setup(A, N, J1, J, K);
291 const std::size_t nv = st.Vlist.size();
292 const std::size_t z0 = 0; // the zero multiplicity vector is enumerated first
293
294 std::vector<Matrix<T>> Lall(K + 1), Ljkall(K + 1);
295 std::vector<std::vector<T>> lamall(K + 1);
296 Lall[0] = Matrix<T>(J, nv, zero);
297
298 std::vector<T> lamChain(K, zero);
299 Matrix<T> Lchain(J, K, zero);
300
301 detail::mvac_part1(1, st, Lall, Ljkall, lamall);
302 lamChain[K - 1] = lamall[K][z0];
303 for (std::size_t j = 0; j < J; ++j) Lchain(j, K - 1) = Ljkall[K](j, z0);
304
305 // ---- part 2: chains that visit at least one IS center ---------------------------
306 const std::size_t D = st.D, S = st.S;
307 const long lmaxK = std::min(static_cast<long>(K) - 1, static_cast<long>(K - S));
308 if (D >= 2 && lmaxK >= static_cast<long>(K - D + 1)) {
309 std::vector<Matrix<T>> L2prev(K + 1), L2cur(K + 1);
310 for (std::size_t k = K - D + 2; k <= K; ++k) {
311 L2cur.assign(K + 1, Matrix<T>());
312 const long lhi = std::min(static_cast<long>(k) - 1, static_cast<long>(K - S));
313 for (long l = static_cast<long>(K - D + 1); l <= lhi; ++l) {
314 Matrix<T> acc(J, nv, zero);
315 for (std::size_t vi = 0; vi < nv; ++vi) {
316 if (static_cast<std::size_t>(st.vsum[vi]) != K - k) continue;
317 for (std::size_t j = 0; j < J; ++j) {
318 const long sj = st.succ(vi, j);
319 if (sj < 0) throw NumericError("pfqn_mvac: multiplicity vector out of range");
320 const Matrix<T>& prev =
321 (l == static_cast<long>(k) - 1) ? Ljkall[k - 1] : L2prev[l];
322 for (std::size_t i = 0; i < J; ++i)
323 acc(i, vi) += Ljkall[k](j, vi) * prev(i, static_cast<std::size_t>(sj));
324 }
325 }
326 L2cur[l] = acc;
327 }
328 if (k == K) {
329 for (long l = static_cast<long>(K - D + 1); l <= lmaxK; ++l) {
330 for (std::size_t j = 0; j < J; ++j)
331 Lchain(j, static_cast<std::size_t>(l) - 1) = L2cur[l](j, z0);
332 std::size_t jIS = J;
333 for (std::size_t j = J1; j < J; ++j)
334 if (st.a(j, static_cast<std::size_t>(l) - 1) > zero) {
335 jIS = j;
336 break;
337 }
338 if (jIS == J) throw NumericError("pfqn_mvac: chain has no IS center");
339 lamChain[static_cast<std::size_t>(l) - 1] =
340 Lchain(jIS, static_cast<std::size_t>(l) - 1) /
341 st.a(jIS, static_cast<std::size_t>(l) - 1);
342 }
343 }
344 L2prev = L2cur;
345 }
346 }
347
348 // ---- part 3: chains that visit only SSFR centers, by label interchange ----------
349 std::vector<std::size_t> perm(K);
350 for (std::size_t k = 0; k < K; ++k) perm[k] = k;
351 for (std::size_t l = 1; l + 1 <= S && S >= 1 && l < S; ++l) {
352 std::swap(perm[K - l - 1], perm[K - 1]);
353 for (std::size_t j = 0; j < J; ++j) {
354 const T tmp = st.a(j, K - l - 1);
355 st.a(j, K - l - 1) = st.a(j, K - 1);
356 st.a(j, K - 1) = tmp;
357 }
358 detail::mvac_part1(K - l, st, Lall, Ljkall, lamall);
359 lamChain[perm[K - 1]] = lamall[K][z0];
360 for (std::size_t j = 0; j < J; ++j) Lchain(j, perm[K - 1]) = Ljkall[K](j, z0);
361 }
362
363 // ---- expand the per-chain measures back to per-class ----------------------------
364 for (std::size_t g = 0; g < D; ++g) {
365 const std::size_t kg = K - D + g;
366 for (std::size_t p = 0; p < st.posr.size(); ++p) {
367 if (st.grpOfClass[p] != st.gorder[g]) continue;
368 const std::size_t r = st.posr[p];
369 const T nr = num_traits<T>::from_int(N[r]);
370 res.X[r] = nr * lamChain[kg];
371 for (std::size_t j = 0; j < J1; ++j) {
372 res.Q(ssfrIdx[j], r) = nr * Lchain(j, kg);
373 res.U(ssfrIdx[j], r) = res.X[r] * L(ssfrIdx[j], r);
374 res.C(ssfrIdx[j], r) = res.Q(ssfrIdx[j], r) / res.X[r];
375 }
376 }
377 }
378 // an empty class reports its bare demand as residence time, as pfqn_mva does
379 for (std::size_t r = 0; r < R; ++r)
380 if (N[r] == 0)
381 for (std::size_t i = 0; i < M; ++i) res.C(i, r) = L(i, r);
382 return res;
383}
384
385/** Overload with the zero think-time default. */
386template <class T>
387MvacResult<T> pfqn_mvac(const Matrix<T>& L, const std::vector<int>& N) {
388 return pfqn_mvac(L, N, Matrix<T>(1, L.cols(), num_traits<T>::from_int(0)));
389}
390
391} // namespace pfqn
392} // namespace line
393
394#endif // LINE_API_PFQN_MVAC_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
std::vector< std::vector< int > > multichoose_rows(int n, int k)
All n-vectors of nonnegative integers summing to k, in MATLAB multichoose(n,k) order.
MvacResult< T > pfqn_mvac(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z)
MVAC: exact mean value analysis BY CHAIN of a closed multichain product-form network (Conway,...
Definition pfqn_mvac.h:246
Number-type abstraction for the templated API port.
Integer-composition enumeration shared by the CoMoM and MVAC ports.
Return value of pfqn_mvac, mirroring [XN, QN, UN, CN].
Definition pfqn_mvac.h:75
Matrix< T > C
(M x R) per-class residence time
Definition pfqn_mvac.h:79
Matrix< T > U
(M x R) per-class utilization
Definition pfqn_mvac.h:78
std::vector< T > X
(R) per-class throughput
Definition pfqn_mvac.h:76
Matrix< T > Q
(M x R) per-class queue length at the SSFR queues
Definition pfqn_mvac.h:77