LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_mva.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_MVA_H
6#define LINE_API_PFQN_MVA_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact Mean Value Analysis for closed product-form networks
12 * (Reiser and Lavenberg 1980).
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_mva.m, cross-checked against
15 * mp_pfqn's mva/mva-multi.c for the exact path. The recursion over the
16 * population lattice is
17 *
18 * C(i,r|n) = L(i,r) (mi(i) + Q(i|n - e_r))
19 * X(r|n) = n_r / (Z_r + sum_i C(i,r|n))
20 * Q(i,r|n) = X(r|n) C(i,r|n)
21 *
22 * every operation of which stays in the field of the inputs, so the algorithm
23 * is exact in rational arithmetic with no reformulation.
24 *
25 * Normalizing constant: MATLAB accumulates lG as -sum log X along the lattice
26 * path (0 -> N) that fills one class at a time. Logs do not exist in an exact
27 * field, so the port accumulates the product of the reciprocals instead and
28 * takes the log once at the end, of a value that is still exact. The two agree
29 * to rounding in double.
30 */
31
32#include <cstddef>
33#include <vector>
34
35#include "line/num/number.h"
36#include "line/util/error.h"
37#include "line/util/matrix.h"
39
40namespace line {
41namespace pfqn {
42
43template <class T>
44struct MvaResult {
45 std::vector<T> XN; ///< (R) per-class throughput
46 Matrix<T> QN; ///< (M x R) mean queue length
47 Matrix<T> UN; ///< (M x R) utilization
48 Matrix<T> CN; ///< (M x R) residence time
49 T G; ///< normalizing constant
50 double lG; ///< log of the normalizing constant
51};
52
53/**
54 * @brief Exact Mean Value Analysis for closed product-form networks (Reiser
55 * and Lavenberg 1980).
56 *
57 * @param L (M x R) service demands
58 * @param N (R) population per class
59 * @param Z (K x R) think times, summed over rows; empty for no delay
60 * @param mi (M) additive term of the residence-time recursion
61 * C(i,s)=L(i,s)*(mi[i]+Qarv), 1 for a queueing station; empty for all ones.
62 * THIS IS NOT A SERVER COUNT: mi[i]=c inflates the residence time by c
63 * rather than adding c servers. For multiserver stations call
64 * pfqn_mvams(lambda, L, N, Z, mi, S), which passes S to the load-dependent
65 * recursion with mu(i,n)=min(n,S(i)).
66 *
67 * Standard arrival theorem. For the interlocked-flow correction of Franks (1999),
68 * Ch. 4, Eq. (4.7), call pfqn_mva_ilock instead.
69 */
70template <class T>
71MvaResult<T> pfqn_mva(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
72 const std::vector<int>& mi) {
73 const std::size_t M = L.rows();
74 const std::size_t R = N.size();
75 if (!L.empty() && L.cols() != R)
76 throw InputError("pfqn_mva: demand matrix and population vector disagree on the class count");
77 if (!mi.empty() && mi.size() != M)
78 throw InputError("pfqn_mva: multiplicity vector has the wrong length");
79
80 const T zero = num_traits<T>::from_int(0);
81 const T one = num_traits<T>::from_int(1);
82
83 MvaResult<T> res;
84 res.XN.assign(R, zero);
85 res.QN = Matrix<T>(M, R, zero);
86 res.UN = Matrix<T>(M, R, zero);
87 res.CN = Matrix<T>(M, R, zero);
88 res.G = one;
89 res.lG = 0.0;
90
91 bool anyPositive = false;
92 for (int v : N) {
93 if (v < 0) throw InputError("pfqn_mva: negative population");
94 if (v > 0) anyPositive = true;
95 }
96 if (!anyPositive || M == 0) return res; // empty closed population: nothing to compute
97
98 std::vector<T> Zsum(R, zero);
99 if (!Z.empty()) {
100 if (Z.cols() != R) throw InputError("pfqn_mva: Z and N disagree on the class count");
101 for (std::size_t k = 0; k < Z.rows(); ++k)
102 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
103 }
104
105 std::vector<T> multi(M, one);
106 for (std::size_t i = 0; i < mi.size(); ++i) multi[i] = num_traits<T>::from_int(mi[i]);
107
108 const std::vector<std::size_t> prods = plane_sizes(N);
109 const std::size_t total = population_count(N);
110
111 // lattice indexing rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
112 std::vector<T> Q(total * M, zero);
113
114 std::vector<int> n(R, 0);
115 bool more = true;
116 while (more) {
117 int npop = 0;
118 for (int v : n) npop += v;
119 if (npop > 0) {
120 const std::size_t idx = pop_index(n, prods);
121 for (std::size_t s = 0; s < R; ++s) {
122 // empty-class zero-population-row rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
123 const std::size_t idx_1s = n[s] > 0 ? idx - prods[s] : 0;
124 T ctot = Zsum[s];
125 for (std::size_t i = 0; i < M; ++i) {
126 const T qarv = Q[idx_1s * M + i];
127 res.CN(i, s) = L(i, s) * (multi[i] + qarv);
128 ctot += res.CN(i, s);
129 }
130 // a class with no jobs in this population has no rate, whatever its
131 // demands: a chain of zero population and zero demand is not degenerate
132 if (ctot == zero && n[s] > 0) throw NumericError(ErrorKind{"Degenerate"}, "pfqn_mva: zero total residence time");
133 res.XN[s] = n[s] > 0 ? num_traits<T>::from_int(n[s]) / ctot : zero;
134 for (std::size_t i = 0; i < M; ++i) {
135 res.QN(i, s) = res.XN[s] * res.CN(i, s);
136 Q[idx * M + i] += res.QN(i, s);
137 }
138 }
139
140 // normalizing-constant accumulation rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
141 long last_nnz = -1;
142 for (long r = static_cast<long>(R) - 1; r >= 0; --r)
143 if (n[r] != 0) {
144 last_nnz = r;
145 break;
146 }
147 if (last_nnz >= 0) {
148 bool prefixFull = true;
149 for (long r = 0; r < last_nnz; ++r)
150 if (n[r] != N[r]) {
151 prefixFull = false;
152 break;
153 }
154 bool suffixEmpty = true;
155 for (std::size_t r = static_cast<std::size_t>(last_nnz) + 1; r < R; ++r)
156 if (n[r] != 0) {
157 suffixEmpty = false;
158 break;
159 }
160 if (prefixFull && suffixEmpty) {
161 const T& x = res.XN[static_cast<std::size_t>(last_nnz)];
162 if (x == zero) throw NumericError(ErrorKind{"Degenerate"}, "pfqn_mva: zero throughput on the G path");
163 res.G /= x;
164 }
165 }
166 }
167 more = next_pop(n, N);
168 }
169
170 for (std::size_t i = 0; i < M; ++i)
171 for (std::size_t r = 0; r < R; ++r) res.UN(i, r) = res.XN[r] * L(i, r);
172
174 return res;
175}
176
177/**
178 * Exact MVA recursion carrying the interlocked-flow correction.
179 *
180 * The correction replaces the arrival theorem term Q(n-1_s,i) by a per-class weighted
181 * sum, so the recursion has to carry per-class queue lengths that pfqn_mva does not
182 * need. Closed single-server models only.
183 *
184 * The discount splits each class's queue at the in-service component, as lqns
185 * MVA::queueOnly_adjusted does: the queue-only part max(ILw*Qc-Uc, 0) is floored at
186 * zero, and the in-service part is discounted on its own as ILw*Uc. Flooring the whole
187 * term at Uc instead cancelled the correction wherever a class holds one customer
188 * (Qc == Uc), i.e. every task-layer chain (BUGS.md BUG-103).
189 * sum_s XN[s]*L(i,s) <= mi[i] is asserted nowhere.
190 *
191 * @param L (M x R) service demands
192 * @param N (R) population per class
193 * @param Z (K x R) think times, summed over rows; empty for no delay
194 * @param mi (M) additive term of the residence-time recursion
195 * C(i,s)=L(i,s)*(mi[i]+Qarv), 1 for a queueing station; empty for all ones.
196 * THIS IS NOT A SERVER COUNT: mi[i]=c inflates the residence time by c
197 * rather than adding c servers. For multiserver stations call
198 * pfqn_mvams(lambda, L, N, Z, mi, S), which passes S to the load-dependent
199 * recursion with mu(i,n)=min(n,S(i)).
200 * @param IL (R x R) interlock matrix of Franks (1999), Eq. (4.7): IL(r,s) is the
201 * share of the class-s queue that a class-r arrival cannot see, because that
202 * work was itself caused by the class-r request.
203 * Required. The model is outside product form under it, so G and lG are not
204 * meaningful and come back as one and zero.
205 */
206template <class T>
207MvaResult<T> pfqn_mva_ilock(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
208 const std::vector<int>& mi, const Matrix<T>& IL) {
209 const std::size_t M = L.rows();
210 const std::size_t R = N.size();
211 if (!L.empty() && L.cols() != R)
212 throw InputError("pfqn_mva_ilock: demand matrix and population vector disagree on the class count");
213 if (!mi.empty() && mi.size() != M)
214 throw InputError("pfqn_mva_ilock: multiplicity vector has the wrong length");
215
216 const T zero = num_traits<T>::from_int(0);
217 const T one = num_traits<T>::from_int(1);
218
219 MvaResult<T> res;
220 res.XN.assign(R, zero);
221 res.QN = Matrix<T>(M, R, zero);
222 res.UN = Matrix<T>(M, R, zero);
223 res.CN = Matrix<T>(M, R, zero);
224 res.G = one;
225 res.lG = 0.0;
226
227 bool anyPositive = false;
228 for (int v : N) {
229 if (v < 0) throw InputError("pfqn_mva_ilock: negative population");
230 if (v > 0) anyPositive = true;
231 }
232 if (!anyPositive || M == 0) return res; // empty closed population: nothing to compute
233
234 std::vector<T> Zsum(R, zero);
235 if (!Z.empty()) {
236 if (Z.cols() != R) throw InputError("pfqn_mva_ilock: Z and N disagree on the class count");
237 for (std::size_t k = 0; k < Z.rows(); ++k)
238 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
239 }
240
241 std::vector<T> multi(M, one);
242 for (std::size_t i = 0; i < mi.size(); ++i) multi[i] = num_traits<T>::from_int(mi[i]);
243
244 if (IL.empty())
245 throw InputError("pfqn_mva_ilock: an interlock matrix is required; use pfqn_mva for the standard arrival theorem");
246 Matrix<T> ILw;
247 {
248 if (IL.rows() != R || IL.cols() != R)
249 throw InputError("pfqn_mva_ilock: the interlock matrix must be nclasses x nclasses");
250 ILw = Matrix<T>(R, R, zero);
251 for (std::size_t r = 0; r < R; ++r)
252 for (std::size_t s = 0; s < R; ++s) {
253 T w = (r == s) ? one : T(one - IL(r, s));
254 if (w < zero) w = zero;
255 if (w > one) w = one;
256 ILw(r, s) = w;
257 }
258 }
259
260 const std::vector<std::size_t> prods = plane_sizes(N);
261 const std::size_t total = population_count(N);
262
263 // lattice indexing rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
264 std::vector<T> Q(total * M, zero);
265 // per-class queue lengths, needed by the interlock
266 std::vector<T> Qc(total * M * R, zero);
267 // per-class in-service component, where the interlock splits each queue
268 std::vector<T> Uc(total * M * R, zero);
269
270 std::vector<int> n(R, 0);
271 bool more = true;
272 while (more) {
273 int npop = 0;
274 for (int v : n) npop += v;
275 if (npop > 0) {
276 const std::size_t idx = pop_index(n, prods);
277 // Arrival-instant queues seen at this level, in full and discounted, as lqns
278 // MVA::queueOnly_adjusted splits them: the queue-only part max(ILw*Qc-Uc, 0)
279 // floored at zero plus the in-service part ILw*Uc.
280 Matrix<T> qfull(M, R, zero), qdisc(M, R, zero);
281 for (std::size_t s = 0; s < R; ++s) {
282 // empty-class zero-population-row rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
283 const std::size_t idx_1s = n[s] > 0 ? idx - prods[s] : 0;
284 for (std::size_t i = 0; i < M; ++i)
285 for (std::size_t r = 0; r < R; ++r) {
286 const T qc = Qc[(idx_1s * M + i) * R + r];
287 const T inSvc = Uc[(idx_1s * M + i) * R + r];
288 const T wait = T(ILw(s, r) * qc - inSvc);
289 qfull(i, s) += qc;
290 qdisc(i, s) += T((wait > zero ? wait : zero) + ILw(s, r) * inSvc);
291 }
292 }
293 // THE DISCOUNT MAY NOT TAKE A STATION PAST ITS CAPACITY. Two classes that each
294 // remove the other's interlocked share, in service included, break Little's law
295 // at the station: two single-thread tasks on one PS processor reported Util 1.02,
296 // a throughput above the demand bound lqsim sits on (BUGS.md BUG-107). The full
297 // discount is kept where feasible, else the largest feasible fraction of it.
298 auto level = [&](const T& theta, std::vector<T>& X, Matrix<T>& C) -> bool {
299 for (std::size_t s = 0; s < R; ++s) {
300 T ctot = Zsum[s];
301 for (std::size_t i = 0; i < M; ++i) {
302 C(i, s) = L(i, s) * (multi[i] + qfull(i, s) - theta * (qfull(i, s) - qdisc(i, s)));
303 ctot += C(i, s);
304 }
305 if (ctot == zero)
306 throw NumericError(ErrorKind{"Degenerate"}, "pfqn_mva_ilock: zero total residence time");
307 X[s] = num_traits<T>::from_int(n[s]) / ctot;
308 }
309 for (std::size_t i = 0; i < M; ++i) {
310 T u = zero;
311 for (std::size_t s = 0; s < R; ++s) u += X[s] * L(i, s);
312 if (num_traits<T>::to_double(u) > num_traits<T>::to_double(multi[i]) * (1.0 + 1e-12))
313 return false;
314 }
315 return true;
316 };
317 std::vector<T> Xl(R, zero);
318 Matrix<T> Cl(M, R, zero);
319 if (!level(one, Xl, Cl)) {
320 double lo = 0.0, hi = 1.0;
321 for (int it = 0; it < 60; ++it) {
322 const double mid = 0.5 * (lo + hi);
323 if (level(num_traits<T>::from_double(mid), Xl, Cl)) lo = mid;
324 else hi = mid;
325 }
326 level(num_traits<T>::from_double(lo), Xl, Cl);
327 }
328 for (std::size_t s = 0; s < R; ++s) {
329 res.XN[s] = Xl[s];
330 for (std::size_t i = 0; i < M; ++i) {
331 res.CN(i, s) = Cl(i, s);
332 res.QN(i, s) = res.XN[s] * res.CN(i, s);
333 Q[idx * M + i] += res.QN(i, s);
334 Qc[(idx * M + i) * R + s] = res.QN(i, s);
335 Uc[(idx * M + i) * R + s] = res.XN[s] * L(i, s);
336 }
337 }
338
339 // the interlock leaves the model outside product form, so G is never
340 // accumulated and res.G/res.lG keep their neutral initial values
341 }
342 more = next_pop(n, N);
343 }
344
345 for (std::size_t i = 0; i < M; ++i)
346 for (std::size_t r = 0; r < R; ++r) res.UN(i, r) = res.XN[r] * L(i, r);
347
349 return res;
350}
351
352template <class T>
353MvaResult<T> pfqn_mva(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z) {
354 return pfqn_mva(L, N, Z, std::vector<int>());
355}
356
357template <class T>
358MvaResult<T> pfqn_mva(const Matrix<T>& L, const std::vector<int>& N) {
359 return pfqn_mva(L, N, Matrix<T>(), std::vector<int>());
360}
361
362} // namespace pfqn
363} // namespace line
364
365#endif // LINE_API_PFQN_MVA_H
InputError(const std::string &what)
Definition error.h:105
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:112
The exception types the port throws.
Dense matrix and non-owning view.
MvaResult< T > pfqn_mva(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &mi)
Exact Mean Value Analysis for closed product-form networks (Reiser and Lavenberg 1980).
Definition pfqn_mva.h:71
MvaResult< T > pfqn_mva_ilock(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &mi, const Matrix< T > &IL)
Exact MVA recursion carrying the interlocked-flow correction.
Definition pfqn_mva.h:207
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
std::size_t population_count(const std::vector< int > &N)
Number of population vectors n with 0 <= n <= N.
Definition population.h:38
std::vector< std::size_t > plane_sizes(const std::vector< int > &N)
Mixed-radix plane sizes: prods[r] = prod_{s<r} (N[s]+1).
Definition population.h:27
bool next_pop(std::vector< int > &n, const std::vector< int > &N)
Advance n to the next population vector in the lattice 0 <= n <= N, odometer order with the last clas...
Definition population.h:56
std::size_t pop_index(const std::vector< int > &n, const std::vector< std::size_t > &prods)
Index of n in the lattice, 0-based (MATLAB hashpop is 1-based).
Definition population.h:45
Number-type abstraction for the templated API port.
Population-vector enumeration and combinatorics.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
std::vector< T > XN
(R) per-class throughput
Definition pfqn_mva.h:45
Matrix< T > QN
(M x R) mean queue length
Definition pfqn_mva.h:46
double lG
log of the normalizing constant
Definition pfqn_mva.h:50
Matrix< T > CN
(M x R) residence time
Definition pfqn_mva.h:48
T G
normalizing constant
Definition pfqn_mva.h:49
Matrix< T > UN
(M x R) utilization
Definition pfqn_mva.h:47