LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_marie.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_MARIE_H
6#define LINE_API_PFQN_MARIE_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Marie's iterative aggregation-decomposition for closed networks with FCFS
12 * general (Coxian) service.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_marie.m. There is no JAR
15 * counterpart, so MATLAB is the only reference.
16 *
17 * SINGLE CLASS (R = 1). Each station's service is given a Coxian phase
18 * representation matching its mean L(i) and its squared coefficient of
19 * variation scv(i). The aggregate model is the exact load-dependent product
20 * form pfqn_mvald driven by a multiplier lattice mu(i,n), from which the
21 * marginal queue-length distribution P_i(n) at the full population is read off.
22 * Flow balance across the n <-> n+1 cut of that birth-death marginal gives the
23 * complementary arrival rate seen by station i,
24 *
25 * lambda_i(n) = (mu(i,n+1)/L(i)) P_i(n+1) / P_i(n), n = 0, ..., N-1,
26 *
27 * and the lambda(n)/Cox/1(-m) isolation chain -- states (n,k) with n present
28 * and the head job in phase k, phase rates scaled by min(n,m) for m servers --
29 * is solved for its stationary distribution. Its conditional departure rate
30 *
31 * mu_i(n) = sum_k p(n,k) rate_k phi_k min(n,m) / sum_k p(n,k)
32 *
33 * is converted back to a multiplier (multiplied by L(i)) and fed to the next
34 * aggregate solve. The iteration stops when max|mu_new - mu| < tol. For
35 * exponential service (scv == 1) the isolation chain is the M/M/1(-m) queue,
36 * mu_i(n) = min(n,m)/L(i), so the multiplier lattice is the initial one, the
37 * first iteration already reproduces exact product form and the loop exits on
38 * the second pass.
39 *
40 * MULTIPLE CLASSES (R > 1). Exponential and class-independent demands at every
41 * station is genuine BCMP FCFS and is dispatched to exact pfqn_mva. Otherwise
42 * the aggregate is a Schweitzer-style multiclass AMVA in which the class-r
43 * demand at station i is divided by a class-dependent scaling
44 * beta_{i,r}(nvec) = muCox_{i,r}(nvec) / muExp_{i,r}(nvec), the ratio of the
45 * conditional class-r throughput of a multiclass Cox/1 FCFS isolation chain to
46 * that of the same chain with exponential service of the same means. beta == 1
47 * therefore recovers plain FCFS AMVA and carries only the non-exponential
48 * correction. The isolation chain is fed the aggregate per-class throughput
49 * (Baynat-Dallery isolation) and the outer loop iterates to a fixed point on X.
50 * beta is tabulated on the integer population box and read at the real-valued
51 * arrival-instant populations by multilinear interpolation; the reference
52 * guards a non-positive or non-finite ratio by falling back to 1 and clamps the
53 * result to [1e-3, 1e3], and both are reproduced.
54 *
55 * COXIAN FIT. Coxian.fitMeanAndSCV is closed form and is reproduced here as
56 * marie_cox_fit, with tol = GlobalConstants.CoarseTol = 1e-3 (matlab/lineStart.m):
57 *
58 * |scv - 1| <= tol exponential, n = 1, mu = [1/mean], phi = [1]
59 * 0.5 + tol < scv < 1 - tol hypoexponential, n = 2,
60 * mu = 2/mean/(1 +- sqrt(2 scv - 1)), phi = [0,1]
61 * scv <= 0.5 + tol Erlang, n = ceil(1/scv), mu = (n/mean) 1, phi = e_n
62 * scv > 1 + tol Coxian-2, mu = [2/mean, 1/(scv mean)],
63 * phi = [1 - 1/(2 scv), 1]
64 *
65 * The exponential, hypoexponential and Coxian-2 branches match the requested
66 * mean and scv exactly. The Erlang branch does NOT: ceil(1/scv) is an integer,
67 * so the fitted scv is 1/ceil(1/scv) <= scv, with equality only when 1/scv is
68 * an integer. That is the reference's behaviour and is reproduced, not
69 * improved. The phase count also goes through a double-valued ceiling, which is
70 * the one place the fit is not a pure field computation.
71 *
72 * DIVERGENCE from the reference, deliberate and documented. MATLAB solves the
73 * isolation chains as the overdetermined least-squares system
74 * [Q'; ones] p = [0; 1] via backslash. This port calls mc::ctmc_solve, which
75 * replaces one balance equation by the normalization and solves the resulting
76 * square system, and which additionally splits a reducible generator into its
77 * weakly connected components. The two are the same computation whenever the
78 * isolation chain is irreducible, which is the case whenever every
79 * complementary arrival rate lambda_i(n) is positive -- the only regime in
80 * which the reference's own least-squares answer is a probability vector at
81 * all.
82 *
83 * Arithmetic: INEXACT BY CONSTRUCTION, and additionally gated on
84 * has_transcendental. The method is a fixed-point decomposition stopped on a
85 * tolerance, the Coxian fit of an scv in (0.5, 1) needs a square root that has
86 * no exact rational counterpart, and the multiclass path reports throughputs
87 * from an approximate MVA. It is therefore unavailable at T = Rational.
88 */
89
90#include <cmath>
91#include <cstddef>
92#include <limits>
93#include <vector>
94
98#include "line/num/number.h"
99#include "line/util/error.h"
100#include "line/util/matrix.h"
101
102namespace line {
103namespace pfqn {
104
105/** Coxian phase representation: phase rates and per-phase completion probabilities. */
106template <class T>
108 std::vector<T> mu; ///< phase rates
109 std::vector<T> phi; ///< completion probability out of each phase, phi.back() == 1
110};
111
112/**
113 * Closed-form Coxian fit of a mean and an SCV (matlab/src/lang/processes/Coxian.m,
114 * fitMeanAndSCV), with the branch thresholds at CoarseTol = 1e-3.
115 *
116 * @param mean strictly positive mean
117 * @param scv strictly positive squared coefficient of variation
118 */
119template <class T>
120MarieCoxFit<T> marie_cox_fit(const T& mean, const T& scv) {
122 "marie_cox_fit requires transcendental arithmetic: the hypoexponential branch "
123 "matches the second moment through a square root");
124 using std::sqrt;
125
126 const T zero = num_traits<T>::from_int(0);
127 const T one = num_traits<T>::from_int(1);
128 const T two = num_traits<T>::from_int(2);
129 const T half = num_traits<T>::from_rational(1, 2);
130 const T coarse = num_traits<T>::from_double(1e-3); // GlobalConstants.CoarseTol
131
132 if (!(mean > zero)) throw InputError("marie_cox_fit: the mean must be strictly positive");
133 if (!(scv > zero)) throw InputError("marie_cox_fit: the SCV must be strictly positive");
134
136 if (scv >= T(one - coarse) && scv <= T(one + coarse)) {
137 // Exponential.
138 f.mu.push_back(T(one / mean));
139 f.phi.push_back(one);
140 } else if (scv > T(half + coarse) && scv < T(one - coarse)) {
141 // Hypoexponential: two phases in series, neither completing early.
142 const T d = T(sqrt(T(two * scv - one)));
143 f.mu.push_back(T(two / mean / T(one + d)));
144 f.mu.push_back(T(two / mean / T(one - d)));
145 f.phi.push_back(zero);
146 f.phi.push_back(one);
147 } else if (scv <= T(half + coarse)) {
148 // Erlang-n with n = ceil(1/scv); the fitted SCV is 1/n, not scv.
149 const double inv = num_traits<T>::to_double(T(one / scv));
150 const long n = static_cast<long>(std::ceil(inv));
151 if (n < 1 || n > 100000)
152 throw InputError("marie_cox_fit: the Erlang branch needs an unreasonable phase count");
153 const T rate = num_traits<T>::from_int(n) / mean;
154 f.mu.assign(static_cast<std::size_t>(n), rate);
155 f.phi.assign(static_cast<std::size_t>(n), zero);
156 f.phi.back() = one;
157 } else {
158 // Coxian-2, the hyperexponential rewritten in Coxian form.
159 const T mu1 = T(two / mean);
160 const T mu2 = T(mu1 / T(two * scv));
161 f.mu.push_back(mu1);
162 f.mu.push_back(mu2);
163 f.phi.push_back(T(one - mu2 / mu1));
164 f.phi.push_back(one);
165 }
166 return f;
167}
168
169namespace detail {
170
171/**
172 * Stationary analysis of a lambda(n)/Cox/1(-m) queue in isolation, returning the
173 * conditional departure rate mu(n) given n present, n = 1, ..., N.
174 *
175 * @param lam (N) arrival rate with n present, lam[n] for n = 0, ..., N-1
176 * @param rate (P) Coxian phase rates
177 * @param phi (P) per-phase completion probabilities
178 * @param m server count; the phase rate at population n is scaled by min(n,m)
179 * @param N population of the isolated station
180 */
181template <class T>
182std::vector<T> marie_isol_condtput(const std::vector<T>& lam, const std::vector<T>& rate,
183 const std::vector<T>& phi, int N, int m) {
184 const T zero = num_traits<T>::from_int(0);
185 const T one = num_traits<T>::from_int(1);
186 const std::size_t P = rate.size();
187 const std::size_t S = 1 + static_cast<std::size_t>(N) * P;
188
189 // State layout: 0 = empty; (n,k) -> 1 + (n-1)P + (k-1), n = 1..N, k = 1..P.
190 const auto idx = [P](int n, std::size_t k) {
191 return 1 + static_cast<std::size_t>(n - 1) * P + k;
192 };
193
194 Matrix<T> Gq(S, S, zero);
195 Gq(0, idx(1, 0)) += lam[0];
196 for (int n = 1; n <= N; ++n) {
197 const T sc = num_traits<T>::from_int(n < m ? n : m);
198 for (std::size_t k = 0; k < P; ++k) {
199 const std::size_t r = idx(n, k);
200 if (n < N) Gq(r, idx(n + 1, k)) += lam[static_cast<std::size_t>(n)];
201 const T compl_ = rate[k] * phi[k] * sc;
202 const T adv = rate[k] * T(one - phi[k]) * sc;
203 if (adv > zero && k + 1 < P) Gq(r, idx(n, k + 1)) += adv;
204 if (compl_ > zero) {
205 if (n > 1)
206 Gq(r, idx(n - 1, 0)) += compl_;
207 else
208 Gq(r, 0) += compl_;
209 }
210 }
211 }
212
213 const std::vector<T> p = mc::ctmc_solve(Gq);
214
215 std::vector<T> muvec(static_cast<std::size_t>(N), zero);
216 for (int n = 1; n <= N; ++n) {
217 const T sc = num_traits<T>::from_int(n < m ? n : m);
218 T Pn = zero, dep = zero;
219 for (std::size_t k = 0; k < P; ++k) {
220 const T pk = p[idx(n, k)];
221 Pn += pk;
222 dep += pk * rate[k] * phi[k] * sc;
223 }
224 if (Pn > zero) {
225 muvec[static_cast<std::size_t>(n - 1)] = dep / Pn;
226 } else {
227 // Fallback: the exponential-equivalent rate of the phase chain.
228 T mean = zero;
229 for (std::size_t k = 0; k < P; ++k) mean += one / rate[k];
230 muvec[static_cast<std::size_t>(n - 1)] = sc / mean;
231 }
232 }
233 return muvec;
234}
235
236} // namespace detail
237
238/**
239 * Class-dependent scaling of one station, tabulated on the integer population
240 * box. Both the Coxian and the exponential conditional throughputs are kept, so
241 * that the ratio is formed AFTER interpolation exactly as the reference does.
242 * An empty table means beta == 1, the initial value of the outer iteration.
243 */
244template <class T>
246 std::vector<int> N; ///< box bounds; the lattice is [0..N]
247 std::vector<std::vector<T>> muCox; ///< muCox[r][lin], lin the row-major box index
248 std::vector<std::vector<T>> muExp; ///< muExp[r][lin]
249
250 bool identity() const { return muCox.empty(); }
251};
252
253namespace detail {
254
255/** Row-major linear index over the box [0..N]. */
256inline std::size_t marie_box_index(const std::vector<int>& n, const std::vector<int>& N) {
257 std::size_t lin = 0;
258 for (std::size_t d = 0; d < N.size(); ++d)
259 lin = lin * static_cast<std::size_t>(N[d] + 1) + static_cast<std::size_t>(n[d]);
260 return lin;
261}
262
263inline std::size_t marie_box_size(const std::vector<int>& N) {
264 std::size_t sz = 1;
265 for (std::size_t d = 0; d < N.size(); ++d) sz *= static_cast<std::size_t>(N[d] + 1);
266 return sz;
267}
268
269/**
270 * Multilinear interpolation of a table over the box [0..N] at a real point,
271 * clamped to the box (ndlininterp in the reference).
272 */
273template <class T>
274T marie_ndlininterp(const std::vector<T>& A, const std::vector<T>& x, const std::vector<int>& N) {
275 const std::size_t R = N.size();
276 const T zero = num_traits<T>::from_int(0);
277 const T one = num_traits<T>::from_int(1);
278 std::vector<int> lo(R), hi(R);
279 std::vector<T> fr(R);
280 for (std::size_t d = 0; d < R; ++d) {
281 T xd = x[d];
282 if (xd < zero) xd = zero;
283 const T ub = num_traits<T>::from_int(N[d]);
284 if (xd > ub) xd = ub;
285 const double f = std::floor(num_traits<T>::to_double(xd));
286 lo[d] = static_cast<int>(f);
287 if (lo[d] > N[d]) lo[d] = N[d];
288 hi[d] = lo[d] + 1 < N[d] ? lo[d] + 1 : N[d];
289 fr[d] = T(xd - num_traits<T>::from_int(lo[d]));
290 }
291 T v = zero;
292 std::vector<int> sub(R);
293 const unsigned long corners = 1ul << R;
294 for (unsigned long mask = 0; mask < corners; ++mask) {
295 T w = one;
296 for (std::size_t d = 0; d < R; ++d) {
297 if ((mask >> d) & 1ul) {
298 sub[d] = hi[d];
299 w *= fr[d];
300 } else {
301 sub[d] = lo[d];
302 w *= T(one - fr[d]);
303 }
304 }
305 if (w == zero) continue;
306 v += w * A[marie_box_index(sub, N)];
307 }
308 return v;
309}
310
311} // namespace detail
312
313/**
314 * Evaluate a class-dependent scaling at a real-valued population vector
315 * (cdscale_eval in the reference): the interpolated ratio, guarded against a
316 * non-positive or non-finite value and clamped to [1e-3, 1e3].
317 */
318template <class T>
319std::vector<T> marie_cd_eval(const MarieCdScaling<T>& cd, const std::vector<T>& nv) {
320 const std::size_t R = nv.size();
321 const T one = num_traits<T>::from_int(1);
322 std::vector<T> be(R, one);
323 if (cd.identity()) return be;
324 const T zero = num_traits<T>::from_int(0);
325 const T lo = num_traits<T>::from_double(1e-3);
326 const T hi = num_traits<T>::from_double(1e3);
327 const T inf = num_traits<T>::from_double(std::numeric_limits<double>::infinity());
328 for (std::size_t r = 0; r < R; ++r) {
329 const T num = detail::marie_ndlininterp(cd.muCox[r], nv, cd.N);
330 const T den = detail::marie_ndlininterp(cd.muExp[r], nv, cd.N);
331 const bool okNum = num == num && num > zero && num < inf;
332 const bool okDen = den == den && den > zero && den < inf;
333 be[r] = (okNum && okDen) ? T(num / den) : one;
334 if (be[r] < lo) be[r] = lo;
335 if (be[r] > hi) be[r] = hi;
336 }
337 return be;
338}
339
340/** Result of pfqn_marie, mirroring the six MATLAB outputs. */
341template <class T>
343 std::vector<T> X; ///< (R) per-class throughput
344 Matrix<T> Q; ///< (M x R) mean queue length
345 Matrix<T> U; ///< (M x R) utilization
346 /**
347 * Single class: (1 x 1), the CYCLE time returned by pfqn_mvald. Multiple
348 * classes: (M x R) per-station residence times. The shapes differ because
349 * the reference's two paths return different quantities under the same
350 * name; that is reproduced rather than harmonized.
351 */
353 int it; ///< iterations performed
354 Matrix<T> mu; ///< single class: (M x N) converged multiplier lattice; empty for R > 1
355 std::vector<MarieCdScaling<T>> cds; ///< R > 1: converged per-station cd scalings
356};
357
358namespace detail {
359
360/**
361 * Multiclass Schweitzer AMVA with a class-dependent service-rate scaling
362 * (amva_qd in the reference). Gauss-Seidel in the class index, so the queue
363 * lengths written for class r are visible to class r+1 within the same sweep;
364 * that update order is part of the fixed point and is reproduced.
365 */
366template <class T>
367void marie_amva_qd(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
368 const std::vector<MarieCdScaling<T>>& cds, std::vector<T>& X, Matrix<T>& Q,
369 Matrix<T>& U, Matrix<T>& C) {
370 const std::size_t M = L.rows();
371 const std::size_t R = L.cols();
372 const T zero = num_traits<T>::from_int(0);
373 const T one = num_traits<T>::from_int(1);
374 const T tol = num_traits<T>::from_double(1e-9);
375
376 Q = Matrix<T>(M, R, zero);
377 const T Mt = num_traits<T>::from_int(static_cast<long>(M > 1 ? M : 1));
378 for (std::size_t i = 0; i < M; ++i)
379 for (std::size_t r = 0; r < R; ++r) Q(i, r) = num_traits<T>::from_int(N[r]) / Mt;
380
381 Matrix<T> W(M, R, zero);
382 X.assign(R, zero);
383 U = Matrix<T>(M, R, zero);
384 Matrix<T> Qprev(M, R, zero);
385 for (std::size_t i = 0; i < M; ++i)
386 for (std::size_t r = 0; r < R; ++r) Qprev(i, r) = T(Q(i, r) + one);
387
388 std::vector<T> nv(R), Leff(R);
389 int it = 0;
390 for (;;) {
391 T delta = zero;
392 for (std::size_t i = 0; i < M; ++i)
393 for (std::size_t r = 0; r < R; ++r) {
394 const T d = num_abs(T(Q(i, r) - Qprev(i, r)));
395 if (d > delta) delta = d;
396 }
397 if (!(delta > tol) || it >= 5000) break;
398 ++it;
399 Qprev = Q;
400 for (std::size_t r = 0; r < R; ++r) {
401 for (std::size_t i = 0; i < M; ++i) {
402 for (std::size_t s = 0; s < R; ++s) nv[s] = Q(i, s);
403 if (N[r] > 0)
404 nv[r] = Q(i, r) * num_traits<T>::from_int(N[r] - 1) /
406 const std::vector<T> be = marie_cd_eval(cds[i], nv);
407 T w = zero;
408 for (std::size_t s = 0; s < R; ++s) {
409 Leff[s] = L(i, s) / be[s];
410 w += Leff[s] * nv[s];
411 }
412 W(i, r) = Leff[r] + w;
413 }
414 T denom = Z[r];
415 for (std::size_t i = 0; i < M; ++i) denom += W(i, r);
416 X[r] = denom > zero ? T(num_traits<T>::from_int(N[r]) / denom) : zero;
417 for (std::size_t i = 0; i < M; ++i) Q(i, r) = X[r] * W(i, r);
418 }
419 }
420 C = W;
421 for (std::size_t r = 0; r < R; ++r)
422 for (std::size_t i = 0; i < M; ++i) U(i, r) = X[r] * L(i, r);
423}
424
425/**
426 * Stationary analysis of a multiclass lambda_r/Cox/1 FCFS queue in isolation
427 * over the population box [0..N] (isol_mc in the reference). The head-of-line
428 * job is tracked as (class, phase); on a departure the next head class is drawn
429 * in random order, with probability n_c / sum(n). Returns, per class, the
430 * conditional class-r throughput tabulated on the box.
431 */
432template <class T>
433std::vector<std::vector<T>> marie_isol_mc(const std::vector<T>& lam,
434 const std::vector<MarieCoxFit<T>>& fits,
435 const std::vector<int>& N) {
436 const std::size_t R = N.size();
437 const T zero = num_traits<T>::from_int(0);
438 const std::size_t npops = marie_box_size(N);
439
440 // Enumerate the states: 0 is the empty station, then (pop, head class, phase).
441 struct StateId {
442 std::size_t pop;
443 std::size_t cls;
444 std::size_t phase;
445 };
446 std::vector<StateId> ids;
447 ids.push_back(StateId{marie_box_index(std::vector<int>(R, 0), N), 0, 0});
448 // id[pop][c][k] with a flat index; -1 where the state does not exist.
449 std::vector<std::vector<long>> byPop(npops);
450 std::vector<std::vector<int>> popVec(npops, std::vector<int>(R, 0));
451 {
452 std::vector<int> n(R, 0);
453 for (std::size_t p = 0; p < npops; ++p) {
454 // Row-major enumeration matching marie_box_index.
455 std::size_t rem = p;
456 for (std::size_t d = R; d-- > 0;) {
457 const std::size_t w = static_cast<std::size_t>(N[d] + 1);
458 n[d] = static_cast<int>(rem % w);
459 rem /= w;
460 }
461 popVec[p] = n;
462 }
463 }
464 for (std::size_t p = 0; p < npops; ++p) {
465 int tot = 0;
466 for (std::size_t r = 0; r < R; ++r) tot += popVec[p][r];
467 if (tot == 0) continue;
468 std::vector<long> slot;
469 for (std::size_t c = 0; c < R; ++c) {
470 const std::size_t P = fits[c].mu.size();
471 for (std::size_t k = 0; k < P; ++k) {
472 if (popVec[p][c] > 0) {
473 slot.push_back(static_cast<long>(ids.size()));
474 ids.push_back(StateId{p, c, k});
475 } else {
476 slot.push_back(-1);
477 }
478 }
479 }
480 byPop[p] = slot;
481 }
482 std::vector<std::size_t> phaseOff(R + 1, 0);
483 for (std::size_t c = 0; c < R; ++c) phaseOff[c + 1] = phaseOff[c] + fits[c].mu.size();
484
485 const auto getid = [&](std::size_t p, std::size_t c, std::size_t k) -> std::size_t {
486 const long v = byPop[p][phaseOff[c] + k];
487 if (v < 0) throw NumericError("pfqn_marie: isolation state does not exist");
488 return static_cast<std::size_t>(v);
489 };
490
491 const std::size_t S = ids.size();
492 Matrix<T> Gq(S, S, zero);
493 std::vector<int> nn(R);
494 for (std::size_t s = 0; s < S; ++s) {
495 const StateId& st = ids[s];
496 const bool empty = (s == 0);
497 const std::vector<int>& nvec = empty ? popVec[ids[0].pop] : popVec[st.pop];
498 for (std::size_t r = 0; r < R; ++r) {
499 if (nvec[r] < N[r] && lam[r] > zero) {
500 nn = nvec;
501 nn[r] += 1;
502 const std::size_t pn = marie_box_index(nn, N);
503 if (empty)
504 Gq(s, getid(pn, r, 0)) += lam[r];
505 else
506 Gq(s, getid(pn, st.cls, st.phase)) += lam[r];
507 }
508 }
509 if (empty) continue;
510 const std::size_t c = st.cls, k = st.phase;
511 const T rate = fits[c].mu[k];
512 const T compl_ = rate * fits[c].phi[k];
513 const T adv = rate * T(num_traits<T>::from_int(1) - fits[c].phi[k]);
514 if (adv > zero && k + 1 < fits[c].mu.size()) Gq(s, getid(st.pop, c, k + 1)) += adv;
515 if (compl_ > zero) {
516 nn = nvec;
517 nn[c] -= 1;
518 int tot = 0;
519 for (std::size_t r = 0; r < R; ++r) tot += nn[r];
520 if (tot == 0) {
521 Gq(s, 0) += compl_;
522 } else {
523 const std::size_t pn = marie_box_index(nn, N);
524 for (std::size_t cp = 0; cp < R; ++cp)
525 if (nn[cp] > 0)
526 Gq(s, getid(pn, cp, 0)) +=
527 compl_ * num_traits<T>::from_int(nn[cp]) / num_traits<T>::from_int(tot);
528 }
529 }
530 }
531
532 const std::vector<T> p = mc::ctmc_solve(Gq);
533
534 std::vector<T> Ppop(npops, zero);
535 std::vector<std::vector<T>> dep(R, std::vector<T>(npops, zero));
536 for (std::size_t s = 1; s < S; ++s) {
537 const StateId& st = ids[s];
538 Ppop[st.pop] += p[s];
539 dep[st.cls][st.pop] += p[s] * fits[st.cls].mu[st.phase] * fits[st.cls].phi[st.phase];
540 }
541 std::vector<std::vector<T>> mumat(R, std::vector<T>(npops, zero));
542 for (std::size_t r = 0; r < R; ++r)
543 for (std::size_t q = 0; q < npops; ++q)
544 if (Ppop[q] > zero) mumat[r][q] = dep[r][q] / Ppop[q];
545 return mumat;
546}
547
548/** Multiclass path of pfqn_marie (marie_multi in the reference). */
549template <class T>
550MarieResult<T> marie_multi(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
551 const Matrix<T>& scv, double tol, int maxiter) {
552 const std::size_t M = L.rows();
553 const std::size_t R = L.cols();
554 const T zero = num_traits<T>::from_int(0);
555 const T one = num_traits<T>::from_int(1);
556
557 MarieResult<T> res;
558 res.it = 0;
559
560 // Exact product-form dispatch: exponential AND class-independent demands.
561 bool isPF = true;
562 for (std::size_t i = 0; i < M && isPF; ++i)
563 for (std::size_t r = 0; r < R; ++r)
564 if (scv(i, r) != one) {
565 isPF = false;
566 break;
567 }
568 if (isPF) {
569 const T eps = num_traits<T>::from_double(1e-12);
570 for (std::size_t i = 0; i < M && isPF; ++i) {
571 T lo = L(i, 0), hi = L(i, 0);
572 for (std::size_t r = 1; r < R; ++r) {
573 if (L(i, r) < lo) lo = L(i, r);
574 if (L(i, r) > hi) hi = L(i, r);
575 }
576 if (T(hi - lo) > eps) isPF = false;
577 }
578 }
579 if (isPF) {
580 Matrix<T> Zmat(1, R);
581 for (std::size_t r = 0; r < R; ++r) Zmat(0, r) = Z[r];
582 const MvaResult<T> m = pfqn_mva(L, N, Zmat);
583 res.X = m.XN;
584 res.Q = m.QN;
585 res.U = m.UN;
586 res.C = m.CN;
587 return res;
588 }
589
590 // Coxian representation, plus the exponential reference with the same means.
591 std::vector<std::vector<MarieCoxFit<T>>> phCox(M, std::vector<MarieCoxFit<T>>(R));
592 std::vector<std::vector<MarieCoxFit<T>>> phExp(M, std::vector<MarieCoxFit<T>>(R));
593 for (std::size_t i = 0; i < M; ++i)
594 for (std::size_t r = 0; r < R; ++r) {
595 phCox[i][r] = marie_cox_fit(L(i, r), scv(i, r));
596 MarieCoxFit<T> e;
597 e.mu.push_back(T(one / L(i, r)));
598 e.phi.push_back(one);
599 phExp[i][r] = e;
600 }
601
602 res.cds.assign(M, MarieCdScaling<T>());
603 std::vector<T> Xprev(R, num_traits<T>::from_double(std::numeric_limits<double>::infinity()));
604 const T tolT = num_traits<T>::from_double(tol);
605 res.X.assign(R, zero);
606 res.Q = Matrix<T>(M, R, zero);
607 res.U = Matrix<T>(M, R, zero);
608 res.C = Matrix<T>(M, R, zero);
609 while (res.it < maxiter) {
610 ++res.it;
611 marie_amva_qd(L, N, Z, res.cds, res.X, res.Q, res.U, res.C);
612 for (std::size_t i = 0; i < M; ++i) {
613 MarieCdScaling<T> cd;
614 cd.N = N;
615 cd.muCox = marie_isol_mc(res.X, phCox[i], N);
616 cd.muExp = marie_isol_mc(res.X, phExp[i], N);
617 res.cds[i] = cd;
618 }
619 T delta = zero;
620 for (std::size_t r = 0; r < R; ++r) {
621 const T d = num_abs(T(res.X[r] - Xprev[r]));
622 if (d > delta) delta = d;
623 }
624 if (delta < tolT) break;
625 Xprev = res.X;
626 }
627 return res;
628}
629
630} // namespace detail
631
632/**
633 * Marie's method for a closed network with FCFS Coxian service.
634 *
635 * @param L (M x R) service demands, every entry strictly positive
636 * @param N (R) closed population vector
637 * @param Z (R) aggregated think times, one per class; may be empty
638 * @param scv (M x R) per-station per-class squared coefficients of
639 * variation; empty for all-exponential service
640 * @param tol convergence tolerance (reference default 1e-8)
641 * @param maxiter iteration cap (reference default 1000)
642 * @param nservers (M) server counts, single class only; empty for all single
643 * server. The reference does not support a multiserver
644 * multiclass isolation chain and neither does this port.
645 */
646template <class T>
647MarieResult<T> pfqn_marie(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
648 const Matrix<T>& scv, double tol, int maxiter,
649 const std::vector<int>& nservers) {
651 "pfqn_marie requires transcendental arithmetic: it is a fixed-point "
652 "decomposition stopped on a tolerance, and its Coxian fit needs a square root");
653
654 const std::size_t M = L.rows();
655 const std::size_t R = L.cols();
656 if (R != N.size()) throw InputError("pfqn_marie: L and N disagree on the class count");
657 if (M == 0) throw InputError("pfqn_marie: no stations");
658 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_marie: Z has the wrong length");
659 if (!scv.empty() && (scv.rows() != M || scv.cols() != R))
660 throw InputError("pfqn_marie: scv has the wrong shape");
661 if (maxiter < 1) throw InputError("pfqn_marie: maxiter must be at least one");
662
663 const T zero = num_traits<T>::from_int(0);
664 const T one = num_traits<T>::from_int(1);
665 for (std::size_t i = 0; i < M; ++i)
666 for (std::size_t r = 0; r < R; ++r)
667 if (!(L(i, r) > zero))
668 throw InputError("pfqn_marie: every service demand must be strictly positive");
669
670 Matrix<T> SCV = scv;
671 if (SCV.empty()) SCV = Matrix<T>(M, R, one);
672 std::vector<T> Zv = Z;
673 if (Zv.empty()) Zv.assign(R, zero);
674
675 if (R > 1) {
676 if (!nservers.empty())
677 for (std::size_t i = 0; i < nservers.size(); ++i)
678 if (nservers[i] != 1)
679 throw InputError(
680 "pfqn_marie: the multiclass path has no multiserver isolation chain, as in "
681 "the reference");
682 return detail::marie_multi(L, N, Zv, SCV, tol, maxiter);
683 }
684
685 // ------------------------- single class -------------------------------
686 const int Nt = N[0];
687 if (Nt < 1) throw InputError("pfqn_marie: the population must be at least one");
688 std::vector<int> ns = nservers;
689 if (ns.empty()) ns.assign(M, 1);
690 if (ns.size() == 1 && M > 1) ns.assign(M, ns[0]);
691 if (ns.size() != M) throw InputError("pfqn_marie: nservers has the wrong length");
692 for (std::size_t i = 0; i < M; ++i)
693 if (ns[i] < 1) throw InputError("pfqn_marie: the server count must be at least one");
694
695 T Ztot = zero;
696 for (std::size_t r = 0; r < Zv.size(); ++r) Ztot += Zv[r];
697 Matrix<T> Zmat(1, 1);
698 Zmat(0, 0) = Ztot;
699
700 std::vector<MarieCoxFit<T>> fit(M);
701 for (std::size_t i = 0; i < M; ++i) fit[i] = marie_cox_fit(L(i, 0), SCV(i, 0));
702
703 // Initial multipliers relative to the base rate 1/L(i): min(n, m).
704 Matrix<T> mu(M, static_cast<std::size_t>(Nt), one);
705 for (std::size_t i = 0; i < M; ++i)
706 for (int n = 1; n <= Nt; ++n)
707 mu(i, static_cast<std::size_t>(n - 1)) = num_traits<T>::from_int(n < ns[i] ? n : ns[i]);
708
709 MarieResult<T> res;
710 res.it = 0;
711 res.X.assign(1, zero);
712 res.Q = Matrix<T>(M, 1, zero);
713 res.U = Matrix<T>(M, 1, zero);
714 res.C = Matrix<T>(1, 1, zero);
715 const T tolT = num_traits<T>::from_double(tol);
716 std::vector<T> lam(static_cast<std::size_t>(Nt), zero);
717
718 while (res.it < maxiter) {
719 ++res.it;
720 const MvaLdResult<T> ld = pfqn_mvald(L, N, Zmat, mu);
721 Matrix<T> muNew = mu;
722 for (std::size_t i = 0; i < M; ++i) {
723 for (int n = 0; n < Nt; ++n) {
724 const T pn = ld.PI(i, static_cast<std::size_t>(n));
725 lam[static_cast<std::size_t>(n)] =
726 pn > zero ? T(mu(i, static_cast<std::size_t>(n)) / L(i, 0) *
727 ld.PI(i, static_cast<std::size_t>(n) + 1) / pn)
728 : zero;
729 }
730 const std::vector<T> muabs =
731 detail::marie_isol_condtput(lam, fit[i].mu, fit[i].phi, Nt, ns[i]);
732 for (int n = 0; n < Nt; ++n)
733 muNew(i, static_cast<std::size_t>(n)) = muabs[static_cast<std::size_t>(n)] * L(i, 0);
734 }
735 T delta = zero;
736 for (std::size_t i = 0; i < M; ++i)
737 for (std::size_t n = 0; n < static_cast<std::size_t>(Nt); ++n) {
738 const T d = num_abs(T(muNew(i, n) - mu(i, n)));
739 if (d > delta) delta = d;
740 }
741 mu = muNew;
742 res.X[0] = ld.XN[0];
743 for (std::size_t i = 0; i < M; ++i) {
744 res.Q(i, 0) = ld.QN(i, 0);
745 res.U(i, 0) = ld.UN[i];
746 }
747 res.C(0, 0) = ld.CN[0];
748 if (delta < tolT) break;
749 }
750 res.mu = mu;
751 return res;
752}
753
754/** Reference defaults: tol 1e-8, maxiter 1000, single server everywhere. */
755template <class T>
756MarieResult<T> pfqn_marie(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
757 const Matrix<T>& scv) {
758 return pfqn_marie(L, N, Z, scv, 1e-8, 1000, std::vector<int>());
759}
760
761} // namespace pfqn
762} // namespace line
763
764#endif // LINE_API_PFQN_MARIE_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
Steady-state distribution of a continuous-time Markov chain.
The exception types the port throws.
Dense matrix and non-owning view.
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
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
MarieResult< T > pfqn_marie(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const Matrix< T > &scv, double tol, int maxiter, const std::vector< int > &nservers)
Marie's method for a closed network with FCFS Coxian service.
Definition pfqn_marie.h:647
MarieCoxFit< T > marie_cox_fit(const T &mean, const T &scv)
Closed-form Coxian fit of a mean and an SCV (matlab/src/lang/processes/Coxian.m, fitMeanAndSCV),...
Definition pfqn_marie.h:120
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
std::vector< T > marie_cd_eval(const MarieCdScaling< T > &cd, const std::vector< T > &nv)
Evaluate a class-dependent scaling at a real-valued population vector (cdscale_eval in the reference)...
Definition pfqn_marie.h:319
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
Exact Mean Value Analysis for closed product-form networks (Reiser and Lavenberg 1980).
Exact Mean Value Analysis for mixed open/closed networks with multiserver stations.
Class-dependent scaling of one station, tabulated on the integer population box.
Definition pfqn_marie.h:245
std::vector< std::vector< T > > muCox
muCox[r][lin], lin the row-major box index
Definition pfqn_marie.h:247
std::vector< std::vector< T > > muExp
muExp[r][lin]
Definition pfqn_marie.h:248
std::vector< int > N
box bounds; the lattice is [0..N]
Definition pfqn_marie.h:246
Coxian phase representation: phase rates and per-phase completion probabilities.
Definition pfqn_marie.h:107
std::vector< T > mu
phase rates
Definition pfqn_marie.h:108
std::vector< T > phi
completion probability out of each phase, phi.back() == 1
Definition pfqn_marie.h:109
Result of pfqn_marie, mirroring the six MATLAB outputs.
Definition pfqn_marie.h:342
int it
iterations performed
Definition pfqn_marie.h:353
Matrix< T > Q
(M x R) mean queue length
Definition pfqn_marie.h:344
Matrix< T > C
Single class: (1 x 1), the CYCLE time returned by pfqn_mvald.
Definition pfqn_marie.h:352
std::vector< T > X
(R) per-class throughput
Definition pfqn_marie.h:343
std::vector< MarieCdScaling< T > > cds
R > 1: converged per-station cd scalings.
Definition pfqn_marie.h:355
Matrix< T > mu
single class: (M x N) converged multiplier lattice; empty for R > 1
Definition pfqn_marie.h:354
Matrix< T > U
(M x R) utilization
Definition pfqn_marie.h:345
Result of pfqn_mvald, mirroring the seven MATLAB outputs.
Definition pfqn_mvams.h:96
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
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