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