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