LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_mvacld.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_MVACLD_H
6#define LINE_API_PFQN_MVACLD_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * MVAC for networks with queue-length dependent (QLD) service centers, the
12 * Section V extension of Conway, de Souza e Silva and Lavenberg (1989).
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_mvacld.m. pfqn_mvac implements
15 * Sections II-IV, which cover single-server fixed-rate and infinite-server
16 * centers only.
17 *
18 * Where pfqn_mvac propagates the MEAN queue lengths through (7), the QLD
19 * extension propagates the MARGINAL DISTRIBUTIONS P^k_j(n,v). That is forced
20 * by load dependence, since the rate seen by a job depends on the whole
21 * occupancy, but it also SIMPLIFIES the recursion: (21)-(25) read level k-1
22 * only at the shifted vectors v + 1_i, so the basic step sweeps I_k alone where
23 * pfqn_mvac must sweep I_k u ... u I_K. The marginals come out as a first-class
24 * output for free. In the reference-station-free form used here,
25 *
26 * c_i(k,v) = sum_{n=0}^{k-1} P^{k-1}_i(n, v+1_i) mu_i(n+v_i+1)/(n+v_i+1)
27 * L^k_{jk}(v) = (a_jk/c_j) / sum_m (a_mk/c_m)
28 * lambda^k_k(v) = 1 / sum_m (a_mk/c_m)
29 * P^k_j(n,v) = L^k_{jk}(v) P^{k-1}_j(n-1, v+1_j)
30 * + sum_{m != j} L^k_{mk}(v) P^{k-1}_j(n, v+1_m)
31 *
32 * with c_i = 1 identically at an IS center, which is (22). Equation (25) is
33 * self-normalizing, sum_n P^k_j(n,v) = sum_m L^k_{mk}(v) = 1, so no normalizing
34 * constant is formed and every quantity in the recursion is positive: unlike
35 * the load-dependent MVA of pfqn_mvald it CANNOT produce negative
36 * probabilities and needs no stabilization. That is the practical reason to
37 * prefer it.
38 *
39 * OUTPUT CONVENTIONS differ from pfqn_mvac and follow the load-dependent
40 * family (pfqn_mvald, pfqn_dac): U is PER-STATION, 1 - P_j(0), because for a
41 * load-dependent center the per-class product X_r L_{jr} is not the
42 * utilization; and C is the per-class CYCLE TIME exclusive of think time,
43 * N_r/X_r - Z_r, not an (M x R) residence time.
44 *
45 * Parts 2 and 3 are unchanged from pfqn_mvac, since (6) holds verbatim in the
46 * presence of QLD centers, and are driven from the same chain setup.
47 *
48 * Arithmetic: EXACT-CAPABLE, field operations only.
49 *
50 * REFERENCE DEFECTS: none found. With mu identically one the results agree with
51 * pfqn_mvac, and with a genuine load-dependent rate they agree with
52 * pfqn_mvald.
53 */
54
55#include <algorithm>
56#include <cstddef>
57#include <vector>
58
60#include "line/num/number.h"
61#include "line/util/error.h"
62#include "line/util/matrix.h"
63
64namespace line {
65namespace pfqn {
66
67/** Return value of pfqn_mvacld, mirroring [XN, QN, UN, CN, pij]. */
68template <class T>
70 std::vector<T> X; ///< (R) per-class throughput
71 Matrix<T> Q; ///< (M x R) per-class queue length
72 std::vector<T> U; ///< (M) per-station utilization, 1 - P_j(0)
73 std::vector<T> C; ///< (R) per-class cycle time exclusive of think time
74 Matrix<T> pij; ///< (M x sumN+1) marginal queue-length probabilities
75};
76
77namespace detail {
78
79/**
80 * Level-k marginals over I_k, held as a (J1 * nvk) x (k+1) matrix with row
81 * j * nvk + vloc, so that a plain Matrix carries the reference's three
82 * dimensions without a bespoke tensor type.
83 */
84template <class T>
85inline const T& pall_at(const Matrix<T>& P, std::size_t j, std::size_t vloc, std::size_t n,
86 std::size_t nvk) {
87 return P(j * nvk + vloc, n);
88}
89
90/** Part 1 of the QLD basic step: (21)-(25) for k = k0..K over v in I_k. */
91template <class T>
92void mvacld_part1(std::size_t k0, const MvacSetup<T>& st, const Matrix<T>& MU,
93 std::vector<Matrix<T>>& Pall, std::vector<Matrix<T>>& Ljkall,
94 std::vector<std::vector<T>>& lamall) {
95 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
96 const std::size_t J = st.J, J1 = st.J1, K = st.K;
97 for (std::size_t k = k0; k <= K; ++k) {
98 const std::size_t t = K - k;
99 const std::size_t nvk = st.cnt[t];
100 const std::size_t nvp = st.cnt[t + 1];
101 const Matrix<T>& Pp = Pall[k - 1];
102 Matrix<T> Lk(J, nvk, zero), Pk(J1 * nvk, k + 1, zero);
103 std::vector<T> lam(nvk, zero);
104 for (std::size_t vloc = 0; vloc < nvk; ++vloc) {
105 const std::size_t vi = st.off[t] + vloc;
106 const std::vector<int>& v = st.Vlist[vi];
107 std::vector<std::size_t> sloc(J, 0);
108 for (std::size_t j = 0; j < J; ++j) {
109 const long sj = st.succ(vi, j);
110 if (sj < 0) throw NumericError("pfqn_mvacld: multiplicity vector out of range");
111 sloc[j] = static_cast<std::size_t>(sj) - st.off[t + 1];
112 }
113 // (21): mean rate at which an SCSL chain pinned at center i is served
114 std::vector<T> c(J, one);
115 for (std::size_t i = 0; i < J1; ++i) {
116 T ci = zero;
117 for (std::size_t n = 0; n + 1 <= k; ++n) {
118 const std::size_t occ = n + static_cast<std::size_t>(v[i]) + 1;
119 if (occ > K) throw NumericError("pfqn_mvacld: rate index beyond sum(N)");
120 ci += pall_at(Pp, i, sloc[i], n, nvp) * MU(i, occ - 1) /
121 num_traits<T>::from_int(static_cast<long>(occ));
122 }
123 c[i] = ci;
124 }
125 // (23)-(24) in reference-station-free form
126 T sw = zero;
127 std::vector<T> w(J, zero);
128 for (std::size_t j = 0; j < J; ++j) {
129 if (st.a(j, k - 1) > zero) {
130 if (c[j] <= zero) throw NumericError("pfqn_mvacld: nonpositive service rate");
131 w[j] = st.a(j, k - 1) / c[j];
132 }
133 sw += w[j];
134 }
135 if (sw <= zero) throw NumericError("pfqn_mvacld: a chain has zero total demand");
136 lam[vloc] = one / sw;
137 for (std::size_t j = 0; j < J; ++j) Lk(j, vloc) = w[j] / sw;
138 // (25): condition on the center holding the single chain-k customer
139 for (std::size_t j = 0; j < J1; ++j) {
140 for (std::size_t n = 0; n <= k; ++n) {
141 T s = zero;
142 if (n >= 1) s = Lk(j, vloc) * pall_at(Pp, j, sloc[j], n - 1, nvp);
143 if (n + 1 <= k)
144 for (std::size_t mm = 0; mm < J; ++mm)
145 if (mm != j) s += Lk(mm, vloc) * pall_at(Pp, j, sloc[mm], n, nvp);
146 Pk(j * nvk + vloc, n) = s;
147 }
148 }
149 }
150 Pall[k] = Pk;
151 Ljkall[k] = Lk;
152 lamall[k] = lam;
153 }
154}
155
156} // namespace detail
157
158/**
159 * @brief MVAC for networks with queue-length dependent (QLD) service centers,
160 * the Section V extension of Conway, de Souza e Silva and Lavenberg
161 * (1989).
162 *
163 * @param L (M x R) demands of the queue-length dependent centers
164 * @param N (R) closed populations
165 * @param Z (Mz x R) demands of the infinite-server centers
166 * @param mu (M x n) load-dependent rates, mu(j, k-1) the total rate of center j
167 * with k jobs present; empty for the fixed-rate default
168 */
169template <class T>
170MvacldResult<T> pfqn_mvacld(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
171 const Matrix<T>& mu) {
172 const std::size_t M = L.rows(), R = L.cols();
173 if (M == 0 || R == 0) throw InputError("pfqn_mvacld: empty demand matrix");
174 if (N.size() != R) throw InputError("pfqn_mvacld: L and N disagree on the class count");
175 if (!Z.empty() && Z.cols() != R)
176 throw InputError("pfqn_mvacld: the think time matrix and the demand matrix disagree");
177 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
178
179 std::size_t K = 0;
180 for (std::size_t r = 0; r < R; ++r) {
181 if (N[r] < 0) throw InputError("pfqn_mvacld: the population vector must be nonnegative");
182 K += static_cast<std::size_t>(N[r]);
183 }
184
185 MvacldResult<T> res;
186 res.X.assign(R, zero);
187 res.Q = Matrix<T>(M, R, zero);
188 res.U.assign(M, zero);
189 res.C.assign(R, zero);
190 res.pij = Matrix<T>(M, K + 1, zero);
191 for (std::size_t i = 0; i < M; ++i) res.pij(i, 0) = one; // an unvisited center holds no jobs
192 if (K == 0) return res;
193
194 if (!mu.empty()) {
195 if (mu.rows() != M)
196 throw InputError("pfqn_mvacld: the rate matrix and the demand matrix disagree on centers");
197 if (mu.cols() < K)
198 throw InputError("pfqn_mvacld: the rate matrix must supply a rate for every population up to sum(N)");
199 }
200
201 std::vector<std::size_t> ldIdx, isIdx;
202 for (std::size_t i = 0; i < M; ++i)
203 for (std::size_t r = 0; r < R; ++r)
204 if (L(i, r) > zero) {
205 ldIdx.push_back(i);
206 break;
207 }
208 for (std::size_t i = 0; i < Z.rows(); ++i)
209 for (std::size_t r = 0; r < R; ++r)
210 if (Z(i, r) > zero) {
211 isIdx.push_back(i);
212 break;
213 }
214 const std::size_t J1 = ldIdx.size(), J = J1 + isIdx.size();
215 if (J == 0)
216 throw InputError("pfqn_mvacld: all service demands are zero, the throughput is unbounded");
217
218 Matrix<T> A(J, R, zero);
219 for (std::size_t j = 0; j < J1; ++j)
220 for (std::size_t r = 0; r < R; ++r) A(j, r) = L(ldIdx[j], r);
221 for (std::size_t j = J1; j < J; ++j)
222 for (std::size_t r = 0; r < R; ++r) A(j, r) = Z(isIdx[j - J1], r);
223
224 Matrix<T> MU(J1, K, one);
225 if (!mu.empty())
226 for (std::size_t j = 0; j < J1; ++j)
227 for (std::size_t n = 0; n < K; ++n) {
228 if (mu(ldIdx[j], n) <= zero)
229 throw InputError(
230 "pfqn_mvacld: the service rates must be strictly positive for every "
231 "population up to sum(N)");
232 MU(j, n) = mu(ldIdx[j], n);
233 }
234
235 detail::MvacSetup<T> st = detail::mvac_setup(A, N, J1, J, K);
236 const std::size_t D = st.D, S = st.S;
237
238 std::vector<Matrix<T>> Pall(K + 1), Ljkall(K + 1);
239 std::vector<std::vector<T>> lamall(K + 1);
240 Pall[0] = Matrix<T>(J1 * st.cnt[K], 1, one); // P^0_j(0,v) = 1 over I_0
241
242 std::vector<T> lamChain(K, zero);
243 Matrix<T> Lchain(J, K, zero);
244
245 detail::mvacld_part1(1, st, MU, Pall, Ljkall, lamall);
246 lamChain[K - 1] = lamall[K][0];
247 for (std::size_t j = 0; j < J; ++j) Lchain(j, K - 1) = Ljkall[K](j, 0);
248 // The marginals of the ORIGINAL network live at k = K, v = 0; the label
249 // interchanges of part 3 overwrite that level, so capture them now.
250 for (std::size_t j = 0; j < J1; ++j)
251 for (std::size_t n = 0; n <= K; ++n) res.pij(ldIdx[j], n) = Pall[K](j, n);
252
253 // ---- part 2: chains that visit at least one IS center ---------------------------
254 const long lmaxK = std::min(static_cast<long>(K) - 1, static_cast<long>(K - S));
255 if (D >= 2 && lmaxK >= static_cast<long>(K - D + 1)) {
256 std::vector<Matrix<T>> L2prev(K + 1), L2cur(K + 1);
257 for (std::size_t k = K - D + 2; k <= K; ++k) {
258 const std::size_t t = K - k;
259 L2cur.assign(K + 1, Matrix<T>());
260 const long lhi = std::min(static_cast<long>(k) - 1, static_cast<long>(K - S));
261 for (long l = static_cast<long>(K - D + 1); l <= lhi; ++l) {
262 Matrix<T> acc(J, st.cnt[t], zero);
263 for (std::size_t vloc = 0; vloc < st.cnt[t]; ++vloc) {
264 const std::size_t vi = st.off[t] + vloc;
265 for (std::size_t j = 0; j < J; ++j) {
266 const long sj = st.succ(vi, j);
267 if (sj < 0)
268 throw NumericError("pfqn_mvacld: multiplicity vector out of range");
269 const std::size_t sloc = static_cast<std::size_t>(sj) - st.off[t + 1];
270 const Matrix<T>& prev =
271 (l == static_cast<long>(k) - 1) ? Ljkall[k - 1] : L2prev[l];
272 for (std::size_t i = 0; i < J; ++i)
273 acc(i, vloc) += Ljkall[k](j, vloc) * prev(i, sloc);
274 }
275 }
276 L2cur[l] = acc;
277 }
278 if (k == K) {
279 for (long l = static_cast<long>(K - D + 1); l <= lmaxK; ++l) {
280 const std::size_t l0 = static_cast<std::size_t>(l) - 1;
281 for (std::size_t j = 0; j < J; ++j) Lchain(j, l0) = L2cur[l](j, 0);
282 std::size_t jIS = J;
283 for (std::size_t j = J1; j < J; ++j)
284 if (st.a(j, l0) > zero) {
285 jIS = j;
286 break;
287 }
288 if (jIS == J) throw NumericError("pfqn_mvacld: chain has no IS center");
289 lamChain[l0] = Lchain(jIS, l0) / st.a(jIS, l0);
290 }
291 }
292 L2prev = L2cur;
293 }
294 }
295
296 // ---- part 3: chains that visit no IS center, by label interchange ---------------
297 std::vector<std::size_t> perm(K);
298 for (std::size_t k = 0; k < K; ++k) perm[k] = k;
299 for (std::size_t l = 1; l + 1 <= S; ++l) {
300 std::swap(perm[K - l - 1], perm[K - 1]);
301 for (std::size_t j = 0; j < J; ++j) {
302 const T tmp = st.a(j, K - l - 1);
303 st.a(j, K - l - 1) = st.a(j, K - 1);
304 st.a(j, K - 1) = tmp;
305 }
306 detail::mvacld_part1(K - l, st, MU, Pall, Ljkall, lamall);
307 lamChain[perm[K - 1]] = lamall[K][0];
308 for (std::size_t j = 0; j < J; ++j) Lchain(j, perm[K - 1]) = Ljkall[K](j, 0);
309 }
310
311 // ---- expand the per-chain measures back to per-class ----------------------------
312 for (std::size_t g = 0; g < D; ++g) {
313 const std::size_t kg = K - D + g;
314 for (std::size_t p = 0; p < st.posr.size(); ++p) {
315 if (st.grpOfClass[p] != st.gorder[g]) continue;
316 const std::size_t r = st.posr[p];
317 const T nr = num_traits<T>::from_int(N[r]);
318 res.X[r] = nr * lamChain[kg];
319 for (std::size_t j = 0; j < J1; ++j) res.Q(ldIdx[j], r) = nr * Lchain(j, kg);
320 }
321 }
322 for (std::size_t i = 0; i < M; ++i) res.U[i] = one - res.pij(i, 0);
323 for (std::size_t p = 0; p < st.posr.size(); ++p) {
324 const std::size_t r = st.posr[p];
325 if (res.X[r] <= zero) throw NumericError("pfqn_mvacld: nonpositive throughput");
326 T zr = zero;
327 for (std::size_t i = 0; i < Z.rows(); ++i) zr += Z(i, r);
328 res.C[r] = num_traits<T>::from_int(N[r]) / res.X[r] - zr;
329 }
330 return res;
331}
332
333/** Overload with the fixed-rate default. */
334template <class T>
335MvacldResult<T> pfqn_mvacld(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z) {
336 return pfqn_mvacld(L, N, Z, Matrix<T>());
337}
338
339} // namespace pfqn
340} // namespace line
341
342#endif // LINE_API_PFQN_MVACLD_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.
MvacldResult< T > pfqn_mvacld(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu)
MVAC for networks with queue-length dependent (QLD) service centers, the Section V extension of Conwa...
Number-type abstraction for the templated API port.
MVAC: exact mean value analysis BY CHAIN of a closed multichain product-form network (Conway,...
Return value of pfqn_mvacld, mirroring [XN, QN, UN, CN, pij].
Definition pfqn_mvacld.h:69
std::vector< T > C
(R) per-class cycle time exclusive of think time
Definition pfqn_mvacld.h:73
std::vector< T > X
(R) per-class throughput
Definition pfqn_mvacld.h:70
Matrix< T > Q
(M x R) per-class queue length
Definition pfqn_mvacld.h:71
std::vector< T > U
(M) per-station utilization, 1 - P_j(0)
Definition pfqn_mvacld.h:72
Matrix< T > pij
(M x sumN+1) marginal queue-length probabilities
Definition pfqn_mvacld.h:74