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