LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_mvams.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_MVAMS_H
6#define LINE_API_PFQN_MVAMS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact Mean Value Analysis for mixed open/closed networks with multiserver
12 * stations.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_mvams.m. That function is a
15 * dispatcher over four exact MVA variants, so the port carries them all:
16 *
17 * no multiserver, closed -> pfqn_mva (pfqn_mva.h, already ported)
18 * no multiserver, mixed -> pfqn_mvamx (below)
19 * multiserver, closed -> pfqn_mvald (below)
20 * multiserver, mixed -> pfqn_mvaldms (below, via pfqn_mvaldmx)
21 *
22 * pfqn_mvald is the load-dependent MVA of Reiser and Lavenberg, carrying the
23 * marginal queue-length distribution pi(k|n) along the population lattice;
24 * pfqn_mvaldmx is the Bruell-Balbo-Ashfari mixed extension for limited load
25 * dependence, whose effective-capacity terms come from pfqn_ldmx_ec. A
26 * multiserver station with S servers is the load-dependent station with rates
27 * mu(i,k) = min(k, S), which is where the "ms" in the name comes from.
28 *
29 * Arithmetic. Every step of all four variants is an addition, a subtraction, a
30 * multiplication, a division or an integer power in the field of the inputs:
31 * the family is exact-capable end to end and needs no transcendental function.
32 * The only non-field step in MATLAB is the log used to accumulate lG, and the
33 * port does what pfqn_mva.h already does, accumulating the product of the
34 * reciprocal throughputs along the lattice path and taking the log once at the
35 * end of a value that is still exact.
36 *
37 * Contract notes, where this port deliberately differs from MATLAB:
38 *
39 * 1. UN in the closed multiserver branch. MATLAB documents pfqn_mvams as
40 * returning an (M x R) utilization, and three of its four branches do, but
41 * the closed multiserver branch forwards pfqn_mvald's UN, which is the
42 * (M x 1) aggregate 1 - P(station i empty). This port returns the (M x R)
43 * per-class utilization law U(i,r) = X(r) L(i,r) / S(i), the same formula
44 * pfqn_mvaldms already uses in the mixed multiserver branch, so the metric
45 * set matches MvaResult and is consistent across all four branches. The
46 * aggregate form is still available from pfqn_mvald directly, whose result
47 * carries both UN and the full marginal distribution.
48 *
49 * 2. Queue replicas with multiservers. MATLAB rejects mi > 1 for the mixed
50 * multiserver branch but silently drops mi in the closed multiserver
51 * branch, because pfqn_mvald has no mi argument: the replicas then affect
52 * only the residence time reported for absent classes. This port rejects
53 * mi > 1 in both multiserver branches rather than returning numbers that
54 * ignore an argument the caller supplied.
55 *
56 * Index conventions. An open class is marked by OPEN_CLASS in the population
57 * vector (MATLAB uses Inf, which an int vector cannot hold) and an infinite
58 * server station by INF_SERVERS in the server-count vector (MATLAB uses Inf).
59 */
60
61#include <algorithm>
62#include <cmath>
63#include <cstddef>
64#include <limits>
65#include <vector>
66
68#include "line/num/number.h"
69#include "line/util/error.h"
70#include "line/util/matrix.h"
72
73namespace line {
74namespace pfqn {
75
76/** Marks an open (infinite-population) class in a population vector. */
77constexpr int OPEN_CLASS = -1;
78
79/** Marks an infinite-server station in a server-count vector. */
80constexpr int INF_SERVERS = -1;
81
82/** True when the population entry denotes an open class. */
83inline bool is_open_class(int n) { return n < 0; }
84
85// ---------------------------------------------------------------------------
86// pfqn_mvald: exact MVA for closed load-dependent networks
87// ---------------------------------------------------------------------------
88
89/**
90 * Result of pfqn_mvald, mirroring the seven MATLAB outputs. It is deliberately
91 * not MvaResult: the load-dependent family reports an aggregate utilization
92 * and a per-class cycle time rather than per-station, per-class matrices, and
93 * it additionally exposes the marginal queue-length distribution.
94 */
95template <class T>
97 std::vector<T> XN; ///< (R) per-class throughput
98 Matrix<T> QN; ///< (M x R) mean queue length
99 std::vector<T> UN; ///< (M) utilization, 1 - P(station empty)
100 std::vector<T> CN; ///< (R) cycle time, exclusive of think time
101 Matrix<T> WN; ///< (M x R) residence time at the full population
102 Matrix<T> PI; ///< (M x (Nt+1)) marginal queue-length distribution at N
103 T G; ///< normalizing constant
104 double lG; ///< log of the normalizing constant
105 bool isNumStable; ///< false once a marginal probability had to be clamped
106};
107
108/**
109 * Exact MVA for a closed network of load-dependent stations.
110 *
111 * Port of matlab/src/api/pfqn/pfqn_mvald.m. The recursion over the population
112 * lattice is
113 *
114 * W(i,r|n) = sum_{k=1}^{|n|} L(i,r)/mu(i,k) * k * pi(i,k-1|n - e_r)
115 * X(r|n) = n_r / (Z_r + sum_i W(i,r|n))
116 * pi(i,k|n) = sum_r L(i,r)/mu(i,k) * X(r|n) * pi(i,k-1|n - e_r)
117 * pi(i,0|n) = 1 - sum_{k>=1} pi(i,k|n)
118 *
119 * @param L (M x R) service demands
120 * @param N (R) population per class, all finite and non-negative
121 * @param Z (K x R) think times, summed over rows; may be empty
122 * @param mu (M x Nt') service rates, Nt' >= sum(N); mu(i,k-1) is the
123 * rate of station i while it holds k jobs
124 * @param stabilize when true (the MATLAB default) a marginal probability that
125 * comes out negative is clamped to the double epsilon rather
126 * than propagated. The clamp is a floating-point guard: in
127 * exact arithmetic pi(i,0|n) of a well-posed product-form
128 * model is non-negative and the branch is never taken. The
129 * clamp value is a dyadic rational, so it is representable
130 * without rounding in every supported arithmetic.
131 */
132template <class T>
133MvaLdResult<T> pfqn_mvald(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
134 const Matrix<T>& mu, bool stabilize = true) {
135 const std::size_t M = L.rows();
136 const std::size_t R = N.size();
137 if (!L.empty() && L.cols() != R)
138 throw InputError("pfqn_mvald: demand matrix and population vector disagree on the class count");
139
140 const T zero = num_traits<T>::from_int(0);
141 const T one = num_traits<T>::from_int(1);
142
143 MvaLdResult<T> res;
144 res.XN.assign(R, zero);
145 res.QN = Matrix<T>(M, R, zero);
146 res.UN.assign(M, zero);
147 res.CN.assign(R, zero);
148 res.WN = Matrix<T>(M, R, zero);
149 res.G = one;
150 res.lG = 0.0;
151 res.isNumStable = true;
152
153 long Nt = 0;
154 for (int v : N) {
155 // MATLAB returns all-zero metrics and lGN = -Inf for a negative population.
156 if (v < 0) {
157 res.PI = Matrix<T>(M, 1, zero);
158 res.G = zero;
159 res.lG = -std::numeric_limits<double>::infinity();
160 return res;
161 }
162 Nt += v;
163 }
164
165 if (mu.rows() != M)
166 throw InputError("pfqn_mvald: rate matrix and demand matrix disagree on the station count");
167 if (static_cast<long>(mu.cols()) < Nt)
168 throw InputError("pfqn_mvald: rate matrix needs one column per job in the total population");
169
170 std::vector<T> Zsum(R, zero);
171 if (!Z.empty()) {
172 if (Z.cols() != R) throw InputError("pfqn_mvald: Z and N disagree on the class count");
173 for (std::size_t k = 0; k < Z.rows(); ++k)
174 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
175 }
176
177 const std::size_t K = static_cast<std::size_t>(Nt) + 1; // queue lengths 0 ... Nt
178 const std::vector<std::size_t> prods = plane_sizes(N);
179 const std::size_t total = population_count(N);
180
181 // pi[(idx * M + i) * K + k] is P(station i holds k jobs | population idx).
182 std::vector<T> pi(total * M * K, zero);
183 const T tiny = num_traits<T>::from_double(std::numeric_limits<double>::epsilon());
184
185 std::vector<int> n(R, 0);
186 std::vector<T> x(R, zero);
187 bool more = true;
188 while (more) {
189 const std::size_t idx = pop_index(n, prods);
190 long nsum = 0;
191 for (int v : n) nsum += v;
192
193 res.WN.fill(zero);
194 std::fill(x.begin(), x.end(), zero);
195
196 for (std::size_t s = 0; s < R; ++s) {
197 if (n[s] == 0) continue;
198 const std::size_t idx_s = idx - prods[s];
199 T ctot = Zsum[s];
200 for (std::size_t i = 0; i < M; ++i) {
201 T w = zero;
202 for (long k = 1; k <= nsum; ++k)
203 w += (L(i, s) / mu(i, static_cast<std::size_t>(k - 1))) *
205 pi[(idx_s * M + i) * K + static_cast<std::size_t>(k - 1)];
206 res.WN(i, s) = w;
207 ctot += w;
208 }
209 if (ctot == zero) throw NumericError("pfqn_mvald: zero total residence time");
210 x[s] = num_traits<T>::from_int(n[s]) / ctot;
211 }
212
213 for (long k = 1; k <= nsum; ++k) {
214 for (std::size_t i = 0; i < M; ++i) {
215 T acc = zero;
216 for (std::size_t s = 0; s < R; ++s) {
217 if (n[s] == 0) continue;
218 const std::size_t idx_s = idx - prods[s];
219 acc += (L(i, s) / mu(i, static_cast<std::size_t>(k - 1))) * x[s] *
220 pi[(idx_s * M + i) * K + static_cast<std::size_t>(k - 1)];
221 }
222 pi[(idx * M + i) * K + static_cast<std::size_t>(k)] = acc;
223 }
224 }
225
226 for (std::size_t i = 0; i < M; ++i) {
227 T acc = zero;
228 for (long k = 1; k <= nsum; ++k) acc += pi[(idx * M + i) * K + static_cast<std::size_t>(k)];
229 T p0 = one - acc;
230 if (p0 < zero) {
231 res.isNumStable = false;
232 if (stabilize) p0 = tiny;
233 }
234 pi[(idx * M + i) * K + 0] = p0;
235 }
236
237 // Normalizing constant along the lattice path that fills class 0, then
238 // class 1, and so on, exactly as in pfqn_mva.
239 long last_nnz = -1;
240 for (long r = static_cast<long>(R) - 1; r >= 0; --r)
241 if (n[r] != 0) {
242 last_nnz = r;
243 break;
244 }
245 if (last_nnz >= 0) {
246 bool prefixFull = true;
247 for (long r = 0; r < last_nnz; ++r)
248 if (n[r] != N[r]) {
249 prefixFull = false;
250 break;
251 }
252 bool suffixEmpty = true;
253 for (std::size_t r = static_cast<std::size_t>(last_nnz) + 1; r < R; ++r)
254 if (n[r] != 0) {
255 suffixEmpty = false;
256 break;
257 }
258 if (prefixFull && suffixEmpty) {
259 const T& xr = x[static_cast<std::size_t>(last_nnz)];
260 if (xr == zero) throw NumericError("pfqn_mvald: zero throughput on the G path");
261 res.G /= xr;
262 }
263 }
264
265 more = next_pop(n, N);
266 }
267
268 // The loop leaves x, WN and the pi slice at the full population N.
269 const std::size_t idxN = total - 1;
270 res.XN = x;
271 res.PI = Matrix<T>(M, K, zero);
272 for (std::size_t i = 0; i < M; ++i)
273 for (std::size_t k = 0; k < K; ++k) res.PI(i, k) = pi[(idxN * M + i) * K + k];
274
275 for (std::size_t i = 0; i < M; ++i) {
276 for (std::size_t r = 0; r < R; ++r) res.QN(i, r) = res.WN(i, r) * res.XN[r];
277 res.UN[i] = one - res.PI(i, 0);
278 }
279 for (std::size_t r = 0; r < R; ++r) {
280 // An absent class has zero throughput, so N/X would be 0/0: MATLAB
281 // reports no cycle time for it rather than NaN.
282 if (N[r] > 0) res.CN[r] = num_traits<T>::from_int(N[r]) / res.XN[r] - Zsum[r];
283 }
284
286 return res;
287}
288
289// ---------------------------------------------------------------------------
290// pfqn_mvamx: exact MVA for mixed single-server networks
291// ---------------------------------------------------------------------------
292
293/**
294 * Exact MVA for a mixed open/closed network of single-server stations.
295 *
296 * Port of matlab/src/api/pfqn/pfqn_mvamx.m. The open classes are absorbed by
297 * inflating the closed demands, D_c(i,r) / (1 - sum_{open} lambda D), after
298 * which the closed subnetwork is solved by pfqn_mva; the open metrics then
299 * follow from the closed queue lengths.
300 *
301 * @param lambda (R) arrival rates, zero on closed classes
302 * @param D (M x R) service demands
303 * @param N (R) population, OPEN_CLASS on open classes
304 * @param Z (K x R) think times, summed over rows; may be empty
305 * @param mi (M) station multiplicities; empty for all ones
306 *
307 * G and lG describe the closed subnetwork on the inflated demands, and are set
308 * to 0 and NaN respectively when there is no closed class, as in MATLAB.
309 */
310template <class T>
311MvaResult<T> pfqn_mvamx(const std::vector<T>& lambda, const Matrix<T>& D,
312 const std::vector<int>& N, const Matrix<T>& Z,
313 const std::vector<int>& mi) {
314 const std::size_t M = D.rows();
315 const std::size_t R = N.size();
316 if (!D.empty() && D.cols() != R)
317 throw InputError("pfqn_mvamx: demand matrix and population vector disagree on the class count");
318 if (lambda.size() != R)
319 throw InputError("pfqn_mvamx: arrival-rate vector and population vector disagree on the class count");
320
321 const T zero = num_traits<T>::from_int(0);
322 const T one = num_traits<T>::from_int(1);
323
324 MvaResult<T> res;
325 res.XN.assign(R, zero);
326 res.QN = Matrix<T>(M, R, zero);
327 res.UN = Matrix<T>(M, R, zero);
328 res.CN = Matrix<T>(M, R, zero);
329 res.G = zero;
330 res.lG = std::numeric_limits<double>::quiet_NaN();
331
332 std::vector<std::size_t> openClasses, closedClasses;
333 for (std::size_t r = 0; r < R; ++r) {
334 if (is_open_class(N[r]))
335 openClasses.push_back(r);
336 else
337 closedClasses.push_back(r);
338 }
339 for (std::size_t r = 0; r < R; ++r)
340 if (lambda[r] > zero && !is_open_class(N[r]) && N[r] > 0)
341 throw InputError("pfqn_mvamx: arrival rate cannot be specified on a closed class");
342
343 std::vector<T> Zsum(R, zero);
344 if (!Z.empty()) {
345 if (Z.cols() != R) throw InputError("pfqn_mvamx: Z and N disagree on the class count");
346 for (std::size_t k = 0; k < Z.rows(); ++k)
347 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
348 }
349
350 for (std::size_t r : openClasses) {
351 for (std::size_t i = 0; i < M; ++i) res.UN(i, r) = lambda[r] * D(i, r);
352 res.XN[r] = lambda[r];
353 }
354
355 std::vector<T> UNt(M, zero);
356 for (std::size_t i = 0; i < M; ++i)
357 for (std::size_t r = 0; r < R; ++r) UNt[i] += res.UN(i, r);
358
359 const std::size_t nClosed = closedClasses.size();
360 Matrix<T> QNc;
361 if (nClosed > 0) {
362 Matrix<T> Dc(M, nClosed, zero);
363 for (std::size_t i = 0; i < M; ++i) {
364 const T slack = one - UNt[i];
365 if (!(slack > zero))
366 throw NumericError("pfqn_mvamx: open classes saturate a station, no closed solution exists");
367 for (std::size_t c = 0; c < nClosed; ++c) Dc(i, c) = D(i, closedClasses[c]) / slack;
368 }
369 std::vector<int> Nc(nClosed);
370 Matrix<T> Zc(1, nClosed, zero);
371 for (std::size_t c = 0; c < nClosed; ++c) {
372 Nc[c] = N[closedClasses[c]];
373 Zc(0, c) = Zsum[closedClasses[c]];
374 }
375 MvaResult<T> closed = pfqn_mva(Dc, Nc, Zc, mi);
376 QNc = closed.QN;
377 for (std::size_t c = 0; c < nClosed; ++c) {
378 const std::size_t r = closedClasses[c];
379 res.XN[r] = closed.XN[c];
380 for (std::size_t i = 0; i < M; ++i) {
381 res.QN(i, r) = closed.QN(i, c);
382 res.CN(i, r) = closed.CN(i, c);
383 }
384 }
385 res.G = closed.G;
386 res.lG = closed.lG;
387 }
388
389 for (std::size_t i = 0; i < M; ++i)
390 for (std::size_t c = 0; c < nClosed; ++c) {
391 const std::size_t r = closedClasses[c];
392 res.UN(i, r) = res.XN[r] * D(i, r);
393 }
394
395 for (std::size_t i = 0; i < M; ++i) {
396 T qc = zero;
397 for (std::size_t c = 0; c < nClosed; ++c) qc += QNc(i, c);
398 const T slack = one - UNt[i];
399 for (std::size_t r : openClasses) {
400 if (!(slack > zero))
401 throw NumericError("pfqn_mvamx: open classes saturate a station");
402 res.CN(i, r) = D(i, r) * (one + qc) / slack;
403 res.QN(i, r) = res.CN(i, r) * res.XN[r];
404 }
405 }
406 return res;
407}
408
409// ---------------------------------------------------------------------------
410// pfqn_ldmx_ec: effective-capacity terms of the mixed load-dependent MVA
411// ---------------------------------------------------------------------------
412
413namespace detail {
414
415/** Outputs of pfqn_ldmx_ec, with E and Eprime indexed by queue length 0 ... Nt. */
416template <class T>
417struct LdmxEc {
418 Matrix<T> EC; ///< (M x Nt) effective capacity, EC(i,k-1) for k jobs
419 Matrix<T> E; ///< (M x (Nt+1))
420 Matrix<T> Eprime; ///< (M x (Nt+1))
421 std::vector<T> Lo; ///< (M) open-class load at each station
422};
423
424/**
425 * Port of matlab/src/api/pfqn/pfqn_ldmx_ec.m. Computes the terms that let the
426 * mixed load-dependent MVA absorb the open classes into a station-dependent
427 * effective capacity, under the limited-load-dependence assumption that the
428 * rate saturates from the b(i)-th job onwards.
429 *
430 * Every step is a field operation or an integer power, so the routine is exact.
431 *
432 * @param lambda (R) arrival rates
433 * @param D (M x R) service demands
434 * @param mu (M x Nt) service rates
435 */
436template <class T>
437LdmxEc<T> pfqn_ldmx_ec(const std::vector<T>& lambda, const Matrix<T>& D, const Matrix<T>& mu) {
438 const std::size_t M = mu.rows();
439 const std::size_t Nt = mu.cols();
440 const std::size_t R = D.cols();
441 if (D.rows() != M)
442 throw InputError("pfqn_ldmx_ec: demand matrix and rate matrix disagree on the station count");
443 if (lambda.size() != R)
444 throw InputError("pfqn_ldmx_ec: arrival-rate vector and demand matrix disagree on the class count");
445
446 const T zero = num_traits<T>::from_int(0);
447 const T one = num_traits<T>::from_int(1);
448
449 LdmxEc<T> out;
450 out.Lo.assign(M, zero);
451 for (std::size_t i = 0; i < M; ++i)
452 for (std::size_t r = 0; r < R; ++r) out.Lo[i] += lambda[r] * D(i, r);
453
454 // b(i): first queue length at which the rate has reached its final value.
455 std::vector<std::size_t> b(M, 1);
456 std::size_t maxb = 1;
457 for (std::size_t i = 0; i < M; ++i) {
458 for (std::size_t j = 1; j <= Nt; ++j) {
459 if (mu(i, j - 1) == mu(i, Nt - 1)) {
460 b[i] = j;
461 break;
462 }
463 }
464 if (b[i] > maxb) maxb = b[i];
465 }
466
467 // C(i,j) = 1/mu(i,j), extended past Nt by repeating the saturated rate, so
468 // the F-recursions below can index up to Nt + 1 + maxb as MATLAB does.
469 const std::size_t Cn = Nt + 1 + maxb;
470 Matrix<T> C(M, Cn, zero);
471 for (std::size_t i = 0; i < M; ++i)
472 for (std::size_t j = 1; j <= Cn; ++j) {
473 const T& m = j <= Nt ? mu(i, j - 1) : mu(i, Nt - 1);
474 if (m == zero) throw InputError("pfqn_ldmx_ec: service rate must be nonzero");
475 C(i, j - 1) = one / m;
476 }
477
478 out.EC = Matrix<T>(M, Nt, zero);
479 out.E = Matrix<T>(M, Nt + 1, zero);
480 out.Eprime = Matrix<T>(M, Nt + 1, zero);
481
482 for (std::size_t i = 0; i < M; ++i) {
483 const std::size_t bi = b[i];
484 const T Cb = C(i, bi - 1);
485 const T slack = one - out.Lo[i] * Cb;
486 if (slack == zero)
487 throw NumericError("pfqn_ldmx_ec: open-class load saturates a station, effective capacity diverges");
488 const T invSlack = one / slack;
489
490 // prod_{j=1}^{b-1} C(j)/C(b), shared by E1(0) and F3(0,0).
491 T ratioProd = one;
492 for (std::size_t j = 1; j + 1 <= bi; ++j) ratioProd *= C(i, j - 1) / Cb;
493
494 const long nb = static_cast<long>(bi) - 2; // last F-column index, -1 when b == 1
495 std::vector<T> E1(Nt + 1, zero);
496 T F3prev0 = zero;
497
498 for (std::size_t n = 0; n <= Nt; ++n) {
499 if (n >= bi) {
500 out.E(i, n) = num_pow_int(invSlack, static_cast<unsigned>(n + 1));
501 out.Eprime(i, n) = Cb * out.E(i, n);
502 continue;
503 }
504
505 if (n == 0)
506 E1[0] = invSlack * ratioProd;
507 else
508 E1[n] = invSlack * Cb / C(i, n - 1) * E1[n - 1];
509
510 T E2 = zero, E3 = zero, E2prime = zero;
511
512 T F2 = zero, F3 = zero, F2p = zero;
513 for (long n0 = 0; n0 <= nb; ++n0) {
514 if (n0 == 0) {
515 F2 = one;
516 F3 = (n == 0) ? ratioProd : Cb / C(i, n - 1) * F3prev0;
517 F2p = C(i, n); // C(n+1) in 1-based terms
518 } else {
519 const T fac = num_traits<T>::from_int(static_cast<long>(n) + n0) /
520 num_traits<T>::from_int(n0);
521 F2 = fac * out.Lo[i] * C(i, static_cast<std::size_t>(static_cast<long>(n) + n0) - 1) * F2;
522 F3 = fac * out.Lo[i] * Cb * F3;
523 F2p = fac * out.Lo[i] * C(i, static_cast<std::size_t>(static_cast<long>(n) + n0)) * F2p;
524 }
525 if (n0 == 0) F3prev0 = F3;
526 E2 += F2;
527 E3 += F3;
528 E2prime += F2p;
529 }
530 if (nb < 0) {
531 // b == 1: the F-sums are empty and F3(n,0) is never formed, so
532 // the carry-over stays at its unused zero, as in MATLAB.
533 F3prev0 = zero;
534 }
535
536 out.E(i, n) = E1[n] + E2 - E3;
537 if (n + 1 < bi)
538 out.Eprime(i, n) = Cb * E1[n] + E2prime - Cb * E3;
539 else
540 out.Eprime(i, n) = Cb * out.E(i, n);
541 }
542
543 // E UNDERFLOWS to zero at a saturated station, where Lo*C(b) > 1 drives
544 // the alternating geometric to nothing. MATLAB divides through and gets
545 // the IEEE infinity that solver_mva then caps; only an exact field, where
546 // division by zero is undefined, can refuse it.
547 for (std::size_t n = 1; n <= Nt; ++n) {
548 if (num_traits<T>::is_exact && out.E(i, n - 1) == zero)
549 throw NumericError("pfqn_ldmx_ec: vanishing effective capacity");
550 out.EC(i, n - 1) = C(i, n - 1) * out.E(i, n) / out.E(i, n - 1);
551 }
552 }
553 return out;
554}
555
556} // namespace detail
557
558// ---------------------------------------------------------------------------
559// pfqn_mvaldmx / pfqn_mvaldms: mixed networks with load-dependent stations
560// ---------------------------------------------------------------------------
561
562/**
563 * Exact MVA for mixed open/closed networks with limited load dependence.
564 *
565 * Port of matlab/src/api/pfqn/pfqn_mvaldmx.m (Bruell, Balbo and Ashfari). The
566 * MATLAB signature carries a trailing server-count argument S that its body
567 * never reads; the port drops it, since pfqn_mvaldms is the caller that turns
568 * server counts into rates.
569 *
570 * @param lambda (R) arrival rates, zero on closed classes
571 * @param D (M x R) service demands
572 * @param N (R) population, OPEN_CLASS on open classes
573 * @param Z (K x R) think times, summed over rows; may be empty
574 * @param mu (M x Nc') rates, Nc' >= the total closed population
575 */
576template <class T>
577MvaResult<T> pfqn_mvaldmx(const std::vector<T>& lambda, const Matrix<T>& D,
578 const std::vector<int>& N, const Matrix<T>& Z, const Matrix<T>& mu) {
579 const std::size_t M = D.rows();
580 const std::size_t R = N.size();
581 if (!D.empty() && D.cols() != R)
582 throw InputError("pfqn_mvaldmx: demand matrix and population vector disagree on the class count");
583 if (lambda.size() != R)
584 throw InputError("pfqn_mvaldmx: arrival-rate vector and population vector disagree on the class count");
585 if (mu.rows() != M)
586 throw InputError("pfqn_mvaldmx: rate matrix and demand matrix disagree on the station count");
587
588 const T zero = num_traits<T>::from_int(0);
589 const T one = num_traits<T>::from_int(1);
590
591 std::vector<std::size_t> openClasses, closedClasses;
592 long Nct = 0;
593 for (std::size_t r = 0; r < R; ++r) {
594 if (is_open_class(N[r])) {
595 openClasses.push_back(r);
596 } else {
597 closedClasses.push_back(r);
598 Nct += N[r];
599 }
600 }
601 for (std::size_t r = 0; r < R; ++r)
602 if (lambda[r] > zero && !is_open_class(N[r]) && N[r] > 0)
603 throw InputError("pfqn_mvaldmx: arrival rate cannot be specified on a closed class");
604 if (static_cast<long>(mu.cols()) < Nct)
605 throw InputError(
606 "pfqn_mvaldmx: the load-dependent rates must be given for at least the maximum closed population");
607
608 // MATLAB appends one extra column so the recursion can look one job ahead.
609 Matrix<T> mux(M, mu.cols() + 1, zero);
610 for (std::size_t i = 0; i < M; ++i) {
611 for (std::size_t j = 0; j < mu.cols(); ++j) mux(i, j) = mu(i, j);
612 mux(i, mu.cols()) = mu(i, mu.cols() - 1);
613 }
614 const detail::LdmxEc<T> ec = detail::pfqn_ldmx_ec(lambda, D, mux);
615
616 MvaResult<T> res;
617 res.XN.assign(R, zero);
618 res.QN = Matrix<T>(M, R, zero);
619 res.UN = Matrix<T>(M, R, zero);
620 res.CN = Matrix<T>(M, R, zero);
621 res.G = one;
622 res.lG = 0.0;
623
624 const std::size_t Cc = closedClasses.size();
625 Matrix<T> Dc(M, Cc, zero);
626 std::vector<int> Nc(Cc, 0);
627 std::vector<T> Zc(Cc, zero);
628 for (std::size_t c = 0; c < Cc; ++c) {
629 const std::size_t r = closedClasses[c];
630 Nc[c] = N[r];
631 for (std::size_t i = 0; i < M; ++i) Dc(i, c) = D(i, r);
632 }
633 if (!Z.empty()) {
634 if (Z.cols() != R) throw InputError("pfqn_mvaldmx: Z and N disagree on the class count");
635 for (std::size_t k = 0; k < Z.rows(); ++k)
636 for (std::size_t c = 0; c < Cc; ++c) Zc[c] += Z(k, closedClasses[c]);
637 }
638
639 const std::vector<std::size_t> prods = plane_sizes(Nc);
640 const std::size_t total = population_count(Nc);
641 const std::size_t K = static_cast<std::size_t>(Nct) + 1;
642
643 // Pc[(idx * M + i) * K + k] is P(station i holds k closed jobs | idx).
644 std::vector<T> Pc(total * M * K, zero);
645 for (std::size_t i = 0; i < M; ++i) Pc[(0 * M + i) * K + 0] = one;
646 const T tiny = num_traits<T>::from_double(std::numeric_limits<double>::epsilon());
647
648 Matrix<T> w(M, Cc, zero);
649 std::vector<T> x(Cc, zero);
650 std::vector<int> nvec(Cc, 0);
651 bool more = true;
652 while (more) {
653 const std::size_t idx = pop_index(nvec, prods);
654 long nc = 0;
655 for (int v : nvec) nc += v;
656
657 w.fill(zero);
658 std::fill(x.begin(), x.end(), zero);
659
660 for (std::size_t i = 0; i < M; ++i)
661 for (std::size_t c = 0; c < Cc; ++c) {
662 if (nvec[c] == 0) continue;
663 const std::size_t idx_c = idx - prods[c];
664 T acc = zero;
665 for (long k = 1; k <= nc; ++k)
666 acc += Dc(i, c) * num_traits<T>::from_int(k) *
667 ec.EC(i, static_cast<std::size_t>(k - 1)) *
668 Pc[(idx_c * M + i) * K + static_cast<std::size_t>(k - 1)];
669 w(i, c) = acc;
670 }
671
672 for (std::size_t c = 0; c < Cc; ++c) {
673 if (nvec[c] == 0) continue; // MATLAB forms 0/0 here and never reads it
674 T ctot = Zc[c];
675 for (std::size_t i = 0; i < M; ++i) ctot += w(i, c);
676 if (ctot == zero) throw NumericError("pfqn_mvaldmx: zero total residence time");
677 x[c] = num_traits<T>::from_int(nvec[c]) / ctot;
678 }
679
680 for (std::size_t i = 0; i < M; ++i) {
681 for (long k = 1; k <= nc; ++k) {
682 T acc = zero;
683 for (std::size_t c = 0; c < Cc; ++c) {
684 if (nvec[c] == 0) continue;
685 const std::size_t idx_c = idx - prods[c];
686 acc += Dc(i, c) * ec.EC(i, static_cast<std::size_t>(k - 1)) * x[c] *
687 Pc[(idx_c * M + i) * K + static_cast<std::size_t>(k - 1)];
688 }
689 Pc[(idx * M + i) * K + static_cast<std::size_t>(k)] = acc;
690 }
691 T acc = zero;
692 for (long k = 1; k <= nc; ++k) acc += Pc[(idx * M + i) * K + static_cast<std::size_t>(k)];
693 T p0 = one - acc;
694 if (p0 < tiny) p0 = tiny; // MATLAB: max(eps, 1 - sum)
695 Pc[(idx * M + i) * K + 0] = p0;
696 }
697
698 long last_nnz = -1;
699 for (long c = static_cast<long>(Cc) - 1; c >= 0; --c)
700 if (nvec[c] != 0) {
701 last_nnz = c;
702 break;
703 }
704 if (last_nnz >= 0) {
705 bool prefixFull = true;
706 for (long c = 0; c < last_nnz; ++c)
707 if (nvec[c] != Nc[c]) {
708 prefixFull = false;
709 break;
710 }
711 bool suffixEmpty = true;
712 for (std::size_t c = static_cast<std::size_t>(last_nnz) + 1; c < Cc; ++c)
713 if (nvec[c] != 0) {
714 suffixEmpty = false;
715 break;
716 }
717 if (prefixFull && suffixEmpty) {
718 const T& xr = x[static_cast<std::size_t>(last_nnz)];
719 if (xr > zero) res.G /= xr;
720 }
721 }
722
723 more = next_pop(nvec, Nc);
724 }
725
726 const std::size_t idxN = total - 1;
727
728 for (std::size_t c = 0; c < Cc; ++c) {
729 const std::size_t r = closedClasses[c];
730 res.XN[r] = x[c];
731 for (std::size_t i = 0; i < M; ++i) {
732 res.CN(i, r) = w(i, c);
733 res.QN(i, r) = x[c] * w(i, c);
734 if (Nc[c] > 0) {
735 const std::size_t idx_c = idxN - prods[c];
736 T u = zero;
737 for (long k = 1; k <= Nct; ++k) {
738 const std::size_t kk = static_cast<std::size_t>(k - 1);
739 if (num_traits<T>::is_exact && ec.E(i, kk) == zero)
740 throw NumericError("pfqn_mvaldmx: vanishing effective capacity");
741 u += Dc(i, c) * x[c] * ec.Eprime(i, kk) / ec.E(i, kk) * Pc[(idx_c * M + i) * K + kk];
742 }
743 res.UN(i, r) = u;
744 }
745 }
746 }
747
748 for (std::size_t r : openClasses) {
749 res.XN[r] = lambda[r];
750 for (std::size_t i = 0; i < M; ++i) {
751 T q = zero, u = zero;
752 for (long k = 0; k <= Nct; ++k) {
753 const std::size_t kk = static_cast<std::size_t>(k);
754 q += lambda[r] * D(i, r) * num_traits<T>::from_int(k + 1) * ec.EC(i, kk) *
755 Pc[(idxN * M + i) * K + kk];
756 if (num_traits<T>::is_exact && ec.E(i, kk + 1) == zero)
757 throw NumericError("pfqn_mvaldmx: vanishing effective capacity");
758 u += lambda[r] * ec.Eprime(i, kk + 1) / ec.E(i, kk + 1) * Pc[(idxN * M + i) * K + kk];
759 }
760 res.QN(i, r) = q;
761 if (lambda[r] == zero)
762 throw NumericError("pfqn_mvaldmx: an open class must have a positive arrival rate");
763 res.CN(i, r) = q / lambda[r];
764 res.UN(i, r) = u;
765 }
766 }
767
768 res.lG = Cc > 0 ? num_traits<T>::log_as_double(res.G) : std::numeric_limits<double>::quiet_NaN();
769 return res;
770}
771
772/**
773 * Exact MVA for mixed open/closed networks with multiserver stations.
774 *
775 * Port of matlab/src/api/pfqn/pfqn_mvaldms.m: builds the multiserver rates
776 * mu(i,k) = min(k, S(i)), calls pfqn_mvaldmx and replaces its utilizations by
777 * the per-server utilization law U(i,r) = X(r) D(i,r) / S(i).
778 *
779 * @param S (M) servers per station, INF_SERVERS for an infinite server
780 * @param lambda (R) arrival rates, zero on the closed classes
781 * @param D (M x R) service demands
782 * @param N (R) populations, negative on the open classes
783 * @param Z (K x R) think times
784 */
785template <class T>
786MvaResult<T> pfqn_mvaldms(const std::vector<T>& lambda, const Matrix<T>& D,
787 const std::vector<int>& N, const Matrix<T>& Z, const std::vector<int>& S) {
788 const std::size_t M = D.rows();
789 const std::size_t R = N.size();
790 if (S.size() != M) throw InputError("pfqn_mvaldms: server-count vector has the wrong length");
791
792 const T zero = num_traits<T>::from_int(0);
793 long Nct = 0;
794 for (std::size_t r = 0; r < R; ++r)
795 if (!is_open_class(N[r])) Nct += N[r];
796
797 // pfqn_ldmx_ec reads the limited-load-dependence level b_i off the rate row
798 // -- the first column equal to the LAST one -- and treats every rate beyond
799 // it as saturated. A row cut at the CLOSED population therefore declares a
800 // c-server station saturated at min(n,c) with n<c whenever c exceeds that
801 // population, and with no closed class at all leaves the row flat at one
802 // server, so the open classes see a station with fewer servers than it has.
803 // Build the row out to the servers: at Nct = 0 the EC term is then exactly
804 // the M/M/c mean queue length, since E(0) = 1/p0 and E(1) = L/(p0 Lo).
805 long ncol = Nct > 1 ? Nct : 1;
806 for (std::size_t i = 0; i < M; ++i)
807 if (S[i] != INF_SERVERS && S[i] > ncol) ncol = S[i];
808
809 Matrix<T> mu(M, static_cast<std::size_t>(ncol), num_traits<T>::from_int(1));
810 for (std::size_t i = 0; i < M; ++i)
811 for (long k = 1; k <= ncol; ++k) {
812 const long c = S[i] == INF_SERVERS ? k : (k < S[i] ? k : S[i]);
813 mu(i, static_cast<std::size_t>(k - 1)) = num_traits<T>::from_int(c);
814 }
815
816 MvaResult<T> res = pfqn_mvaldmx(lambda, D, N, Z, mu);
817
818 for (std::size_t i = 0; i < M; ++i) {
819 for (std::size_t r = 0; r < R; ++r) {
820 const T rate = is_open_class(N[r]) ? lambda[r] : res.XN[r];
821 // An infinite server has no server count to divide by: the law
822 // degenerates to the mean number of busy servers, X * D.
823 res.UN(i, r) = S[i] == INF_SERVERS
824 ? T(rate * D(i, r))
825 : T(rate * D(i, r) / num_traits<T>::from_int(S[i]));
826 if (rate == zero) res.UN(i, r) = zero;
827 }
828 }
829 return res;
830}
831
832// ---------------------------------------------------------------------------
833// pfqn_mvams
834// ---------------------------------------------------------------------------
835
836/**
837 * General-purpose exact MVA for mixed networks with multiserver stations.
838 *
839 * @param lambda (R) arrival rates, zero on closed classes; may be empty when
840 * the model has no open class
841 * @param L (M x R) service demands
842 * @param N (R) population, OPEN_CLASS on open classes
843 * @param Z (K x R) think times, summed over rows; may be empty
844 * @param mi (M) station multiplicities; empty for all ones
845 * @param S (M) servers per station, INF_SERVERS for an infinite server;
846 * empty for all ones
847 *
848 * The returned CN is the (M x R) per-station residence time of the pfqn_mva
849 * contract in every branch, and UN the (M x R) per-class utilization; see the
850 * contract notes at the top of this header for the two points at which that
851 * differs from MATLAB. In the mixed multiserver branch the normalizing
852 * constant is not available: G is 0 and lG is NaN, as in MATLAB.
853 *
854 * Standard arrival theorem throughout. For the interlocked-flow correction of
855 * Franks (1999), Ch. 4, Eq. (4.7), call pfqn_mvams_ilock instead.
856 */
857template <class T>
858MvaResult<T> pfqn_mvams(const std::vector<T>& lambda, const Matrix<T>& L,
859 const std::vector<int>& N, const Matrix<T>& Z, const std::vector<int>& mi,
860 const std::vector<int>& S) {
861 const std::size_t M = L.rows();
862 const std::size_t R = N.size();
863 if (!L.empty() && L.cols() != R)
864 throw InputError("pfqn_mvams: demand matrix and population vector disagree on the class count");
865 if (!mi.empty() && mi.size() != M)
866 throw InputError("pfqn_mvams: multiplicity vector has the wrong length");
867 if (!S.empty() && S.size() != M)
868 throw InputError("pfqn_mvams: server-count vector has the wrong length");
869
870 const T zero = num_traits<T>::from_int(0);
871 const T one = num_traits<T>::from_int(1);
872
873 std::vector<int> Sv = S.empty() ? std::vector<int>(M, 1) : S;
874 std::vector<int> miv = mi.empty() ? std::vector<int>(M, 1) : mi;
875 std::vector<T> lam = lambda.empty() ? std::vector<T>(R, zero) : lambda;
876 if (lam.size() != R)
877 throw InputError("pfqn_mvams: arrival-rate vector and population vector disagree on the class count");
878
879 long Ntot = 0;
880 bool hasOpenClasses = false;
881 for (std::size_t r = 0; r < R; ++r) {
882 if (is_open_class(N[r]))
883 hasOpenClasses = true;
884 else
885 Ntot += N[r];
886 }
887 // An infinite server is not counted here, mirroring MATLAB's isfinite guard.
888 bool hasMultiServer = false;
889 for (std::size_t i = 0; i < M; ++i)
890 if (Sv[i] != INF_SERVERS && Sv[i] > 1) {
891 hasMultiServer = true;
892 break;
893 }
894 bool hasReplicas = false;
895 for (std::size_t i = 0; i < M; ++i)
896 if (miv[i] != 1) {
897 hasReplicas = true;
898 break;
899 }
900
901 if (!hasMultiServer) {
902 if (hasOpenClasses) return pfqn_mvamx(lam, L, N, Z, miv);
903 return pfqn_mva(L, N, Z, miv);
904 }
905
906 if (hasReplicas)
907 throw InputError("pfqn_mvams: queue replicas are not available in exact MVA with multiserver stations");
908
909 if (hasOpenClasses) {
910 MvaResult<T> res = pfqn_mvaldms(lam, L, N, Z, Sv);
911 // MATLAB discards the normalizing constant on this branch.
912 res.G = zero;
913 res.lG = std::numeric_limits<double>::quiet_NaN();
914 return res;
915 }
916
917 Matrix<T> mu(M, static_cast<std::size_t>(Ntot > 0 ? Ntot : 1), one);
918 for (std::size_t i = 0; i < M; ++i)
919 for (long k = 1; k <= Ntot; ++k) {
920 const long c = Sv[i] == INF_SERVERS ? k : (k < Sv[i] ? k : Sv[i]);
921 mu(i, static_cast<std::size_t>(k - 1)) = num_traits<T>::from_int(c);
922 }
923
924 const MvaLdResult<T> ld = pfqn_mvald(L, N, Z, mu);
925
926 MvaResult<T> res;
927 res.XN = ld.XN;
928 res.QN = ld.QN;
929 res.UN = Matrix<T>(M, R, zero);
930 res.CN = Matrix<T>(M, R, zero);
931 res.G = ld.G;
932 res.lG = ld.lG;
933
934 for (std::size_t i = 0; i < M; ++i)
935 for (std::size_t r = 0; r < R; ++r) {
936 if (N[r] > 0) {
937 res.CN(i, r) = res.QN(i, r) / res.XN[r];
938 res.UN(i, r) = Sv[i] == INF_SERVERS
939 ? T(res.XN[r] * L(i, r))
940 : T(res.XN[r] * L(i, r) / num_traits<T>::from_int(Sv[i]));
941 } else {
942 // An absent class, as in pfqn_mva.
943 res.CN(i, r) = L(i, r) * num_traits<T>::from_int(miv[i]);
944 }
945 }
946 return res;
947}
948
949/** Overload with unit multiplicities. */
950template <class T>
951MvaResult<T> pfqn_mvams(const std::vector<T>& lambda, const Matrix<T>& L,
952 const std::vector<int>& N, const Matrix<T>& Z, const std::vector<int>& S) {
953 return pfqn_mvams(lambda, L, N, Z, std::vector<int>(), S);
954}
955
956/** Overload with unit multiplicities and a single server everywhere. */
957template <class T>
958MvaResult<T> pfqn_mvams(const std::vector<T>& lambda, const Matrix<T>& L,
959 const std::vector<int>& N, const Matrix<T>& Z) {
960 return pfqn_mvams(lambda, L, N, Z, std::vector<int>(), std::vector<int>());
961}
962
963/**
964 * MVA entry point for models carrying the interlocked-flow correction.
965 *
966 * The interlock of Franks (1999), Ch. 4, Eq. (4.7) is defined only for closed
967 * single-server models, so that is the one shape accepted here; anything else is
968 * refused rather than served without the correction. Models with no interlock go to
969 * pfqn_mvams.
970 *
971 * @param IL (R x R) interlock matrix, see pfqn_mva_ilock. Required.
972 */
973template <class T>
974MvaResult<T> pfqn_mvams_ilock(const std::vector<T>& lambda, const Matrix<T>& L,
975 const std::vector<int>& N, const Matrix<T>& Z,
976 const std::vector<int>& mi, const std::vector<int>& S,
977 const Matrix<T>& IL) {
978 const std::size_t M = L.rows();
979 const std::size_t R = N.size();
980 if (IL.empty())
981 throw InputError("pfqn_mvams_ilock: an interlock matrix is required; use pfqn_mvams for the standard arrival theorem");
982 if (!L.empty() && L.cols() != R)
983 throw InputError("pfqn_mvams_ilock: demand matrix and population vector disagree on the class count");
984 if (!mi.empty() && mi.size() != M)
985 throw InputError("pfqn_mvams_ilock: multiplicity vector has the wrong length");
986 if (!S.empty() && S.size() != M)
987 throw InputError("pfqn_mvams_ilock: server-count vector has the wrong length");
988
989 const T zero = num_traits<T>::from_int(0);
990 std::vector<int> Sv = S.empty() ? std::vector<int>(M, 1) : S;
991 std::vector<int> miv = mi.empty() ? std::vector<int>(M, 1) : mi;
992 std::vector<T> lam = lambda.empty() ? std::vector<T>(R, zero) : lambda;
993 if (lam.size() != R)
994 throw InputError("pfqn_mvams_ilock: arrival-rate vector and population vector disagree on the class count");
995
996 for (std::size_t r = 0; r < R; ++r)
997 if (is_open_class(N[r]) || lam[r] != zero)
998 throw InputError("pfqn_mvams_ilock: the interlock correction is available in exact MVA "
999 "for closed single-server models only; use an AMVA method");
1000 for (std::size_t i = 0; i < M; ++i)
1001 if (Sv[i] != INF_SERVERS && Sv[i] > 1)
1002 throw InputError("pfqn_mvams_ilock: the interlock correction is available in exact MVA "
1003 "for closed single-server models only; use an AMVA method");
1004
1005 return pfqn_mva_ilock(L, N, Z, miv, IL);
1006}
1007
1008} // namespace pfqn
1009} // namespace line
1010
1011#endif // LINE_API_PFQN_MVAMS_H
InputError(const std::string &what)
Definition error.h:39
void fill(const T &x)
Definition matrix.h:108
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_mvams(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &mi, const std::vector< int > &S)
General-purpose exact MVA for mixed networks with multiserver stations.
Definition pfqn_mvams.h:858
constexpr int OPEN_CLASS
Marks an open (infinite-population) class in a population vector.
Definition pfqn_mvams.h:77
MvaResult< T > pfqn_mvamx(const std::vector< T > &lambda, const Matrix< T > &D, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &mi)
Exact MVA for a mixed open/closed network of single-server stations.
Definition pfqn_mvams.h:311
MvaLdResult< T > pfqn_mvald(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu, bool stabilize=true)
Exact MVA for a closed network of load-dependent stations.
Definition pfqn_mvams.h:133
MvaResult< T > pfqn_mvaldms(const std::vector< T > &lambda, const Matrix< T > &D, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &S)
Exact MVA for mixed open/closed networks with multiserver stations.
Definition pfqn_mvams.h:786
MvaResult< T > pfqn_mvaldmx(const std::vector< T > &lambda, const Matrix< T > &D, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu)
Exact MVA for mixed open/closed networks with limited load dependence.
Definition pfqn_mvams.h:577
constexpr int INF_SERVERS
Marks an infinite-server station in a server-count vector.
Definition pfqn_mvams.h:80
bool is_open_class(int n)
True when the population entry denotes an open class.
Definition pfqn_mvams.h:83
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
MvaResult< T > pfqn_mvams_ilock(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &mi, const std::vector< int > &S, const Matrix< T > &IL)
MVA entry point for models carrying the interlocked-flow correction.
Definition pfqn_mvams.h:974
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
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:218
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.
Exact Mean Value Analysis for closed product-form networks (Reiser and Lavenberg 1980).
Population-vector enumeration and combinatorics.
Result of pfqn_mvald, mirroring the seven MATLAB outputs.
Definition pfqn_mvams.h:96
Matrix< T > WN
(M x R) residence time at the full population
Definition pfqn_mvams.h:101
double lG
log of the normalizing constant
Definition pfqn_mvams.h:104
Matrix< T > QN
(M x R) mean queue length
Definition pfqn_mvams.h:98
std::vector< T > XN
(R) per-class throughput
Definition pfqn_mvams.h:97
std::vector< T > UN
(M) utilization, 1 - P(station empty)
Definition pfqn_mvams.h:99
T G
normalizing constant
Definition pfqn_mvams.h:103
std::vector< T > CN
(R) cycle time, exclusive of think time
Definition pfqn_mvams.h:100
Matrix< T > PI
(M x (Nt+1)) marginal queue-length distribution at N
Definition pfqn_mvams.h:102
bool isNumStable
false once a marginal probability had to be clamped
Definition pfqn_mvams.h:105
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