LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mva.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_MVA_SOLVER_MVA_H
6#define LINE_SOLVERS_MVA_SOLVER_MVA_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverMVA over a SolverLN layer.
12 *
13 * Ports of matlab/src/solvers/MVA/solver_mva.m (exact BCMP MVA),
14 * solver_amvald.m + solver_amvald_forward.m (the general approximate MVA), and
15 * solver_amva.m + solver_mva_analyzer.m (the method dispatch that chooses
16 * between them). The chain aggregation they all sit on is in sn_chain.h.
17 *
18 * SCOPE. The dispatch reproduces the `default` ladder in full, since it is what
19 * decides which algorithm each layer gets and therefore what the numbers are.
20 * The algorithms behind it are ported for the disciplines a layered model
21 * produces -- INF, PS, FCFS, SIRO, LCFSPR -- with the `default` and `seidmann`
22 * multiserver approximations and the `default` high-variance setting. Priority
23 * (HOL), discriminatory sharing (DPS), load- and class-dependence, the `suri`
24 * and `softmin` multiserver variants, the queue-line and fraction-line
25 * estimators and open classes are REFUSED by name. Each is a distinct
26 * approximation, and mapping any of them onto a neighbour would return a
27 * plausible number that is not the reference's.
28 *
29 * ARITHMETIC. Everything here is field arithmetic. The AMVA loops stop on a
30 * tolerance, so an exact run returns the iterate its stopping rule selected,
31 * computed without rounding; see the gate note in pfqn_egflinearizer.h.
32 */
33
35#include <algorithm>
36#include <cmath>
37#include <string>
38#include <vector>
39
67#include "line/util/error.h"
68
69namespace line {
70namespace mva {
71
72// ---------------------------------------------------------------------------
73// Exact MVA
74// ---------------------------------------------------------------------------
75
76/**
77 * Port of `solver_mva_lcfsqn.m`: the closed two-station LCFS + LCFS-PR network.
78 *
79 * `pfqn_lcfsqn_mva` returns the throughput, the queue lengths and the
80 * utilizations for the pair directly; everything else here is the mapping back
81 * onto the model's station indexing, with the response times from Little's law
82 * and the cycle time as their sum. `lG` is NaN: this recursion carries no
83 * normalizing constant, and the reference says so rather than reporting zero.
84 */
85template <class T>
87 std::size_t lcfspr_ist) {
88 const T zero = num_traits<T>::from_int(0);
89 const T one = num_traits<T>::from_int(1);
90 const std::size_t M = L.nstations, R = L.nclasses;
91
92 // A CLASS OF ZERO POPULATION NEEDS A PLACEHOLDER RATE, not a zero. The
93 // reference initializes these to zeros (solver_mva_lcfsqn.m:31-34) and gets
94 // away with it because pfqn_lcfsqn_mva.m validates nothing; the C++ api
95 // added a check that rejects a non-positive alpha for EVERY class
96 // regardless of its population (pfqn_lcfsqn_mva.h:97-99), so a closed LCFS
97 // model carrying an empty class threw where MATLAB answers. One is inert:
98 // it enters only as 1^0 in the product A = prod alpha_r^{n_r}, as
99 // alpha_r * T_r with T_r = 0 in the throughput sum, and as U = T*alpha = 0.
100 std::vector<T> alpha(R, one), beta(R, one);
101 std::vector<int> N(R, 0);
102 for (std::size_t r = 0; r < R; ++r) {
103 const double nr = L.classes[r].population;
104 if (!(nr > 0.0)) continue;
105 N[r] = static_cast<int>(std::llround(nr));
106 const T mu_l = L.rates(lcfs_ist - 1, r);
107 const T mu_p = L.rates(lcfspr_ist - 1, r);
108 if (!(mu_l > zero) || !std::isfinite(num_traits<T>::to_double(mu_l)))
109 throw InputError("solver_mva_lcfsqn: invalid service rate at the LCFS station for class " +
110 std::to_string(r + 1));
111 if (!(mu_p > zero) || !std::isfinite(num_traits<T>::to_double(mu_p)))
112 throw InputError("solver_mva_lcfsqn: invalid service rate at the LCFS-PR station for "
113 "class " + std::to_string(r + 1));
114 alpha[r] = T(one / mu_l);
115 beta[r] = T(one / mu_p);
116 }
117
118 const pfqn::LcfsMvaResult<T> res = pfqn::pfqn_lcfsqn_mva(alpha, beta, N);
119
120 MvaSolution<T> out;
121 out.Q = Matrix<T>(M, R, zero);
122 out.U = Matrix<T>(M, R, zero);
123 out.R = Matrix<T>(M, R, zero);
124 out.Tp = Matrix<T>(M, R, zero);
125 out.C.assign(R, zero);
126 out.X.assign(R, zero);
127 for (std::size_t r = 0; r < R; ++r) {
128 out.Q(lcfs_ist - 1, r) = res.Q(0, r);
129 out.Q(lcfspr_ist - 1, r) = res.Q(1, r);
130 out.U(lcfs_ist - 1, r) = res.U(0, r);
131 out.U(lcfspr_ist - 1, r) = res.U(1, r);
132 if (N[r] <= 0) continue;
133 out.X[r] = res.T_[r];
134 out.Tp(lcfs_ist - 1, r) = res.T_[r];
135 out.Tp(lcfspr_ist - 1, r) = res.T_[r];
136 }
137 for (std::size_t i : {lcfs_ist - 1, lcfspr_ist - 1})
138 for (std::size_t r = 0; r < R; ++r)
139 if (out.Tp(i, r) > zero) out.R(i, r) = T(out.Q(i, r) / out.Tp(i, r));
140 for (std::size_t r = 0; r < R; ++r)
141 if (N[r] > 0) out.C[r] = T(out.R(lcfs_ist - 1, r) + out.R(lcfspr_ist - 1, r));
142 // REPORTED AS 'exact', not as 'lcfsqn'. `solver_mva_analyzer` names the
143 // method AFTER solver_mva returns, so the reference reports every internal
144 // branch of it under the one name; a caller that saw 'lcfsqn' here would
145 // disagree with MATLAB on `actualmethod` while agreeing on every number.
146 out.method = "exact";
147 out.lG = std::numeric_limits<double>::quiet_NaN();
148 return out;
149}
150
151/** Port of solver_mva.m, closed and mixed product-form networks. */
152template <class T>
154 using qn::SchedStrategy;
155 const T zero = num_traits<T>::from_int(0);
156 const std::size_t M = L.nstations, C = L.nchains;
157
158 // The special-cased LCFS + LCFS-PR pair, BEFORE the product-form test: an
159 // LCFS station is not product-form on its own, and the reference reaches
160 // this branch on a model the generic path would reject.
161 std::vector<std::size_t> lcfs, lcfspr; // 1-based station indices
162 for (std::size_t i = 0; i < M; ++i) {
163 if (L.stations[i].sched == SchedStrategy::LCFS) lcfs.push_back(i + 1);
164 if (L.stations[i].sched == SchedStrategy::LCFSPR) lcfspr.push_back(i + 1);
165 }
166 if (!lcfs.empty() && !lcfspr.empty()) {
167 if (lcfs.size() != 1 || lcfspr.size() != 1)
168 throw UnsupportedError(
169 "solver_mva: LCFS MVA requires exactly one LCFS and one LCFS-PR station");
170 for (std::size_t c = 0; c < C; ++c)
171 if (std::isinf(d.Nchain[c]))
172 throw UnsupportedError("solver_mva: LCFS MVA requires a closed queueing network");
173 // A self-loop would let a job re-enter the station it just left, which
174 // the two-station recursion has no term for.
175 const std::size_t S = L.nof_stateful(), Rn = L.nclasses;
176 for (std::size_t ist : {lcfs[0], lcfspr[0]}) {
177 const std::size_t sf = L.stateful_of_station(ist) - 1;
178 for (std::size_t r = 0; r < Rn; ++r)
179 if (L.rt.rows() == S * Rn && L.rt(sf * Rn + r, sf * Rn + r) > zero)
180 throw UnsupportedError(
181 "solver_mva: LCFS MVA does not support self-loops at stations");
182 }
183 return solver_mva_lcfsqn(L, lcfs[0], lcfspr[0]);
184 }
185 if (!lcfs.empty())
186 throw UnsupportedError("solver_mva: LCFS scheduling requires a paired LCFS-PR station");
187
188 // METHOD 'mva' IS THE DELIBERATE APPROXIMATION: the dispatch warns that the
189 // exact recursion is being run outside its hypotheses and promises an answer,
190 // so throwing here would contradict its own message. Only an implicit or
191 // 'exact' request is refused.
192 if (!L.has_product_form() && opt.method != "mva")
193 throw UnsupportedError("solver_mva: the layer does not have a product form");
194
195 std::vector<std::size_t> infSET, qSET; // 0-based station indices
196 for (std::size_t i = 0; i < M; ++i) {
197 switch (L.stations[i].sched) {
198 case SchedStrategy::EXT: break;
199 case SchedStrategy::INF: infSET.push_back(i); break;
200 case SchedStrategy::PS:
201 case SchedStrategy::LCFSPR:
202 case SchedStrategy::FCFS:
203 case SchedStrategy::SIRO: qSET.push_back(i); break;
204 default:
205 throw UnsupportedError(std::string("solver_mva: unsupported exact MVA analysis for ") +
206 lang::sched_to_text(L.stations[i].sched) + " scheduling");
207 }
208 }
209
210 // demands at the queueing and delay stations, chain by chain
211 Matrix<T> Lq(qSET.size(), C, zero), Zd(infSET.size(), C, zero);
212 for (std::size_t a = 0; a < qSET.size(); ++a)
213 for (std::size_t c = 0; c < C; ++c)
214 Lq(a, c) = T(d.STchain(qSET[a], c) * d.Vchain(qSET[a], c));
215 for (std::size_t a = 0; a < infSET.size(); ++a)
216 for (std::size_t c = 0; c < C; ++c)
217 Zd(a, c) = T(d.STchain(infSET[a], c) * d.Vchain(infSET[a], c));
218
219 // open-chain arrival-rate rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
220 std::vector<T> lambda(C, zero);
221 std::vector<int> N(C, 0);
222 for (std::size_t c = 0; c < C; ++c) {
223 if (!std::isfinite(d.Nchain[c])) {
224 N[c] = pfqn::OPEN_CLASS;
225 const T st = d.STchain(d.refstatchain[c] - 1, c);
226 lambda[c] = st > zero ? T(num_traits<T>::from_int(1) / st) : zero;
227 continue;
228 }
229 N[c] = static_cast<int>(std::llround(d.Nchain[c]));
230 }
231 std::vector<int> S;
232 for (std::size_t a = 0; a < qSET.size(); ++a) {
233 const double s = L.stations[qSET[a]].nservers;
234 if (!std::isfinite(s))
235 throw UnsupportedError("solver_mva: a queueing station has infinitely many servers but "
236 "is not inf-scheduled");
237 S.push_back(static_cast<int>(std::llround(s)));
238 }
239
240 // Interlocked flow (Franks 1999, Eq. 4.7): a request cannot queue behind work that
241 // its own submission caused, so the arrival-instant queue drops the interlocked share
242 // of the other chains. SolverLN supplies the matrix, class-indexed.
243 const Matrix<T> IL = sn_interlock_chain<T>(L, opt.interlock);
244 // the interlocked recursion is a separate entry point: pfqn_mvams and the pfqn_mva
245 // family it dispatches to carry the standard arrival theorem only
246 const pfqn::MvaResult<T> pf =
247 IL.empty() ? pfqn::pfqn_mvams(lambda, Lq, N, Zd, std::vector<int>(qSET.size(), 1), S)
248 : pfqn::pfqn_mvams_ilock(lambda, Lq, N, Zd, std::vector<int>(qSET.size(), 1), S, IL);
249
250 Matrix<T> Qchain(M, C, zero), Wchain(M, C, zero), Tchain(M, C, zero), Uchain(M, C, zero);
251 std::vector<T> Xchain = pf.XN;
252 for (std::size_t a = 0; a < qSET.size(); ++a)
253 for (std::size_t c = 0; c < C; ++c) Qchain(qSET[a], c) = pf.QN(a, c);
254 for (std::size_t a = 0; a < infSET.size(); ++a)
255 for (std::size_t c = 0; c < C; ++c)
256 Qchain(infSET[a], c) = T(Xchain[c] * d.STchain(infSET[a], c) * d.Vchain(infSET[a], c));
257
258 std::vector<std::size_t> rset;
259 for (std::size_t c = 0; c < C; ++c)
260 if (d.Nchain[c] != 0.0) rset.push_back(c);
261
262 for (std::size_t c : rset) {
263 for (std::size_t i : infSET) Wchain(i, c) = d.STchain(i, c);
264 for (std::size_t i : qSET) {
265 if (std::isinf(L.stations[i].nservers)) {
266 Wchain(i, c) = d.STchain(i, c);
267 } else if (d.Vchain(i, c) == zero || Xchain[c] == zero) {
268 Wchain(i, c) = zero;
269 } else {
270 Wchain(i, c) = T(Qchain(i, c) / (Xchain[c] * d.Vchain(i, c)));
271 }
272 }
273 }
274
275 std::vector<T> Cc(C, zero);
276 for (std::size_t c : rset) {
277 T sw = zero;
278 for (std::size_t i = 0; i < M; ++i) sw += Wchain(i, c);
279 if (sw == zero) {
280 Xchain[c] = zero;
281 } else {
282 T cyc = zero;
283 for (std::size_t i = 0; i < M; ++i) cyc += d.Vchain(i, c) * Wchain(i, c);
284 Cc[c] = cyc;
285 // open-chain throughput rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
286 if (cyc != zero && std::isfinite(d.Nchain[c]))
287 Xchain[c] = T(num_traits<T>::from_double(d.Nchain[c]) / cyc);
288 }
289 for (std::size_t i = 0; i < M; ++i) {
290 Qchain(i, c) = T(Xchain[c] * d.Vchain(i, c) * Wchain(i, c));
291 Tchain(i, c) = T(Xchain[c] * d.Vchain(i, c));
292 }
293 }
294 for (std::size_t i = 0; i < M; ++i)
295 for (std::size_t c : rset) {
296 const T u = T(d.Vchain(i, c) * d.STchain(i, c) * Xchain[c]);
297 Uchain(i, c) = std::isinf(L.stations[i].nservers)
298 ? u
299 : T(u / num_traits<T>::from_double(L.stations[i].nservers));
300 }
301
302 // renormalise a utilization that the product-form formula pushed past one
303 for (std::size_t i = 0; i < M; ++i) {
304 if (!(L.stations[i].sched == SchedStrategy::FCFS || L.stations[i].sched == SchedStrategy::PS))
305 continue;
306 T usum = zero;
307 for (std::size_t c = 0; c < C; ++c) usum += Uchain(i, c);
308 if (num_traits<T>::to_double(usum) <= 1.0 + opt.tol) continue;
309 T den = zero;
310 for (std::size_t c = 0; c < C; ++c) den += d.Vchain(i, c) * d.STchain(i, c) * Xchain[c];
311 if (den == zero) continue;
312 const T cap = usum > num_traits<T>::from_int(1) ? num_traits<T>::from_int(1) : usum;
313 for (std::size_t c = 0; c < C; ++c) {
314 if (!(num_traits<T>::to_double(d.Vchain(i, c) * d.STchain(i, c)) > opt.tol)) continue;
315 Uchain(i, c) = T(cap * d.Vchain(i, c) * d.STchain(i, c) * Xchain[c] / den);
316 }
317 }
318
319 Matrix<T> Rchain(M, C, zero);
320 for (std::size_t i = 0; i < M; ++i)
321 for (std::size_t c = 0; c < C; ++c)
322 if (Tchain(i, c) != zero) Rchain(i, c) = T(Qchain(i, c) / Tchain(i, c));
323 for (std::size_t c = 0; c < C; ++c) {
324 if (d.Nchain[c] != 0.0) continue;
325 Xchain[c] = zero;
326 for (std::size_t i = 0; i < M; ++i) {
327 Uchain(i, c) = zero;
328 Qchain(i, c) = zero;
329 Rchain(i, c) = zero;
330 Tchain(i, c) = zero;
331 Wchain(i, c) = zero;
332 }
333 }
334
336 L, d, Matrix<T>(), Matrix<T>(), Rchain, Tchain, Xchain);
337 MvaSolution<T> out;
338 out.Q = cr.Q;
339 out.U = cr.U;
340 out.R = cr.R;
341 out.Tp = cr.Tp;
342 out.C = cr.C;
343 out.X = cr.X;
344 out.method = "exact";
345 out.lG = pf.lG;
346 return out;
347}
348
349// ---------------------------------------------------------------------------
350// Approximate MVA, the general (chain-level) path
351// ---------------------------------------------------------------------------
352
353namespace detail {
354
355/**
356 * The multiserver capacity term of solver_amvald_forward, all four rules.
357 *
358 * softmin 1/softmin(n_i, c_i, 20) at EVERY station
359 * seidmann 1/c_i, and 1 at an infinite server
360 * default softmin, then the FCFS-family stations overridden with 1/c_i
361 * suri 1 everywhere; the multiplicity is carried by the Suri factor
362 * applied to the queue length inside the residence formula
363 *
364 * softmin is a genuine transcendental (exp), so the exact backend can evaluate
365 * this only where softmin is not reached: seidmann and suri everywhere, and
366 * default at an infinite server or an FCFS-family station. Anything else
367 * refuses by name.
368 */
369template <class T>
370std::vector<T> ms_term(const qn::NetworkStruct<T>& L, const std::vector<T>& narrival,
371 const std::string& multiserver) {
372 using qn::SchedStrategy;
373 const std::size_t M = L.nstations;
374 const T one = num_traits<T>::from_int(1);
375 std::vector<T> r(M, one);
376 if (multiserver == "suri") return r; // no demand scaling; see suri_factor
377 if (!(multiserver == "default" || multiserver == "seidmann" || multiserver == "softmin"))
378 throw UnsupportedError("solver_amvald: multiserver approximation '" + multiserver +
379 "' is not implemented in this port");
380 for (std::size_t i = 0; i < M; ++i) {
381 const double c = L.stations[i].nservers;
382 const SchedStrategy sc = L.stations[i].sched;
383 const bool fcfs_family =
384 sc == SchedStrategy::FCFS || sc == SchedStrategy::SIRO || sc == SchedStrategy::LCFSPR;
385 if (multiserver == "seidmann" || (multiserver == "default" && fcfs_family)) {
386 r[i] = std::isinf(c) ? one : T(one / num_traits<T>::from_double(c));
387 continue;
388 }
389 if (std::isinf(c)) {
390 r[i] = one; // delay server, pfqn_lldfun returns 1
391 continue;
392 }
393 if constexpr (num_traits<T>::has_transcendental) {
394 // 1/softmin(n, c, alpha), in the reference's numerically safe form
395 const double alpha = 20.0;
396 const double n = num_traits<T>::to_double(narrival[i]);
397 const double lo = std::min(n, c), hi = std::max(n, c);
398 const double gap = hi - lo;
399 const double w = alpha * gap > 745.0 ? 0.0 : std::exp(-alpha * gap);
400 const double sm = lo + gap * w / (1.0 + w);
401 r[i] = num_traits<T>::from_double(sm > 0.0 ? 1.0 / sm : 1.0 / std::min(n, c));
402 } else {
403 throw UnsupportedError(
404 "solver_amvald: the soft-minimum multiserver term evaluates exp, which exact "
405 "arithmetic has no representation for; use the double or real backend, the "
406 "seidmann or suri multiserver rule, or a layer whose queueing stations are "
407 "FCFS-family or infinite-server");
408 }
409 }
410 return r;
411}
412
413/**
414 * The Suri multiserver factor, applied to the arrival queue length rather than
415 * to the demand: rho^(4.464 (c^0.676 - 1)) / c at a finite multiserver station,
416 * 1/c when its utilization is zero, and 0 at an infinite or zero server.
417 *
418 * The utilization it reads is the ITERATE's, so the factor moves with the fixed
419 * point; the constants are the reference's fitted ones.
420 */
421template <class T>
422std::vector<T> suri_factor(const qn::NetworkStruct<T>& L, const Matrix<T>& Uin, double tol) {
423 const std::size_t M = L.nstations, K = Uin.cols();
424 const T one = num_traits<T>::from_int(1), zero = num_traits<T>::from_int(0);
425 std::vector<T> f(M, one);
426 if constexpr (!num_traits<T>::has_transcendental) {
427 throw UnsupportedError(
428 "solver_amvald: the Suri multiserver factor is a real power of the utilization, "
429 "which exact arithmetic has no representation for; use the double or real backend");
430 } else {
431 const double alpha = 4.464, beta = 0.676;
432 for (std::size_t i = 0; i < M; ++i) {
433 const double c = L.stations[i].nservers;
434 if (std::isinf(c) || c == 0.0) {
435 f[i] = zero;
436 continue;
437 }
438 if (!(c > 1.0)) continue; // single server: the factor stays 1
439 T usum = zero;
440 for (std::size_t s = 0; s < K; ++s) usum += Uin(i, s);
441 double rho = num_traits<T>::to_double(usum) / c;
442 if (rho > 1.0 - tol) rho = 1.0 - tol;
443 f[i] = rho > 0.0 ? num_traits<T>::from_double(
444 std::pow(rho, alpha * (std::pow(c, beta) - 1.0)) / c)
445 : T(one / num_traits<T>::from_double(c));
446 }
447 return f;
448 }
449}
450
451} // namespace detail
452
453/**
454 * Port of solver_amvald.m together with solver_amvald_forward.m, restricted to
455 * the INF / PS / FCFS-family disciplines with no load or class dependence.
456 */
457template <class T>
459 const MvaOptions& opt, const std::string& method, bool& converged,
460 const Matrix<T>& init_sol = Matrix<T>()) {
461 using qn::SchedStrategy;
462 // EXACT ARITHMETIC HAS NO BOUNDED ITERATE HERE, which is why this refuses
463 // rather than runs slowly. The exact product-form recursion is a fixed
464 // number of field operations and stays rational; this is a Picard fixed
465 // point, and every sweep multiplies the denominators of the one before it,
466 // so the bit length grows geometrically in the iteration count. A layer of
467 // lqn_ofbiz that solves in milliseconds under `double` was measured at over
468 // four and a half hours under `Rational`, with operands millions of bits
469 // wide and no answer -- and it only reaches here at all because the model
470 // failed the product-form gate. Refusing by name is the same rule the
471 // load-dependent soft minimum and the Suri factor follow below.
472 if constexpr (!num_traits<T>::has_transcendental) {
473 throw UnsupportedError(
474 "solver_amvald: the approximate MVA is an iterative fixed point whose iterates "
475 "accumulate the product of every denominator seen so far, so exact rational "
476 "arithmetic grows without bound and the solve does not terminate; rerun with "
477 "--arith double or --arith real, or give the model a product form (BCMP type 1 "
478 "asks FCFS service to be exponential) so the exact recursion applies");
479 }
480 const T zero = num_traits<T>::from_int(0);
481 const T one = num_traits<T>::from_int(1);
482 const std::size_t M = L.nstations, K = L.nchains;
483
484 for (std::size_t i = 0; i < M; ++i) {
485 const SchedStrategy s = L.stations[i].sched;
486 if (!(s == SchedStrategy::INF || s == SchedStrategy::PS || s == SchedStrategy::FCFS ||
487 s == SchedStrategy::SIRO || s == SchedStrategy::LCFSPR || s == SchedStrategy::EXT ||
488 s == SchedStrategy::DPS || s == SchedStrategy::HOL ||
489 s == SchedStrategy::FCFSPRPRIO))
490 throw UnsupportedError(std::string("solver_amvald: ") + lang::sched_to_text(s) +
491 " scheduling is not implemented in this port");
492 }
493 // A PURE-DELAY MODEL SELECTS NO APPROXIMATION, so the name is not asked
494 // about there. With no queueing station the arrival-instant queue length is
495 // identically zero, every AMVA scheme collapses to the same exact delay
496 // solution, and this analyzer is where `solver_amva` sends that model
497 // BECAUSE the product-form kernels would be handed a zero-row demand matrix
498 // (solver_amva.m says so in as many words). Asking the whitelist first
499 // refused twelve names -- ab, schmidt, schmidt-ext, scat, lcp, chow, pamb,
500 // pami, pamt, clust, dmlin and qsa -- on a model that needs no arm for any
501 // of them, while the report went on offering all twelve.
502 bool any_queue = false;
503 for (std::size_t i = 0; i < M && !any_queue; ++i)
504 if (L.stations[i].sched != SchedStrategy::INF && L.stations[i].sched != SchedStrategy::EXT)
505 any_queue = true;
506 const bool linmethod = (method == "lin" || method == "qdlin");
507 if (any_queue &&
508 !(linmethod || method == "qd" || method == "default" || method == "bs" ||
509 method == "egflin" || method == "gflin" || method == "qli" || method == "fli" ||
510 method == "aql" || method == "qdaql" || method == "tay" || method == "priomva"))
511 throw UnsupportedError("solver_amvald: method '" + method + "' is not implemented");
512
513 // Nt closed-population-only rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
514 double Nt = 0.0;
515 for (std::size_t c = 0; c < K; ++c)
516 if (std::isfinite(d.Nchain[c])) Nt += d.Nchain[c];
517 const T deltaT = Nt > 0.0 ? num_traits<T>::from_double((Nt - 1.0) / Nt) : one;
518 // deltaclass open-chain rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
519 std::vector<T> deltaclass(K, one);
520 for (std::size_t c = 0; c < K; ++c)
521 if (std::isfinite(d.Nchain[c]) && d.Nchain[c] > 0.0)
522 deltaclass[c] = num_traits<T>::from_double((d.Nchain[c] - 1.0) / d.Nchain[c]);
523
524 // nnz/ccl/ocl rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
525 std::vector<std::size_t> nnz, ccl, ocl;
526 std::vector<bool> isopen(K, false);
527 for (std::size_t c = 0; c < K; ++c) {
528 if (!(d.Nchain[c] > 0.0)) continue;
529 nnz.push_back(c);
530 if (std::isfinite(d.Nchain[c])) ccl.push_back(c);
531 else { ocl.push_back(c); isopen[c] = true; }
532 }
533
534 // warm-start-under-outer-fixed-point rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
535 Matrix<T> Qchain(M, K, zero);
536 if (init_sol.rows() == M && init_sol.cols() == K) {
537 Qchain = init_sol;
538 } else {
539 // open-chain warm-start zeroing rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
540 for (std::size_t c = 0; c < K; ++c) {
541 if (!std::isfinite(d.Nchain[c])) continue;
542 for (std::size_t i = 0; i < M; ++i)
543 Qchain(i, c) =
545 }
546 }
547
548 std::vector<T> Xchain(K, zero);
549 for (std::size_t c = 0; c < K; ++c) {
550 if (isopen[c]) {
551 // open-chain arrival-rate rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
552 const T st = d.STchain(d.refstatchain[c] - 1, c);
553 Xchain[c] = st > zero ? T(one / st) : zero;
554 continue;
555 }
556 T s = zero;
557 for (std::size_t i = 0; i < M; ++i) s += d.STchain(i, c);
558 if (s != zero) Xchain[c] = T(one / s);
559 }
560 Matrix<T> Uchain(M, K, zero), Tchain(M, K, zero), Wchain(M, K, zero), STeff = d.STchain;
561 for (std::size_t i = 0; i < M; ++i)
562 for (std::size_t c : nnz) {
563 const T u = T(d.Vchain(i, c) * d.STchain(i, c) * Xchain[c]);
564 Uchain(i, c) = std::isinf(L.stations[i].nservers)
565 ? u
566 : T(u / num_traits<T>::from_double(L.stations[i].nservers));
567 }
568
569 // gamma(s, i, r) for 'lin'; gamma(s, i) otherwise
570 std::vector<Matrix<T>> gamma(K, Matrix<T>(M, K, zero));
571
572 const T omicron = num_traits<T>::from_double(0.5);
573 int totiter = 0;
574 const int max_totiter = std::min(opt.iter_max, 10000);
575 line::util::LineConsole::loop("running the AMVA fixed point (tolerance %g)", opt.tol);
576 const int inner_cap = static_cast<int>(std::sqrt(static_cast<double>(opt.iter_max)));
577
578 // priority set rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
579 const bool has_prio = [&] {
580 int lo = 0, hi = 0;
581 bool first = true;
582 for (std::size_t c = 0; c < K; ++c) {
583 const int p = L.classes.empty() ? 0 : L.classes[c].prio;
584 if (first) { lo = hi = p; first = false; }
585 lo = std::min(lo, p);
586 hi = std::max(hi, p);
587 }
588 return hi != lo;
589 }();
590 std::vector<std::vector<std::size_t>> ehprio(K), hprio(K), eprio(K), lprio(K);
591 for (std::size_t r : nnz) {
592 if (!has_prio) {
593 // every class ties, so HOL degenerates to FCFS: all classes are equal-or-higher
594 for (std::size_t s : nnz) {
595 ehprio[r].push_back(s);
596 eprio[r].push_back(s);
597 }
598 continue;
599 }
600 for (std::size_t s : nnz) {
601 const int pr = L.classes[r].prio, ps = L.classes[s].prio;
602 if (ps <= pr) ehprio[r].push_back(s);
603 if (ps < pr) hprio[r].push_back(s);
604 if (ps == pr) eprio[r].push_back(s);
605 if (ps > pr) lprio[r].push_back(s);
606 }
607 }
608
609 // lldscaling/cdscaling emptiness rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
610 std::size_t smax = 0;
611 for (const auto& st : L.stations) smax = std::max(smax, st.lldscaling.size());
612 Matrix<T> lldscaling;
613 if (smax > 0) {
614 lldscaling = Matrix<T>(M, smax, one);
615 for (std::size_t i = 0; i < M; ++i)
616 for (std::size_t k = 0; k < L.stations[i].lldscaling.size(); ++k)
617 lldscaling(i, k) = L.stations[i].lldscaling[k];
618 }
619 std::vector<lang::CdScaling<T>> cdscaling;
620 for (const auto& st : L.stations)
621 if (st.cdscaling) {
622 cdscaling.assign(M, lang::CdScaling<T>());
623 break;
624 }
625 if (!cdscaling.empty())
626 for (std::size_t i = 0; i < M; ++i) cdscaling[i] = L.stations[i].cdscaling;
627 // sn.jdscaling is a SEPARATE list, never folded into cdscaling: the two are
628 // evaluated at DIFFERENT points below, so a single list would lose the one
629 // piece of information that tells them apart. Assembled by the same rule --
630 // empty means "no station has one", which is what pfqn_jdfun tests.
631 std::vector<lang::CdScaling<T>> jdscaling;
632 for (const auto& st : L.stations)
633 if (st.jdscaling) {
634 jdscaling.assign(M, lang::CdScaling<T>());
635 break;
636 }
637 if (!jdscaling.empty())
638 for (std::size_t i = 0; i < M; ++i) jdscaling[i] = L.stations[i].jdscaling;
639 const bool has_scaling = (smax > 0) || !cdscaling.empty() || !jdscaling.empty();
640
641 // tau(s,r) rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
642 std::vector<std::vector<T>> tau(K, std::vector<T>(K, zero));
643 // Throughput vector the tau differences were taken against, so that Xref[r] + tau[s][r]
644 // is an arrival-instant throughput from one and the same sweep. Adding tau to the moving
645 // inner iterate instead mixes two sweeps and can exceed the service capacity. Empty when
646 // no Linearizer recursion runs, and then the inner iterate is the reference.
647 std::vector<T> Xref;
648
649 // Interlocked flow (Franks 1999, Eq. 4.7): the option carries the matrix CLASS-indexed,
650 // and the forward step below works in the chain basis.
651 const Matrix<T> ILchain = sn_interlock_chain<T>(L, opt.interlock);
652
653 // one forward evaluation: residence times from the current queue lengths
654 auto forward = [&](const Matrix<T>& Qin, const std::vector<T>& Xin, const Matrix<T>& Uin,
655 const std::vector<double>& Nc, Matrix<T>& Wout, Matrix<T>& STeffOut) {
656 double Ntl = 0.0;
657 for (std::size_t c = 0; c < K; ++c)
658 if (std::isfinite(Nc[c])) Ntl += Nc[c];
659 const T deltaL = Ntl > 0.0 ? num_traits<T>::from_double((Ntl - 1.0) / Ntl) : one;
660 std::vector<T> dcl(K, one);
661 for (std::size_t c = 0; c < K; ++c)
662 if (std::isfinite(Nc[c]) && Nc[c] > 0.0)
663 dcl[c] = num_traits<T>::from_double((Nc[c] - 1.0) / Nc[c]);
664
665 std::vector<T> interpTot(M, zero);
666 Matrix<T> selfArvl(M, K, zero), totArvl(M, K, zero), totArvlOpen(M, K, zero);
667 for (std::size_t i = 0; i < M; ++i) {
668 T s = zero;
669 for (std::size_t c : nnz) s += Qin(i, c);
670 interpTot[i] = T(deltaL * s);
671 const bool hol = L.stations[i].sched == SchedStrategy::HOL;
672 for (std::size_t c : nnz) {
673 selfArvl(i, c) = T(dcl[c] * Qin(i, c));
674 if (hol) {
675 T eh = zero, ehx = zero;
676 for (std::size_t s2 : ehprio[c]) {
677 eh += Qin(i, s2);
678 if (s2 != c) ehx += Qin(i, s2);
679 }
680 totArvlOpen(i, c) = eh;
681 totArvl(i, c) = T(dcl[c] * Qin(i, c) + ehx);
682 } else {
683 totArvlOpen(i, c) = s;
684 totArvl(i, c) = T(dcl[c] * Qin(i, c) + s - Qin(i, c));
685 }
686 }
687 }
688
689 // multiserver softmin population rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
690 std::vector<T> gmean(M, zero);
691 if (!ccl.empty()) {
692 if (linmethod) {
693 // g(s,i) = sum_r deltaL * N_r * gamma(s,i,r); then mean over s
694 const T nccl = num_traits<T>::from_int(int(ccl.size()));
695 for (std::size_t i = 0; i < M; ++i) {
696 T acc = zero;
697 for (std::size_t s : ccl) {
698 T gs = zero;
699 for (std::size_t r : ccl)
700 gs += T(num_traits<T>::from_double(Nc[r]) * gamma[s](i, r));
701 acc += T(deltaL * gs);
702 }
703 gmean[i] = T(acc / nccl);
704 }
705 } else {
706 // mean(g) scalar-collapse rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
707 T acc = zero;
708 for (std::size_t i = 0; i < M; ++i)
709 for (std::size_t r : ccl)
710 acc += T(num_traits<T>::from_double(Ntl - 1.0) * gamma[r](i, r));
711 const T sc = T(acc / num_traits<T>::from_int(int(M)));
712 for (std::size_t i = 0; i < M; ++i) gmean[i] = sc;
713 }
714 }
715 std::vector<T> narrival(M, zero);
716 for (std::size_t i = 0; i < M; ++i) narrival[i] = T(one + interpTot[i] + gmean[i]);
717 const std::vector<T> msterm = detail::ms_term(L, narrival, opt.multiserver);
718 std::vector<T> suri(M, one);
719 if (opt.multiserver == "suri") suri = detail::suri_factor(L, Uin, opt.tol);
720
721 // The load-dependent term, per class for the Linearizer (whose
722 // correction is class specific) and shared otherwise.
723 Matrix<T> lldterm(M, K, one);
724 if (smax > 0) {
725 if constexpr (!num_traits<T>::has_transcendental) {
726 throw UnsupportedError(
727 "solver_amvald: load-dependent scaling interpolates a rate lattice with a "
728 "soft minimum, which exact arithmetic has no representation for; use the "
729 "double or real backend");
730 } else {
731 if (linmethod && !ccl.empty() && !nnz.empty()) {
732 for (std::size_t r : nnz) {
733 std::vector<T> arg(M, zero);
734 for (std::size_t i = 0; i < M; ++i) {
735 T gcorr = zero;
736 for (std::size_t s : ccl)
737 gcorr += T(num_traits<T>::from_double(Nc[s]) * gamma[r](i, s));
738 gcorr -= gamma[r](i, r);
739 arg[i] = T(one + interpTot[i] + gcorr);
740 }
741 const std::vector<T> v =
742 pfqn::pfqn_lldfun(arg, lldscaling, std::vector<double>());
743 for (std::size_t i = 0; i < M; ++i) lldterm(i, r) = v[i];
744 }
745 } else {
746 std::vector<T> arg(M, zero);
747 for (std::size_t i = 0; i < M; ++i) arg[i] = T(one + interpTot[i]);
748 const std::vector<T> v =
749 pfqn::pfqn_lldfun(arg, lldscaling, std::vector<double>());
750 for (std::size_t i = 0; i < M; ++i)
751 for (std::size_t c = 0; c < K; ++c) lldterm(i, c) = v[i];
752 }
753 }
754 }
755
756 // class-dependent term rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
757 Matrix<T> cdterm(M, K, one);
758 if (!cdscaling.empty()) {
759 for (std::size_t r : nnz) {
760 Matrix<T> arg(M, K, zero);
761 const bool closed = std::isfinite(Nc[r]);
762 for (std::size_t i = 0; i < M; ++i)
763 for (std::size_t c = 0; c < K; ++c)
764 arg(i, c) = T(one + (closed ? selfArvl(i, c) : Qin(i, c)));
765 if (closed && linmethod)
766 for (std::size_t i = 0; i < M; ++i) {
767 const T gself =
768 T(num_traits<T>::from_double(Nc[r] - 1.0) * gamma[r](i, r));
769 for (std::size_t c = 0; c < K; ++c) arg(i, c) = T(arg(i, c) + gself);
770 }
771 const std::vector<T> v = pfqn::pfqn_cdfun(arg, cdscaling, r);
772 for (std::size_t i = 0; i < M; ++i) cdterm(i, r) = v[i];
773 }
774 }
775
776 // joint-dependence term eta_i (non-product-form). It is NOT evaluated at
777 // the cdterm point: beta_{i,r} reads only its OWN marginal, so the +1
778 // cdterm puts on the non-arriving classes is inert there, while eta reads
779 // the WHOLE occupancy row and every coordinate matters. The arrival
780 // theorem gives the arriving class-r job one extra job OF ITS OWN CLASS
781 // and leaves the others at their means, so the point is
782 // eta_i(Q_{i,1}, ..., 1 + delta_r Q_{i,r}, ..., Q_{i,R}),
783 // only coordinate r shifted. Classes outside nnz stay at zero, matching
784 // the stationaryQlen of solver_amvald_forward.m rather than Qin, whose
785 // rows for an empty class are not part of that point.
786 // See _kb/06-solver-catalog.md (joint-dependence section).
787 Matrix<T> jdterm(M, K, one);
788 if (!jdscaling.empty()) {
789 for (std::size_t r : nnz) {
790 const bool closed = std::isfinite(Nc[r]);
791 Matrix<T> arg(M, K, zero);
792 for (std::size_t i = 0; i < M; ++i) {
793 for (std::size_t c : nnz) arg(i, c) = Qin(i, c);
794 T self = T(one + (closed ? selfArvl(i, r) : Qin(i, r)));
795 if (closed && linmethod)
796 self = T(self + num_traits<T>::from_double(Nc[r] - 1.0) * gamma[r](i, r));
797 arg(i, r) = self;
798 }
799 const std::vector<T> v = pfqn::pfqn_jdfun(arg, jdscaling, r);
800 for (std::size_t i = 0; i < M; ++i) jdterm(i, r) = v[i];
801 }
802 }
803
804 STeffOut = Matrix<T>(M, K, zero);
805 for (std::size_t c : nnz)
806 for (std::size_t i = 0; i < M; ++i)
807 STeffOut(i, c) =
808 T(d.STchain(i, c) * lldterm(i, c) * msterm[i] * cdterm(i, c) * jdterm(i, c));
809
810 // Wang-Sevcik queue line / fraction line: both REPLACE the arrival queue
811 // length with an interpolation that needs STeff, so they run after it.
812 if (method == "qli" || method == "fli") {
813 const bool qli = (method == "qli");
814 for (std::size_t i = 0; i < M; ++i) {
815 const bool hol = L.stations[i].sched == SchedStrategy::HOL;
816 for (std::size_t r : nnz) {
817 T tot = zero;
818 if (hol) {
819 for (std::size_t s2 : ehprio[r]) tot += Qin(i, s2);
820 } else {
821 for (std::size_t s2 : nnz) tot += Qin(i, s2);
822 }
823 if (Nc[r] == 1.0) {
824 totArvl(i, r) = T(tot - Qin(i, r));
825 continue;
826 }
827 const T num = T(STeffOut(i, r) * (one + tot - Qin(i, r)));
828 T den = zero;
829 for (std::size_t m = 0; m < M; ++m)
830 if (L.stations[m].sched == SchedStrategy::INF) den += STeffOut(m, r);
831 for (std::size_t m = 0; m < M; ++m) {
832 T totm = zero;
833 if (L.stations[m].sched == SchedStrategy::HOL) {
834 for (std::size_t s2 : ehprio[r]) totm += Qin(m, s2);
835 } else {
836 for (std::size_t s2 : nnz) totm += Qin(m, s2);
837 }
838 den += T(STeffOut(m, r) * (one + totm - Qin(m, r)));
839 }
840 if (den == zero) continue;
841 if (qli) {
842 const T f = T(one / num_traits<T>::from_double(Nc[r] - 1.0));
843 totArvl(i, r) = T(tot - f * (Qin(i, r) - num / den));
844 } else {
845 const T f = T(num_traits<T>::from_int(2) / num_traits<T>::from_double(Nc[r]));
846 totArvl(i, r) = T(tot - f * Qin(i, r) + num / den);
847 }
848 }
849 }
850 }
851
852 // Interlocked flow (Franks 1999, Eq. 4.7)
853 // A request cannot queue behind work that its own submission caused, so the
854 // arrival-instant queue drops the interlocked share of every other chain. The
855 // own-class term is never removed. The matrix is empty for every model but the
856 // layers of SolverLN, where it comes from the interlock path tables.
857 if (!ILchain.empty()) {
858 for (std::size_t i = 0; i < M; ++i)
859 for (std::size_t c : nnz) {
860 T ilq = zero;
861 for (std::size_t c2 : nnz)
862 if (c2 != c) ilq += ILchain(c, c2) * Qin(i, c2);
863 if (ilq > zero) {
864 T adj = T(totArvl(i, c) - ilq);
865 if (adj < selfArvl(i, c)) adj = selfArvl(i, c);
866 totArvl(i, c) = adj;
867 }
868 }
869 }
870
871 Wout = Matrix<T>(M, K, zero);
872 for (std::size_t c : nnz) {
873 for (std::size_t i = 0; i < M; ++i) {
874 const SchedStrategy sc = L.stations[i].sched;
875 // Source residence-time rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
876 if (sc == SchedStrategy::EXT) continue;
877 if (sc == SchedStrategy::INF) {
878 Wout(i, c) = STeffOut(i, c);
879 continue;
880 }
881 const double cs = L.stations[i].nservers;
882
883 if (sc == SchedStrategy::PS) {
884 T corr = zero;
885 if (linmethod) {
886 for (std::size_t s : ccl)
887 corr += num_traits<T>::from_double(Nc[s]) * gamma[c](i, s);
888 corr -= gamma[c](i, c);
889 } else {
890 corr = T(num_traits<T>::from_double(Ntl - 1.0) * gamma[c](i, c));
891 }
892 if (opt.multiserver == "suri") {
893 // W = S (1 + Lm * suriFactor), against the arrival-instant
894 // queue seen by class c -- see the note below.
895 const T Lm = isopen[c] ? totArvlOpen(i, c)
896 : T(totArvl(i, c) + corr);
897 Wout(i, c) = T(STeffOut(i, c) * (one + Lm * suri[i]));
898 continue;
899 }
900 if (opt.multiserver == "seidmann")
901 Wout(i, c) = T(STeffOut(i, c) * num_traits<T>::from_double(cs - 1.0));
902 if (isopen[c]) {
903 // PASTA no-discount rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
904 Wout(i, c) = T(Wout(i, c) + STeffOut(i, c) * (one + totArvlOpen(i, c)));
905 } else {
906 // The arrival-instant queue is the one seen BY CLASS c:
907 // interpTot is a per-station total scaled by the GLOBAL
908 // (Ntot-1)/Ntot, so it cannot drop the arriving job's own
909 // chain and it charges a chain for jobs that never visit
910 // this station. totArvl(i,c) does drop it. Identical when
911 // the model has one chain -- see _kb/06-solver-catalog.md.
912 Wout(i, c) = T(Wout(i, c) + STeffOut(i, c) * (one + totArvl(i, c) + corr));
913 }
914 continue;
915 }
916
917 if (sc == SchedStrategy::DPS) {
918 // Discriminatory sharing: the arriving class waits behind
919 // each other class in proportion to the weight ratio.
920 const std::vector<T>& w = L.stations[i].schedparam;
921 if (w.size() != K)
922 throw UnsupportedError(
923 "solver_amvald: the DPS station '" + L.stations[i].name +
924 "' has no per-class weights");
925 T acc = zero;
926 if (cs > 1.0 && std::isfinite(cs))
927 acc = T(STeffOut(i, c) * num_traits<T>::from_double(cs - 1.0));
928 acc += T(STeffOut(i, c) * (one + selfArvl(i, c)));
929 for (std::size_t s : nnz) {
930 if (s == c) continue;
931 if (w[s] == w[c])
932 acc += T(STeffOut(i, c) * Qin(i, s));
933 else
934 acc += T(STeffOut(i, c) * Qin(i, s) * w[s] / w[c]);
935 }
936 Wout(i, c) = acc;
937 continue;
938 }
939
940 const bool fcfs_family = (sc == SchedStrategy::FCFS || sc == SchedStrategy::SIRO ||
941 sc == SchedStrategy::LCFSPR);
942 if (!fcfs_family && sc != SchedStrategy::HOL && sc != SchedStrategy::FCFSPRPRIO)
943 throw UnsupportedError(std::string("solver_amvald: ") + lang::sched_to_text(sc) +
944 " scheduling is not implemented in this port");
945 if (STeffOut(i, c) <= zero) continue;
946
947 const std::vector<T>& Xarv = Xref.empty() ? Xin : Xref;
948
949 // Preemptive-resume priority (PRIOMVA), Chandy-Lakshmi [ChaL83] applied
950 // where it was derived: a job in service IS preempted by a higher-priority
951 // arrival. Two terms separate this arm from the HOL path below.
952 // (a) no non-preemptive residual -- the lower-priority job found in
953 // service is preempted, so it delays nobody;
954 // (b) the tagged job's OWN service is interrupted too, so it is scaled
955 // by 1/(1-sigma_{k-1}) as well as the queued work.
956 // Together: E[T_k] = E[S_k]/(1-sigma_{k-1}) + <queued>/(1-sigma_{k-1}).
957 // Taken BEFORE the Bk / multiserver machinery because the reference
958 // restricts the arm to single-server stations and refuses the rest.
959 // Port of solver_amvald_forward.m (FCFSPRPRIO arm).
960 if (sc == SchedStrategy::FCFSPRPRIO) {
961 if (cs > 1.0 && std::isfinite(cs))
962 throw UnsupportedError(
963 "solver_amvald: FCFSPRPRIO with more than one server. The "
964 "preemptive-resume priority arm (priomva) is implemented for "
965 "single-server stations only; use SolverCTMC or SolverSSA for "
966 "multiserver PRS.");
967
968 // higher-priority utilization seen at the arrival instant
969 T uh_prs = zero;
970 for (std::size_t h : hprio[c])
971 uh_prs += T(d.Vchain(i, h) * STeffOut(i, h) * T(Xarv[h] + tau[c][h]));
972 double vps = 1.0 - num_traits<T>::to_double(uh_prs);
973 vps = std::max(opt.tol, std::min(vps, 1.0 - opt.tol));
974 const T ps_prs = num_traits<T>::from_double(vps);
975
976 // work of EQUAL OR HIGHER priority already queued ahead
977 T queued = T(STeffOut(i, c) * (isopen[c] ? Qin(i, c) : selfArvl(i, c)));
978 for (std::size_t s : ehprio[c])
979 if (s != c) queued += T(STeffOut(i, s) * Qin(i, s));
980
981 Wout(i, c) = T((STeffOut(i, c) + queued) / ps_prs);
982 continue;
983 }
984 // Uchain_r rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
985 auto Ur = [&](std::size_t k, std::size_t s) -> T {
986 if (!(Xin[s] > zero)) return Uin(k, s);
987 return T(Uin(k, s) / Xin[s] * (Xarv[s] + tau[c][s]));
988 };
989 // prioScaling: the fraction of the server left by the strictly
990 // higher-priority classes, clamped into [tol, 1-tol].
991 auto prio_scaling = [&](std::size_t r) -> T {
992 if (sc != SchedStrategy::HOL) return one;
993 T uh = zero;
994 for (std::size_t h : hprio[r]) {
995 // shadow: Sevcik's server at the current population; cl: Eager-Lipscomb,
996 // the utilization seen at arrival, i.e. at population N - 1_r
997 const T xh = opt.np_priority == "shadow" ? Xarv[h] : T(Xarv[h] + tau[r][h]);
998 uh += T(d.Vchain(i, h) * STeffOut(i, h) * xh);
999 }
1000 double v = 1.0 - num_traits<T>::to_double(uh);
1001 v = std::max(opt.tol, std::min(v, 1.0 - opt.tol));
1003 };
1004 const T ps_r = prio_scaling(c);
1005
1006 // Work of EQUAL OR HIGHER priority queued ahead of the arriving job, and the
1007 // residual of a strictly lower-priority job found in service, which HOL does not
1008 // preempt. This backlog is disjoint from the 1/ps_r inflation, which counts only
1009 // the higher-priority work that overtakes the job while it waits;
1010 // see _kb/06-solver-catalog.md
1011 auto hol_ehprio_backlog = [&](const std::vector<T>& Bkw) -> T {
1012 T acc = zero;
1013 for (std::size_t s : ehprio[c])
1014 if (s != c) acc += T(STeffOut(i, s) * Qin(i, s) * Bkw[s]);
1015 return acc;
1016 };
1017 auto hol_np_residual = [&](const std::vector<T>& Bkw) -> T {
1018 if (opt.highvar == "hvmva") return zero; // hvmva already spans every class
1019 T acc = zero;
1020 for (std::size_t s : lprio[c])
1021 acc += T(d.Vchain(i, s) * STeffOut(i, s) * (Xarv[s] + tau[c][s]) *
1022 STeffOut(i, s) * Bkw[s]);
1023 return acc;
1024 };
1025
1026 // Bk backlog-reduction rationale (FCFS/SIRO/LCFSPR vs HOL): see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
1027 std::vector<T> Bk(K, one);
1028 if (cs > 1.0 && std::isfinite(cs)) {
1029 T load = zero;
1030 for (std::size_t s : nnz) {
1031 const T dr = (sc == SchedStrategy::HOL) ? dcl[s] : ((s == c) ? dcl[c] : one);
1032 load += dr * Xin[s] * d.Vchain(i, s) * STeffOut(i, s);
1033 }
1034 const bool light = num_traits<T>::to_double(load) < 0.75;
1035 for (std::size_t s : nnz) {
1036 const T dr = (sc == SchedStrategy::HOL) ? dcl[s] : ((s == c) ? dcl[c] : one);
1037 T base = T(dr * Xin[s] * d.Vchain(i, s) * STeffOut(i, s));
1038 if (sc == SchedStrategy::HOL && opt.multiserver != "softmin")
1039 base = T(base / num_traits<T>::from_double(cs));
1040 if (light) {
1041 Bk[s] = base;
1042 } else {
1043 const unsigned e = static_cast<unsigned>(
1044 std::llround(cs) - (sc == SchedStrategy::HOL ? 0 : 1));
1045 Bk[s] = num_pow_int(base, e);
1046 }
1047 }
1048 }
1049
1050 // single-server-with-scaling branch rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
1051 if (cs == 1.0) {
1052 T w = zero;
1053 if (opt.highvar == "hvmva") {
1054 T usum = zero;
1055 for (std::size_t s : ccl) usum += Ur(i, s);
1056 w = T(STeffOut(i, c) * (one - usum));
1057 for (std::size_t s : ccl)
1058 w += T(STeffOut(i, s) / prio_scaling(s) * Ur(i, s) *
1059 (one + d.SCVchain(i, s)) / num_traits<T>::from_int(2));
1060 } else if (opt.highvar != "default") {
1061 throw UnsupportedError("solver_amvald: highvar '" + opt.highvar +
1062 "' is not implemented");
1063 } else {
1064 w = STeffOut(i, c);
1065 }
1066 if (sc == SchedStrategy::HOL) {
1067 // priority-branch charging rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
1068 const std::vector<T> ones(K, one);
1069 w += T((STeffOut(i, c) * (isopen[c] ? Qin(i, c) : selfArvl(i, c)) +
1070 hol_ehprio_backlog(ones) + hol_np_residual(ones)) / ps_r);
1071 } else {
1072 w += STeffOut(i, c) * (isopen[c] ? Qin(i, c) : selfArvl(i, c));
1073 for (std::size_t s : nnz)
1074 if (s != c) w += STeffOut(i, s) * Qin(i, s);
1075 }
1076 Wout(i, c) = w;
1077 continue;
1078 }
1079
1080 // Multiserver.
1081 if (opt.multiserver == "suri") {
1082 T Lm = isopen[c] ? T(deltaclass[c] * Qin(i, c)) : selfArvl(i, c);
1083 if (sc != SchedStrategy::HOL)
1084 for (std::size_t s : nnz)
1085 if (s != c) Lm += Qin(i, s);
1086 if (sc == SchedStrategy::HOL) {
1087 Lm = isopen[c] ? Qin(i, c) : selfArvl(i, c);
1088 const std::vector<T> ones(K, one);
1089 Wout(i, c) = T(STeffOut(i, c) +
1090 ((STeffOut(i, c) * Lm + hol_ehprio_backlog(ones)) * suri[i] +
1091 hol_np_residual(ones)) / ps_r);
1092 continue;
1093 }
1094 Wout(i, c) = T(STeffOut(i, c) / ps_r + STeffOut(i, c) * Lm * suri[i] / ps_r);
1095 continue;
1096 }
1097 if (opt.multiserver == "softmin") {
1098 T w = STeffOut(i, c);
1099 if (sc == SchedStrategy::HOL)
1100 w += T((STeffOut(i, c) * (isopen[c] ? Qin(i, c) : selfArvl(i, c)) * Bk[c] +
1101 hol_ehprio_backlog(Bk) + hol_np_residual(Bk)) / ps_r);
1102 else
1103 w += T(STeffOut(i, c) * (isopen[c] ? Qin(i, c) : selfArvl(i, c)) * Bk[c] / ps_r);
1104 if (sc != SchedStrategy::HOL)
1105 for (std::size_t s : nnz)
1106 if (s != c) w += STeffOut(i, s) * Bk[s] * Qin(i, s);
1107 Wout(i, c) = w;
1108 continue;
1109 }
1110 // default / seidmann
1111 T w = T(STeffOut(i, c) * num_traits<T>::from_double(cs - 1.0));
1112 w += STeffOut(i, c);
1113 if (sc == SchedStrategy::HOL) {
1114 w += T((STeffOut(i, c) * (isopen[c] ? Qin(i, c) : selfArvl(i, c)) * Bk[c] +
1115 hol_ehprio_backlog(Bk) + hol_np_residual(Bk)) / ps_r);
1116 } else {
1117 w += STeffOut(i, c) *
1118 (isopen[c] ? T(deltaclass[c] * Qin(i, c)) : selfArvl(i, c)) * Bk[c];
1119 for (std::size_t s : nnz)
1120 if (s != c) w += STeffOut(i, s) * Bk[s] * Qin(i, s);
1121 }
1122 Wout(i, c) = w;
1123 }
1124 }
1125 };
1126
1127 // one inner fixed point at a given population vector
1128 auto inner_loop = [&](Matrix<T>& Q, std::vector<T>& X, Matrix<T>& U,
1129 const std::vector<double>& Nc, Matrix<T>& Wout, Matrix<T>& STeffOut) {
1130 int iter = 0;
1131 Matrix<T> Qprev = Q;
1132 while (true) {
1133 Qprev = Q;
1134 const std::vector<T> Xprev = X;
1135 const Matrix<T> Uprev = U;
1136 ++iter;
1137 forward(Qprev, Xprev, Uprev, Nc, Wout, STeffOut);
1138 ++totiter;
1140 double resid = 0.0;
1141 for (std::size_t i = 0; i < Q.rows(); ++i)
1142 for (std::size_t c = 0; c < Q.cols(); ++c)
1143 resid = std::max(resid, std::abs(num_traits<T>::to_double(Q(i, c)) -
1144 num_traits<T>::to_double(Qprev(i, c))));
1146 "AMVA sweep %d: queue-length residual %.3e",
1147 totiter, resid);
1148 }
1149 if (totiter >= max_totiter) break;
1150 for (std::size_t c : nnz) {
1151 T sw = zero;
1152 for (std::size_t i = 0; i < M; ++i) sw += Wout(i, c);
1153 if (sw == zero) {
1154 X[c] = zero;
1155 } else if (!isopen[c]) {
1156 T cyc = zero;
1157 for (std::size_t i = 0; i < M; ++i) cyc += d.Vchain(i, c) * Wout(i, c);
1158 if (cyc != zero)
1159 X[c] = T(omicron * num_traits<T>::from_double(Nc[c]) / cyc +
1160 (one - omicron) * Xprev[c]);
1161 }
1162 // an open chain's X is its arrival rate and stays put; only its
1163 // queue lengths and utilizations follow from the residence times
1164 for (std::size_t i = 0; i < M; ++i) {
1165 Q(i, c) = T(omicron * X[c] * d.Vchain(i, c) * Wout(i, c) +
1166 (one - omicron) * Qprev(i, c));
1167 U(i, c) = T(omicron * d.Vchain(i, c) * STeffOut(i, c) * X[c] +
1168 (one - omicron) * Uprev(i, c));
1169 }
1170 }
1171 if (iter >= 2) {
1172 double err = 0.0;
1173 for (std::size_t i = 0; i < M; ++i)
1174 for (std::size_t c = 0; c < K; ++c)
1175 err = std::max(err, std::fabs(num_traits<T>::to_double(Q(i, c) - Qprev(i, c))));
1176 if (err <= opt.iter_tol) break;
1177 }
1178 if (iter > inner_cap) break;
1179 }
1180 return Qprev;
1181 };
1182
1183 Matrix<T> QouterPrev = Qchain;
1184 int outer = 0;
1185 converged = false;
1186 while (true) {
1187 ++outer;
1188 QouterPrev = Qchain;
1189 const std::vector<T> XouterPrev = Xchain;
1190 // baseline the tau differences below are taken against; empty when no recursion runs
1191 if (linmethod) Xref = XouterPrev;
1192
1193 // Linearizer correction: solve at each reduced population N - e_s
1194 if (linmethod && std::isfinite(Nt) && Nt > 0.0) {
1195 for (std::size_t s = 0; s < K; ++s) {
1196 if (!std::isfinite(d.Nchain[s])) continue;
1197 std::vector<double> Ns = d.Nchain;
1198 Ns[s] -= 1.0;
1199 const T shrink = num_traits<T>::from_double((Nt - 1.0) / Nt);
1200 Matrix<T> Qs = Qchain;
1201 std::vector<T> Xs = Xchain;
1202 Matrix<T> Us = Uchain;
1203 for (std::size_t i = 0; i < M; ++i)
1204 for (std::size_t c = 0; c < K; ++c) {
1205 Qs(i, c) = T(Qs(i, c) * shrink);
1206 Us(i, c) = T(Us(i, c) * shrink);
1207 }
1208 for (std::size_t c = 0; c < K; ++c) Xs[c] = T(Xs[c] * shrink);
1209 Matrix<T> Ws, STs;
1210 const std::vector<T> Xs_in = Xs;
1211 const Matrix<T> Qs_prev = inner_loop(Qs, Xs, Us, Ns, Ws, STs);
1212 // tau(s,r) rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
1213 for (std::size_t c : nnz) tau[s][c] = T(Xs[c] - XouterPrev[c]);
1214 (void)Xs_in;
1215 // ONLY 'lin' TAKES THE PER-CLASS CORRECTION. 'qdlin' takes the
1216 // class-AGGREGATE one and stores it in column 0, leaving the
1217 // rest of the row zero, which is what the reference does:
1218 // solver_amvald.m allocates the (K,M,K) per-class array for
1219 // qdlin but writes `gamma(s,k) = sum_r Q_s(k,r)/(Nt-1) -
1220 // sum_r Q(k,r)/Nt` into it with two subscripts, and MATLAB
1221 // linear-indexes that to (s,k,1). Every reader below still
1222 // indexes gamma PER CLASS, so the correction that reaches the
1223 // residence time is N_0*gamma(r,i,0) - [r==0]*gamma(r,i,0). It
1224 // coincides with the queue-dependent AMVA form (Nt-1)*gamma_agg
1225 // iff K = 1, so a single-chain model is unaffected and lin and
1226 // qdlin stay bit-identical there. This port filled it per class
1227 // for both methods until 2026-09-04, which made C++ qdlin an
1228 // alias of C++ lin and put it at odds with MATLAB, the JAR and
1229 // native Python on every multichain model. Do not "restore" it
1230 // without re-baselining the AMVA goldens in all four codebases;
1231 // see _kb/06-solver-catalog.md.
1232 if (method == "qdlin") {
1233 for (std::size_t i = 0; i < M; ++i) {
1234 T qs = zero, qo = zero;
1235 for (std::size_t c = 0; c < K; ++c) {
1236 qs += Qs_prev(i, c);
1237 qo += QouterPrev(i, c);
1238 }
1239 // Nt = 1 leaves the reduced population empty and the
1240 // reference divides by zero there; native Python guards
1241 // it with a zero and this port follows Python.
1242 gamma[s](i, 0) =
1243 (Nt > 1.0) ? T(qs / num_traits<T>::from_double(Nt - 1.0) -
1245 : zero;
1246 }
1247 } else {
1248 for (std::size_t i = 0; i < M; ++i)
1249 for (std::size_t c : nnz)
1250 if (std::isfinite(d.Nchain[c]) && Ns[c] > 0.0)
1251 gamma[s](i, c) =
1252 T(Qs_prev(i, c) / num_traits<T>::from_double(Ns[c]) -
1253 QouterPrev(i, c) / num_traits<T>::from_double(d.Nchain[c]));
1254 }
1255 if (totiter >= max_totiter) break;
1256 }
1257 }
1258 if (totiter >= max_totiter) break;
1259
1260 Matrix<T> Wtmp, STtmp;
1261 const Matrix<T> Qprev = inner_loop(Qchain, Xchain, Uchain, d.Nchain, Wtmp, STtmp);
1262 Wchain = Wtmp;
1263 STeff = STtmp;
1264
1265 double err = 0.0;
1266 for (std::size_t i = 0; i < M; ++i)
1267 for (std::size_t c = 0; c < K; ++c)
1268 err = std::max(err, std::fabs(num_traits<T>::to_double(Qchain(i, c) - QouterPrev(i, c))));
1269 converged = err <= opt.iter_tol;
1270 if (outer >= 2 && converged) break;
1271 if (outer >= inner_cap || totiter > max_totiter) break;
1272 }
1273
1274 for (std::size_t i = 0; i < M; ++i)
1275 for (std::size_t c = 0; c < K; ++c) Tchain(i, c) = T(Xchain[c] * d.Vchain(i, c));
1276
1277 // renormalise utilizations past one, as the reference does
1278 for (std::size_t i = 0; i < M; ++i) {
1279 const SchedStrategy sc = L.stations[i].sched;
1280 if (!(sc == SchedStrategy::FCFS || sc == SchedStrategy::SIRO || sc == SchedStrategy::PS ||
1281 sc == SchedStrategy::LCFSPR || sc == SchedStrategy::DPS || sc == SchedStrategy::HOL))
1282 continue;
1283 T usum = zero;
1284 for (std::size_t c = 0; c < K; ++c) usum += Uchain(i, c);
1285 if (num_traits<T>::to_double(usum) <= 1.0) continue;
1286 T den = zero;
1287 for (std::size_t c = 0; c < K; ++c) den += d.Vchain(i, c) * STeff(i, c) * Xchain[c];
1288 if (den == zero) continue;
1289 for (std::size_t c = 0; c < K; ++c) {
1290 if (!(d.Vchain(i, c) * STeff(i, c) > zero)) continue;
1291 Uchain(i, c) = T(one * d.Vchain(i, c) * STeff(i, c) * Xchain[c] / den);
1292 }
1293 }
1294
1295 Matrix<T> Rchain(M, K, zero);
1296 for (std::size_t i = 0; i < M; ++i)
1297 for (std::size_t c = 0; c < K; ++c)
1298 if (Tchain(i, c) != zero) Rchain(i, c) = T(Qchain(i, c) / Tchain(i, c));
1299 for (std::size_t c = 0; c < K; ++c) {
1300 if (d.Nchain[c] != 0.0) continue;
1301 Xchain[c] = zero;
1302 for (std::size_t i = 0; i < M; ++i) {
1303 Uchain(i, c) = zero;
1304 Rchain(i, c) = zero;
1305 Tchain(i, c) = zero;
1306 }
1307 }
1308
1309 const ClassResults<T> cr =
1310 sn_deaggregate_chain_results(L, d, Matrix<T>(), Matrix<T>(), Rchain, Tchain, Xchain);
1311 MvaSolution<T> out;
1312 out.Q = cr.Q;
1313 out.U = cr.U;
1314 out.R = cr.R;
1315 out.Tp = cr.Tp;
1316 out.C = cr.C;
1317 out.X = cr.X;
1318
1319 // A station with limited class or joint dependence reports utilization as
1320 // T*S/peak from the declared peak rate scaling, matching the T*S/c
1321 // convention of an ordinary multiserver station (solver_amvald.m:255-291).
1322 //
1323 // THE PEAK IS A MODEL INPUT, NOT A DEFAULT. Without it this pass has no
1324 // normalizer, and returning the unnormalized column -- or, as it did before,
1325 // a column of zeros -- is worse than refusing: both read as a utilization.
1326 // SolverCTMC, solver_nc_conv and both SSA engines already refuse it by name
1327 // here, and MATLAB's getLimitedClassDependencePeak.m refuses it in the
1328 // model. This check sits ahead of the closed-model guard below because a
1329 // missing peak is a defect whether or not the pass would have run.
1330 for (std::size_t ist = 0; ist < M; ++ist) {
1331 if (L.stations[ist].cdscaling && L.stations[ist].cdscalingpeak.empty())
1332 throw InputError(
1333 "SolverMVA: station '" + L.stations[ist].name +
1334 "' declares class-dependent service without a peak rate. Utilization at a "
1335 "class-dependent station is reported as T*S/peak, so pass the peak to "
1336 "setClassDependence");
1337 if (L.stations[ist].jdscaling && L.stations[ist].jdscalingpeak.empty())
1338 throw InputError(
1339 "SolverMVA: station '" + L.stations[ist].name +
1340 "' declares joint-dependent service without a peak rate; pass the peak to "
1341 "setJointDependence");
1342 }
1343 bool anyOpen = false;
1344 for (const auto& c : L.classes)
1345 if (std::isinf(c.population)) anyOpen = true;
1346 if (!anyOpen) {
1347 const std::size_t Kcls = L.nclasses;
1348 for (std::size_t ist = 0; ist < M; ++ist) {
1349 const std::vector<T>* peak = nullptr;
1350 if (L.stations[ist].cdscaling)
1351 peak = &L.stations[ist].cdscalingpeak;
1352 else if (L.stations[ist].jdscaling)
1353 peak = &L.stations[ist].jdscalingpeak;
1354 if (peak == nullptr) continue;
1355 for (std::size_t k = 0; k < Kcls; ++k) {
1356 const double rate = num_traits<T>::to_double(L.rates(ist, k));
1357 const T bmax = k < peak->size() ? (*peak)[k] : zero;
1358 if (std::isfinite(rate) && rate > 0.0 && bmax > zero)
1359 out.U(ist, k) = T(out.Tp(ist, k) / L.rates(ist, k) / bmax);
1360 else
1361 out.U(ist, k) = zero;
1362 }
1363 }
1364 }
1365 out.method = method;
1366 out.iter = totiter;
1367 // through DETAIL, not STEP: SolverLN runs this analyzer once per layer per
1368 // iteration, inside its own run, so a step line here repeats hundreds of
1369 // times; detail() collapses repeats of the same shape
1370 {
1371 char buf[96];
1372 std::snprintf(buf, sizeof(buf), "AMVA finished after %d sweeps", totiter);
1373 line::util::LineConsole::detail(buf);
1374 }
1375 return out;
1376}
1377
1378// ---------------------------------------------------------------------------
1379// solver_amva: method resolution and the product-form fast paths
1380// ---------------------------------------------------------------------------
1381
1382/**
1383 * The @c amva.* spellings of solver_amva.m, mapped onto the bare method names.
1384 *
1385 * The reference accepts both, and a model or a test may use either, so the
1386 * alias table is part of the interface rather than a convenience: an
1387 * unrecognised name must reach the method switch and be refused there by its
1388 * own name, not silently resolved to `default`.
1389 */
1390inline std::string amva_method_alias(const std::string& m) {
1391 if (m == "amva.qli") return "qli";
1392 if (m == "amva.qd" || m == "amva.qdamva" || m == "qdamva") return "qd";
1393 if (m == "amva.aql") return "aql";
1394 if (m == "amva.qsa") return "qsa";
1395 if (m == "amva.qdaql") return "qdaql";
1396 if (m == "amva.tay") return "tay";
1397 if (m == "amva.scat") return "scat";
1398 if (m == "amva.lin") return "lin";
1399 if (m == "amva.qdlin") return "qdlin";
1400 if (m == "amva.fli") return "fli";
1401 if (m == "amva.bs") return "bs";
1402 if (m == "amva.ab") return "ab";
1403 if (m == "amva.schmidt") return "schmidt";
1404 if (m == "amva.schmidt-ext") return "schmidt-ext";
1405 if (m == "amva.lcp") return "lcp";
1406 if (m == "amva.chow") return "chow";
1407 if (m == "amva.pamb") return "pamb";
1408 if (m == "amva.pami") return "pami";
1409 if (m == "amva.pamt") return "pamt";
1410 if (m == "amva.clust") return "clust";
1411 if (m == "amva.dmlin") return "dmlin";
1412 if (m == "amva.priomva") return "priomva";
1413 if (m == "amva.marie") return "marie";
1414 return m;
1415}
1416
1417template <class T>
1419 const Matrix<T>& init_sol, bool& converged) {
1420 using qn::SchedStrategy;
1421 const T zero = num_traits<T>::from_int(0);
1422 const std::size_t M = L.nstations, C = L.nchains;
1423 opt.iter_max = std::min(opt.iter_max, 10000);
1424 converged = true;
1425
1426 std::string method = amva_method_alias(opt.method);
1427 if (method == "default" || method == "amva") {
1428 double Nsum = 0.0;
1429 bool anysmall = false;
1430 for (std::size_t c = 0; c < C; ++c) {
1431 Nsum += d.Nchain[c];
1432 if (d.Nchain[c] < 1.0) anysmall = true;
1433 }
1434 if (Nsum <= 2.0 || anysmall) {
1435 method = "qd";
1436 } else {
1437 bool anyfinite = false, allone = true;
1438 for (const auto& s : L.stations)
1439 if (std::isfinite(s.nservers)) {
1440 anyfinite = true;
1441 if (s.nservers != 1.0) allone = false;
1442 }
1443 // empty finite-server max rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
1444 method = (anyfinite && allone) ? "egflin" : "lin";
1445 }
1446 }
1447
1448 // The closed-population AMVA family (Bard-Schweitzer, SQNI, Tay, SCAT, AQL, QSA,
1449 // Bard LCP, Chow SA, Hsieh-Lam PAM, clustering, Improved Linearizer,
1450 // Akyildiz-Bolch, Schmidt) lives ONLY in the product-form branch below. The same
1451 // predicate the report gates on decides here, so a name the report offers is a
1452 // name that runs and a name it withholds errors rather than falling through to
1453 // solver_amvald and returning the qd-family answer under a method the caller did
1454 // not ask for.
1455 {
1456 const std::string amva_reason = mva_closed_population_reason(L, method);
1457 if (!amva_reason.empty()) throw UnsupportedError(amva_reason);
1458 }
1459
1460 // MATLAB's "trivial models" early exit tests sn_has_homogeneous_scheduling,
1461 // which reduces to nstations == 1; see the note on that predicate.
1462 if (L.has_homogeneous_scheduling(SchedStrategy::INF)) {
1463 MvaOptions o2 = opt;
1464 o2.multiserver = "default";
1465 return solver_amvald(L, d, o2, method, converged, init_sol);
1466 }
1467
1468 // ab / schmidt / schmidt-ext ARE the class-dependent FCFS algorithms and live only in
1469 // the product-form branch below, so the het-FCFS exclusion must not divert them.
1470 const bool het_fcfs_own =
1471 (method == "ab" || method == "schmidt" || method == "schmidt-ext");
1472 const bool cond1 = L.has_product_form_not_het_fcfs() ||
1473 (het_fcfs_own && L.has_product_form_not_het_fcfs(false));
1474 bool cond2 = true; // ~sn_has_load_dependence: the pf kernels ignore lldscaling
1475 for (const auto& st : L.stations)
1476 if (!st.lldscaling.empty()) cond2 = false;
1477 // MATLAB keeps ONE mixed model in this branch (solver_amva.m:146): a strict
1478 // product-form network under 'lin', where pfqn_linearizermx solves the open classes
1479 // in closed form and inflates the closed demands by their utilization. Every other
1480 // open model goes to solver_amvald, the only path carrying the open corrections.
1481 const bool cond3 = !L.has_open_classes() || (L.has_product_form() && method == "lin");
1482 if (!(cond1 && cond2 && cond3)) return solver_amvald(L, d, opt, method, converged, init_sol);
1483
1484 // Interlocked flow (Franks 1999, Eq. 4.7): only solver_amvald carries the correction, so
1485 // an interlocked model goes there rather than to the product-form kernels below, which
1486 // have no interlock term and would drop it silently. Mirrors solver_amva.m and the JAR.
1487 if (!sn_interlock_chain<T>(L, opt.interlock).empty())
1488 return solver_amvald(L, d, opt, method, converged, init_sol);
1489
1491 if (pf.queue_stations.empty()) return solver_amvald(L, d, opt, method, converged, init_sol);
1492
1493 const std::size_t nq = pf.queue_stations.size(), nz = pf.delay_stations.size();
1494 // An open chain has no population: it takes pfqn's kOpenClass sentinel, which stands
1495 // in for MATLAB's Inf, and llround(Inf) is undefined behaviour rather than a marker.
1496 std::vector<int> N(C, 0);
1497 for (std::size_t c = 0; c < C; ++c)
1498 N[c] = std::isfinite(d.Nchain[c]) ? static_cast<int>(std::llround(d.Nchain[c]))
1500 // Nt is read only by the closed-only kernels, which no open model reaches, and an
1501 // exact field has no value for from_double(Inf); an open chain is left at zero.
1502 std::vector<T> Nt(C, zero);
1503 for (std::size_t c = 0; c < C; ++c)
1504 if (std::isfinite(d.Nchain[c])) Nt[c] = num_traits<T>::from_double(d.Nchain[c]);
1505
1506 // The scheduling of the queueing stations, in the api layer's enum.
1507 std::vector<pfqn::SchedStrategy> types;
1508 for (std::size_t a = 0; a < nq; ++a) {
1509 switch (L.stations[pf.queue_stations[a] - 1].sched) {
1510 case SchedStrategy::PS: types.push_back(pfqn::SchedStrategy::PS); break;
1511 case SchedStrategy::INF: types.push_back(pfqn::SchedStrategy::INF); break;
1512 default: types.push_back(pfqn::SchedStrategy::FCFS); break;
1513 }
1514 }
1515
1516 // Warm start, aligned to the queueing-station rows; a malformed seed is
1517 // dropped rather than repaired, as the reference does.
1518 Matrix<T> Q0;
1519 if (init_sol.rows() == L.nstations && init_sol.cols() == C) {
1520 Q0 = Matrix<T>(nq, C, zero);
1521 bool bad = false;
1522 for (std::size_t a = 0; a < nq && !bad; ++a)
1523 for (std::size_t c = 0; c < C; ++c) {
1524 Q0(a, c) = init_sol(pf.queue_stations[a] - 1, c);
1525 if (Q0(a, c) < zero) {
1526 bad = true;
1527 break;
1528 }
1529 }
1530 if (bad) Q0 = Matrix<T>();
1531 }
1532
1533 // Seidmann-scaling exemption rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
1534 const bool direct_ms = (method == "ab" || method == "schmidt" || method == "schmidt-ext");
1535 const bool seidmann = (opt.multiserver == "default" || opt.multiserver == "seidmann");
1536
1537 Matrix<T> Dm = pf.D, Zm = pf.Z;
1538 if (!direct_ms) {
1539 if (seidmann) {
1540 // move the (c-1)/c share of each multiserver demand into the first
1541 // delay station
1542 for (std::size_t a = 0; a < nq; ++a) {
1543 const double c = pf.S[a];
1544 if (!std::isfinite(c)) continue;
1545 for (std::size_t k = 0; k < C; ++k)
1546 Dm(a, k) = T(Dm(a, k) / num_traits<T>::from_double(c));
1547 if (Zm.rows() > 0)
1548 for (std::size_t k = 0; k < C; ++k)
1549 Zm(0, k) =
1550 T(Zm(0, k) + pf.D(a, k) * num_traits<T>::from_double((c - 1.0) / c));
1551 }
1552 } else if (opt.multiserver == "softmin") {
1553 return solver_amvald(L, d, opt, method, converged, init_sol);
1554 }
1555 // conway, krzesinski and erlang leave the demands alone: they carry the
1556 // multiplicity into the algorithm itself.
1557 }
1558
1559 // Think time per class, which is what the single-server api routines take.
1560 std::vector<T> Zsum(C, zero);
1561 for (std::size_t c = 0; c < C; ++c)
1562 for (std::size_t a = 0; a < Zm.rows(); ++a) Zsum[c] += Zm(a, c);
1563
1564 bool allone = true, anyinf = false;
1565 for (double s : pf.S) {
1566 if (!std::isfinite(s)) anyinf = true;
1567 else if (s != 1.0) allone = false;
1568 }
1569
1570 // The per-method solve fills these, over the QUEUEING stations only.
1571 Matrix<T> Qq(nq, C, zero), Uq(nq, C, zero), Qz(nz, C, zero);
1572 std::vector<T> X(C, zero);
1573 int iters = 0;
1574 bool have_delay_rows = false; // set by the methods that solve the delays too
1575
1576 if (method == "sqni") {
1577 // square-root approximation gate rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
1578 if (!(M == 2 && nq == 1 && nz == 1))
1579 throw UnsupportedError(
1580 "solver_amva: method 'sqni' applies only to a model of one queueing station and "
1581 "one infinite server");
1582 if constexpr (!num_traits<T>::has_transcendental) {
1583 throw UnsupportedError(
1584 "solver_amva: method 'sqni' solves a quadratic and evaluates a square root, "
1585 "which exact arithmetic has no representation for; use the double or real "
1586 "backend");
1587 } else {
1588 std::vector<T> Lv(C, zero);
1589 for (std::size_t c = 0; c < C; ++c) Lv[c] = Dm(0, c);
1590 const pfqn::SqniResult<T> r = pfqn::pfqn_sqni(Nt, Lv, Zsum);
1591 for (std::size_t c = 0; c < C; ++c) {
1592 Qq(0, c) = r.Q[c];
1593 Uq(0, c) = r.U[c];
1594 X[c] = r.X[c];
1595 }
1596 iters = 1;
1597 }
1598 } else if (method == "bs") {
1599 std::vector<pfqn::AmvaSched> bstype;
1600 for (std::size_t a = 0; a < nq; ++a)
1601 bstype.push_back(L.stations[pf.queue_stations[a] - 1].sched == SchedStrategy::PS
1604 const pfqn::AmvaResult<T> r =
1605 pfqn::pfqn_bs(Dm, Nt, Zsum, bstype, opt.tol, static_cast<std::size_t>(opt.iter_max), Q0);
1606 Qq = r.QN;
1607 Uq = r.UN;
1608 X = r.XN;
1609 iters = static_cast<int>(r.iterations);
1610 } else if (method == "lcp" || method == "chow") {
1611 // Bard LCP and the Chow Second Approximation built on it: both are
1612 // Bard-Schweitzer variants in the arrival-instant estimate, so they take
1613 // the same Seidmann treatment and the same scheduling tags as 'bs'.
1614 std::vector<pfqn::AmvaSched> lctype;
1615 for (std::size_t a = 0; a < nq; ++a)
1616 lctype.push_back(L.stations[pf.queue_stations[a] - 1].sched == SchedStrategy::PS
1619 const pfqn::AmvaResult<T> r =
1620 (method == "lcp")
1621 ? pfqn::pfqn_lcp(Dm, Nt, Zsum, lctype, opt.tol,
1622 static_cast<std::size_t>(opt.iter_max), Q0)
1623 : pfqn::pfqn_chow(Dm, Nt, Zsum, lctype, opt.tol,
1624 static_cast<std::size_t>(opt.iter_max), Q0);
1625 Qq = r.QN;
1626 Uq = r.UN;
1627 X = r.XN;
1628 iters = static_cast<int>(r.iterations);
1629 } else if (method == "pamb" || method == "pami" || method == "pamt") {
1630 // Hsieh-Lam proportional approximations, noniterative
1631 const pfqn::PamVariant pv = (method == "pamb") ? pfqn::PamVariant::Basic
1632 : (method == "pami") ? pfqn::PamVariant::Improved
1634 const pfqn::AmvaResult<T> r = pfqn::pfqn_pam(Dm, Nt, Zsum, pv);
1635 Qq = r.QN;
1636 Uq = r.UN;
1637 X = r.XN;
1638 iters = 1;
1639 } else if (method == "clust") {
1640 // de Souza e Silva-Lavenberg-Muntz clustering approximation, with the
1641 // decomposition derived automatically from the PAMB utilizations.
1643 Dm, Nt, Zsum, std::vector<std::vector<std::size_t> >(),
1644 std::vector<std::vector<std::size_t> >(), pfqn::ClustInner::Linearizer, opt.tol,
1645 static_cast<std::size_t>(opt.iter_max));
1646 Qq = r.QN;
1647 Uq = r.UN;
1648 X = r.XN;
1649 iters = static_cast<int>(r.iterations);
1650 } else if (method == "dmlin") {
1651 // de Souza e Silva-Muntz Improved Linearizer: the Linearizer fixed point
1652 // with the Delta-terms pre-aggregated, so it agrees with 'lin' by
1653 // construction and only the cost differs.
1655 pfqn::pfqn_dmlin(Dm, N, Zm, types, opt.tol, opt.iter_max, Q0);
1656 Qq = r.Q;
1657 Uq = r.U;
1658 X = r.X;
1659 iters = r.totiter;
1660 } else if (method == "tay") {
1661 // Between 'bs' and 'aql', which is the reference's own branch order in
1662 // solver_amva.m. MATLAB raises here rather than approximating: the
1663 // elasticity equations are derived for single servers throughout, and
1664 // there is no multiserver correction to fall back on.
1665 if (L.has_multi_server())
1666 throw UnsupportedError(
1667 "solver_amva: Tay's approximation is defined for single-server stations; use "
1668 "'default' or 'lin'");
1669 if constexpr (!num_traits<T>::has_transcendental) {
1670 throw UnsupportedError(
1671 "solver_amva: method 'tay' stops on a tolerance evaluated with transcendentals, "
1672 "which exact arithmetic has no representation for; use the double or real "
1673 "backend");
1674 } else {
1676 Dm, Nt, Zsum, opt.tol, static_cast<std::size_t>(opt.iter_max), Q0);
1677 Qq = r.QN;
1678 Uq = r.UN;
1679 X = r.XN;
1680 iters = static_cast<int>(r.iterations);
1681 }
1682 } else if (method == "scat") {
1683 // Neuse-Chandy SCAT: the Linearizer fixed point with one Delta refresh
1684 // instead of three. Unlike tay/aql/qsa this takes multiserver stations,
1685 // because they reach it already Seidmann-scaled, exactly as for 'bs'.
1687 pfqn::pfqn_scat(Dm, N, Zm, types, opt.tol, opt.iter_max, Q0);
1688 Qq = r.Q;
1689 Uq = r.U;
1690 X = r.X;
1691 iters = r.totiter;
1692 } else if (method == "aql") {
1693 // MATLAB raises here rather than approximating: the AQL recursion has
1694 // no multiserver correction at all.
1695 if (L.has_multi_server())
1696 throw UnsupportedError(
1697 "solver_amva: AQL cannot handle multi-server stations; use 'default' or 'lin'");
1698 if constexpr (!num_traits<T>::has_transcendental) {
1699 throw UnsupportedError(
1700 "solver_amva: method 'aql' stops on a relative tolerance evaluated with "
1701 "transcendentals, which exact arithmetic has no representation for; use the "
1702 "double or real backend");
1703 } else {
1704 const pfqn::AmvaResult<T> r =
1705 pfqn::pfqn_aql(Dm, Nt, Zsum, opt.tol, static_cast<std::size_t>(opt.iter_max));
1706 Qq = r.QN;
1707 Uq = r.UN;
1708 X = r.XN;
1709 iters = static_cast<int>(r.iterations);
1710 }
1711 } else if (method == "qsa") {
1712 // MATLAB raises here rather than approximating: the QSA core carries an
1713 // aggregate queue length with no multiserver correction at all.
1714 if (L.has_multi_server())
1715 throw UnsupportedError(
1716 "solver_amva: QSA cannot handle multi-server stations; use 'default' or 'lin'");
1717 if constexpr (!num_traits<T>::has_transcendental) {
1718 throw UnsupportedError(
1719 "solver_amva: method 'qsa' stops on a residual tolerance evaluated with "
1720 "transcendentals, which exact arithmetic has no representation for; use the "
1721 "double or real backend");
1722 } else {
1723 // Dm holds the queueing stations only, the delays already folded
1724 // into Zsum, so every row here is a queueing centre.
1726 Dm, Nt, Zsum, std::vector<pfqn::AmvaSched>(nq, pfqn::AmvaSched::PS), opt.tol,
1727 static_cast<std::size_t>(opt.iter_max));
1728 Qq = r.QN;
1729 Uq = r.UN;
1730 X = r.XN;
1731 iters = static_cast<int>(r.iterations);
1732 }
1733 } else if (direct_ms) {
1734 // The demands of the delays are stacked ON TOP of the queues, which is
1735 // the [Z0; L0] layout these three routines expect.
1736 Matrix<T> Dfull(nz + nq, C, zero), Vfull(nz + nq, C, num_traits<T>::from_int(1));
1737 std::vector<int> nsfull;
1738 std::vector<pfqn::SchedStrategy> schedfull;
1739 Matrix<int> Sfull(nz + nq, 1, 1);
1740 for (std::size_t a = 0; a < nz; ++a) {
1741 for (std::size_t c = 0; c < C; ++c) Dfull(a, c) = pf.Z(a, c);
1742 nsfull.push_back(1); // an infinite server is marked by its discipline
1743 schedfull.push_back(pfqn::SchedStrategy::INF);
1744 Sfull(a, 0) = 1;
1745 }
1746 for (std::size_t a = 0; a < nq; ++a) {
1747 for (std::size_t c = 0; c < C; ++c) {
1748 Dfull(nz + a, c) = pf.D(a, c);
1749 Vfull(nz + a, c) = d.Vchain(pf.queue_stations[a] - 1, c) > zero
1751 : zero;
1752 }
1753 const int c_i = std::isfinite(pf.S[a]) ? static_cast<int>(std::llround(pf.S[a])) : 1;
1754 nsfull.push_back(c_i);
1755 Sfull(nz + a, 0) = c_i;
1756 schedfull.push_back(types[a]);
1757 }
1758 if constexpr (!num_traits<T>::has_transcendental) {
1759 throw UnsupportedError(
1760 "solver_amva: the Akyildiz-Bolch and Schmidt multiserver methods use marginal "
1761 "weights with non-integer powers and floors, which exact arithmetic has no "
1762 "representation for; use the double or real backend");
1763 } else if (method == "ab") {
1765 Dfull, N, Vfull, nsfull, schedfull, false, pfqn::AbMarginalMethod::Ab);
1766 for (std::size_t a = 0; a < nq; ++a)
1767 for (std::size_t c = 0; c < C; ++c) {
1768 Qq(a, c) = r.QN(nz + a, c);
1769 Uq(a, c) = r.UN(nz + a, c);
1770 }
1771 for (std::size_t a = 0; a < nz; ++a)
1772 for (std::size_t c = 0; c < C; ++c) Qz(a, c) = r.QN(a, c);
1773 X = r.XN;
1774 iters = static_cast<int>(r.totiter);
1775 } else if (method == "schmidt") {
1776 const pfqn::SchmidtResult<T> r = pfqn::pfqn_schmidt(Dfull, N, Sfull, schedfull, Vfull);
1777 for (std::size_t a = 0; a < nq; ++a)
1778 for (std::size_t c = 0; c < C; ++c) Qq(a, c) = r.QN(nz + a, c);
1779 for (std::size_t a = 0; a < nz; ++a)
1780 for (std::size_t c = 0; c < C; ++c) Qz(a, c) = r.QN(a, c);
1781 X = r.XN;
1782 iters = 1;
1783 } else {
1784 // One predicate for the gate and the run, asked about the numbers THIS
1785 // arm passes: pfqn_schmidt_ext forms its alpha correction from the
1786 // network with one class-r customer tagged, and a chain holding no
1787 // customer has none to tag.
1788 {
1789 std::vector<double> sx_n(C, 0.0);
1790 for (std::size_t c = 0; c < C; ++c) sx_n[c] = d.Nchain[c];
1791 std::vector<bool> sx_fcfs;
1792 for (std::size_t a = 0; a < nq; ++a)
1793 sx_fcfs.push_back(L.stations[pf.queue_stations[a] - 1].sched ==
1794 SchedStrategy::FCFS);
1795 const std::string sx_reason =
1796 mva_schmidt_ext_reason(sx_n, sx_fcfs, "schmidt-ext");
1797 if (!sx_reason.empty()) throw UnsupportedError(sx_reason);
1798 }
1799 const pfqn::SchmidtExtResult<T> r = pfqn::pfqn_schmidt_ext(Dfull, N, Sfull, schedfull);
1800 for (std::size_t a = 0; a < nq; ++a)
1801 for (std::size_t c = 0; c < C; ++c) Qq(a, c) = r.QN(nz + a, c);
1802 for (std::size_t a = 0; a < nz; ++a)
1803 for (std::size_t c = 0; c < C; ++c) Qz(a, c) = r.QN(a, c);
1804 X = r.XN;
1805 iters = 1;
1806 }
1807 have_delay_rows = true;
1808 // U = X D / c, which is what the reference recomputes for both Schmidt
1809 // variants rather than reading it back from the algorithm.
1810 for (std::size_t a = 0; a < nq; ++a) {
1811 const double c_i = std::isfinite(pf.S[a]) ? pf.S[a] : 1.0;
1812 for (std::size_t k = 0; k < C; ++k)
1813 Uq(a, k) = T(X[k] * pf.D(a, k) / num_traits<T>::from_double(c_i));
1814 }
1815 } else if (method == "lin" || method == "gflin" || method == "egflin") {
1816 // class- or joint-dependent models go to solver_amvald, which handles
1817 // them; the linearizer kernels do not (solver_amva.m:304-307)
1818 for (const auto& st : L.stations)
1819 if (st.cdscaling || st.jdscaling)
1820 return solver_amvald(L, d, opt, method, converged, init_sol);
1821 const pfqn::LinearizerMxMethod mx = method == "lin" ? pfqn::LinearizerMxMethod::Lin
1822 : method == "gflin" ? pfqn::LinearizerMxMethod::Gflin
1824 if (anyinf) return solver_amvald(L, d, opt, method, converged, init_sol);
1825 if (allone || opt.multiserver == "krzesinski") {
1826 std::vector<int> ns;
1827 for (std::size_t a = 0; a < nq; ++a)
1828 ns.push_back(std::isfinite(pf.S[a]) ? static_cast<int>(std::llround(pf.S[a])) : 1);
1829 if (allone) ns.assign(nq, 1);
1831 pf.lambda, Dm, N, Zm, ns, types, opt.tol, opt.iter_max, mx, Q0);
1832 Qq = r.Q;
1833 Uq = r.U;
1834 X = r.X;
1835 iters = r.totiter;
1836 } else if (opt.multiserver == "conway") {
1837 if constexpr (!num_traits<T>::has_transcendental) {
1838 throw UnsupportedError(
1839 "solver_amva: the Conway multiserver correction evaluates transcendentals, "
1840 "which exact arithmetic has no representation for; use the double or real "
1841 "backend");
1842 } else {
1843 std::vector<int> ns;
1844 for (std::size_t a = 0; a < nq; ++a)
1845 ns.push_back(std::isfinite(pf.S[a]) ? static_cast<int>(std::llround(pf.S[a]))
1846 : 1);
1848 pfqn::pfqn_conwayms(pf.D, N, pf.Z, ns, types, opt.tol, opt.iter_max, Q0);
1849 Qq = r.Q;
1850 Uq = r.U;
1851 X = r.X;
1852 iters = r.totiter;
1853 }
1854 } else {
1855 // default, seidmann, softmin and suri all go to the general AMVA
1856 return solver_amvald(L, d, opt, method, converged, init_sol);
1857 }
1858 } else {
1859 // multiserver-rule reset rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
1860 MvaOptions o2 = opt;
1861 if (o2.multiserver == "conway" || o2.multiserver == "erlang" ||
1862 o2.multiserver == "krzesinski")
1863 o2.multiserver = "default";
1864 return solver_amvald(L, d, o2, method, converged, init_sol);
1865 }
1866
1867 Matrix<T> Q(M, C, zero), U(M, C, zero), Tp(M, C, zero), R(M, C, zero);
1868 for (std::size_t a = 0; a < nq; ++a)
1869 for (std::size_t c = 0; c < C; ++c) {
1870 Q(pf.queue_stations[a] - 1, c) = Qq(a, c);
1871 U(pf.queue_stations[a] - 1, c) = Uq(a, c);
1872 }
1873 // Delay stations: Q = X Z with the ORIGINAL think times, for every method.
1874 // Seidmann folds L(m-1)/m of each multiserver station into Z, but that
1875 // population is in service at the station and is given back to it just
1876 // below; charging Zm here too counted it twice and sum(Q) exceeded N.
1877 for (std::size_t a = 0; a < nz; ++a)
1878 for (std::size_t c = 0; c < C; ++c) {
1879 Q(pf.delay_stations[a] - 1, c) = T(X[c] * pf.Z(a, c));
1880 U(pf.delay_stations[a] - 1, c) = Q(pf.delay_stations[a] - 1, c);
1881 }
1882 (void)have_delay_rows; // the reference overwrites the solved delay rows too
1883 // Un-apply Seidmann back onto the originating queue. ab and schmidt never
1884 // had it applied, so they are exempt, which is what the reference tests.
1885 if (seidmann && !direct_ms) {
1886 for (std::size_t a = 0; a < nq; ++a) {
1887 const double c = pf.S[a];
1888 if (!std::isfinite(c) || c <= 1.0) continue;
1889 for (std::size_t k = 0; k < C; ++k)
1890 Q(pf.queue_stations[a] - 1, k) =
1891 T(Q(pf.queue_stations[a] - 1, k) +
1892 pf.D(a, k) * num_traits<T>::from_double((c - 1.0) / c) * X[k]);
1893 }
1894 }
1895 for (std::size_t i = 0; i < M; ++i)
1896 for (std::size_t c = 0; c < C; ++c) {
1897 Tp(i, c) = T(d.Vchain(i, c) * X[c]);
1898 if (Tp(i, c) != zero) R(i, c) = T(Q(i, c) / Tp(i, c));
1899 }
1900 // Cycle time excludes the think time actually spent at the delays, which is
1901 // the ORIGINAL Z summed over them; the Seidmann Zm disagreed with the station
1902 // residence times sum(R.*V) that Q now reports.
1903 std::vector<T> Cyc(C, zero);
1904 for (std::size_t c = 0; c < C; ++c) {
1905 if (!(X[c] > zero)) continue;
1906 T z = zero;
1907 for (std::size_t a = 0; a < nz; ++a) z += pf.Z(a, c);
1908 Cyc[c] = T(num_traits<T>::from_double(d.Nchain[c]) / X[c] - z);
1909 }
1910
1911 MvaSolution<T> out;
1912 out.method = method;
1913 out.iter = iters;
1914 if (L.has_class_switching()) {
1915 // de-aggregation input rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
1916 const ClassResults<T> cr =
1917 sn_deaggregate_chain_results(L, d, Matrix<T>(), Matrix<T>(), R, Tp, X);
1918 out.Q = cr.Q;
1919 out.U = cr.U;
1920 out.R = cr.R;
1921 out.Tp = cr.Tp;
1922 out.C = cr.C;
1923 out.X = cr.X;
1924 } else {
1925 out.Q = Q;
1926 out.U = U;
1927 out.R = R;
1928 out.Tp = Tp;
1929 out.X = X;
1930 out.C = Cyc;
1931 }
1932 return out;
1933}
1934
1935// ---------------------------------------------------------------------------
1936// solver_mvald: the exact load-dependent recursion
1937// ---------------------------------------------------------------------------
1938
1939/**
1940 * Port of `solver_mvald.m`: exact MVA on a load-dependent model, through
1941 * `pfqn_mvaldmx`.
1942 *
1943 * The rate lattice `mu` is built per station: an infinite server gets the
1944 * linear ramp 1, 2, ..., Nt (which is what makes it a delay in a
1945 * load-dependent recursion), a station with `lldscaling` gets its lattice, and
1946 * everything else gets ones.
1947 *
1948 * THE UTILIZATION IS NOT the recursion's. Under load-dependent scaling the
1949 * reference replaces it with the carried load over the EFFECTIVE capacity
1950 * `max(nservers, max(lldscaling))`, the NC convention, rather than the
1951 * P(busy)-style estimator `pfqn_mvaldmx` returns; the two disagree wherever the
1952 * lattice exceeds the server count.
1953 */
1954template <class T>
1956 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
1958 const std::size_t M = L.nstations, C = L.nchains;
1959
1960 double Nt = 0.0;
1961 for (std::size_t c = 0; c < C; ++c)
1962 if (std::isfinite(d.Nchain[c])) Nt += d.Nchain[c];
1963 const std::size_t NT = static_cast<std::size_t>(std::llround(Nt));
1964
1965 // An open chain contributes its arrival rate and is marked OPEN_CLASS. The
1966 // rate is read at CHAIN level: STchain at an open chain's reference station
1967 // (its Source) is one over the SUM of the class arrival rates, which also
1968 // covers a chain whose classes arrive at several rates.
1969 std::vector<T> lambda(C, zero);
1970 std::vector<int> N(C, 0);
1971 std::vector<bool> openChain(C, false);
1972 std::size_t nOpenChains = 0;
1973 for (std::size_t c = 0; c < C; ++c) {
1974 if (std::isfinite(d.Nchain[c])) {
1975 N[c] = static_cast<int>(std::llround(d.Nchain[c]));
1976 continue;
1977 }
1978 N[c] = pfqn::OPEN_CLASS;
1979 openChain[c] = true;
1980 ++nOpenChains;
1981 const T st = d.STchain(d.refstatchain[c] - 1, c);
1982 if (st > zero) lambda[c] = T(one / st);
1983 }
1984
1985 Matrix<T> Qchain(M, C, zero), Uchain(M, C, zero);
1986 std::vector<T> Xchain(C, zero);
1987 if (nOpenChains == 0) {
1988 // PURELY CLOSED. Every station enters the recursion, an infinite server
1989 // as the load-dependent rate mu(n)=n, exact because n cannot then exceed
1990 // the closed population.
1991 if (NT == 0) throw UnsupportedError("solver_mvald: the model has no closed population");
1992 Matrix<T> mu(M, NT, one);
1993 for (std::size_t i = 0; i < M; ++i) {
1994 if (std::isinf(L.stations[i].nservers)) {
1995 for (std::size_t n = 0; n < NT; ++n)
1996 mu(i, n) = num_traits<T>::from_int(static_cast<long>(n) + 1);
1997 } else if (!L.stations[i].lldscaling.empty()) {
1998 const std::vector<T>& a = L.stations[i].lldscaling;
1999 for (std::size_t n = 0; n < NT; ++n) mu(i, n) = n < a.size() ? a[n] : a.back();
2000 }
2001 }
2002 Matrix<T> Dm(M, C, zero);
2003 for (std::size_t i = 0; i < M; ++i)
2004 for (std::size_t c = 0; c < C; ++c) Dm(i, c) = d.Lchain(i, c);
2005 const pfqn::MvaResult<T> pf = pfqn::pfqn_mvaldmx(lambda, Dm, N, Matrix<T>(), mu);
2006 Xchain = pf.XN;
2007 Qchain = pf.QN;
2008 Uchain = pf.UN;
2009 } else {
2010 // MIXED OR PURELY OPEN. Three kinds of row are not the same thing to
2011 // pfqn_mvaldmx and have to be separated before it is called; this is the
2012 // partition solver_ncld makes for the same recursion.
2013 // - THE SOURCE IS NOT A STATION. Its chain demand is the interarrival
2014 // time 1/lambda, so it carries offered load Lo=1 exactly and
2015 // pfqn_ldmx_ec then forms 1/(1-Lo/mu) = inf, poisoning every chain.
2016 // - A DELAY IS AN INFINITE SERVER FOR THE OPEN CHAINS TOO. mu(n)=n cut
2017 // at the closed population declares it saturated at NT jobs. It enters
2018 // as chain think time instead and its queue length is X*L, exact.
2019 // - A QUEUEING STATION KEEPS ITS WHOLE RATE ROW. pfqn_ldmx_ec reads the
2020 // limited-load-dependence level b off the row itself, so a row cut at
2021 // the closed population is read as a slower station, and with no closed
2022 // class at all it collapses to mu(1), a single fixed-rate server.
2023 std::vector<bool> sourceStation(M, false), delayStation(M, false);
2024 for (std::size_t c = 0; c < C; ++c)
2025 if (openChain[c]) sourceStation[static_cast<std::size_t>(d.refstatchain[c] - 1)] = true;
2026 std::vector<std::size_t> queueStations;
2027 for (std::size_t i = 0; i < M; ++i) {
2028 if (sourceStation[i]) continue;
2029 if (std::isinf(L.stations[i].nservers))
2030 delayStation[i] = true;
2031 else
2032 queueStations.push_back(i);
2033 }
2034 const std::size_t nq = queueStations.size();
2035
2036 Matrix<T> Z(1, C, zero);
2037 for (std::size_t c = 0; c < C; ++c) {
2038 T z = zero;
2039 for (std::size_t i = 0; i < M; ++i)
2040 if (delayStation[i]) z = T(z + d.Lchain(i, c));
2041 Z(0, c) = z;
2042 }
2043
2044 std::size_t ncol = NT > 0 ? NT : 1;
2045 for (std::size_t k = 0; k < nq; ++k) {
2046 // first column of the trailing constant run, the level b of pfqn_ldmx_ec
2047 const std::vector<T>& a = L.stations[queueStations[k]].lldscaling;
2048 std::size_t b = a.size();
2049 while (b > 1 && a[b - 2] == a[b - 1]) --b;
2050 ncol = std::max(ncol, b);
2051 }
2052 Matrix<T> mu(nq, ncol, one);
2053 for (std::size_t k = 0; k < nq; ++k) {
2054 const std::vector<T>& a = L.stations[queueStations[k]].lldscaling;
2055 if (a.empty()) continue;
2056 // held at the row's last value past its own end: limited load dependence
2057 for (std::size_t n = 0; n < ncol; ++n) mu(k, n) = n < a.size() ? a[n] : a.back();
2058 }
2059 Matrix<T> Dq(nq, C, zero);
2060 for (std::size_t k = 0; k < nq; ++k)
2061 for (std::size_t c = 0; c < C; ++c) Dq(k, c) = d.Lchain(queueStations[k], c);
2062
2063 const pfqn::MvaResult<T> pf = pfqn::pfqn_mvaldmx(lambda, Dq, N, Z, mu);
2064 Xchain = pf.XN;
2065 for (std::size_t k = 0; k < nq; ++k)
2066 for (std::size_t c = 0; c < C; ++c) {
2067 Qchain(queueStations[k], c) = pf.QN(k, c);
2068 Uchain(queueStations[k], c) = pf.UN(k, c);
2069 }
2070 for (std::size_t i = 0; i < M; ++i)
2071 if (delayStation[i])
2072 for (std::size_t c = 0; c < C; ++c)
2073 // infinite server: X*L for a closed chain, lambda*L for an open one
2074 Qchain(i, c) = T(d.Lchain(i, c) * Xchain[c]);
2075 }
2076
2077 // Rchain IS THE PER-VISIT RESPONSE TIME, Qchain / Tchain, not the residence
2078 // time Qchain / Xchain. `sn_deaggregate_chain_results` rebuilds the class
2079 // queue length as `Rchain * Xchain * Vchain(i,c)/Vchain(refstat,c)`, so it
2080 // multiplies the visit ratio back IN; handing it a residence time counts
2081 // that ratio twice and the reported queue lengths then no longer sum to the
2082 // population. Invisible on every load-dependent model whose LD station has
2083 // the reference station's chain visits (the shipped examples all do), and
2084 // decisive on one that does not -- the closed delayed-hit retrieval cache,
2085 // whose fetch station is visited once per MISS. Every sibling analyzer here
2086 // (:300, :1046, :1690) already divides by Tchain; this line did not.
2087 // MATLAB's solver_mvald.m:45 carried the same divisor and is fixed with it;
2088 // Python's exact LD path already divided by Tchain and was right.
2089 Matrix<T> Tchain(M, C, zero), Rchain(M, C, zero);
2090 for (std::size_t i = 0; i < M; ++i)
2091 for (std::size_t c = 0; c < C; ++c) {
2092 Tchain(i, c) = T(Xchain[c] * d.Vchain(i, c));
2093 if (Tchain(i, c) > zero) Rchain(i, c) = T(Qchain(i, c) / Tchain(i, c));
2094 }
2095
2096 const ClassResults<T> cr =
2097 sn_deaggregate_chain_results(L, d, Matrix<T>(), Uchain, Rchain, Tchain, Xchain);
2098 MvaSolution<T> out;
2099 out.Q = cr.Q;
2100 out.U = cr.U;
2101 out.R = cr.R;
2102 out.Tp = cr.Tp;
2103 out.C = cr.C;
2104 out.X = cr.X;
2105 out.method = "exact";
2106 out.iter = 1;
2107
2108 for (std::size_t i = 0; i < M; ++i) {
2109 if (L.stations[i].lldscaling.empty() || !std::isfinite(L.stations[i].nservers)) continue;
2110 double ceff = L.stations[i].nservers;
2111 for (const T& v : L.stations[i].lldscaling)
2112 ceff = std::max(ceff, num_traits<T>::to_double(v));
2113 for (std::size_t r = 0; r < L.nclasses; ++r) {
2114 if (L.disabled[i][r]) continue;
2115 const T st = L.rates(i, r) > zero ? T(one / L.rates(i, r)) : zero;
2116 if (st > zero) out.U(i, r) = T(out.Tp(i, r) * st / num_traits<T>::from_double(ceff));
2117 }
2118 }
2119 return out;
2120}
2121
2122// ---------------------------------------------------------------------------
2123// solver_mva_sum: the summation method
2124// ---------------------------------------------------------------------------
2125
2126/**
2127 * Port of `solver_mva_sum.m`: the SUM / ESUM summation method (Bolch et al.,
2128 * Secs. 9.2, 10.1.4.4 and 10.1.5).
2129 *
2130 * A closed model goes to `sum_closed`; an open or mixed one to `sum_closing`
2131 * with the reference's Kclosed = 5000. The SCV each station is given is the
2132 * ESUM discrimination: FCFS and SIRO are service-time sensitive and get the
2133 * chain SCV, while PS, LCFSPR and the infinite servers are insensitive and are
2134 * passed 1 -- handing them their real SCV would apply a correction that the
2135 * product form says does not exist.
2136 */
2137template <class T>
2139 using qn::SchedStrategy;
2140 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
2142 const std::size_t M = L.nstations, K = L.nchains;
2143
2144 if constexpr (!num_traits<T>::has_transcendental) {
2145 throw UnsupportedError(
2146 "solver_mva_sum: the summation method locates the root of its population constraint "
2147 "by bisection and evaluates the Erlang-C waiting probability, neither of which exact "
2148 "arithmetic has a representation for; use the double or real backend");
2149 } else {
2150 std::vector<std::size_t> rows;
2151 std::vector<sum::Servers> mi;
2152 for (std::size_t i = 0; i < M; ++i) {
2153 const SchedStrategy sc = L.stations[i].sched;
2154 if (sc == SchedStrategy::EXT) continue; // the external world is lambda
2155 if (sc == SchedStrategy::INF) {
2156 rows.push_back(i);
2157 mi.push_back(sum::Servers::inf());
2158 continue;
2159 }
2160 if (sc == SchedStrategy::PS || sc == SchedStrategy::LCFSPR ||
2161 sc == SchedStrategy::FCFS || sc == SchedStrategy::SIRO) {
2162 rows.push_back(i);
2163 const double c = L.stations[i].nservers;
2164 mi.push_back(std::isfinite(c) ? sum::Servers::of(static_cast<long>(std::llround(c)))
2165 : sum::Servers::inf());
2166 continue;
2167 }
2168 throw UnsupportedError(std::string("solver_mva_sum: the summation method does not "
2169 "support ") +
2170 lang::sched_to_text(sc) + " scheduling");
2171 }
2172
2173 const std::size_t Mr = rows.size();
2174 Matrix<T> Lm(Mr, K, zero), scv(Mr, K, one);
2175 for (std::size_t a = 0; a < Mr; ++a) {
2176 const std::size_t i = rows[a];
2177 const bool sensitive = L.stations[i].sched == SchedStrategy::FCFS ||
2178 L.stations[i].sched == SchedStrategy::SIRO;
2179 for (std::size_t c = 0; c < K; ++c) {
2180 Lm(a, c) = T(d.STchain(i, c) * d.Vchain(i, c));
2181 if (!sensitive) continue;
2182 const double v = num_traits<T>::to_double(d.SCVchain(i, c));
2183 if (std::isfinite(v) && v > 0.0) scv(a, c) = num_traits<T>::from_double(v);
2184 }
2185 }
2186
2187 std::vector<long> N(K, 0);
2188 std::vector<T> Z(K, zero);
2189 std::vector<std::size_t> ocl;
2190 for (std::size_t c = 0; c < K; ++c) {
2191 if (std::isfinite(d.Nchain[c])) N[c] = static_cast<long>(std::llround(d.Nchain[c]));
2192 else ocl.push_back(c);
2193 }
2194
2195 Matrix<T> Qrows, Urows;
2196 std::vector<T> Xchain;
2197 std::size_t iters = 0;
2198 if (ocl.empty()) {
2199 sum::SumOptions so;
2200 so.tol = opt.iter_tol;
2201 so.maxiter = static_cast<std::size_t>(opt.iter_max);
2202 const sum::SumClosedResult<T> r = sum::sum_closed(Lm, N, Z, mi, scv, so);
2203 Qrows = r.QN;
2204 Urows = r.UN;
2205 Xchain = r.XN;
2206 iters = r.it;
2207 } else {
2208 std::vector<T> lambda(K, zero), scva(K, one);
2209 for (std::size_t c : ocl) {
2210 const T st = d.STchain(d.refstatchain[c] - 1, c);
2211 // AN OPEN CHAIN WITH NO ARRIVAL RATE IS NOT A CHAIN OF RATE ZERO.
2212 // Leaving lambda[c] at zero makes `sum_closing` treat it as a CLOSED
2213 // chain of zero population, which is a different model solved
2214 // without complaint. The service time at the reference station is
2215 // where the arrival rate comes from, so a non-positive one means the
2216 // chain is unspecified, not idle.
2217 if (!(st > zero))
2218 throw InputError(
2219 "solver_mva_sum: open chain " + std::to_string(c + 1) +
2220 " has a non-positive service time at its reference station, so it carries no "
2221 "arrival rate; the summation method cannot place it");
2222 lambda[c] = T(one / st);
2223 const double v = num_traits<T>::to_double(d.SCVchain(d.refstatchain[c] - 1, c));
2224 if (std::isfinite(v) && v > 0.0) scva[c] = num_traits<T>::from_double(v);
2225 }
2227 co.sum.tol = opt.iter_tol;
2228 co.sum.maxiter = static_cast<std::size_t>(opt.iter_max);
2229 const sum::SumClosingResult<T> r = sum::sum_closing(lambda, scva, Lm, mi, scv, N, Z, co);
2230 Qrows = r.QN;
2231 Urows = r.UN;
2232 Xchain = r.XN;
2233 iters = r.it;
2234 }
2235
2236 Matrix<T> Qchain(M, K, zero), Uchain(M, K, zero), Tchain(M, K, zero), Rchain(M, K, zero);
2237 for (std::size_t a = 0; a < Mr; ++a)
2238 for (std::size_t c = 0; c < K; ++c) {
2239 Qchain(rows[a], c) = Qrows(a, c);
2240 Uchain(rows[a], c) = Urows(a, c);
2241 }
2242 for (std::size_t i = 0; i < M; ++i)
2243 for (std::size_t c = 0; c < K; ++c) {
2244 Tchain(i, c) = T(Xchain[c] * d.Vchain(i, c));
2245 if (Tchain(i, c) > zero) Rchain(i, c) = T(Qchain(i, c) / Tchain(i, c));
2246 }
2247 for (std::size_t c = 0; c < K; ++c) {
2248 if (d.Nchain[c] != 0.0) continue;
2249 Xchain[c] = zero;
2250 for (std::size_t i = 0; i < M; ++i) {
2251 Qchain(i, c) = zero;
2252 Uchain(i, c) = zero;
2253 Rchain(i, c) = zero;
2254 Tchain(i, c) = zero;
2255 }
2256 }
2257
2258 const ClassResults<T> cr =
2259 sn_deaggregate_chain_results(L, d, Matrix<T>(), Matrix<T>(), Rchain, Tchain, Xchain);
2260 MvaSolution<T> out;
2261 out.Q = cr.Q;
2262 out.U = cr.U;
2263 out.R = cr.R;
2264 out.Tp = cr.Tp;
2265 out.C = cr.C;
2266 out.X = cr.X;
2267 out.method = opt.method;
2268 out.iter = static_cast<int>(iters);
2269 return out;
2270 }
2271}
2272
2273// ---------------------------------------------------------------------------
2274// Blocking-after-service: the SQD handler
2275// ---------------------------------------------------------------------------
2276
2277/**
2278 * Port of `solver_sqd.m`: Smith queue decomposition for blocking-after-service.
2279 *
2280 * `npfqn_sqd` is single-chain by construction -- it solves ONE circulating
2281 * population -- so a multichain model is refused. The reference warns and
2282 * returns a matrix of NaN there; refusing by name is the same information
2283 * without a result that reads as solved.
2284 *
2285 * The unpacking is the one `npfqn_sqd.h` documents: the chain-aggregated demand
2286 * and visit columns, an infinite capacity at every delay, and `sn.rt` with its
2287 * stateful indexing.
2288 */
2289template <class T>
2291 using qn::SchedStrategy;
2292 // The effective-rate calibration evaluates exp, log and real powers, and the
2293 // M/M/1/K blocking probability a real power, so there is no exact-field
2294 // version of this method. Refuse at RUN time rather than at compile time,
2295 // as every other transcendental analyzer in this port does.
2296 if constexpr (!num_traits<T>::has_transcendental) {
2297 (void)L;
2298 (void)d;
2299 throw UnsupportedError(
2300 "solver_sqd: the blocking-after-service calibration evaluates exp, log and real "
2301 "powers, which exact rational arithmetic cannot represent");
2302 } else {
2303 const T zero = num_traits<T>::from_int(0);
2304 const std::size_t M = L.nstations;
2305 if (L.nchains != 1)
2306 throw UnsupportedError(
2307 "solver_sqd: Smith queue decomposition supports single-chain closed networks only; "
2308 "this model has " + std::to_string(L.nchains) + " chains");
2309 // AND CLOSED, which the chain count alone does not establish. The automatic
2310 // route here is gated by `mva_is_bas_model`, which excludes open classes, but
2311 // the explicit `method == "sqd"` route is not: an open model reaches
2312 // npfqn_sqd with a population counting only the closed classes and gets a
2313 // silently wrong answer rather than a refusal.
2314 if (L.has_open_classes())
2315 throw UnsupportedError(
2316 "solver_sqd: Smith queue decomposition is defined for a CLOSED population; this model "
2317 "has an open class, whose jobs the fixed point cannot count");
2318
2319 std::vector<T> ST(M, zero), V(M, zero), cap(M, zero);
2320 std::vector<bool> isDelay(M, false);
2321 std::vector<std::size_t> s2sf(M, 0);
2322 const T inf = num_traits<T>::from_double(std::numeric_limits<double>::infinity());
2323 for (std::size_t i = 0; i < M; ++i) {
2324 ST[i] = d.STchain(i, 0);
2325 V[i] = d.Vchain(i, 0);
2326 isDelay[i] = (L.stations[i].sched == SchedStrategy::INF ||
2327 L.stations[i].sched == SchedStrategy::EXT);
2328 // a capacity above 1e14 is MATLAB's own "effectively unbounded" test
2329 const double c = L.cap.empty() ? std::numeric_limits<double>::infinity() : L.cap[i];
2330 cap[i] = (isDelay[i] || !(c <= 1e14)) ? inf : num_traits<T>::from_double(c);
2331 s2sf[i] = L.stateful_of_station(i + 1);
2332 }
2333
2335 const npfqn::SqdResult<T> r =
2336 npfqn::npfqn_sqd(ST, V, cap, isDelay, L.rt, s2sf, L.nclasses,
2337 static_cast<int>(std::llround(L.nclosedjobs())), sopt);
2338
2339 // Vchain is normalized to 1 at the reference station, so the per-station
2340 // throughput there IS the chain reference throughput.
2341 const std::size_t refstat = d.refstatchain.empty() ? 1 : d.refstatchain[0];
2342 Matrix<T> Tchain(M, 1, zero), Qchain(M, 1, zero), Uchain(M, 1, zero), Rchain(M, 1, zero);
2343 for (std::size_t i = 0; i < M; ++i) {
2344 Tchain(i, 0) = r.X[i];
2345 Qchain(i, 0) = r.Q[i];
2346 Uchain(i, 0) = r.U[i];
2347 Rchain(i, 0) = r.R[i];
2348 }
2349 std::vector<T> Xchain(1, r.X[refstat - 1]);
2350
2351 const ClassResults<T> cr =
2352 sn_deaggregate_chain_results(L, d, Qchain, Uchain, Rchain, Tchain, Xchain);
2353 MvaSolution<T> out;
2354 out.Q = cr.Q;
2355 out.U = cr.U;
2356 out.R = cr.R;
2357 out.Tp = cr.Tp;
2358 out.C = cr.C;
2359 out.X = cr.X;
2360 out.method = "sqd";
2361 out.lG = std::numeric_limits<double>::quiet_NaN();
2362 return out;
2363 }
2364}
2365
2366/**
2367 * Port of `isBasModel` in `solver_mva_analyzer.m`: a closed single-CHAIN model
2368 * with at least one blocking-after-service drop rule, which SQD handles and
2369 * neither exact MVA nor AMVA does. This is the DISPATCH test.
2370 */
2371template <class T>
2373 if (L.nchains != 1 || !(L.nclosedjobs() > 0.0) || L.droprule.empty()) return false;
2374 for (const auto& c : L.classes)
2375 if (std::isinf(c.population)) return false; // an open class present
2376 for (const auto& row : L.droprule)
2377 for (lang::DropStrategy s : row)
2378 if (s == lang::DropStrategy::BAS) return true;
2379 return false;
2380}
2381
2382/**
2383 * Port of `matlab/src/api/sn/sn_is_bas_model.m`: a closed single-CLASS model
2384 * with a BAS drop rule. This is the GATE test, and it is deliberately NARROWER
2385 * than mva_is_bas_model above: the reference keeps two predicates because SQD's
2386 * Smith decomposition models ONE circulating population, so a chain built from
2387 * several classes is dispatched to SQD but is NOT exempted from the
2388 * finite-capacity refusal. Collapsing the two would let a multiclass BAS model
2389 * past the gate on the strength of a decomposition that does not represent it.
2390 */
2391template <class T>
2393 if (L.nclasses != 1 || !(L.nclosedjobs() > 0.0) || L.droprule.empty()) return false;
2394 for (const auto& c : L.classes)
2395 if (std::isinf(c.population)) return false; // an open class present
2396 for (const auto& row : L.droprule)
2397 for (lang::DropStrategy s : row)
2398 if (s == lang::DropStrategy::BAS) return true;
2399 return false;
2400}
2401
2402/**
2403 * Port of `matlab/src/api/sn/sn_is_mm1k_loss.m`: a single-class open
2404 * Source-Queue-Sink system whose queue is a single-server exponential M/M/1/K
2405 * with tail drop. Both the MVA moment-based branch (qsys_mg1k_loss_mgs) and the
2406 * NC probability-based one (qsys_mm1k_loss) gate their finite-capacity loss
2407 * path on it, and it is the one shape whose finite buffer IS honoured here.
2408 */
2409template <class T>
2411 if (L.nclasses != 1 || L.nclosedjobs() != 0.0 || L.nodes.size() != 3) return false;
2412 std::size_t nsrc = 0, nq = 0, nsnk = 0;
2413 for (const qn::NodeDef& nd : L.nodes) {
2414 if (nd.nodetype == qn::NodeType::Source) ++nsrc;
2415 else if (nd.nodetype == qn::NodeType::Queue) ++nq;
2416 else if (nd.nodetype == qn::NodeType::Sink) ++nsnk;
2417 }
2418 if (nsrc != 1 || nq != 1 || nsnk != 1) return false;
2419 std::size_t qist = 0, sist = 0;
2420 for (std::size_t i = 0; i < L.stations.size(); ++i) {
2421 if (L.stations[i].nodetype == qn::NodeType::Queue) qist = i + 1;
2422 else if (L.stations[i].nodetype == qn::NodeType::Source) sist = i + 1;
2423 }
2424 if (qist == 0 || sist == 0) return false;
2425 if (L.stations[qist - 1].nservers != 1.0) return false;
2426 if (L.droprule.size() < qist || L.droprule[qist - 1].empty()) return false;
2427 if (L.droprule[qist - 1][0] != lang::DropStrategy::DROP) return false;
2428 if (L.cap.size() < qist) return false;
2429 if (!std::isfinite(L.cap[qist - 1]) || L.cap[qist - 1] <= 0.0) return false;
2430 const double scvs = num_traits<T>::to_double(L.scv(sist - 1, 0));
2431 const double scvq = num_traits<T>::to_double(L.scv(qist - 1, 0));
2432 return std::fabs(scvs - 1.0) <= 1e-6 && std::fabs(scvq - 1.0) <= 1e-6;
2433}
2434
2435/**
2436 * Conservative form of the branch test in solver_amva: true when this model may be solved by
2437 * the product-form AMVA kernels rather than by solver_amvald. The mixed case is reported true
2438 * for every resolved method, while the branch itself takes it only for some, so a caller is
2439 * never told that solver_amvald will run when it might not.
2440 */
2441template <class T>
2443 bool has_ld = false;
2444 for (const auto& st : L.stations)
2445 if (!st.lldscaling.empty() || st.cdscaling || st.jdscaling) has_ld = true;
2446 return L.has_product_form_not_het_fcfs() && !has_ld &&
2447 (!L.has_open_classes() || L.has_product_form());
2448}
2449
2450/**
2451 * True when the MVA path this model already dispatches to carries a class-level interlock
2452 * matrix (Franks 1999, Eq. 4.7) itself, so that supplying one does not silently move the model
2453 * to a DIFFERENT algorithm.
2454 *
2455 * Only two kernels implement the correction: pfqn_mva (exact, closed single-server) and the
2456 * AMVA forward step of solver_amvald. A model that would otherwise be solved by exact
2457 * multiserver or mixed MVA, or by the product-form AMVA kernels, cannot take the matrix
2458 * without swapping its algorithm, and the swap is worth far more than the correction it
2459 * carries: inside SolverLN it can turn a converging Picard iteration into a limit cycle. A
2460 * caller holding a matrix such a model cannot carry must apply its own correction instead.
2461 */
2462template <class T>
2464 const std::string& method = opt.method;
2465 bool has_open = false, has_closed = false, integral = true, has_finite_server = false;
2466 double maxsrv = -1.0, Nsum = 0.0;
2467 for (const auto& c : L.classes) {
2468 if (std::isinf(c.population)) {
2469 has_open = true;
2470 } else {
2471 if (c.population > 0.0) has_closed = true;
2472 if (c.population != std::floor(c.population)) integral = false;
2473 }
2474 Nsum += c.population;
2475 }
2476 for (const auto& st : L.stations)
2477 if (std::isfinite(st.nservers)) {
2478 has_finite_server = true;
2479 maxsrv = std::max(maxsrv, st.nservers);
2480 }
2481 // pfqn_mva takes the matrix for a closed single-server model, and for nothing else
2482 const bool pfqn_mva_can_take_it = !has_open && integral && (maxsrv < 0.0 || maxsrv <= 1.0);
2483
2484 if (method == "exact" || method == "mva") return pfqn_mva_can_take_it;
2485 static const char* amva_methods[] = {
2486 "amva", "bs", "qd", "qli", "fli", "lin", "qdlin", "sqni",
2487 "gflin", "egflin", "ab", "schmidt", "schmidt-ext", "tay", "scat", "aql",
2488 "qsa", "lcp", "chow", "pamb", "pami", "pamt", "clust", "dmlin",
2489 "priomva"};
2490 for (const char* m : amva_methods)
2491 if (method == m) return !amva_uses_pf_kernels(L); // only solver_amvald carries it
2492 if (method == "default") {
2493 if (mva_is_bas_model(L)) return false; // solver_sqd has no interlock term
2494 const bool exact_mixed = has_open && has_closed && has_finite_server && maxsrv == 1.0 &&
2495 L.has_product_form() && integral;
2496 const bool exact_small = L.nchains <= 4 && Nsum <= 20.0 && L.has_product_form() &&
2498 if (exact_mixed || exact_small) return pfqn_mva_can_take_it;
2499 return !amva_uses_pf_kernels(L);
2500 }
2501 // mvac, sqd, sum, qna, rqna and rqt reach neither kernel
2502 return false;
2503}
2504
2505// ---------------------------------------------------------------------------
2506// solver_mva_analyzer: the `default` ladder
2507// ---------------------------------------------------------------------------
2508
2509/** Port of solver_mva_analyzer.m, `default` and the explicit method names. */
2510template <class T>
2512 const Matrix<T>& init_sol) {
2514 bool converged = true;
2515
2516 if (opt.method == "exact" || opt.method == "mva") return solver_mva(L, d, opt);
2517 if (opt.method == "sum" || opt.method == "esum") return solver_mva_sum(L, opt);
2518 if (opt.method == "sqd") return solver_sqd(L, d);
2519 // `qna` is handled one level up, in mva_dispatch, purely so that
2520 // solver_qna.h need not include this header; reaching it here means the
2521 // analyzer was called directly, which no ported path does.
2522 if (opt.method == "qna")
2523 throw UnsupportedError(
2524 "solver_mva_analyzer: 'qna' is dispatched by mva_dispatch, not by the analyzer");
2525 if (opt.method == "rqt")
2526 throw UnsupportedError(
2527 "solver_mva_analyzer: 'rqt' is dispatched by mva_dispatch, not by the analyzer");
2528 if (opt.method == "rqna")
2529 throw UnsupportedError(
2530 "solver_mva_analyzer: 'rqna' is dispatched by mva_dispatch, not by the analyzer");
2531 if (opt.method != "default") {
2532 MvaOptions o = opt;
2533 return solver_amva(L, d, o, init_sol, converged);
2534 }
2535
2536 // BAS: neither exact MVA nor AMVA represents the blocked-server time, so the
2537 // reference sends it to SQD before every product-form test below.
2538 if (mva_is_bas_model(L)) return solver_sqd(L, d);
2539
2540 // Force AMVA for class- or joint-dependent models: exact MVA does not
2541 // support them (solver_mva_analyzer.m:63-67).
2542 for (const auto& st : L.stations)
2543 if (st.cdscaling || st.jdscaling) {
2544 MvaOptions o = opt;
2545 return solver_amva(L, d, o, init_sol, converged);
2546 }
2547
2548 // An interlock matrix that exact MVA cannot honour sends the model to AMVA, which
2549 // applies the same Eq. (4.7) correction to the arrival-instant queue length: pfqn_mva
2550 // carries it for closed single-server models only.
2551 bool il_needs_amva = false;
2552 if (!opt.interlock.empty()) {
2553 il_needs_amva = L.has_open_classes();
2554 for (const auto& st : L.stations)
2555 if (std::isfinite(st.nservers) && st.nservers > 1.0) il_needs_amva = true;
2556 }
2557
2558 // mixed open+closed exact-MVA gate rationale: see _kb/06-solver-catalog.md (cpp port notes: solver_mva.h)
2559 if (!il_needs_amva && L.has_open_classes()) {
2560 bool anyopen = false, anyclosed = false, integral = true;
2561 for (const auto& cl : L.classes) {
2562 if (std::isinf(cl.population)) {
2563 anyopen = true;
2564 continue;
2565 }
2566 if (cl.population > 0.0) anyclosed = true;
2567 if (cl.population != std::floor(cl.population)) integral = false;
2568 }
2569 bool anyfinite = false, allone = true;
2570 for (const auto& st : L.stations)
2571 if (std::isfinite(st.nservers)) {
2572 anyfinite = true;
2573 if (st.nservers != 1.0) allone = false;
2574 }
2575 if (anyopen && anyclosed && anyfinite && allone && integral && L.has_product_form())
2576 return solver_mva(L, d, opt);
2577 }
2578
2579 double Nsum = 0.0;
2580 for (std::size_t c = 0; c < L.nchains; ++c) Nsum += d.Nchain[c];
2581 if (!il_needs_amva && L.nchains <= 4 && Nsum <= 20.0 && L.has_product_form() &&
2583 return solver_mva(L, d, opt);
2584
2585 MvaOptions o = opt;
2586 return solver_amva(L, d, o, init_sol, converged);
2587}
2588
2589} // namespace mva
2590} // namespace line
2591
2592#endif // LINE_SOLVERS_MVA_SOLVER_MVA_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::size_t stateful_of_station(std::size_t st) const
std::size_t nof_stateful() const
bool has_homogeneous_scheduling(SchedStrategy) const
Port of sn_has_homogeneous_scheduling.
std::vector< std::vector< bool > > disabled
Matrix< T > rt
sn.rt and sn.rtnodes: the class-expanded routing.
std::vector< double > cap
sn.cap and sn.classcap: the total and per-class buffers.
std::vector< JobClass > classes
bool has_product_form_not_het_fcfs(bool check_means=true) const
Port of sn_has_product_form_not_het_fcfs: LCFS is excluded, and at FCFS the service must be exponenti...
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< NodeDef > nodes
every node, in creation order
std::vector< std::vector< DropStrategy > > droprule
double nclosedjobs() const
sn.nclosedjobs: the total population of the closed classes.
bool has_fractional_populations() const
static void loop(const char *fmt,...)
Announce an iteration loop and reset its reporting budget.
static void iter(long k, const char *fmt,...)
Report iteration k of the current loop.
static bool owns_log()
True only inside the OUTERMOST open run; gates EMISSION.
The exception types the port throws.
Running progress log of a LINE solver run (the "solver console").
The option and result types every MVA analyzer shares.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:424
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:639
PfChainParams< T > sn_get_product_form_chain_params(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d)
Port of sn_get_product_form_chain_params.
Definition sn_chain.h:311
MvaSolution< T > solver_mva(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, const MvaOptions &opt)
Port of solver_mva.m, closed and mixed product-form networks.
Definition solver_mva.h:153
MvaSolution< T > solver_amvald(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, const MvaOptions &opt, const std::string &method, bool &converged, const Matrix< T > &init_sol=Matrix< T >())
Port of solver_amvald.m together with solver_amvald_forward.m, restricted to the INF / PS / FCFS-fami...
Definition solver_mva.h:458
std::string mva_schmidt_ext_reason(const std::vector< double > &njobs, const std::vector< bool > &fcfs, const std::string &method)
The extended Schmidt method needs a customer of every class to tag.
Definition mva_types.h:219
bool sn_is_mm1k_loss(const qn::NetworkStruct< T > &L)
Port of matlab/src/api/sn/sn_is_mm1k_loss.m: a single-class open Source-Queue-Sink system whose queue...
bool amva_uses_pf_kernels(const qn::NetworkStruct< T > &L)
Conservative form of the branch test in solver_amva: true when this model may be solved by the produc...
std::string amva_method_alias(const std::string &m)
The amva.
bool sn_is_bas_model(const qn::NetworkStruct< T > &L)
Port of matlab/src/api/sn/sn_is_bas_model.m: a closed single-CLASS model with a BAS drop rule.
Matrix< T > sn_interlock_chain(const qn::NetworkStruct< T > &L, const std::vector< std::vector< double > > &ILclass)
Aggregate a class-indexed interlock matrix to the chain basis the MVA analyzers work in.
Definition sn_chain.h:358
MvaSolution< T > solver_mva_sum(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_sum.m: the SUM / ESUM summation method (Bolch et al., Secs.
bool mva_is_bas_model(const qn::NetworkStruct< T > &L)
Port of isBasModel in solver_mva_analyzer.m: a closed single-CHAIN model with at least one blocking-a...
ClassResults< T > sn_deaggregate_chain_results(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, const Matrix< T > &Qchain, const Matrix< T > &Uchain, const Matrix< T > &Rchain, const Matrix< T > &Tchain, const std::vector< T > &Xchain)
Port of sn_deaggregate_chain_results.
Definition sn_chain.h:214
MvaSolution< T > solver_sqd(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d)
Port of solver_sqd.m: Smith queue decomposition for blocking-after-service.
std::string mva_closed_population_reason(const qn::NetworkStruct< T > &L, const std::string &method)
Per-method structural gates, shared by list_valid_methods and by the analyzers themselves.
Definition mva_types.h:145
MvaSolution< T > solver_mvald(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mvald.m: exact MVA on a load-dependent model, through pfqn_mvaldmx.
MvaSolution< T > solver_mva_lcfsqn(const qn::NetworkStruct< T > &L, std::size_t lcfs_ist, std::size_t lcfspr_ist)
Port of solver_mva_lcfsqn.m: the closed two-station LCFS + LCFS-PR network.
Definition solver_mva.h:86
MvaSolution< T > solver_amva(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, MvaOptions opt, const Matrix< T > &init_sol, bool &converged)
bool mva_carries_interlock(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
True when the MVA path this model already dispatches to carries a class-level interlock matrix (Frank...
MvaSolution< T > solver_mva_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt, const Matrix< T > &init_sol)
Port of solver_mva_analyzer.m, default and the explicit method names.
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
SqdResult< T > npfqn_sqd(const std::vector< T > &ST, const std::vector< T > &V, const std::vector< T > &cap, const std::vector< bool > &isDelay, const Matrix< T > &rt, const std::vector< std::size_t > &stationToStateful, std::size_t nclasses, int N, const SqdOptions< T > &opt)
Smith Queue Decomposition (SQD): approximate MVA for closed networks under Blocking-After-Service (ma...
Definition npfqn_sqd.h:248
SqniResult< T > pfqn_sqni(const std::vector< T > &N, const std::vector< T > &L, const std::vector< T > &Z)
Square-root non-iterative (SQNI) approximation for a single queueing station with per-class delay.
Definition pfqn_sqni.h:57
SchmidtExtResult< T > pfqn_schmidt_ext(const Matrix< T > &D, const std::vector< int > &N, const Matrix< int > &S, const std::vector< SchedStrategy > &sched)
Extended Schmidt MVA with queue-aware alpha corrections.
LinearizerResult< T > pfqn_scat(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< SchedStrategy > &type, double tol, int maxiter, const Matrix< T > &QN0)
Neuse-Chandy SCAT (Self-Correcting Approximation Technique) approximate MVA.
Definition pfqn_scat.h:71
AmvaResult< T > pfqn_clust(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< std::vector< std::size_t > > &subnets, const std::vector< std::vector< std::size_t > > &localclasses, ClustInner inner=ClustInner::Linearizer, double tol=1e-6, std::size_t maxiter=1000)
de Souza e Silva-Lavenberg-Muntz Clustering Approximation (CA).
Definition pfqn_clust.h:214
AmvaResult< T > pfqn_pam(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, PamVariant variant=PamVariant::Basic)
Hsieh-Lam Proportional Approximation Methods (PAMB / PAMI / PAMT).
Definition pfqn_pam.h:55
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
LinearizerMxMethod
Which Linearizer variant solves the closed subnetwork.
std::vector< T > pfqn_jdfun(const Matrix< T > &nvec, const std::vector< JdScaling< T > > &jdscaling, std::size_t classIdx)
AMVA joint-dependence function for non-product-form scaling.
Definition pfqn_jdfun.h:59
LinearizerResult< T > pfqn_conwayms(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &nservers, const std::vector< SchedStrategy > &type, double tol, int maxiter, const Matrix< T > &QN0)
Conway's multiserver Linearizer for chain-dependent FCFS queues (Conway 1989, "Fast Approximate Solut...
LinearizerResult< T > pfqn_dmlin(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< SchedStrategy > &type, double tol, int maxiter, const Matrix< T > &QN0, int npasses=3)
de Souza e Silva-Muntz Improved Linearizer (IL).
Definition pfqn_dmlin.h:132
AmvaResult< T > pfqn_tay(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >())
Tay's arrival-instant approximate MVA.
Definition pfqn_tay.h:83
AmvaResult< T > pfqn_qsa(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-10, std::size_t maxiter=100, int levels=3)
Queue-Shift Approximation (QSA) for closed product-form networks.
Definition pfqn_qsa.h:221
std::vector< T > pfqn_lldfun(const std::vector< T > &n, const Matrix< T > &lldscaling, const std::vector< double > &nservers)
AMVA-QD limited-load-dependence function.
Definition pfqn_lldfun.h:81
SchmidtResult< T > pfqn_schmidt(const Matrix< T > &D, const std::vector< int > &N, const Matrix< int > &S, const std::vector< SchedStrategy > &sched, const Matrix< T > &v)
Schmidt's MVA for closed networks with general scheduling disciplines and class-dependent multiserver...
AmvaResult< T > pfqn_chow(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >(), ChowVariant variant=ChowVariant::Forward)
Chow Second Approximation (SA) approximate MVA.
Definition pfqn_chow.h:61
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 kOpenClass
Population sentinel marking an open class, standing in for MATLAB's Inf.
LinearizerResult< T > pfqn_linearizermx(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &nservers, const std::vector< SchedStrategy > &type, double tol, int maxiter, LinearizerMxMethod method, const Matrix< T > &QN0)
Linearizer for mixed open/closed queueing networks.
AmvaResult< T > pfqn_bs(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >())
Bard-Schweitzer approximate MVA.
Definition pfqn_bs.h:73
AbAmvaResult< T > pfqn_ab_amva(const Matrix< T > &S, const std::vector< int > &N, const Matrix< T > &v, const std::vector< int > &nservers, const std::vector< SchedStrategy > &sched, bool fcfsSchmidt, AbMarginalMethod method)
Akyildiz-Bolch approximate MVA for multi-server BCMP networks.
AmvaResult< T > pfqn_lcp(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >())
Bard Large Customer Population (LCP) approximate MVA.
Definition pfqn_lcp.h:57
AmvaResult< T > pfqn_aql(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, double tol=1e-7, std::size_t maxiter=1000)
Aggregate Queue Length (AQL) approximate MVA.
Definition pfqn_aql.h:51
PamVariant
Which of the three proportional approximations to run.
Definition pfqn_pam.h:46
std::vector< T > pfqn_cdfun(const Matrix< T > &nvec, const std::vector< CdScaling< T > > &cdscaling, std::size_t classIdx)
AMVA-QD class-dependence function.
Definition pfqn_cdfun.h:56
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
LcfsMvaResult< T > pfqn_lcfsqn_mva(const std::vector< T > &alpha, const std::vector< T > &beta, const std::vector< int > &N)
Exact mean value analysis of the two-station multiclass LCFS network of Casale, QUESTA 2026 (station ...
@ Ab
the Akyildiz-Bolch weight function
SumClosedResult< T > sum_closed(const Matrix< T > &L, const std::vector< long > &N, const std::vector< T > &Z, const std::vector< Servers > &mi, const Matrix< T > &scv, const SumOptions &options=SumOptions())
Summation method (SUM) and its extension (ESUM) for closed queueing networks, including non-product-f...
Definition sum_closed.h:196
SumClosingResult< T > sum_closing(const std::vector< T > &lambda0, const std::vector< T > &scva, const Matrix< T > &L, const std::vector< Servers > &mi, const Matrix< T > &scv, const std::vector< long > &N, const std::vector< T > &Z, const ClosingOptions &options=ClosingOptions())
Closing method for open and mixed non-product-form queueing networks, solved with the summation metho...
Definition sum_closing.h:81
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
std::vector< T > ones(std::size_t n)
Column vector of ones, the ubiquitous e in MAP algebra.
Definition linalg.h:104
A queueing network and its refreshed NetworkStruct.
Smith Queue Decomposition (SQD): approximate MVA for closed networks under Blocking-After-Service (ma...
Akyildiz-Bolch approximate MVA for multi-server BCMP networks.
Aggregate Queue Length (AQL) approximate MVA.
Bard-Schweitzer approximate MVA.
AMVA-QD class-dependence function.
Chow Second Approximation (SA) approximate MVA.
de Souza e Silva-Lavenberg-Muntz Clustering Approximation (CA).
Conway's multiserver Linearizer for chain-dependent FCFS queues (Conway 1989, "Fast Approximate Solut...
de Souza e Silva-Muntz Improved Linearizer (IL).
AMVA joint-dependence function for non-product-form scaling.
Exact mean value analysis of the two-station multiclass LCFS network of Casale, QUESTA 2026 (station ...
Bard Large Customer Population (LCP) approximate MVA.
Linearizer for mixed open/closed queueing networks.
AMVA-QD limited-load-dependence function.
Exact Mean Value Analysis for mixed open/closed networks with multiserver stations.
Hsieh-Lam Proportional Approximation Methods (PAMB / PAMI / PAMT).
Queue-Shift Approximation (QSA) for closed product-form networks.
Neuse-Chandy SCAT (Self-Correcting Approximation Technique) approximate MVA.
Schmidt's MVA for closed networks with general scheduling disciplines and class-dependent multiserver...
Extended Schmidt MVA with queue-aware alpha corrections.
Square-root non-iterative (SQNI) approximation for a single queueing station with per-class delay.
Tay's arrival-instant approximate MVA.
Chain aggregation and de-aggregation.
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
std::vector< std::size_t > refstatchain
(C) 1-based reference station
Definition sn_chain.h:53
std::vector< double > Nchain
(C) population, infinite for an open chain
Definition sn_chain.h:51
Matrix< T > STchain
(M x C) mean service time
Definition sn_chain.h:48
Matrix< T > Lchain
(M x C) demand
Definition sn_chain.h:47
Matrix< T > Vchain
(M x C) visits
Definition sn_chain.h:49
Matrix< T > SCVchain
(M x C)
Definition sn_chain.h:52
Class-level results, as sn_deaggregate_chain_results returns them.
Definition sn_chain.h:191
std::vector< T > C
Definition sn_chain.h:193
std::vector< T > X
Definition sn_chain.h:193
The options SolverMVA reads.
Definition mva_types.h:31
std::string multiserver
Definition mva_types.h:36
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
double lG
log of the normalizing constant, the reference's lG.
Definition mva_types.h:118
std::vector< T > C
Definition mva_types.h:98
Product-form chain parameters, the split into queueing and delay stations.
Definition sn_chain.h:292
Matrix< T > D
(Mq x C) demands at queueing stations
Definition sn_chain.h:293
std::vector< T > lambda
(C) chain arrival rates, zero on a closed chain
Definition sn_chain.h:295
Matrix< T > Z
(Md x C) demands at delay stations
Definition sn_chain.h:294
std::vector< std::size_t > delay_stations
1-based
Definition sn_chain.h:299
std::vector< std::size_t > queue_stations
1-based
Definition sn_chain.h:298
std::vector< double > S
(Mq) server counts
Definition sn_chain.h:297
The four option switches of the reference, with its defaults.
Definition npfqn_sqd.h:102
Return value of npfqn_sqd, mirroring [X, Q, U, R].
Definition npfqn_sqd.h:93
std::vector< T > R
(M) per-station residence time
Definition npfqn_sqd.h:97
std::vector< T > Q
(M) per-station queue length
Definition npfqn_sqd.h:95
std::vector< T > U
(M) per-station utilization, capped at one
Definition npfqn_sqd.h:96
std::vector< T > X
(M) per-station throughput
Definition npfqn_sqd.h:94
Return value of pfqn_ab_amva, mirroring [QN,UN,RN,CN,XN,totiter].
Matrix< T > UN
(M x R) utilization
std::size_t totiter
iterations of the final core pass
std::vector< T > XN
(R) class throughput AT STATION 1
Matrix< T > QN
(M x R) mean queue length
std::vector< T > XN
(R) throughput
Definition pfqn_bs.h:49
Matrix< T > UN
(M x R) utilization
Definition pfqn_bs.h:51
std::size_t iterations
Definition pfqn_bs.h:53
Matrix< T > QN
(M x R) queue length
Definition pfqn_bs.h:50
Matrix< T > Q
(2 x R) mean queue lengths
std::vector< T > T_
(R) per-class throughput
Matrix< T > U
(2 x R) utilizations
Return value of the Linearizer family, mirroring [Q,U,W,C,X,totiter].
Matrix< T > U
(M x R) utilization
std::vector< T > X
(R) per-class throughput
Matrix< T > Q
(M x R) mean queue length
int totiter
total inner iterations across all Core calls
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 > UN
(M x R) utilization
Definition pfqn_mva.h:47
Return value of pfqn_schmidt_ext, mirroring [XN,QN,UN,CN].
Matrix< T > QN
(M x R) mean queue length
std::vector< T > XN
(R) per-class throughput
Return value of pfqn_schmidt, mirroring [XN,QN,UN,CN].
std::vector< T > XN
(R) per-class throughput
Matrix< T > QN
(M x R) mean queue length
Return value of pfqn_sqni, mirroring [Q, U, X] for the single station.
Definition pfqn_sqni.h:43
std::vector< T > X
Definition pfqn_sqni.h:46
std::vector< T > Q
Definition pfqn_sqni.h:44
std::vector< T > U
Definition pfqn_sqni.h:45
A node of the network.
Closing controls; Kclosed is the population given to the open classes.
Definition sum_closing.h:62
static Servers of(long m)
Definition sum_closed.h:68
static Servers inf()
Definition sum_closed.h:74
Mirrors the [XN, QN, UN, RN, it] return list of the MATLAB function.
Definition sum_closed.h:84
Matrix< T > QN
(M x R) mean queue lengths
Definition sum_closed.h:86
Matrix< T > UN
(M x R) utilizations, per server at queueing stations
Definition sum_closed.h:87
std::size_t it
iterations of the outer loop
Definition sum_closed.h:89
std::vector< T > XN
(R) class throughputs
Definition sum_closed.h:85
Mirrors the [XN, QN, UN, RN, TN, it] return list of the MATLAB function.
Definition sum_closing.h:52
std::vector< T > XN
(R) class throughputs
Definition sum_closing.h:53
Matrix< T > QN
(M x R) queue lengths at the original stations
Definition sum_closing.h:54
Matrix< T > UN
(M x R) utilizations at the original stations
Definition sum_closing.h:55
Convergence controls, mirroring the trailing (tol, maxiter) arguments.
Definition sum_closed.h:93
std::size_t maxiter
Definition sum_closed.h:95
Summation method (SUM) and its extension (ESUM) for closed queueing networks, including non-product-f...
Closing method for open and mixed non-product-form queueing networks, solved with the summation metho...