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