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 for (std::size_t n = 1; n <= Nt; ++n) {
544 if (out.E(i, n - 1) == zero)
545 throw NumericError("pfqn_ldmx_ec: vanishing effective capacity");
546 out.EC(i, n - 1) = C(i, n - 1) * out.E(i, n) / out.E(i, n - 1);
547 }
548 }
549 return out;
550}
551
552} // namespace detail
553
554// ---------------------------------------------------------------------------
555// pfqn_mvaldmx / pfqn_mvaldms: mixed networks with load-dependent stations
556// ---------------------------------------------------------------------------
557
558/**
559 * Exact MVA for mixed open/closed networks with limited load dependence.
560 *
561 * Port of matlab/src/api/pfqn/pfqn_mvaldmx.m (Bruell, Balbo and Ashfari). The
562 * MATLAB signature carries a trailing server-count argument S that its body
563 * never reads; the port drops it, since pfqn_mvaldms is the caller that turns
564 * server counts into rates.
565 *
566 * @param lambda (R) arrival rates, zero on closed classes
567 * @param D (M x R) service demands
568 * @param N (R) population, OPEN_CLASS on open classes
569 * @param Z (K x R) think times, summed over rows; may be empty
570 * @param mu (M x Nc') rates, Nc' >= the total closed population
571 */
572template <class T>
573MvaResult<T> pfqn_mvaldmx(const std::vector<T>& lambda, const Matrix<T>& D,
574 const std::vector<int>& N, const Matrix<T>& Z, const Matrix<T>& mu) {
575 const std::size_t M = D.rows();
576 const std::size_t R = N.size();
577 if (!D.empty() && D.cols() != R)
578 throw InputError("pfqn_mvaldmx: demand matrix and population vector disagree on the class count");
579 if (lambda.size() != R)
580 throw InputError("pfqn_mvaldmx: arrival-rate vector and population vector disagree on the class count");
581 if (mu.rows() != M)
582 throw InputError("pfqn_mvaldmx: rate matrix and demand matrix disagree on the station count");
583
584 const T zero = num_traits<T>::from_int(0);
585 const T one = num_traits<T>::from_int(1);
586
587 std::vector<std::size_t> openClasses, closedClasses;
588 long Nct = 0;
589 for (std::size_t r = 0; r < R; ++r) {
590 if (is_open_class(N[r])) {
591 openClasses.push_back(r);
592 } else {
593 closedClasses.push_back(r);
594 Nct += N[r];
595 }
596 }
597 for (std::size_t r = 0; r < R; ++r)
598 if (lambda[r] > zero && !is_open_class(N[r]) && N[r] > 0)
599 throw InputError("pfqn_mvaldmx: arrival rate cannot be specified on a closed class");
600 if (static_cast<long>(mu.cols()) < Nct)
601 throw InputError(
602 "pfqn_mvaldmx: the load-dependent rates must be given for at least the maximum closed population");
603
604 // MATLAB appends one extra column so the recursion can look one job ahead.
605 Matrix<T> mux(M, mu.cols() + 1, zero);
606 for (std::size_t i = 0; i < M; ++i) {
607 for (std::size_t j = 0; j < mu.cols(); ++j) mux(i, j) = mu(i, j);
608 mux(i, mu.cols()) = mu(i, mu.cols() - 1);
609 }
610 const detail::LdmxEc<T> ec = detail::pfqn_ldmx_ec(lambda, D, mux);
611
612 MvaResult<T> res;
613 res.XN.assign(R, zero);
614 res.QN = Matrix<T>(M, R, zero);
615 res.UN = Matrix<T>(M, R, zero);
616 res.CN = Matrix<T>(M, R, zero);
617 res.G = one;
618 res.lG = 0.0;
619
620 const std::size_t Cc = closedClasses.size();
621 Matrix<T> Dc(M, Cc, zero);
622 std::vector<int> Nc(Cc, 0);
623 std::vector<T> Zc(Cc, zero);
624 for (std::size_t c = 0; c < Cc; ++c) {
625 const std::size_t r = closedClasses[c];
626 Nc[c] = N[r];
627 for (std::size_t i = 0; i < M; ++i) Dc(i, c) = D(i, r);
628 }
629 if (!Z.empty()) {
630 if (Z.cols() != R) throw InputError("pfqn_mvaldmx: Z and N disagree on the class count");
631 for (std::size_t k = 0; k < Z.rows(); ++k)
632 for (std::size_t c = 0; c < Cc; ++c) Zc[c] += Z(k, closedClasses[c]);
633 }
634
635 const std::vector<std::size_t> prods = plane_sizes(Nc);
636 const std::size_t total = population_count(Nc);
637 const std::size_t K = static_cast<std::size_t>(Nct) + 1;
638
639 // Pc[(idx * M + i) * K + k] is P(station i holds k closed jobs | idx).
640 std::vector<T> Pc(total * M * K, zero);
641 for (std::size_t i = 0; i < M; ++i) Pc[(0 * M + i) * K + 0] = one;
642 const T tiny = num_traits<T>::from_double(std::numeric_limits<double>::epsilon());
643
644 Matrix<T> w(M, Cc, zero);
645 std::vector<T> x(Cc, zero);
646 std::vector<int> nvec(Cc, 0);
647 bool more = true;
648 while (more) {
649 const std::size_t idx = pop_index(nvec, prods);
650 long nc = 0;
651 for (int v : nvec) nc += v;
652
653 w.fill(zero);
654 std::fill(x.begin(), x.end(), zero);
655
656 for (std::size_t i = 0; i < M; ++i)
657 for (std::size_t c = 0; c < Cc; ++c) {
658 if (nvec[c] == 0) continue;
659 const std::size_t idx_c = idx - prods[c];
660 T acc = zero;
661 for (long k = 1; k <= nc; ++k)
662 acc += Dc(i, c) * num_traits<T>::from_int(k) *
663 ec.EC(i, static_cast<std::size_t>(k - 1)) *
664 Pc[(idx_c * M + i) * K + static_cast<std::size_t>(k - 1)];
665 w(i, c) = acc;
666 }
667
668 for (std::size_t c = 0; c < Cc; ++c) {
669 if (nvec[c] == 0) continue; // MATLAB forms 0/0 here and never reads it
670 T ctot = Zc[c];
671 for (std::size_t i = 0; i < M; ++i) ctot += w(i, c);
672 if (ctot == zero) throw NumericError("pfqn_mvaldmx: zero total residence time");
673 x[c] = num_traits<T>::from_int(nvec[c]) / ctot;
674 }
675
676 for (std::size_t i = 0; i < M; ++i) {
677 for (long k = 1; k <= nc; ++k) {
678 T acc = zero;
679 for (std::size_t c = 0; c < Cc; ++c) {
680 if (nvec[c] == 0) continue;
681 const std::size_t idx_c = idx - prods[c];
682 acc += Dc(i, c) * ec.EC(i, static_cast<std::size_t>(k - 1)) * x[c] *
683 Pc[(idx_c * M + i) * K + static_cast<std::size_t>(k - 1)];
684 }
685 Pc[(idx * M + i) * K + static_cast<std::size_t>(k)] = acc;
686 }
687 T acc = zero;
688 for (long k = 1; k <= nc; ++k) acc += Pc[(idx * M + i) * K + static_cast<std::size_t>(k)];
689 T p0 = one - acc;
690 if (p0 < tiny) p0 = tiny; // MATLAB: max(eps, 1 - sum)
691 Pc[(idx * M + i) * K + 0] = p0;
692 }
693
694 long last_nnz = -1;
695 for (long c = static_cast<long>(Cc) - 1; c >= 0; --c)
696 if (nvec[c] != 0) {
697 last_nnz = c;
698 break;
699 }
700 if (last_nnz >= 0) {
701 bool prefixFull = true;
702 for (long c = 0; c < last_nnz; ++c)
703 if (nvec[c] != Nc[c]) {
704 prefixFull = false;
705 break;
706 }
707 bool suffixEmpty = true;
708 for (std::size_t c = static_cast<std::size_t>(last_nnz) + 1; c < Cc; ++c)
709 if (nvec[c] != 0) {
710 suffixEmpty = false;
711 break;
712 }
713 if (prefixFull && suffixEmpty) {
714 const T& xr = x[static_cast<std::size_t>(last_nnz)];
715 if (xr > zero) res.G /= xr;
716 }
717 }
718
719 more = next_pop(nvec, Nc);
720 }
721
722 const std::size_t idxN = total - 1;
723
724 for (std::size_t c = 0; c < Cc; ++c) {
725 const std::size_t r = closedClasses[c];
726 res.XN[r] = x[c];
727 for (std::size_t i = 0; i < M; ++i) {
728 res.CN(i, r) = w(i, c);
729 res.QN(i, r) = x[c] * w(i, c);
730 if (Nc[c] > 0) {
731 const std::size_t idx_c = idxN - prods[c];
732 T u = zero;
733 for (long k = 1; k <= Nct; ++k) {
734 const std::size_t kk = static_cast<std::size_t>(k - 1);
735 if (ec.E(i, kk) == zero) throw NumericError("pfqn_mvaldmx: vanishing effective capacity");
736 u += Dc(i, c) * x[c] * ec.Eprime(i, kk) / ec.E(i, kk) * Pc[(idx_c * M + i) * K + kk];
737 }
738 res.UN(i, r) = u;
739 }
740 }
741 }
742
743 for (std::size_t r : openClasses) {
744 res.XN[r] = lambda[r];
745 for (std::size_t i = 0; i < M; ++i) {
746 T q = zero, u = zero;
747 for (long k = 0; k <= Nct; ++k) {
748 const std::size_t kk = static_cast<std::size_t>(k);
749 q += lambda[r] * D(i, r) * num_traits<T>::from_int(k + 1) * ec.EC(i, kk) *
750 Pc[(idxN * M + i) * K + kk];
751 if (ec.E(i, kk + 1) == zero) throw NumericError("pfqn_mvaldmx: vanishing effective capacity");
752 u += lambda[r] * ec.Eprime(i, kk + 1) / ec.E(i, kk + 1) * Pc[(idxN * M + i) * K + kk];
753 }
754 res.QN(i, r) = q;
755 if (lambda[r] == zero)
756 throw NumericError("pfqn_mvaldmx: an open class must have a positive arrival rate");
757 res.CN(i, r) = q / lambda[r];
758 res.UN(i, r) = u;
759 }
760 }
761
762 res.lG = Cc > 0 ? num_traits<T>::log_as_double(res.G) : std::numeric_limits<double>::quiet_NaN();
763 return res;
764}
765
766/**
767 * Exact MVA for mixed open/closed networks with multiserver stations.
768 *
769 * Port of matlab/src/api/pfqn/pfqn_mvaldms.m: builds the multiserver rates
770 * mu(i,k) = min(k, S(i)), calls pfqn_mvaldmx and replaces its utilizations by
771 * the per-server utilization law U(i,r) = X(r) D(i,r) / S(i).
772 *
773 * @param S (M) servers per station, INF_SERVERS for an infinite server
774 * @param lambda (R) arrival rates, zero on the closed classes
775 * @param D (M x R) service demands
776 * @param N (R) populations, negative on the open classes
777 * @param Z (K x R) think times
778 */
779template <class T>
780MvaResult<T> pfqn_mvaldms(const std::vector<T>& lambda, const Matrix<T>& D,
781 const std::vector<int>& N, const Matrix<T>& Z, const std::vector<int>& S) {
782 const std::size_t M = D.rows();
783 const std::size_t R = N.size();
784 if (S.size() != M) throw InputError("pfqn_mvaldms: server-count vector has the wrong length");
785
786 const T zero = num_traits<T>::from_int(0);
787 long Nct = 0;
788 for (std::size_t r = 0; r < R; ++r)
789 if (!is_open_class(N[r])) Nct += N[r];
790
791 Matrix<T> mu(M, static_cast<std::size_t>(Nct > 0 ? Nct : 1), num_traits<T>::from_int(1));
792 for (std::size_t i = 0; i < M; ++i)
793 for (long k = 1; k <= Nct; ++k) {
794 const long c = S[i] == INF_SERVERS ? k : (k < S[i] ? k : S[i]);
795 mu(i, static_cast<std::size_t>(k - 1)) = num_traits<T>::from_int(c);
796 }
797
798 MvaResult<T> res = pfqn_mvaldmx(lambda, D, N, Z, mu);
799
800 for (std::size_t i = 0; i < M; ++i) {
801 for (std::size_t r = 0; r < R; ++r) {
802 const T rate = is_open_class(N[r]) ? lambda[r] : res.XN[r];
803 // An infinite server has no server count to divide by: the law
804 // degenerates to the mean number of busy servers, X * D.
805 res.UN(i, r) = S[i] == INF_SERVERS
806 ? T(rate * D(i, r))
807 : T(rate * D(i, r) / num_traits<T>::from_int(S[i]));
808 if (rate == zero) res.UN(i, r) = zero;
809 }
810 }
811 return res;
812}
813
814// ---------------------------------------------------------------------------
815// pfqn_mvams
816// ---------------------------------------------------------------------------
817
818/**
819 * General-purpose exact MVA for mixed networks with multiserver stations.
820 *
821 * @param lambda (R) arrival rates, zero on closed classes; may be empty when
822 * the model has no open class
823 * @param L (M x R) service demands
824 * @param N (R) population, OPEN_CLASS on open classes
825 * @param Z (K x R) think times, summed over rows; may be empty
826 * @param mi (M) station multiplicities; empty for all ones
827 * @param S (M) servers per station, INF_SERVERS for an infinite server;
828 * empty for all ones
829 *
830 * The returned CN is the (M x R) per-station residence time of the pfqn_mva
831 * contract in every branch, and UN the (M x R) per-class utilization; see the
832 * contract notes at the top of this header for the two points at which that
833 * differs from MATLAB. In the mixed multiserver branch the normalizing
834 * constant is not available: G is 0 and lG is NaN, as in MATLAB.
835 *
836 * Standard arrival theorem throughout. For the interlocked-flow correction of
837 * Franks (1999), Ch. 4, Eq. (4.7), call pfqn_mvams_ilock instead.
838 */
839template <class T>
840MvaResult<T> pfqn_mvams(const std::vector<T>& lambda, const Matrix<T>& L,
841 const std::vector<int>& N, const Matrix<T>& Z, const std::vector<int>& mi,
842 const std::vector<int>& S) {
843 const std::size_t M = L.rows();
844 const std::size_t R = N.size();
845 if (!L.empty() && L.cols() != R)
846 throw InputError("pfqn_mvams: demand matrix and population vector disagree on the class count");
847 if (!mi.empty() && mi.size() != M)
848 throw InputError("pfqn_mvams: multiplicity vector has the wrong length");
849 if (!S.empty() && S.size() != M)
850 throw InputError("pfqn_mvams: server-count vector has the wrong length");
851
852 const T zero = num_traits<T>::from_int(0);
853 const T one = num_traits<T>::from_int(1);
854
855 std::vector<int> Sv = S.empty() ? std::vector<int>(M, 1) : S;
856 std::vector<int> miv = mi.empty() ? std::vector<int>(M, 1) : mi;
857 std::vector<T> lam = lambda.empty() ? std::vector<T>(R, zero) : lambda;
858 if (lam.size() != R)
859 throw InputError("pfqn_mvams: arrival-rate vector and population vector disagree on the class count");
860
861 long Ntot = 0;
862 bool hasOpenClasses = false;
863 for (std::size_t r = 0; r < R; ++r) {
864 if (is_open_class(N[r]))
865 hasOpenClasses = true;
866 else
867 Ntot += N[r];
868 }
869 // An infinite server is not counted here, mirroring MATLAB's isfinite guard.
870 bool hasMultiServer = false;
871 for (std::size_t i = 0; i < M; ++i)
872 if (Sv[i] != INF_SERVERS && Sv[i] > 1) {
873 hasMultiServer = true;
874 break;
875 }
876 bool hasReplicas = false;
877 for (std::size_t i = 0; i < M; ++i)
878 if (miv[i] != 1) {
879 hasReplicas = true;
880 break;
881 }
882
883 if (!hasMultiServer) {
884 if (hasOpenClasses) return pfqn_mvamx(lam, L, N, Z, miv);
885 return pfqn_mva(L, N, Z, miv);
886 }
887
888 if (hasReplicas)
889 throw InputError("pfqn_mvams: queue replicas are not available in exact MVA with multiserver stations");
890
891 if (hasOpenClasses) {
892 MvaResult<T> res = pfqn_mvaldms(lam, L, N, Z, Sv);
893 // MATLAB discards the normalizing constant on this branch.
894 res.G = zero;
895 res.lG = std::numeric_limits<double>::quiet_NaN();
896 return res;
897 }
898
899 Matrix<T> mu(M, static_cast<std::size_t>(Ntot > 0 ? Ntot : 1), one);
900 for (std::size_t i = 0; i < M; ++i)
901 for (long k = 1; k <= Ntot; ++k) {
902 const long c = Sv[i] == INF_SERVERS ? k : (k < Sv[i] ? k : Sv[i]);
903 mu(i, static_cast<std::size_t>(k - 1)) = num_traits<T>::from_int(c);
904 }
905
906 const MvaLdResult<T> ld = pfqn_mvald(L, N, Z, mu);
907
908 MvaResult<T> res;
909 res.XN = ld.XN;
910 res.QN = ld.QN;
911 res.UN = Matrix<T>(M, R, zero);
912 res.CN = Matrix<T>(M, R, zero);
913 res.G = ld.G;
914 res.lG = ld.lG;
915
916 for (std::size_t i = 0; i < M; ++i)
917 for (std::size_t r = 0; r < R; ++r) {
918 if (N[r] > 0) {
919 res.CN(i, r) = res.QN(i, r) / res.XN[r];
920 res.UN(i, r) = Sv[i] == INF_SERVERS
921 ? T(res.XN[r] * L(i, r))
922 : T(res.XN[r] * L(i, r) / num_traits<T>::from_int(Sv[i]));
923 } else {
924 // An absent class, as in pfqn_mva.
925 res.CN(i, r) = L(i, r) * num_traits<T>::from_int(miv[i]);
926 }
927 }
928 return res;
929}
930
931/** Overload with unit multiplicities. */
932template <class T>
933MvaResult<T> pfqn_mvams(const std::vector<T>& lambda, const Matrix<T>& L,
934 const std::vector<int>& N, const Matrix<T>& Z, const std::vector<int>& S) {
935 return pfqn_mvams(lambda, L, N, Z, std::vector<int>(), S);
936}
937
938/** Overload with unit multiplicities and a single server everywhere. */
939template <class T>
940MvaResult<T> pfqn_mvams(const std::vector<T>& lambda, const Matrix<T>& L,
941 const std::vector<int>& N, const Matrix<T>& Z) {
942 return pfqn_mvams(lambda, L, N, Z, std::vector<int>(), std::vector<int>());
943}
944
945/**
946 * MVA entry point for models carrying the interlocked-flow correction.
947 *
948 * The interlock of Franks (1999), Ch. 4, Eq. (4.7) is defined only for closed
949 * single-server models, so that is the one shape accepted here; anything else is
950 * refused rather than served without the correction. Models with no interlock go to
951 * pfqn_mvams.
952 *
953 * @param IL (R x R) interlock matrix, see pfqn_mva_ilock. Required.
954 */
955template <class T>
956MvaResult<T> pfqn_mvams_ilock(const std::vector<T>& lambda, const Matrix<T>& L,
957 const std::vector<int>& N, const Matrix<T>& Z,
958 const std::vector<int>& mi, const std::vector<int>& S,
959 const Matrix<T>& IL) {
960 const std::size_t M = L.rows();
961 const std::size_t R = N.size();
962 if (IL.empty())
963 throw InputError("pfqn_mvams_ilock: an interlock matrix is required; use pfqn_mvams for the standard arrival theorem");
964 if (!L.empty() && L.cols() != R)
965 throw InputError("pfqn_mvams_ilock: demand matrix and population vector disagree on the class count");
966 if (!mi.empty() && mi.size() != M)
967 throw InputError("pfqn_mvams_ilock: multiplicity vector has the wrong length");
968 if (!S.empty() && S.size() != M)
969 throw InputError("pfqn_mvams_ilock: server-count vector has the wrong length");
970
971 const T zero = num_traits<T>::from_int(0);
972 std::vector<int> Sv = S.empty() ? std::vector<int>(M, 1) : S;
973 std::vector<int> miv = mi.empty() ? std::vector<int>(M, 1) : mi;
974 std::vector<T> lam = lambda.empty() ? std::vector<T>(R, zero) : lambda;
975 if (lam.size() != R)
976 throw InputError("pfqn_mvams_ilock: arrival-rate vector and population vector disagree on the class count");
977
978 for (std::size_t r = 0; r < R; ++r)
979 if (is_open_class(N[r]) || lam[r] != zero)
980 throw InputError("pfqn_mvams_ilock: the interlock correction is available in exact MVA "
981 "for closed single-server models only; use an AMVA method");
982 for (std::size_t i = 0; i < M; ++i)
983 if (Sv[i] != INF_SERVERS && Sv[i] > 1)
984 throw InputError("pfqn_mvams_ilock: the interlock correction is available in exact MVA "
985 "for closed single-server models only; use an AMVA method");
986
987 return pfqn_mva_ilock(L, N, Z, miv, IL);
988}
989
990} // namespace pfqn
991} // namespace line
992
993#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:840
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:780
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:573
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:956
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:192
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