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 if (ctot == zero) throw NumericError("pfqn_mva: zero total residence time");
131 res.XN[s] = num_traits<T>::from_int(n[s]) / ctot;
132 for (std::size_t i = 0; i < M; ++i) {
133 res.QN(i, s) = res.XN[s] * res.CN(i, s);
134 Q[idx * M + i] += res.QN(i, s);
135 }
136 }
137
138 // normalizing-constant accumulation rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
139 long last_nnz = -1;
140 for (long r = static_cast<long>(R) - 1; r >= 0; --r)
141 if (n[r] != 0) {
142 last_nnz = r;
143 break;
144 }
145 if (last_nnz >= 0) {
146 bool prefixFull = true;
147 for (long r = 0; r < last_nnz; ++r)
148 if (n[r] != N[r]) {
149 prefixFull = false;
150 break;
151 }
152 bool suffixEmpty = true;
153 for (std::size_t r = static_cast<std::size_t>(last_nnz) + 1; r < R; ++r)
154 if (n[r] != 0) {
155 suffixEmpty = false;
156 break;
157 }
158 if (prefixFull && suffixEmpty) {
159 const T& x = res.XN[static_cast<std::size_t>(last_nnz)];
160 if (x == zero) throw NumericError("pfqn_mva: zero throughput on the G path");
161 res.G /= x;
162 }
163 }
164 }
165 more = next_pop(n, N);
166 }
167
168 for (std::size_t i = 0; i < M; ++i)
169 for (std::size_t r = 0; r < R; ++r) res.UN(i, r) = res.XN[r] * L(i, r);
170
172 return res;
173}
174
175/**
176 * Exact MVA recursion carrying the interlocked-flow correction.
177 *
178 * The correction replaces the arrival theorem term Q(n-1_s,i) by a per-class weighted
179 * sum, so the recursion has to carry per-class queue lengths that pfqn_mva does not
180 * need. Closed single-server models only.
181 *
182 * The discounted arrival-instant queue is floored at the in-service component, as in
183 * lqns MVA::queueOnly_adjusted, so the correction damps itself out as a station
184 * saturates. That is a self-limiting guard, NOT a hard capacity test:
185 * sum_s XN[s]*L(i,s) <= mi[i] is still asserted nowhere.
186 * See git show 8bad654e7:_kb/log.md.
187 *
188 * @param L (M x R) service demands
189 * @param N (R) population per class
190 * @param Z (K x R) think times, summed over rows; empty for no delay
191 * @param mi (M) additive term of the residence-time recursion
192 * C(i,s)=L(i,s)*(mi[i]+Qarv), 1 for a queueing station; empty for all ones.
193 * THIS IS NOT A SERVER COUNT: mi[i]=c inflates the residence time by c
194 * rather than adding c servers. For multiserver stations call
195 * pfqn_mvams(lambda, L, N, Z, mi, S), which passes S to the load-dependent
196 * recursion with mu(i,n)=min(n,S(i)).
197 * @param IL (R x R) interlock matrix of Franks (1999), Eq. (4.7): IL(r,s) is the
198 * share of the class-s queue that a class-r arrival cannot see, because that
199 * work was itself caused by the class-r request.
200 * Required. The model is outside product form under it, so G and lG are not
201 * meaningful and come back as one and zero.
202 */
203template <class T>
204MvaResult<T> pfqn_mva_ilock(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
205 const std::vector<int>& mi, const Matrix<T>& IL) {
206 const std::size_t M = L.rows();
207 const std::size_t R = N.size();
208 if (!L.empty() && L.cols() != R)
209 throw InputError("pfqn_mva_ilock: demand matrix and population vector disagree on the class count");
210 if (!mi.empty() && mi.size() != M)
211 throw InputError("pfqn_mva_ilock: multiplicity vector has the wrong length");
212
213 const T zero = num_traits<T>::from_int(0);
214 const T one = num_traits<T>::from_int(1);
215
216 MvaResult<T> res;
217 res.XN.assign(R, zero);
218 res.QN = Matrix<T>(M, R, zero);
219 res.UN = Matrix<T>(M, R, zero);
220 res.CN = Matrix<T>(M, R, zero);
221 res.G = one;
222 res.lG = 0.0;
223
224 bool anyPositive = false;
225 for (int v : N) {
226 if (v < 0) throw InputError("pfqn_mva_ilock: negative population");
227 if (v > 0) anyPositive = true;
228 }
229 if (!anyPositive || M == 0) return res; // empty closed population: nothing to compute
230
231 std::vector<T> Zsum(R, zero);
232 if (!Z.empty()) {
233 if (Z.cols() != R) throw InputError("pfqn_mva_ilock: Z and N disagree on the class count");
234 for (std::size_t k = 0; k < Z.rows(); ++k)
235 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
236 }
237
238 std::vector<T> multi(M, one);
239 for (std::size_t i = 0; i < mi.size(); ++i) multi[i] = num_traits<T>::from_int(mi[i]);
240
241 if (IL.empty())
242 throw InputError("pfqn_mva_ilock: an interlock matrix is required; use pfqn_mva for the standard arrival theorem");
243 Matrix<T> ILw;
244 {
245 if (IL.rows() != R || IL.cols() != R)
246 throw InputError("pfqn_mva_ilock: the interlock matrix must be nclasses x nclasses");
247 ILw = Matrix<T>(R, R, zero);
248 for (std::size_t r = 0; r < R; ++r)
249 for (std::size_t s = 0; s < R; ++s) {
250 T w = (r == s) ? one : T(one - IL(r, s));
251 if (w < zero) w = zero;
252 if (w > one) w = one;
253 ILw(r, s) = w;
254 }
255 }
256
257 const std::vector<std::size_t> prods = plane_sizes(N);
258 const std::size_t total = population_count(N);
259
260 // lattice indexing rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
261 std::vector<T> Q(total * M, zero);
262 // per-class queue lengths, needed by the interlock
263 std::vector<T> Qc(total * M * R, zero);
264 // per-class in-service component, the interlock's floor
265 std::vector<T> Uc(total * M * R, zero);
266
267 std::vector<int> n(R, 0);
268 bool more = true;
269 while (more) {
270 int npop = 0;
271 for (int v : n) npop += v;
272 if (npop > 0) {
273 const std::size_t idx = pop_index(n, prods);
274 for (std::size_t s = 0; s < R; ++s) {
275 // empty-class zero-population-row rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
276 const std::size_t idx_1s = n[s] > 0 ? idx - prods[s] : 0;
277 T ctot = Zsum[s];
278 for (std::size_t i = 0; i < M; ++i) {
279 T qarv = zero;
280 for (std::size_t r = 0; r < R; ++r) {
281 // In-service protection, as in lqns MVA::queueOnly_adjusted: the
282 // discount bites on the WAITING part only, never on the job already
283 // in service, so it damps itself out as the station saturates.
284 const T disc = ILw(s, r) * Qc[(idx_1s * M + i) * R + r];
285 const T inSvc = Uc[(idx_1s * M + i) * R + r];
286 qarv += disc > inSvc ? disc : inSvc;
287 }
288 res.CN(i, s) = L(i, s) * (multi[i] + qarv);
289 ctot += res.CN(i, s);
290 }
291 if (ctot == zero) throw NumericError("pfqn_mva_ilock: zero total residence time");
292 res.XN[s] = num_traits<T>::from_int(n[s]) / ctot;
293 for (std::size_t i = 0; i < M; ++i) {
294 res.QN(i, s) = res.XN[s] * res.CN(i, s);
295 Q[idx * M + i] += res.QN(i, s);
296 Qc[(idx * M + i) * R + s] = res.QN(i, s);
297 Uc[(idx * M + i) * R + s] = res.XN[s] * L(i, s);
298 }
299 }
300
301 // the interlock leaves the model outside product form, so G is never
302 // accumulated and res.G/res.lG keep their neutral initial values
303 }
304 more = next_pop(n, N);
305 }
306
307 for (std::size_t i = 0; i < M; ++i)
308 for (std::size_t r = 0; r < R; ++r) res.UN(i, r) = res.XN[r] * L(i, r);
309
311 return res;
312}
313
314template <class T>
315MvaResult<T> pfqn_mva(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z) {
316 return pfqn_mva(L, N, Z, std::vector<int>());
317}
318
319template <class T>
320MvaResult<T> pfqn_mva(const Matrix<T>& L, const std::vector<int>& N) {
321 return pfqn_mva(L, N, Matrix<T>(), std::vector<int>());
322}
323
324} // namespace pfqn
325} // namespace line
326
327#endif // LINE_API_PFQN_MVA_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.
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:204
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.
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