LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mva_dispatch.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_MVA_DISPATCH_H
6#define LINE_SOLVERS_MVA_MVA_DISPATCH_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `@@SolverMVA/mvaDispatch.m`: one inner solve, choosing the analyzer
12 * that fits the model.
13 *
14 * THE ORDER IS THE CONTRACT. The branches are NOT disjoint -- a single-class
15 * open Source-Queue-Sink model with a size-based discipline satisfies two of
16 * them, a cache model with load dependence satisfies two more -- so the first
17 * match wins and reordering silently changes which algorithm a model gets.
18 * The sequence below is the reference's, top to bottom:
19 *
20 * 0 closed models with a shortest-job-next station <- ported
21 * 1 order-independent / pass-and-swap stations
22 * 2 delayed-hit retrieval caches, open then closed
23 * 3 size-based scheduling in an open Source-Queue-Sink model
24 * 4 single-class open Source-Queue-Sink <- ported
25 * 5 multiclass open polling
26 * 6 multiclass open HOL priority <- ported
27 * 7 multiclass open DPS, at most three classes <- ported
28 * 8 non-reentrant cache (Source-Cache-Sink)
29 * 9 integrated caching-queueing
30 * 10 bound methods (moved to SolverBA in the reference)
31 * 11 Marie's aggregation-decomposition
32 * 12 load- or class-dependent scaling <- ported
33 * 13 everything else <- solver_mva_analyzer,
34 * of whose method switch 'mvac' is dispatched here (solver_mvac.h)
35 *
36 * WHAT IS NOT PORTED IS REFUSED BY NAME, never allowed to fall through to the
37 * generic analyzer. A cache model solved as an ordinary queueing network
38 * returns numbers -- they are simply not the model's -- and the same holds for
39 * a polling system and for an OI station, whose rate function the AMVA cannot
40 * represent at all.
41 */
42
43#include <algorithm>
44#include <cmath>
45#include <limits>
46#include <memory>
47#include <string>
48#include <vector>
49
85#include "line/util/rootfind.h"
104
105namespace line {
106namespace mva {
107
108/** What the dispatch returns: the metrics plus the algorithm that produced them. */
109template <class T>
112 /** The concrete algorithm, as the reference's `actualmethod`. */
113 std::string actualmethod;
114 /**
115 * Set only by the integrated cacheqn branch: the converged struct whose
116 * routing carries the actual hit/miss probabilities, from which the runner
117 * derives ArvR and ResidT. Empty for every other model.
118 */
119 std::shared_ptr<qn::NetworkStruct<T>> refreshed_struct;
120 /**
121 * What a cache branch measured, EMPTY on every model without a Cache.
122 *
123 * A cache's hit / miss / delayed-hit split is a SOLVER RESULT, not model
124 * state, and it is the only part of the answer the (station x class) tables
125 * cannot carry. Dropping it here is what left `-a cache` refusing under
126 * `-s mva` and, through CPPLINE, left MATLAB's Cache node holding whatever
127 * the PREVIOUS solver wrote -- retrieval_simple printed the LDES table under
128 * the MVA label. Assembled by `solvers::cache_metrics_of`, one rule for
129 * every branch of every solver.
130 */
132 /**
133 * Non-empty when the analyzer that ran would have raised a reference
134 * `line_warning` and still returned a usable answer, carrying its text.
135 * Set only by the SJN branch today (solver_mva_sjn.h).
136 *
137 * KNOWN DIVERGENCE, DELIBERATE. This port has no general warning channel --
138 * no line_warning equivalent exists under cpp/include/line/ -- so the text
139 * stops here, at the dispatch boundary. A caller of `mva_dispatch` sees it;
140 * a user going through SolverMVA does NOT, because solver_mva_runner.h does
141 * not read the field. Closing that last hop needs a runner change and a
142 * decision about where a solver-level warning should surface at all, which
143 * is wider than this analyzer.
144 */
145 std::string warning;
146};
147
148namespace detail {
149
150/** True when the model is exactly a Source, a Queue and a Sink. */
151template <class T>
152bool is_open_sqs(const qn::NetworkStruct<T>& L) {
153 if (L.nof_nodes() != 3) return false;
154 int src = 0, q = 0, snk = 0;
155 for (const qn::NodeDef& nd : L.nodes) {
156 if (nd.nodetype == qn::NodeType::Source) ++src;
157 else if (nd.nodetype == qn::NodeType::Queue) ++q;
158 else if (nd.nodetype == qn::NodeType::Sink) ++snk;
159 }
160 return src == 1 && q == 1 && snk == 1 && L.nclosedjobs() == 0.0;
161}
162
163/**
164 * The method names `solver_mva_qsys_analyzer` has an arm for.
165 *
166 * ONE PREDICATE FOR THE INTERCEPTION AND THE RUN. The Source-Queue-Sink shape is
167 * claimed by that analyzer, which answers this FIXED list of closed forms and
168 * refuses every other name -- so each general network method the report offers
169 * on an open model was advertised on the one open shape it could not run on and
170 * threw "not available for a model with one station and one class" the moment it
171 * was asked for: `mva`, `amva`, `sum`, `esum`, `lin`, `gflin`, `egflin`, `qli`,
172 * `fli`, `qd` and `qdlin`, eleven of them. They are NETWORK methods, and the
173 * general branch solves a one-queue network exactly as it solves a larger one,
174 * so the interception stands aside for them rather than claiming a model it
175 * cannot answer. `qna` was the first name found this way and used to be excluded
176 * by hand at the call site; it needs no special case now, having no arm here
177 * either.
178 *
179 * Judged on the ALIASED name, the same `amva_method_alias` the analyzer applies
180 * on the way in. Mirrors `matlab/src/solvers/MVA/@SolverMVA/mvaDispatch.m` and
181 * its JAR and native python twins.
182 *
183 * The list itself is `qn::mva_qsys_serves_method`, in lang/qn, so that
184 * `mva_feature_set` can read the same names when it withdraws the
185 * state-dependence features from the ones that are closed forms only.
186 */
187inline bool qsys_serves_method(const std::string& method) {
188 return qn::mva_qsys_serves_method(method);
189}
190
191/**
192 * A DECLARED STATE DEPENDENCE DISQUALIFIES EVERY SINGLE-STATION CLOSED FORM.
193 *
194 * `solver_mva_qsys_analyzer` reads `L.rates(queue, 0)`, ONE number, and a station
195 * carrying lldscaling, cdscaling or jdscaling has a lattice of them, so claiming
196 * such a model for mm1/mmk/mg1/gig1 answers the UNSCALED queue: a wrong number,
197 * not a coarse one, and silent. The model goes to the load-dependent branch
198 * instead, and `mva_feature_set` refuses the names that have no other arm. The
199 * same rule `sn_is_mm1k_loss` states for the loss shape.
200 */
201template <class T>
202bool has_state_dependent_rates(const qn::NetworkStruct<T>& L) {
203 for (const auto& st : L.stations)
204 if (!st.lldscaling.empty() || st.cdscaling || st.jdscaling) return true;
205 return false;
206}
207
208/** The same, with a Cache in place of the Queue. */
209template <class T>
210bool is_open_scs(const qn::NetworkStruct<T>& L) {
211 if (L.nof_nodes() != 3) return false;
212 int src = 0, ca = 0, snk = 0;
213 for (const qn::NodeDef& nd : L.nodes) {
214 if (nd.nodetype == qn::NodeType::Source) ++src;
215 else if (nd.nodetype == qn::NodeType::Cache) ++ca;
216 else if (nd.nodetype == qn::NodeType::Sink) ++snk;
217 }
218 return src == 1 && ca == 1 && snk == 1 && L.nclosedjobs() == 0.0;
219}
220
221/** The size-based disciplines the reference routes to its own analyzer. */
222inline bool is_size_based(qn::SchedStrategy s) {
223 return s == qn::SchedStrategy::SRPT || s == qn::SchedStrategy::PSJF ||
224 s == qn::SchedStrategy::FB || s == qn::SchedStrategy::LRPT ||
225 s == qn::SchedStrategy::SETF;
226}
227
228/** 1-based station index of the single node of this type, 0 when absent. */
229template <class T>
230std::size_t station_of_type(const qn::NetworkStruct<T>& L, qn::NodeType ty) {
231 for (std::size_t i = 0; i < L.nof_nodes(); ++i)
232 if (L.nodes[i].nodetype == ty) return L.nodes[i].station;
233 return 0;
234}
235
236/** True when every finite SCV of a station's row is 1, as the reference tests. */
237template <class T>
238bool row_is_exponential(const qn::NetworkStruct<T>& L, std::size_t ist) {
239 for (std::size_t r = 0; r < L.nclasses; ++r) {
240 if (L.disabled[ist - 1][r]) continue;
241 const double v = num_traits<T>::to_double(L.scv(ist - 1, r));
242 if (!std::isfinite(v)) continue;
243 if (std::fabs(v - 1.0) >= 1e-6) return false;
244 }
245 return true;
246}
247
248} // namespace detail
249
250/**
251 * Port of `solver_mva_qsys_analyzer.m`: the closed forms for a single-class
252 * open Source-Queue-Sink model.
253 *
254 * `exact` resolves to M/M/1, M/M/k, M/G/1 or G/M/1 and REFUSES anything else,
255 * since no closed form covers it; `default` additionally falls back to the
256 * G/G/k approximation and to the KLB G/G/1 approximation.
257 */
258template <class T>
260 // The open-queue closed forms evaluate transcendentals (square roots, LSTs,
261 // Brent roots), so under exact/Rational arithmetic the whole body is
262 // discarded and the model is refused by name -- the field-arithmetic ladder
263 // branches stay exact. Guarding the body (not just a static_assert) is what
264 // lets mva_dispatch<Rational> compile at all.
265 if constexpr (!num_traits<T>::has_transcendental) {
266 throw UnsupportedError(
267 "solver_mva_qsys_analyzer: the open queueing-system closed forms need transcendental "
268 "arithmetic; rerun this model with --arith double or --arith real");
269 } else {
270 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
271 const std::size_t M = L.nstations;
272 const std::size_t src = detail::station_of_type(L, qn::NodeType::Source);
273 const std::size_t q = detail::station_of_type(L, qn::NodeType::Queue);
274 if (src == 0 || q == 0) throw InputError("solver_mva_qsys_analyzer: no Source-Queue pair");
275
276 // The visit ratio of the queue, which for a feedback model exceeds one and
277 // separates the per-visit quantities from the per-job ones.
278 const std::size_t qstateful = L.stateful_of_station(q);
279 const T Vq = L.visits[0](qstateful - 1, 0);
280 const T srcRate = L.rates(src - 1, 0);
281 const T lambda = T(srcRate * Vq);
282 const T mu = L.rates(q - 1, 0);
283 const double k = L.stations[q - 1].nservers;
284 const unsigned ku = std::isfinite(k) ? static_cast<unsigned>(std::llround(k)) : 1u;
285 const T ca = num_traits<T>::from_double(std::sqrt(num_traits<T>::to_double(L.scv(src - 1, 0))));
286 const T cs = num_traits<T>::from_double(std::sqrt(num_traits<T>::to_double(L.scv(q - 1, 0))));
287 const bool ca1 = std::fabs(num_traits<T>::to_double(ca) - 1.0) < 1e-12;
288 const bool cs1 = std::fabs(num_traits<T>::to_double(cs) - 1.0) < 1e-12;
289
290 // Finite-capacity loss branch (M/M/1/K with tail drop), the ONE finite
291 // buffer this solver honours: the moment-based MacGregor Smith loss
292 // probability, exact only at scv=1, with the queue length taken from the
293 // truncated M/M/1/K distribution. Being an approximation in general it is
294 // not offered under method='exact'; the capacity gate in the runner exempts
295 // exactly this shape for every other method, so the two must agree.
296 if (sn_is_mm1k_loss(L)) {
297 if (opt.method == "exact")
298 throw UnsupportedError(
299 "solver_mva_qsys_analyzer: M/M/1/K tail-drop is solved by the approximate "
300 "'mg1k.mgs' method (MacGregor Smith); it is not available under method='exact'. "
301 "Use the default method, or SolverCTMC/SolverNC for an exact result");
302 const double Kcap = L.cap[q - 1];
303 const T rho = T(lambda / mu);
304 const T Ploss =
305 qsys::qsys_mg1k_loss_mgs(lambda, mu, T(cs * cs), static_cast<unsigned>(std::llround(Kcap)))
306 .lossProbability;
307 const T Tq = T(lambda * (one - Ploss)); // carried throughput
308 const double rhod = num_traits<T>::to_double(rho);
309 T Lsys = zero;
310 if (std::fabs(rhod - 1.0) < 1e-10) {
311 Lsys = num_traits<T>::from_double(Kcap / 2.0); // L'Hopital limit at rho = 1
312 } else {
313 const T Kp1 = num_traits<T>::from_double(Kcap + 1.0);
314 const T rKp1 = qsys::detail::num_pow(rho, Kp1);
315 Lsys = T(rho / (one - rho) - Kp1 * rKp1 / (one - rKp1));
316 }
318 MvaSolution<T>& ls = lout.sol;
319 ls.Q = Matrix<T>(M, 1, zero);
320 ls.U = Matrix<T>(M, 1, zero);
321 ls.R = Matrix<T>(M, 1, zero);
322 ls.Tp = Matrix<T>(M, 1, zero);
323 ls.X.assign(1, zero);
324 ls.C.assign(1, zero);
325 ls.method = opt.method;
326 ls.iter = 1;
327 ls.R(q - 1, 0) = T(Lsys / Tq); // per-visit response time, by Little
328 ls.Q(q - 1, 0) = Lsys;
329 ls.U(q - 1, 0) = T(Tq / mu); // single-server utilization
330 ls.Tp(q - 1, 0) = Tq; // carried (effective) rate
331 ls.Tp(src - 1, 0) = lambda; // offered arrival rate
332 ls.X[0] = Tq; // system throughput = carried rate
333 ls.C[0] = T(ls.R(q - 1, 0) * Vq);
334 lout.actualmethod = "mg1k.mgs";
335 return lout;
336 }
337
338 // Empty unless the queue reneges.
339 const api::PatienceHandles<T> hpat = api::sn_patience_handles(L, q - 1, 0);
340
341 std::string method = amva_method_alias(opt.method);
342 if (method == "exact") {
343 if (ca1 && cs1 && ku == 1) method = "mm1";
344 else if (ca1 && cs1 && ku > 1) method = "mmk";
345 else if (ca1 && ku == 1) method = "mg1";
346 else if (cs1 && ku == 1) method = "gm1";
347 else
348 throw UnsupportedError(
349 "solver_mva_qsys_analyzer: no exact closed form for this queueing system "
350 "(neither arrivals nor service are exponential, or it has several servers)");
351 } else if (method == "default") {
352 // A station customers walk away from is a different model, not a
353 // correction to one: nothing in the G/G/k family below carries an
354 // abandonment rate, so the choice is made here and not by ca/cs.
355 if (hpat.present) method = hpat.isExponential ? "erlanga" : "mgisrgi";
356 else if (ca1 && cs1 && ku == 1) method = "mm1";
357 else if (ca1 && cs1 && ku > 1) method = "mmk";
358 else if (ca1 && ku == 1) method = "mg1";
359 else if (cs1 && ku == 1) method = "gm1";
360 else if (ku > 1) method = "gigk";
361 else method = "gig1.klb";
362 }
363
364 // Whitt family, full metric set. These methods answer a station whose
365 // CARRIED throughput is below the offered rate -- customers abandon, or are
366 // blocked -- so Little's law on lambda would silently overstate the queue
367 // and the common tail below cannot be used.
368 if (method == "erlanga" || method == "mgisrgi" || method == "gigk.diffusion") {
369 const double cap = L.cap[q - 1];
370 // waiting spaces, servers excluded
371 const double room = std::isfinite(cap)
372 ? std::max(0.0, cap - static_cast<double>(ku))
373 : std::numeric_limits<double>::infinity();
374 T Lsys = zero, Tq = zero, Uq = zero;
375 if (method == "gigk.diffusion") {
376 const qsys::QsysGgnmResult<T> d = qsys::qsys_ggnm_diffusion(lambda, mu, ku, room, ca, cs);
377 Lsys = d.meanNumber;
378 Tq = d.throughput;
379 Uq = d.utilization;
380 } else {
381 if (!hpat.present)
382 throw UnsupportedError("solver_mva_qsys_analyzer: method '" + method +
383 "' needs a reneging patience law on the queue");
385 (method == "erlanga" || hpat.isExponential)
386 // Exponential patience makes the state-dependent
387 // approximation exact, so take the exact chain either way.
388 ? qsys::qsys_erlanga(lambda, mu, hpat.rate, ku, room)
389 : qsys::qsys_mgisrgi_whitt(lambda, mu, ku, room, hpat.as_patience());
390 Lsys = ab.meanNumber;
391 Tq = ab.throughput;
392 Uq = ab.utilization;
393 }
395 MvaSolution<T>& as = aout.sol;
396 as.Q = Matrix<T>(M, 1, zero);
397 as.U = Matrix<T>(M, 1, zero);
398 as.R = Matrix<T>(M, 1, zero);
399 as.Tp = Matrix<T>(M, 1, zero);
400 as.X.assign(1, zero);
401 as.C.assign(1, zero);
402 as.method = opt.method;
403 as.iter = 1;
404 // Little's law on the CARRIED rate, as the loss branch above and
405 // SolverCTMC report it.
406 as.R(q - 1, 0) = Tq > zero ? T(Lsys / Tq) : zero;
407 as.Q(q - 1, 0) = Lsys;
408 as.U(q - 1, 0) = Uq;
409 as.Tp(q - 1, 0) = Tq; // carried rate
410 as.Tp(src - 1, 0) = srcRate; // offered arrival rate
411 as.X[0] = Tq;
412 as.C[0] = T(as.R(q - 1, 0) * Vq);
413 aout.actualmethod = method;
414 return aout;
415 }
416
417 T R = zero;
418 if (method == "mm1") R = qsys::qsys_mm1(lambda, mu).W;
419 else if (method == "mmk") R = qsys::qsys_mmk(lambda, mu, ku).W;
420 else if (method == "mg1" || method == "mgi1") R = qsys::qsys_mg1(lambda, mu, cs).W;
421 else if (method == "gigk") R = qsys::qsys_gigk_approx(lambda, mu, ca, cs, ku).W;
422 else if (method == "gigk.kingman_approx")
423 R = qsys::qsys_gigk_approx_kingman(lambda, mu, ca, cs, ku).W;
424 else if (method == "gig1.kingman") R = qsys::qsys_gig1_ubnd_kingman(lambda, mu, ca, cs).W;
425 else if (method == "gig1.heyman") R = qsys::qsys_gig1_approx_heyman(lambda, mu, ca, cs).W;
426 else if (method == "gig1" || method == "gig1.allen")
427 R = qsys::qsys_gig1_approx_allencunneen(lambda, mu, ca, cs).W;
428 else if (method == "gig1.kobayashi") R = qsys::qsys_gig1_approx_kobayashi(lambda, mu, ca, cs).W;
429 else if (method == "gig1.klb") R = qsys::qsys_gig1_approx_klb(lambda, mu, ca, cs).W;
430 else if (method == "gig1.marchal") R = qsys::qsys_gig1_approx_marchal(lambda, mu, ca, cs).W;
431 else if (method == "gig1.gelenbe") R = qsys::qsys_gig1_approx_gelenbe(lambda, mu, ca, cs).W;
432 else if (method == "gig1.kimura") R = qsys::qsys_gig1_approx_kimura(lambda, mu, ca, cs).W;
433 else if (method == "gigk.whitt") R = qsys::qsys_gigk_approx_whitt(lambda, mu, ca, cs, ku).W;
434 // RQNA and RQT reach the SINGLE-QUEUE robust formulas, not the network
435 // analyzers of solver_rqna.h / solver_rqt.h: this shape is one node, and
436 // the reference answers it here (solver_mva_qsys_analyzer.m cases rqna
437 // and rqt). Without these two arms the qsys interception below claimed the
438 // model and then refused the method, so `rqna` and `rqt` were advertised
439 // on every Source-Queue-Sink model and ran on none of them.
440 else if (method == "rqna") {
441 // The arrival flow enters through its index of dispersion for counts.
442 const mam::Map<T> arvMAP = lang::dist_to_map(L.service[src - 1][0]);
443 const T rho1 = lambda / mu;
444 auto IaFun = [&](const T& x) { return mam::map_count_idc(arvMAP, x); };
445 R = T(qsys::qsys_gig1_rq(rho1, mu, T(cs * cs), IaFun).W + one / mu);
446 } else if (method == "rqt") {
447 // Polyhedral uncertainty sets for the arrival and service flows.
448 const T rho1 = lambda / (num_traits<T>::from_int(static_cast<int>(ku)) * mu);
449 const T Gamma_a = ca / lambda;
450 const T two = num_traits<T>::from_int(2);
451 const T Gamma_s =
452 qsys::qsys_gigk_rqt_gamma(rho1, mu, Gamma_a, T(cs / mu), ku, two);
453 R = qsys::qsys_gigk_rqt(lambda, mu, Gamma_a, Gamma_s, ku, two, two).W;
454 }
455 else if (method == "qed") R = T(qsys::qsys_mmk_qed(lambda, mu, ku).meanWait + one / mu);
456 // The upper end, gig1.kingman already reporting a bound.
457 else if (method == "gig1.extremal")
458 R = T(qsys::qsys_gig1_bnds_extremal(lambda, mu, ca, cs).upperBound + one / mu);
459 else if (method == "gm1" || method == "gim1") {
460 // The PH path is exact only when sn.proc holds the arrival law ITSELF.
461 // For a non-Markovian arrival the refresh substitutes an Erlang-n SCV
462 // fit, so the PH sigma-root would answer a different model; those cases
463 // take the transform's sigma-root instead, as the reference does.
464 const lang::Distrib<T>& arv = L.service[src - 1][0];
465 const bool markovian = arv.has_map();
466 bool done = false;
467 if (markovian) {
468 const mam::Map<T> m = lang::dist_to_map(arv);
469 const std::vector<T> pie = mam::map_pie(m);
470 R = qsys::qsys_phm1(pie, m.D0, mu, num_traits<T>::from_double(1e-16)).meanSojournTime;
471 done = true;
472 }
473 if (!done) {
474 // sigma solves A*(mu - mu sigma) = sigma on (0,1)
475 //
476 // THE UPPER BRACKET CANNOT SIT AT 1. sigma = 1 is ALWAYS a root of
477 // this equation, and near it the transform is evaluated at s = mu(1-x)
478 // -> 0, where A*(s) is a difference of two exponentials that both
479 // tend to 1: at x = 1-1e-12 the cancellation leaves A* correct only to
480 // about 1e-4, so f came out POSITIVE at both ends and Brent refused
481 // the bracket outright ("the bracket endpoints do not straddle a
482 // root"), which killed SolverMVA on every G/M/1 with a non-Markovian
483 // arrival -- gallery_um1 (Uniform(1,2) -> Exp(2)) among them. f(0) is
484 // A*(mu) > 0 always, so only the upper end has to be found: walk it
485 // toward 1 until the sign turns, exactly as far as the sought root
486 // requires and no further. The reference sidesteps the same trap by
487 // seeding an UNBRACKETED fzero at 0.5 (solver_mva_qsys_analyzer.m).
488 auto f = [&](const T& x) { return T(lang::dist_lst(arv, T(mu - mu * x)) - x); };
489 double hi = 0.5;
490 bool straddles = false;
491 for (int i = 0; i < 8; ++i) {
493 straddles = true;
494 break;
495 }
496 hi = 1.0 - (1.0 - hi) / 10.0;
497 }
498 const RootResult<T> rr =
499 straddles ? root_brent(f, num_traits<T>::from_double(1e-12),
502 : RootResult<T>();
503 if (straddles && rr.converged) {
504 R = qsys::qsys_gm1(rr.root, mu);
505 } else {
506 R = qsys::qsys_gg1(lambda, mu, T(ca * ca), one).W;
507 }
508 }
509 } else {
510 throw UnsupportedError("solver_mva_qsys_analyzer: method '" + method +
511 "' is not available for a model with one station and one class");
512 }
513
515 MvaSolution<T>& s = out.sol;
516 s.Q = Matrix<T>(M, 1, zero);
517 s.U = Matrix<T>(M, 1, zero);
518 s.R = Matrix<T>(M, 1, zero);
519 s.Tp = Matrix<T>(M, 1, zero);
520 s.X.assign(1, zero);
521 s.C.assign(1, zero);
522 s.method = opt.method;
523 s.iter = 1;
524 // Per-visit response time and queue length at the queue; per-job cycle time
525 // and system throughput. For a feedback model (Vq > 1) the two differ.
526 s.R(q - 1, 0) = R;
527 s.C[0] = T(R * Vq);
528 s.X[0] = srcRate;
529 s.U(q - 1, 0) = T(lambda / mu / num_traits<T>::from_double(std::isfinite(k) ? k : 1.0));
530 s.Tp(src - 1, 0) = srcRate;
531 s.Tp(q - 1, 0) = lambda;
532 s.Q(q - 1, 0) = T(lambda * R);
533 out.actualmethod = method;
534 return out;
535 } // if constexpr has_transcendental
536}
537
538/**
539 * Port of `solver_mva_qsys_prio_analyzer.m`: the exact Cobham formula for a
540 * single open M/G/1 queue with non-preemptive priorities.
541 *
542 * The classes are handed to `qsys_mg1_prio` in PRIORITY order (lowest value
543 * first), which is the order the formula's nested sums assume.
544 */
545template <class T>
547 const MvaOptions& opt) {
548 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
549 const std::size_t M = L.nstations, K = L.nclasses;
550 const std::size_t src = detail::station_of_type(L, qn::NodeType::Source);
551 const std::size_t q = detail::station_of_type(L, qn::NodeType::Queue);
552
553 std::vector<std::size_t> order(K);
554 for (std::size_t r = 0; r < K; ++r) order[r] = r;
555 std::stable_sort(order.begin(), order.end(), [&](std::size_t a, std::size_t b) {
556 return L.classes[a].prio < L.classes[b].prio;
557 });
558
559 std::vector<T> lam(K, zero), mu(K, zero), cs(K, one);
560 for (std::size_t j = 0; j < K; ++j) {
561 const std::size_t r = order[j];
562 lam[j] = L.rates(src - 1, r);
563 mu[j] = L.rates(q - 1, r);
564 const double v = num_traits<T>::to_double(L.scv(q - 1, r));
565 cs[j] = (std::isfinite(v) && v > 0.0) ? num_traits<T>::from_double(std::sqrt(v)) : one;
566 }
567 const std::vector<T> W = qsys::qsys_mg1_prio(lam, mu, cs).W;
568
570 MvaSolution<T>& s = out.sol;
571 s.Q = Matrix<T>(M, K, zero);
572 s.U = Matrix<T>(M, K, zero);
573 s.R = Matrix<T>(M, K, zero);
574 s.Tp = Matrix<T>(M, K, zero);
575 s.C.assign(K, zero);
576 s.X.assign(K, zero);
577 s.method = opt.method;
578 s.iter = 0;
579 for (std::size_t j = 0; j < K; ++j) {
580 const std::size_t r = order[j];
581 if (!(lam[j] > zero) || !std::isfinite(num_traits<T>::to_double(mu[j]))) continue;
582 s.R(q - 1, r) = W[j];
583 s.C[r] = W[j];
584 s.X[r] = lam[j];
585 s.U(q - 1, r) = T(lam[j] / mu[j]);
586 s.Tp(q - 1, r) = lam[j];
587 s.Tp(src - 1, r) = lam[j];
588 s.Q(q - 1, r) = T(lam[j] * W[j]);
589 }
590 out.actualmethod = "mg1.prio";
591 return out;
592}
593
594/**
595 * The exact multiclass M/M/1-DPS solve the reference inlines in mvaDispatch:
596 * the truncated multiclass chain of `qsys_mm1_dps`, which the AMVA cross-term
597 * correction cannot reproduce (it violates the equal-rate conservation law).
598 */
599template <class T>
601 // The truncated-chain M/M/1-DPS closed form (qsys_mm1_dps) needs
602 // transcendental arithmetic; refuse by name under exact/Rational.
603 if constexpr (!num_traits<T>::has_transcendental) {
604 throw UnsupportedError(
605 "solver_mva_dps_exact: the exact DPS closed form needs transcendental arithmetic; "
606 "rerun this model with --arith double or --arith real");
607 } else {
608 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
609 const std::size_t M = L.nstations, K = L.nclasses;
610 const std::size_t src = detail::station_of_type(L, qn::NodeType::Source);
611 const std::size_t q = detail::station_of_type(L, qn::NodeType::Queue);
612
613 std::vector<T> lam(K, zero), mu(K, zero), w(K, one);
614 for (std::size_t r = 0; r < K; ++r) {
615 lam[r] = L.rates(src - 1, r);
616 mu[r] = L.rates(q - 1, r);
617 const std::vector<T>& sp = L.stations[q - 1].schedparam;
618 if (sp.size() > r && sp[r] > zero) w[r] = sp[r];
619 }
620 const std::vector<T> Wd = qsys::qsys_mm1_dps(lam, mu, w).T_;
621
623 MvaSolution<T>& s = out.sol;
624 s.Q = Matrix<T>(M, K, zero);
625 s.U = Matrix<T>(M, K, zero);
626 s.R = Matrix<T>(M, K, zero);
627 s.Tp = Matrix<T>(M, K, zero);
628 s.C.assign(K, zero);
629 s.X.assign(K, zero);
630 s.method = opt.method;
631 s.iter = 0;
632 for (std::size_t r = 0; r < K; ++r) {
633 s.R(q - 1, r) = Wd[r];
634 s.C[r] = Wd[r];
635 s.X[r] = lam[r];
636 if (mu[r] > zero) s.U(q - 1, r) = T(lam[r] / mu[r]);
637 s.Tp(q - 1, r) = lam[r];
638 s.Tp(src - 1, r) = lam[r];
639 s.Q(q - 1, r) = T(lam[r] * Wd[r]);
640 }
641 out.actualmethod = "mm1.dps";
642 return out;
643 } // if constexpr has_transcendental
644}
645
646/**
647 * Port of `solver_mvald_analyzer.m`: the load-dependent branch.
648 *
649 * `exact` and `mva` take the exact load-dependent recursion; `default` takes it
650 * too on a small closed product-form model, mirroring the default-to-exact
651 * upgrade the non-LD analyzer applies; everything else goes to the AMVA, whose
652 * lld and cd terms are now in place.
653 */
654template <class T>
656 const Matrix<T>& init_sol) {
657 const std::string method = amva_method_alias(opt.method);
658 // BOTH dependences disqualify the exact recursion, and both disqualify the
659 // default-to-exact upgrade below. The test read cdscaling alone until
660 // 2026-09-13, so `exact` on a jdscaling station ran solver_mvald, which
661 // reads only the lld lattice and returned the UNSCALED queue rather than
662 // refusing; the reference names both (solver_mvald_analyzer.m:19,26).
663 const bool has_cd_or_jd = [&] {
664 for (const auto& st : L.stations)
665 if (st.cdscaling || st.jdscaling) return true;
666 return false;
667 }();
668
669 if (method == "exact" || method == "mva") {
670 if (has_cd_or_jd)
671 throw UnsupportedError(
672 "solver_mvald_analyzer: there is no exact class-/joint-dependent solver in MVA");
674 out.sol = solver_mvald(L, opt);
675 out.actualmethod = "exact";
676 return out;
677 }
678 if (!(method == "default" || method == "amva" || method == "qd" || method == "lin" ||
679 method == "qdlin"))
680 throw UnsupportedError("solver_mvald_analyzer: the '" + method +
681 "' method is not supported by the load-dependent MVA solver");
682
683 if (method == "default" && !has_cd_or_jd) {
684 double Nsum = 0.0;
685 bool integral = true, finite = true;
686 for (const qn::JobClass& c : L.classes) {
687 if (!std::isfinite(c.population)) finite = false;
688 else {
689 Nsum += c.population;
690 if (c.population != std::floor(c.population)) integral = false;
691 }
692 }
693 if (finite && integral && L.nchains <= 4 && Nsum <= 20.0 && L.has_product_form()) {
695 out.sol = solver_mvald(L, opt);
696 out.actualmethod = "exact";
697 return out;
698 }
699 }
700 bool converged = true;
702 MvaOptions o = opt;
704 out.sol = solver_amva(L, d, o, init_sol, converged);
705 // solver_amva computes the outer residual and reports it here; dropping the
706 // flag on the floor left every caller with only `iter`, which on this route
707 // aggregates the nested sweeps and cannot decide convergence.
708 out.sol.converged = converged;
709 out.actualmethod = out.sol.method;
710 return out;
711}
712
713/**
714 * Port of `solver_mva_qsys_sizebased_analyzer.m`: the M/G/1 formulas for the
715 * size-based disciplines (Wierman and Harchol-Balter, SIGMETRICS 2003).
716 *
717 * The arrival rate of each class is the source rate times the queue's VISIT
718 * ratio, and the response time comes back per visit, so both are multiplied
719 * out the same way the reference does.
720 *
721 * PER-VISIT VERSUS PER-JOB. `W` comes back per visit, so the response time is
722 * `W * visits` while the queue length stays `lambda * W` with lambda already
723 * carrying the visit ratio. That makes `Q != T * R` at the station whenever the
724 * visit ratio differs from one: the reference inflates R by the visits but not
725 * Q, so Little's law fails on a feedback routing. It is a defect of the
726 * reference, reproduced here rather than silently corrected -- this branch is
727 * only ever dispatched on a plain Source-Queue-Sink, where the visit ratio is
728 * one and the discrepancy cannot arise.
729 */
730template <class T>
732 const MvaOptions& opt) {
733 // The M/G/1 size-based closed forms (SRPT/PSJF/FB/LRPT/SETF) evaluate
734 // transcendentals; refuse by name under exact/Rational.
735 if constexpr (!num_traits<T>::has_transcendental) {
736 throw UnsupportedError(
737 "solver_mva_qsys_sizebased_analyzer: the M/G/1 size-based closed forms need "
738 "transcendental arithmetic; rerun this model with --arith double or --arith real");
739 } else {
740 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
741 const std::size_t M = L.nstations, K = L.nclasses;
742 const std::size_t src = detail::station_of_type(L, qn::NodeType::Source);
743 const std::size_t q = detail::station_of_type(L, qn::NodeType::Queue);
744 const std::size_t qstateful = L.stateful_of_station(q);
745
746 // sn.visits is indexed by CHAIN. The reference wrote `sn.visits{source_ist}`
747 // -- the chain whose number happens to equal the Source's STATION index --
748 // so every class outside chain 1 read a visit of 0 and the analyzer rejected
749 // its own multiclass model. Fixed in MATLAB in the same change; take the
750 // chain each class belongs to.
751 std::vector<std::size_t> chain_of(K, 0);
752 for (std::size_t c = 0; c < L.nchains; ++c)
753 for (std::size_t r : L.inchain[c]) chain_of[r - 1] = c;
754
755 std::vector<T> lambda(K, zero), mu(K, zero), cs(K, one), vis(K, one);
756 for (std::size_t r = 0; r < K; ++r) {
757 vis[r] = L.visits[chain_of[r]](qstateful - 1, r);
758 lambda[r] = T(L.rates(src - 1, r) * vis[r]);
759 mu[r] = L.rates(q - 1, r);
760 const double v = num_traits<T>::to_double(L.scv(q - 1, r));
761 cs[r] = (std::isfinite(v) && v > 0.0) ? num_traits<T>::from_double(std::sqrt(v)) : one;
762 if (!(lambda[r] > zero) || !(mu[r] > zero))
763 throw InputError(
764 "solver_mva_qsys_sizebased_analyzer: the arrival and service rates must be "
765 "positive");
766 }
767
768 const qn::SchedStrategy sched = L.stations[q - 1].sched;
769 std::vector<T> W;
770 std::string actual;
771 switch (sched) {
772 case qn::SchedStrategy::SRPT:
773 W = qsys::qsys_mg1_srpt(lambda, mu, cs).W;
774 actual = "mg1.srpt";
775 break;
776 case qn::SchedStrategy::PSJF:
777 W = qsys::qsys_mg1_psjf(lambda, mu, cs).W;
778 actual = "mg1.psjf";
779 break;
780 case qn::SchedStrategy::FB:
781 W = qsys::qsys_mg1_fb(lambda, mu, cs).W;
782 actual = "mg1.fb";
783 break;
784 case qn::SchedStrategy::LRPT:
785 W = qsys::qsys_mg1_lrpt(lambda, mu, cs).W;
786 actual = "mg1.lrpt";
787 break;
788 case qn::SchedStrategy::SETF:
789 W = qsys::qsys_mg1_setf(lambda, mu, cs).W;
790 actual = "mg1.setf";
791 break;
792 default:
793 throw UnsupportedError(std::string("solver_mva_qsys_sizebased_analyzer: ") +
794 lang::sched_to_text(sched) + " is not a size-based discipline");
795 }
796
798 MvaSolution<T>& s = out.sol;
799 s.Q = Matrix<T>(M, K, zero);
800 s.U = Matrix<T>(M, K, zero);
801 s.R = Matrix<T>(M, K, zero);
802 s.Tp = Matrix<T>(M, K, zero);
803 s.C.assign(K, zero);
804 s.X.assign(K, zero);
805 s.method = opt.method;
806 s.iter = 1;
807 for (std::size_t r = 0; r < K; ++r) {
808 s.R(q - 1, r) = T(W[r] * vis[r]);
809 s.C[r] = s.R(q - 1, r);
810 s.X[r] = lambda[r];
811 s.U(q - 1, r) = T(lambda[r] / mu[r]);
812 s.Tp(q - 1, r) = lambda[r];
813 s.Tp(src - 1, r) = lambda[r];
814 s.Q(q - 1, r) = T(lambda[r] * W[r]);
815 }
816 out.actualmethod = actual;
817 return out;
818 } // if constexpr has_transcendental
819}
820
821/**
822 * Port of `solver_mva_marie_analyzer.m`: Marie's iterative
823 * aggregation-decomposition for a CLOSED network with non-exponential FCFS
824 * service.
825 *
826 * Only FCFS is service-time sensitive; PS and LCFSPR are insensitive
827 * (product form) and are handed an SCV of one, which is what makes the
828 * decomposition exact for them. Delay stations fold into the per-chain think
829 * time rather than entering the isolation, and multiserver isolation is
830 * single-chain only -- all three restrictions are the reference's, and each is
831 * refused by name rather than approximated.
832 */
833template <class T>
835 // Marie's decomposition fits a Coxian per isolated station (square roots) and
836 // stops on a tolerance; refuse by name under exact/Rational.
837 if constexpr (!num_traits<T>::has_transcendental) {
838 throw UnsupportedError(
839 "solver_mva_marie_analyzer: Marie's aggregation-decomposition needs transcendental "
840 "arithmetic; rerun this model with --arith double or --arith real");
841 } else {
842 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
844 const std::size_t M = L.nstations, C = L.nchains;
845
846 for (std::size_t c = 0; c < C; ++c)
847 if (!std::isfinite(d.Nchain[c]))
848 throw UnsupportedError(
849 "solver_mva_marie_analyzer: the 'marie' method supports closed models only; this "
850 "model has open classes");
851 for (const auto& nd : L.nodes)
852 if (nd.nodetype == qn::NodeType::Source)
853 throw UnsupportedError(
854 "solver_mva_marie_analyzer: the 'marie' method supports closed models only; this "
855 "model has a Source");
856
857 std::vector<std::size_t> qrows, drows;
858 for (std::size_t i = 0; i < M; ++i) {
859 const qn::SchedStrategy sc = L.stations[i].sched;
860 const bool delay = std::isinf(L.stations[i].nservers) || sc == qn::SchedStrategy::INF;
861 if (delay) {
862 drows.push_back(i);
863 continue;
864 }
865 if (!(sc == qn::SchedStrategy::FCFS || sc == qn::SchedStrategy::PS ||
866 sc == qn::SchedStrategy::LCFSPR))
867 throw UnsupportedError(std::string("solver_mva_marie_analyzer: the 'marie' method "
868 "supports FCFS, PS, LCFSPR and Delay stations "
869 "only; station '") +
870 L.stations[i].name + "' is " + lang::sched_to_text(sc));
871 qrows.push_back(i);
872 }
873
874 std::vector<T> Z(C, zero);
875 for (std::size_t c = 0; c < C; ++c)
876 for (std::size_t i : drows) Z[c] += d.Lchain(i, c);
877
878 const std::size_t Mq = qrows.size();
879 Matrix<T> Lq(Mq, C, zero), SCV(Mq, C, one);
880 std::vector<int> ns(Mq, 1);
881 bool multiserver = false;
882 for (std::size_t a = 0; a < Mq; ++a) {
883 const std::size_t i = qrows[a];
884 const double srv = L.stations[i].nservers;
885 ns[a] = std::isfinite(srv) ? static_cast<int>(std::llround(srv)) : 1;
886 if (ns[a] > 1) multiserver = true;
887 for (std::size_t c = 0; c < C; ++c) {
888 Lq(a, c) = d.Lchain(i, c);
889 if (L.stations[i].sched != qn::SchedStrategy::FCFS) continue;
890 const double v = num_traits<T>::to_double(d.SCVchain(i, c));
891 if (std::isfinite(v) && v > 0.0) SCV(a, c) = num_traits<T>::from_double(v);
892 }
893 }
894 if (C > 1 && multiserver)
895 throw UnsupportedError(
896 "solver_mva_marie_analyzer: the 'marie' method supports multiserver stations for "
897 "single-chain models only");
898
899 std::vector<int> N(C, 0);
900 for (std::size_t c = 0; c < C; ++c) N[c] = static_cast<int>(std::llround(d.Nchain[c]));
902 if (Mq == 0) {
903 // Nothing to isolate: with every station an infinite server the
904 // aggregation-decomposition degenerates to the exact delay solution
905 // X_c = N_c / Z_c, and pfqn_marie would be handed a zero-row demand matrix.
906 mr.X.assign(C, zero);
907 mr.Q = Matrix<T>(0, C, zero);
908 mr.U = Matrix<T>(0, C, zero);
909 mr.it = 1;
910 for (std::size_t c = 0; c < C; ++c)
911 if (Z[c] > zero) mr.X[c] = T(num_traits<T>::from_double(d.Nchain[c]) / Z[c]);
912 } else {
913 mr = C == 1 ? pfqn::pfqn_marie(Lq, N, Z, SCV, 1e-8, 1000, ns)
914 : pfqn::pfqn_marie(Lq, N, Z, SCV, 1e-8, 1000, std::vector<int>());
915 }
916
917 Matrix<T> Qchain(M, C, zero), Uchain(M, C, zero), Rchain(M, C, zero), Tchain(M, C, zero);
918 for (std::size_t a = 0; a < Mq; ++a)
919 for (std::size_t c = 0; c < C; ++c) {
920 Qchain(qrows[a], c) = mr.Q(a, c);
921 Uchain(qrows[a], c) = mr.U(a, c);
922 }
923 for (std::size_t i = 0; i < M; ++i)
924 for (std::size_t c = 0; c < C; ++c) Tchain(i, c) = T(mr.X[c] * d.Vchain(i, c));
925 for (std::size_t i = 0; i < M; ++i)
926 for (std::size_t c = 0; c < C; ++c)
927 if (Tchain(i, c) > zero) Rchain(i, c) = T(Qchain(i, c) / Tchain(i, c));
928 // A delay station holds T S jobs, all of them in service.
929 for (std::size_t i : drows)
930 for (std::size_t c = 0; c < C; ++c) {
931 Qchain(i, c) = T(Tchain(i, c) * d.STchain(i, c));
932 Uchain(i, c) = Qchain(i, c);
933 Rchain(i, c) = d.STchain(i, c);
934 }
935
936 const ClassResults<T> cr =
937 sn_deaggregate_chain_results(L, d, Qchain, Uchain, Rchain, Tchain, mr.X);
939 out.sol.Q = cr.Q;
940 out.sol.U = cr.U;
941 out.sol.R = cr.R;
942 out.sol.Tp = cr.Tp;
943 out.sol.C = cr.C;
944 out.sol.X = cr.X;
945 out.sol.method = opt.method;
946 out.sol.iter = mr.it;
947 out.actualmethod = "marie";
948 return out;
949 } // if constexpr has_transcendental
950}
951
952/**
953 * The ladder itself. `init_sol` is the warm start the outer solver carries
954 * across iterations; it reaches the AMVA paths only, as it does in the
955 * reference.
956 */
957template <class T>
959 const Matrix<T>& init_sol) {
960 using qn::NodeType;
961 using qn::SchedStrategy;
962 const std::string method = amva_method_alias(opt.method);
963
964 // 0. closed models with a shortest-job-next station. This sits ABOVE every
965 // other branch because the reference tests hasSJN first and the branches
966 // are not disjoint: an SJF station in a closed model with a delay would
967 // otherwise reach the generic AMVA path, which reads only the mean service
968 // time and would report the station as if it scheduled size-blind. The
969 // OPEN case has no population to recur over and is refused by name rather
970 // than solved as though the discipline did nothing.
971 if (sn_has_sjn(L)) {
972 bool open = false;
973 for (const auto& c : L.classes)
974 if (std::isinf(c.population)) open = true;
975 if (open)
976 throw UnsupportedError(
977 "SolverMVA supports shortest-job-next (SJF) scheduling only in closed models, the "
978 "conditional waiting time equation being a population recursion. Use SolverLDES, "
979 "or SolverMVA with SRPT or PSJF for the preemptive size-based open queue");
982 sout.sol = sr.sol;
983 sout.actualmethod = sr.actualmethod;
984 sout.warning = sr.warning;
985 return sout;
986 }
987
988 // 1. order-independent / pass-and-swap stations. Detection is
989 // `nc_is_oi_model`'s: PAS or OI scheduling, a rate function, and an
990 // ALL-ZERO swap graph. A station with those disciplines that does not
991 // qualify is a genuine pass-and-swap station, which is not product-form
992 // and has no exact mean-value analyzer, so it is refused by name rather
993 // than handed to an AMVA that cannot represent its rate function at all.
994 // The gate is the reference's, and all three conjuncts matter: an OI
995 // station must be present, the WHOLE model must be order-independent
996 // (nc_is_oi_model: closed, and every other station product-form), and the
997 // method must be `default` or `exact`. MVA reaches OI/PAS ONLY through the
998 // exact analyzer; any other method would fall through to an AMVA that only
999 // ever sees the single-job rates and would silently report a zero queue
1000 // length at the OI station.
1001 {
1002 const bool has_oi_station = std::any_of(
1003 L.stations.begin(), L.stations.end(), [](const qn::Station<T>& st) {
1004 return st.sched == SchedStrategy::OI || st.sched == SchedStrategy::PAS;
1005 });
1006 const bool exact_method = (opt.method == "default" || opt.method == "exact");
1007 if (!find_oi_stations(L).empty() && nc_is_oi_model(L) && exact_method) {
1008 DispatchResult<T> oout;
1009 oout.sol = solver_mva_oi_analyzer(L, opt);
1010 oout.actualmethod = "oi";
1011 return oout;
1012 }
1013 if (has_oi_station)
1014 throw UnsupportedError(
1015 "SolverMVA supports order-independent (OI) and pass-and-swap (PAS) stations only "
1016 "through its exact order-independent analyzer, which requires method 'default' or "
1017 "'exact' (got '" + opt.method +
1018 "'), an empty/zero swap graph at every OI/PAS station, a closed model, and every "
1019 "other station to be product-form (INF, PS, LCFS-PR, SIRO, or "
1020 "class-independent-rate FCFS). Use SolverCTMC or SolverLDES for this model");
1021 }
1022
1023 // 2. delayed-hit retrieval caches (Cache.setRetrievalSystem). The two
1024 // variants are DIFFERENT METHODS, not one method on two topologies, which
1025 // is why the Source test decides between them rather than parameterizing
1026 // one call. The OPEN model has no population to recur over, so the hit /
1027 // miss / delayed-hit split comes from the open fixed point
1028 // (`retrieval_fpi`) and all three are reported. The CLOSED model has no
1029 // exogenous arrival rate to run that fixed point on, so the split emerges
1030 // from a decomposition-aggregation sweep instead, and the delayed-hit
1031 // fraction folds into the miss (see solver_mva_cacheqn_retrieval.h).
1032 for (const auto& kv : L.nodeparam) {
1033 if (kv.second.retrieval_capacity <= 0) continue;
1034 DispatchResult<T> rout;
1035 if (detail::station_of_type(L, NodeType::Source) == 0) {
1037 rout.sol = cr.sol;
1039 cr.latency, cr.hitproblist, Matrix<T>(),
1040 std::vector<T>());
1041 } else {
1043 rout.sol = solver_mva_retrieval_analyzer(L, opt, &co);
1045 co.latency, co.hitproblist, co.itemprob,
1046 std::vector<T>());
1047 }
1048 rout.actualmethod = "fpi";
1049 return rout;
1050 }
1051
1052 const bool sqs = detail::is_open_sqs(L);
1053 const std::size_t qst = detail::station_of_type(L, NodeType::Queue);
1054 const SchedStrategy qsched = qst ? L.stations[qst - 1].sched : SchedStrategy::NONE;
1055 const std::size_t srcst = detail::station_of_type(L, NodeType::Source);
1056
1057 // 8. non-reentrant cache: a Source-Cache-Sink model
1058 if (detail::is_open_scs(L)) {
1059 DispatchResult<T> cout;
1061 cout.sol = cr.sol;
1062 cout.cache = solvers::cache_metrics_of(L, cr.hitprob, cr.missprob, std::vector<T>(),
1063 std::vector<T>(), cr.hitproblist, cr.itemprob,
1064 std::vector<T>());
1065 cout.actualmethod = cr.actualmethod;
1066 return cout;
1067 }
1068
1069 // 9. any OTHER cache -- one embedded in a queueing network -- is the
1070 // integrated caching-queueing analyzer (decomposition-aggregation).
1071 if (!L.nodeparam.empty()) {
1072 DispatchResult<T> qout;
1073 std::shared_ptr<qn::NetworkStruct<T>> refreshed(new qn::NetworkStruct<T>());
1075 qout.sol = solver_mva_cacheqn_analyzer(L, opt, refreshed.get(), &co);
1076 qout.refreshed_struct = refreshed;
1077 // The integrated branch reports the split per (cache, class) and no
1078 // delayed-hit fraction; the per-item law rides in separately because
1079 // only this branch has one per cache rather than one per model.
1081 for (std::size_t ci = 0; ci < qout.cache.caches.size() && ci < co.itemprob.size(); ++ci)
1082 qout.cache.caches[ci].itemprob = co.itemprob[ci];
1083 qout.actualmethod = (opt.method == "exact") ? "exact" : "default";
1084 return qout;
1085 }
1086
1087 // 3. size-based scheduling
1088 if (sqs && detail::is_size_based(qsched)) return solver_mva_qsys_sizebased_analyzer(L, opt);
1089
1090 // 4. single-class open Source-Queue-Sink.
1091 //
1092 // CLAIMED ONLY FOR THE NAMES THE ANALYZER ANSWERS, `detail::qsys_serves_method`.
1093 // Anything else -- 'qna', 'mva', 'amva', the linearizers, the qd family,
1094 // sum/esum -- falls through to the network branches below and is solved
1095 // there, as it is on every larger open network; this branch used to claim
1096 // the model for them and then refuse the method by name. 'rqna' and 'rqt'
1097 // are served here: the analyzer answers them with single-queue robust
1098 // formulas of their own.
1099 //
1100 // THE M/M/1/K LOSS SHAPE IS THE EXCEPTION and is claimed whatever the name.
1101 // Its branch is chosen by the SHAPE and not by the method, and the capacity
1102 // gate in the runner exempts exactly this shape for every method but
1103 // 'exact', so the two have to agree on which models come here.
1104 // AND ONLY FOR A STATION WHOSE RATE IS A CONSTANT, `detail::has_state_dependent_rates`:
1105 // a declared lld/cd/jd scaling sends the model on to branch 12, whose analyzer
1106 // carries the scaling terms, rather than to a closed form that cannot read them.
1107 if (sqs && L.nclasses == 1 &&
1108 ((detail::qsys_serves_method(method) && !detail::has_state_dependent_rates(L)) ||
1109 sn_is_mm1k_loss(L)))
1110 return solver_mva_qsys_analyzer(L, opt);
1111
1112 // 5. multiclass open polling
1113 if (sqs && L.nclasses > 1 && qsched == SchedStrategy::POLLING) {
1114 DispatchResult<T> pout;
1116 pout.actualmethod = "stationtime";
1117 return pout;
1118 }
1119
1120 // 6. multiclass open HOL priority, single server, Poisson arrivals
1121 if (sqs && L.nclasses > 1 && qsched == SchedStrategy::HOL &&
1122 L.stations[qst - 1].nservers == 1.0 && detail::row_is_exponential(L, srcst))
1124
1125 // 7. multiclass open DPS, at most three classes, exponential everywhere
1126 if (sqs && L.nclasses > 1 && L.nclasses <= 3 && qsched == SchedStrategy::DPS &&
1127 L.stations[qst - 1].nservers == 1.0 && detail::row_is_exponential(L, srcst) &&
1128 detail::row_is_exponential(L, qst))
1129 return solver_mva_dps_exact(L, opt);
1130
1131 // 10. the bound family, which the reference moved to SolverBA
1132 static const char* kBounds[] = {"aba.upper", "aba.lower", "bjb.upper", "bjb.lower",
1133 "pb.upper", "pb.lower", "gb.upper", "gb.lower",
1134 "sb.upper", "sb.lower", "mw.upper", "mw.lower"};
1135 for (const char* b : kBounds)
1136 if (method == b)
1137 throw UnsupportedError("SolverMVA: bound methods have moved to SolverBA; use "
1138 "SolverBA with method '" +
1139 method + "'");
1140
1141 // 11. Marie's aggregation-decomposition
1142 if (method == "marie") return solver_mva_marie_analyzer(L, opt);
1143
1144 // 12. load-, class- or joint-dependent scaling. THE TEST MUST NAME ALL THREE:
1145 // it read lldscaling and cdscaling only until 2026-09-13, so a station whose
1146 // ONLY declared dependence was jdscaling fell through to branch 13 and was
1147 // solved at its unscaled rate, where the reference (mvaDispatch.m:240) sends
1148 // it to the load-dependent analyzer. Same predicate as branch 4's.
1149 if (detail::has_state_dependent_rates(L)) return solver_mvald_analyzer(L, opt, init_sol);
1150
1151 // 13. an ordinary queueing network. MVAC is a case of the analyzer's method
1152 // switch (solver_mva_analyzer.m:21-23) and so belongs BELOW branch 12: a
1153 // load-dependent model reaches solver_mvald_analyzer in the reference and
1154 // never sees the 'mvac' case, which is why this is not tested higher up.
1155 if (method == "mvac") {
1156 DispatchResult<T> mout;
1157 mout.sol = solver_mvac_analyzer(L, opt);
1158 mout.actualmethod = "mvac";
1159 return mout;
1160 }
1161 // QNA is dispatched here rather than in solver_mva_analyzer only because
1162 // solver_qna.h cannot include solver_mva.h without a cycle; the reference
1163 // reaches it from the analyzer's method table, and no branch above claims
1164 // 'qna', so the model it sees is the same.
1165 if (method == "qna") {
1166 DispatchResult<T> qout;
1167 qout.sol = solver_qna(L, opt);
1168 qout.actualmethod = "qna";
1169 return qout;
1170 }
1171 // RQNA (robust queueing-network analyzer) is likewise reached by method name,
1172 // and by the resolveMethod default->rqna upgrade for a bursty single-class
1173 // open network; dispatched here for the same include-cycle reason as qna.
1174 if (method == "rqna") {
1175 DispatchResult<T> rout;
1176 rout.sol = solver_rqna(L, opt);
1177 rout.actualmethod = "rqna";
1178 return rout;
1179 }
1180 // RQT (robust queueing theory) is reached by method name only; its
1181 // uncertainty-set decomposition has no default-dispatch upgrade.
1182 if (method == "rqt") {
1183 DispatchResult<T> tout;
1184 tout.sol = solver_rqt(L, opt);
1185 tout.actualmethod = "rqt";
1186 return tout;
1187 }
1188 // The horizontal-cut MVA for one exponential delay and one FCFS MAP queue
1189 // (mapqn_amva), reached by name; its shape gate is mva_mapqn_reason.
1190 if (method == "amva.mapqn" || method == "mapqn") {
1191 DispatchResult<T> mout;
1192 mout.sol = solver_mapqn(L, opt);
1193 mout.actualmethod = "amva.mapqn";
1194 return mout;
1195 }
1196 // bursty single-class open network: 'default' upgrades to RQNA, exactly as
1197 // the terminal branch of solver_mva_analyzer.m does. resolveMethod computes
1198 // this only for the feature gate and never writes it back to options, so the
1199 // substitution belongs HERE, after the structural special cases -- a 3-node
1200 // MAP/M/1 is a G/M/1 the qsys analyzer (branch 4) already solved and must
1201 // keep method='default'. A fork-transformed model is exempt: it is forced to
1202 // amva just below.
1203 if (method == "default" && !opt.base_has_fork && !L.has_fork() && L.nclasses == 1) {
1204 bool all_open = true;
1205 for (std::size_t r = 0; r < L.nclasses; ++r)
1206 if (std::isfinite(L.classes[r].population)) all_open = false;
1207 if (all_open && api::sn_has_bursty_arrival(L)) {
1208 DispatchResult<T> rout;
1209 rout.sol = solver_rqna(L, opt);
1210 rout.actualmethod = "rqna";
1211 return rout;
1212 }
1213 }
1214
1215 MvaOptions o = opt;
1216 if ((opt.base_has_fork || L.has_fork()) && method == "default") {
1217 // The fork transform yields a mixed model with auxiliary near-zero-rate
1218 // open classes. The default ladder would send it to exact mixed MVA,
1219 // which degenerates to zero there; the approximation needs the AMVA.
1220 //
1221 // The test is on the BASE model, as the reference writes it: by the
1222 // time this runs the transform has already removed the fork, so
1223 // `L.has_fork()` alone is false on exactly the models the rule is for.
1224 o.method = "amva";
1225 }
1227 out.sol = solver_mva_analyzer(L, o, init_sol);
1228 out.actualmethod = out.sol.method;
1229 return out;
1230}
1231
1232} // namespace mva
1233} // namespace line
1234
1235#endif // LINE_SOLVERS_MVA_MVA_DISPATCH_H
What a solver observed about the Cache nodes of a model.
InputError(const std::string &what)
Definition error.h:39
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_nodes() const
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::map< std::size_t, CacheParam< T > > nodeparam
Cache parameters by 1-based NODE index; only Cache nodes have an entry.
std::vector< double > cap
sn.cap and sn.classcap: the total and per-class buffers.
std::vector< JobClass > classes
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< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
double nclosedjobs() const
sn.nclosedjobs: the total population of the closed classes.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
Index of dispersion for counts (IDC) of a MAP at resolution t.
PatienceHandles< T > sn_patience_handles(const qn::NetworkStruct< T > &sn, std::size_t ist, std::size_t r)
Build the patience handles of station ist (0-based), class r.
bool sn_has_bursty_arrival(const qn::NetworkStruct< T > &sn)
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
T dist_lst(const Distrib< T > &d, const T &s)
sn.lst: the Laplace-Stieltjes transform E[exp(-sX)].
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
std::vector< T > map_count_idc(const Map< T > &m, const std::vector< T > &t)
Index of dispersion for counts (IDC) of a MAP at resolution t.
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
DispatchResult< T > solver_mva_qsys_prio_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_qsys_prio_analyzer.m: the exact Cobham formula for a single open M/G/1 queue with ...
DispatchResult< T > solver_mva_qsys_sizebased_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_qsys_sizebased_analyzer.m: the M/G/1 formulas for the size-based disciplines (Wier...
MvaSolution< T > solver_mva_polling_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_polling_analyzer.m.
MvaSolution< T > solver_mva_cacheqn_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt, qn::NetworkStruct< T > *refreshed_out=nullptr, MvaCacheqnCacheOutputs< T > *cache_out=nullptr)
bool sn_has_sjn(const qn::NetworkStruct< T > &L)
True when the layer has an SJF station, the reference's any(sn.sched == SchedStrategy....
std::vector< std::size_t > find_oi_stations(const qn::NetworkStruct< T > &L)
The OI stations of a model, 1-based station indices.
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...
MvaSolution< T > solver_rqna(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
SjnAnalyzerResult< T > solver_mva_sjn_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_sjn_analyzer.m.
std::string amva_method_alias(const std::string &m)
The amva.
DispatchResult< T > solver_mva_marie_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_marie_analyzer.m: Marie's iterative aggregation-decomposition for a CLOSED network...
CacheResult< T > solver_mva_cache_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_cache_analyzer.m for a Source-Cache-Sink model.
MvaSolution< T > solver_mvac_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mvac.m.
Definition solver_mvac.h:71
MvaSolution< T > solver_mapqn(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
MvaSolution< T > solver_qna(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_qna.m.
Definition solver_qna.h:76
DispatchResult< T > solver_mvald_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt, const Matrix< T > &init_sol)
Port of solver_mvald_analyzer.m: the load-dependent branch.
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_mvald(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mvald.m: exact MVA on a load-dependent model, through pfqn_mvaldmx.
MvaCacheqnRetrievalSolution< T > solver_mva_cacheqn_retrieval_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_cacheqn_retrieval_analyzer.m.
MvaSolution< T > solver_mva_oi_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_oi_analyzer.m.
MvaSolution< T > solver_rqt(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Definition solver_rqt.h:59
DispatchResult< T > solver_mva_qsys_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_qsys_analyzer.m: the closed forms for a single-class open Source-Queue-Sink model.
DispatchResult< T > solver_mva_dps_exact(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
The exact multiclass M/M/1-DPS solve the reference inlines in mvaDispatch: the truncated multiclass c...
MvaSolution< T > solver_amva(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, MvaOptions opt, const Matrix< T > &init_sol, bool &converged)
bool nc_is_oi_model(const qn::NetworkStruct< T > &L)
Port of nc_is_oi_model: whether the model as a WHOLE is order-independent, which is a strictly strong...
MvaSolution< T > solver_mva_retrieval_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt, MvaRetrievalCacheOutputs< T > *cache_out=nullptr)
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.
DispatchResult< T > mva_dispatch(const qn::NetworkStruct< T > &L, const MvaOptions &opt, const Matrix< T > &init_sol)
The ladder itself.
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
MarieResult< T > pfqn_marie(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const Matrix< T > &scv, double tol, int maxiter, const std::vector< int > &nservers)
Marie's method for a closed network with FCFS Coxian service.
Definition pfqn_marie.h:647
bool mva_qsys_serves_method(const std::string &method)
The method names solver_mva_qsys_analyzer has an arm for.
GigkRqtResult< T > qsys_gigk_rqt(const T &lambda, const T &mu, const T &Gamma_a, const T &Gamma_s, std::size_t k, const T &alpha_a, const T &alpha_s)
Robust Queueing Theory (RQT) worst-case system time of a G/G/k FCFS queue.
QsysQedResult< T > qsys_mmk_qed(const T &lambda, const T &mu, unsigned s)
Halfin-Whitt QED approximation for the M/M/s queue, and the square-root staffing rule that inverts it...
QsysResult< T > qsys_gigk_approx_kingman(const T &lambda, const T &mu, const T &ca, const T &cs, unsigned k)
Kingman (Lee-Longton) scaling of the exact M/M/k waiting time.
QsysResult< T > qsys_mm1(const T &lambda, const T &mu)
Exact mean response time of the M/M/1 queue.
Definition qsys_mm1.h:35
Gig1ExtremalResult< T > qsys_gig1_bnds_extremal(const T &lambda, const T &mu, const T &ca, const T &cs, std::size_t K=4000, std::size_t N=2000, bool skipTight=false)
Extremal two-moment bounds for the GI/GI/1 queue.
QsysResult< T > qsys_gigk_approx(const T &lambda, const T &mu, const T &ca, const T &cs, unsigned k)
Default G/I/G/k approximation of the mean response time.
QsysGgnmResult< T > qsys_ggnm_diffusion(const T &lambda, const T &mu, unsigned n, double m, const T &ca, const T &cs, const std::function< T(const T &)> &serviceCcdf=std::function< T(const T &)>(), double tol=1e-12, std::size_t panels=4000)
Diffusion approximation for the G/GI/n/m queue.
Mg1DisciplineResult< T > qsys_mg1_srpt(const std::vector< T > &lambda, const std::vector< T > &mu, const std::vector< T > &cs)
M/G/1 under SRPT (shortest remaining processing time), by the Schrage-Miller formula.
QsysAbandonResult< T > qsys_erlanga(const T &lambda, const T &mu, const T &theta, unsigned s, double r=std::numeric_limits< double >::infinity(), const MgisrgiOptions &opts=MgisrgiOptions())
Exact analysis of the Erlang A model M/M/s/r+M.
T qsys_gm1(const T &sigma, const T &mu)
Exact mean response time of the G/M/1 queue.
Definition qsys_gm1.h:40
QsysResult< T > qsys_gig1_approx_gelenbe(const T &lambda, const T &mu, const T &ca, const T &cs)
Gelenbe diffusion approximation with instantaneous-return boundary.
QsysResult< T > qsys_gig1_approx_marchal(const T &lambda, const T &mu, const T &ca, const T &cs)
Marchal approximation of the mean response time of a G/I/G/1 queue.
Mg1DisciplineResult< T > qsys_mg1_fb(const std::vector< T > &lambda, const std::vector< T > &mu, const std::vector< T > &cs)
M/G/1 under FB (feedback), also called LAS (least attained service).
Definition qsys_mg1_fb.h:82
QsysAbandonResult< T > qsys_mgisrgi_whitt(const T &lambda, const T &mu, unsigned s, double r, const Patience< T > &patience, const MgisrgiOptions &opts=MgisrgiOptions())
Engineering solution of the call-center model M/GI/s/r+GI.
QsysResult< T > qsys_gigk_approx_whitt(const T &lambda, const T &mu, const T &ca, const T &cs, unsigned k)
Whitt (1993) approximation for the GI/G/k queue, eqs.
Mg1DisciplineResult< T > qsys_mg1_lrpt(const std::vector< T > &lambda, const std::vector< T > &mu, const std::vector< T > &cs)
M/G/1 under LRPT (longest remaining processing time).
PhM1Result< T > qsys_phm1(const std::vector< T > &alpha, const Matrix< T > &Tm, const T &mu, const T &tol)
Exact PH/M/1, the GI/M/1 queue with phase-type interarrival times.
Definition qsys_phm1.h:92
Mm1DpsResult< T > qsys_mm1_dps(const std::vector< T > &lambda, const std::vector< T > &mu, const std::vector< T > &w, const T &tol, unsigned maxCutoff)
Multiclass M/M/1 under DPS (discriminatory processor sharing), solved numerically on the truncated po...
QsysResult< T > qsys_mg1(const T &lambda, const T &mu, const T &cs)
Exact mean response time of the M/G/1 queue (Pollaczek-Khinchine).
Definition qsys_mg1.h:39
QsysResult< T > qsys_gig1_approx_kobayashi(const T &lambda, const T &mu, const T &ca, const T &cs)
Kobayashi diffusion approximation for the G/I/G/1 queue.
Mg1kLossMgsResult< T > qsys_mg1k_loss_mgs(const T &lambda, const T &mu, const T &mu_scv, unsigned K)
MacGregor Smith's closed-form approximation of the M/G/1/K loss probability.
T qsys_gigk_rqt_gamma(const T &rho, const T &mu, const T &Gamma_a, const T &sigma_s, std::size_t k, const T &alpha_a, const std::string &regime="independent")
Service variability parameter of the Robust Queueing Theory (RQT) framework.
QsysResult< T > qsys_gig1_approx_allencunneen(const T &lambda, const T &mu, const T &ca, const T &cs)
Allen-Cunneen approximation of the mean response time of a G/I/G/1 queue.
QsysResult< T > qsys_mmk(const T &lambda, const T &mu, unsigned k)
Exact mean response time of the M/M/k queue (Erlang-C).
Definition qsys_mmk.h:60
QsysResult< T > qsys_gig1_approx_heyman(const T &lambda, const T &mu, const T &ca, const T &cs)
Heyman approximation of the mean response time of a G/I/G/1 queue.
QsysResult< T > qsys_gg1(const T &lambda, const T &mu, const T &ca2, const T &cs2)
G/G/1 dispatcher: exact where a two-moment description determines the answer, Allen-Cunneen otherwise...
Definition qsys_gg1.h:118
QsysResult< T > qsys_gig1_approx_klb(const T &lambda, const T &mu, const T &ca, const T &cs)
Kraemer and Langenbach-Belz approximation for the G/I/G/1 queue.
Mg1PrioResult< T > qsys_mg1_prio(const std::vector< T > &lambda, const std::vector< T > &mu, const std::vector< T > &cs)
M/G/1 with non-preemptive head-of-line priorities: per-class mean response times from the Cobham/Klei...
Mg1DisciplineResult< T > qsys_mg1_psjf(const std::vector< T > &lambda, const std::vector< T > &mu, const std::vector< T > &cs)
M/G/1 under PSJF (preemptive shortest job first).
QsysResult< T > qsys_gig1_ubnd_kingman(const T &lambda, const T &mu, const T &ca, const T &cs)
Kingman upper bound on the mean waiting time of a G/G/1 queue.
Mg1DisciplineResult< T > qsys_mg1_setf(const std::vector< T > &lambda, const std::vector< T > &mu, const std::vector< T > &cs)
M/G/1 under SETF (shortest elapsed time first), the non-preemptive counterpart of FB/LAS.
Gig1RqResult< T > qsys_gig1_rq(const T &rho, const T &mu, const T &cs2, IaFun &&IaFun_)
Robust Queueing (RQ) approximation of a G/GI/1 queue characterized by its arrival index of dispersion...
QsysResult< T > qsys_gig1_approx_kimura(const T &lambda, const T &mu, const T &ca, const T &cs)
Kimura diffusion-interpolation approximation for the G/I/G/1 queue.
CacheMetrics< T > cache_metrics_of(const qn::NetworkStruct< T > &sn, const std::vector< T > &hitprob, const std::vector< T > &missprob, const std::vector< T > &delayedprob, const std::vector< T > &latency, const Matrix< T > &hitproblist, const Matrix< T > &itemprob, const std::vector< T > &listcost)
Assemble CacheMetrics from what a cache analyzer returned.
CacheMetrics< T > cache_metrics_of_matrix(const qn::NetworkStruct< T > &sn, const Matrix< T > &hitprob, const Matrix< T > &missprob)
The same, for the integrated caching-queueing branch, whose hit and miss probabilities are (ncaches x...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
RootResult< T > root_brent(F f, const T &a0, const T &b0, const T &tol, unsigned maxiter=200)
Brent's method on a bracket with a sign change.
Definition rootfind.h:130
A queueing network and its refreshed NetworkStruct.
Marie's iterative aggregation-decomposition for closed networks with FCFS general (Coxian) service.
G/G/1 dispatcher: exact where a two-moment description determines the answer, Allen-Cunneen otherwise...
Diffusion approximation for the G/GI/n/m queue.
Allen-Cunneen approximation of the mean response time of a G/I/G/1 queue.
Gelenbe diffusion approximation with instantaneous-return boundary.
Heyman approximation of the mean response time of a G/I/G/1 queue.
Kimura diffusion-interpolation approximation for the G/I/G/1 queue.
Kraemer and Langenbach-Belz approximation for the G/I/G/1 queue.
Kobayashi diffusion approximation for the G/I/G/1 queue.
Marchal approximation of the mean response time of a G/I/G/1 queue.
Extremal two-moment bounds for the GI/GI/1 queue.
Robust Queueing (RQ) approximation of a G/GI/1 queue characterized by its arrival index of dispersion...
Kingman upper bound on the mean waiting time of a G/G/1 queue.
Default G/I/G/k approximation of the mean response time.
Kingman (Lee-Longton) scaling of the exact M/M/k waiting time.
Whitt (1993) approximation for the GI/G/k queue, eqs.
Robust Queueing Theory (RQT) worst-case system time of a G/G/k FCFS queue.
Service variability parameter of the Robust Queueing Theory (RQT) framework.
Exact mean response time of the G/M/1 queue.
Exact mean response time of the M/G/1 queue (Pollaczek-Khinchine).
M/G/1 under FB (feedback), also called LAS (least attained service).
M/G/1 under LRPT (longest remaining processing time).
M/G/1 with non-preemptive head-of-line priorities: per-class mean response times from the Cobham/Klei...
M/G/1 under PSJF (preemptive shortest job first).
M/G/1 under SETF (shortest elapsed time first), the non-preemptive counterpart of FB/LAS.
M/G/1 under SRPT (shortest remaining processing time), by the Schrage-Miller formula.
MacGregor Smith's closed-form approximation of the M/G/1/K loss probability.
Engineering solution of the call-center model M/GI/s/r+GI.
Exact mean response time of the M/M/1 queue.
Multiclass M/M/1 under DPS (discriminatory processor sharing), solved numerically on the truncated po...
Exact mean response time of the M/M/k queue (Erlang-C).
Halfin-Whitt QED approximation for the M/M/s queue, and the square-root staffing rule that inverts it...
Exact PH/M/1, the GI/M/1 queue with phase-type interarrival times.
Deterministic scalar root finding.
Port of matlab/src/api/sn/sn_has_bursty_arrival.m.
Port of matlab/src/api/sn/sn_patience_handles.m.
The DECLARED side of the gate: one feature set per solver.
SolverMVA method 'amva.mapqn': the horizontal-cut mean value analysis (mapqn_amva) of a closed multic...
SolverMVA over a SolverLN layer.
The non-reentrant cache analyzer: a Source-Cache-Sink model.
Integrated caching-queueing analyzer, a port of matlab/src/solvers/MVA/solver_mva_cacheqn_analyzer....
Port of solver_mva_cacheqn_retrieval_analyzer.m: a CLOSED integrated cache-queueing model whose Cache...
Exact mean-value analysis for networks with order-independent stations.
The multiclass open polling analyzer (ladder branch 5).
Delayed-hit (retrieval-system) cache analyzer, a port of matlab/src/solvers/MVA/solver_mva_retrieval_...
Closed networks with shortest-job-next (SJF) stations, ladder branch 0.
MVAC, exact mean value analysis BY CHAIN (Conway, de Souza e Silva and Lavenberg, IEEE Trans.
QNA, the two-moment open-network decomposition analyzer.
Robust Queueing Network Analyzer (RQNA), a port of matlab/src/solvers/MVA/solver_rqna....
Robust Queueing Network Analyzer (RQNA) of Robust Queueing Theory, a port of matlab/src/solvers/MVA/s...
Outcome of a scalar solve.
Definition rootfind.h:52
bool converged
tolerance was met before the iteration cap
Definition rootfind.h:57
T root
best estimate of the root
Definition rootfind.h:53
The patience law of one station-class pair, in the forms the solvers consume.
qsys::Patience< T > as_patience() const
This law as the Patience argument of the abandonment solvers.
bool present
Whether the pair declares reneging at all; everything below is unset when false.
bool isExponential
Whether the law is exponential, in which case the analysis is exact.
T rate
The abandonment rate 1/mean.
bool has_map() const
True when the type carries a (D0,D1) pair of its own.
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D0
Definition map_moment.h:54
What the cache analyzer reports beyond the [Q,U,R,T] block.
std::vector< T > hitprob
per class, NaN where the class does not read
std::vector< T > missprob
Matrix< T > itemprob
(n x h+1) per-item occupancy, column 0 = miss.
Matrix< T > hitproblist
(R x h) per-list hit fractions, access-weighted over items.
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
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
What the dispatch returns: the metrics plus the algorithm that produced them.
std::string actualmethod
The concrete algorithm, as the reference's actualmethod.
std::string warning
Non-empty when the analyzer that ran would have raised a reference line_warning and still returned a ...
solvers::CacheMetrics< T > cache
What a cache branch measured, EMPTY on every model without a Cache.
std::shared_ptr< qn::NetworkStruct< T > > refreshed_struct
Set only by the integrated cacheqn branch: the converged struct whose routing carries the actual hit/...
The cache half of the reference's return list: the (ncaches x nclasses) hit and miss split,...
Matrix< T > missprob
(ncaches x nclasses)
Matrix< T > hitprob
(ncaches x nclasses)
std::vector< Matrix< T > > itemprob
per cache, (n x h+1); EMPTY = not computed
What the closed delayed-hit analyzer returns.
std::vector< T > delayedprob
(K) zero on this path, by the reference's convention
Matrix< T > hitproblist
(K x h) NaN: not computed on this path
std::vector< T > missprob
(K) the delayed fraction is folded in here
std::vector< T > latency
(K) NaN: not computed on this path
std::vector< T > hitprob
(K) P(item cached), read class only
The options SolverMVA reads.
Definition mva_types.h:31
std::string method
Definition mva_types.h:32
The cache half of the reference's return list: hitprob, missprob, delayedprob, latency,...
Matrix< T > itemprob
(n x h+1), column 0 = miss
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
std::vector< T > C
Definition mva_types.h:98
What the analyzer returns, the reference's metrics plus its actualmethod.
std::string warning
Non-empty when the reference would have called line_warning AND RETURNED, carrying its text.
Result of pfqn_marie, mirroring the six MATLAB outputs.
Definition pfqn_marie.h:342
int it
iterations performed
Definition pfqn_marie.h:353
Matrix< T > Q
(M x R) mean queue length
Definition pfqn_marie.h:344
std::vector< T > X
(R) per-class throughput
Definition pfqn_marie.h:343
Matrix< T > U
(M x R) utilization
Definition pfqn_marie.h:345
One job class of the network.
double population
infinite for an open class
A node of the network.
One station of the network.
Steady-state measures of a multiserver queue with customer abandonment.
Steady-state measures of the G/GI/n/m diffusion approximation.
T meanNumber
mean number in system
Every Cache node of the model, in node order; empty on a model with none.