LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mva_runner.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_MVA_SOLVER_MVA_RUNNER_H
6#define LINE_SOLVERS_MVA_SOLVER_MVA_RUNNER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The SolverMVA class surface: `@@SolverMVA/runAnalyzer.m` and the gates around
12 * it.
13 *
14 * What sits here rather than in the dispatch is everything that happens BEFORE
15 * and AFTER one inner solve: the method whitelist, the structural gates, the
16 * conversions from response time to residence time and from throughput to
17 * arrival rate, and the metric filter. `mvaDispatch` is the callback; on a
18 * model without forks it runs exactly once.
19 *
20 * THE FILTER IS LOAD BEARING, not cosmetic. `@@NetworkSolver/getAvg` is not a
21 * getter: between the analyzer and the caller it zeroes a metric wherever the
22 * class has no visit, snaps anything below FineTol to zero, and masks the
23 * queue length and the utilization where the response time is below
24 * 10 FineTol. In a layered model every entry, task and call class is served by
25 * an Immediate distribution somewhere, whose response time is exactly 1e-8, so
26 * without the mask a layer reports the immediate classes' share of the
27 * population as real queue length.
28 */
29
30#include <algorithm>
31#include <cmath>
32#include <limits>
33#include <memory>
34#include <optional>
35#include <string>
36#include <vector>
37
41#include "line/util/lu.h"
51
52namespace line {
53namespace mva {
54
55/** The metrics `getAvg` returns, after filtering. */
56template <class T>
57struct AvgResult {
58 Matrix<T> QN; ///< queue length
59 Matrix<T> UN; ///< utilization
60 Matrix<T> RN; ///< response time, per visit
61 Matrix<T> TN; ///< throughput
62 Matrix<T> AN; ///< arrival rate
63 Matrix<T> WN; ///< residence time, per job
64 std::vector<T> CN; ///< system response time per class
65 std::vector<T> XN; ///< system throughput per class
66 std::string method; ///< the method asked for
67 std::string actualmethod;///< the algorithm that ran
68 /**
69 * The reference's own warning text, verbatim, empty when it did not warn.
70 *
71 * It belongs beside `actualmethod` because it is the same kind of thing: a
72 * statement about HOW the numbers were produced rather than a metric. It is
73 * carried, not thrown, because the answer is usable -- the SJN starvation
74 * cap leaves the population law exact and only the ACCURACY unwarranted,
75 * which is solver_nc_cdf.h:83's precedent, not solver_mam_retrial.h:437's.
76 * Dropping it silently is worse than a wrong number: the answer then looks
77 * authoritative exactly where the reference declines to stand behind it.
78 */
79 std::string warning;
80 /**
81 * (h) mean storage cost held by each cache list, K_j = sum_i sigma_i pi_ij,
82 * filled only by the NC cache branch on a model with item sizes; EMPTY
83 * otherwise. See ton21cache Sec. IX and [[09-ldes-and-cache]].
84 */
85 std::vector<T> listcost;
86 int iter = 0;
87 /**
88 * Whether the fixed point met its tolerance, empty when the handler reports
89 * none. Carried beside `iter` because the count alone cannot decide it on
90 * the AMVA route, where it aggregates the nested inner sweeps and reaches
91 * the cap on solves whose outer residual is exactly zero. A caller that
92 * reads the count as a convergence test raises false warnings; that is why
93 * the flag exists and why it has to reach the CLI envelope.
94 */
95 std::optional<bool> converged;
96 /**
97 * `@@SolverNC/getProbNormConstAggr`, i.e. the reference's
98 * `result.Prob.logNormConstAggr`, filled by the NC runner alone and EMPTY
99 * for every other solver.
100 *
101 * It sits here for `listcost`'s reason: it is a statement about the solve
102 * that only one solver can make, and the alternative -- calling
103 * `solver_nc_solve` a second time to recover it -- would solve the model
104 * twice for one number. Absent is not zero: log G = 0 is the constant of an
105 * empty network, so a caller must test the option rather than the value.
106 */
107 std::optional<double> lognormconst;
108 /**
109 * What the cache branches observed, EMPTY on a model with no Cache node and
110 * on every solver that does not analyze one. `getAvgCacheTable` and
111 * `getAvgItemTable` are built from it; see `solvers::CacheMetrics`.
112 */
114 /**
115 * The struct whose cache self-switch carries the CONVERGED hit/miss split,
116 * filled by the cacheqn branches and NULL for every other model.
117 *
118 * It has to travel with the result because the split is a SOLVER OUTPUT, not
119 * model structure: `link()` leaves the self-switch at the offered 1/2-1/2,
120 * and every quantity derived from a visit ratio -- ArvR, ResidT, and the
121 * node-level hit and miss throughputs -- is wrong by that ratio until the
122 * solve replaces it. The runner already uses this struct for the STATION
123 * table (see `refL` below); handing the caller the base struct instead makes
124 * `-a avg` and `-a node` disagree on one column of one model, which is the
125 * divergence `node_metrics` exists to avoid. Measured on cache_replc_routing:
126 * Cache/Router/Sink hit and miss read 1/1 off the base struct against 0.8/1.2.
127 */
128 std::shared_ptr<qn::NetworkStruct<T>> refreshed_struct;
129};
130
131/**
132 * Port of `SolverMVA.listValidMethods`.
133 *
134 * A LISTED NAME MUST ACTUALLY RUN. The reference records why: the bound family
135 * was delisted when it moved to SolverBA, because listing names that error made
136 * the list a false claim. `qdaql` is implemented in `solver_amva` and dispatched
137 * by no analyzer, so it stays off the list here too and the gate below refuses
138 * it.
139 */
140template <class T>
141std::vector<std::string> list_valid_methods(const qn::NetworkStruct<T>& L) {
142 std::vector<std::string> m{"default", "mva", "exact", "amva",
143 "sum", "esum", "qdlin", "amva.qdlin",
144 "bs", "amva.bs", "sqni", "qd",
145 "amva.qd", "qli", "amva.qli", "fli",
146 "amva.fli", "ab", "amva.ab", "schmidt",
147 "amva.schmidt", "schmidt-ext", "amva.schmidt-ext",
148 "scat", "amva.scat",
149 "lcp", "amva.lcp", "chow", "amva.chow",
150 "pamb", "amva.pamb", "pami", "amva.pami",
151 "pamt", "amva.pamt", "clust", "amva.clust",
152 "dmlin", "amva.dmlin"};
153
154 // AQL (pfqn_aql), QSA (pfqn_qsa) and Tay (pfqn_tay) reject multiserver
155 // stations in solver_amva, exactly as MATLAB SolverMVA.listValidMethods
156 // does, so all three are advertised for single-server models only.
157 if (!L.has_multi_server()) {
158 m.push_back("aql");
159 m.push_back("amva.aql");
160 m.push_back("qsa");
161 m.push_back("amva.qsa");
162 m.push_back("tay");
163 m.push_back("amva.tay");
164 }
165
166 bool open = true, closed = false;
167 for (const qn::JobClass& c : L.classes) {
168 if (std::isfinite(c.population) && c.population > 0.0) {
169 closed = true;
170 open = false;
171 }
172 }
173 (void)closed;
174 // SQD is the Smith queue-decomposition handler for a closed single-class
175 // Blocking-After-Service model; mva_is_bas_model advertises it only there --
176 // a closed single class with a BAS drop rule -- not for every single-chain
177 // closed model, which would let check_method accept 'sqd' where MATLAB
178 // rejects it by name.
179 bool has_bas = false;
180 for (const qn::Station<T>& st : L.stations)
181 for (int dr : st.droprule)
182 if (dr == static_cast<int>(lang::DropStrategy::BAS)) has_bas = true;
183 if (L.nclasses == 1 && !open && has_bas) m.push_back("sqd");
184
185 // MVAC recurs on the chains and has no population to remove from an open
186 // one, so SolverMVA.m:80 advertises it on ~any(isinf(njobs)) -- FULLY
187 // closed, not merely "has a closed class": a mixed model would otherwise
188 // pass the gate here and be refused again by solver_mvac_analyzer.
189 bool any_open_class = false;
190 for (const qn::JobClass& c : L.classes)
191 if (std::isinf(c.population)) any_open_class = true;
192 if (!any_open_class) m.push_back("mvac");
193
194 // SJN (shortest-job-next, pfqn_mvasjn / pfqn_amvasjn): the conditional
195 // waiting time equation is a population recursion, so the family runs on a
196 // CLOSED model with an SJF station and nowhere else -- mva_dispatch rejects
197 // an open one by name. Advertised only there, for the reason 'sqni' is
198 // gated: a listed name must actually run, and an unlisted one that IS
199 // dispatched is refused by check_method before the analyzer sees it.
200 if (!any_open_class && sn_has_sjn(L)) {
201 m.push_back("sjn.mva");
202 m.push_back("sjn.amva");
203 }
204
205 if (open) {
206 // QNA and RQNA are open-network decomposition analyzers; on a model with
207 // closed chains they have no fixed point and the reference advertises
208 // them only for a fully open model.
209 m.insert(m.begin() + 4, "rqt");
210 m.insert(m.begin() + 4, "rqna");
211 m.insert(m.begin() + 4, "qna");
212 } else {
213 // Marie is a closed-network method, and is withheld when the classes
214 // are not routed alike: measured against CTMC it is exact for a single
215 // class and for classes routed identically, and 6.2 off once they visit
216 // different queues, so the reference withholds it rather than let it
217 // answer wrongly.
218 bool classdep_routing = false;
219 for (std::size_t r = 0; r + 1 < L.nclasses; ++r)
220 for (std::size_t i = 1; i <= L.nof_nodes() && !classdep_routing; ++i)
221 for (std::size_t j = 1; j <= L.nof_nodes(); ++j)
222 if (L.route_eff(r + 1, r + 1, i, j) != L.route_eff(r + 2, r + 2, i, j)) {
223 classdep_routing = true;
224 break;
225 }
226 if (!classdep_routing) {
227 m.push_back("marie");
228 m.push_back("amva.marie");
229 }
230 }
231 // priomva: preemptive-resume priority arm (Chandy-Lakshmi [ChaL83]), offered
232 // only when a station actually uses FCFSPRPRIO. The arm itself lives in
233 // solver_amvald's forward step. Mirrors SolverMVA.m.
234 for (const qn::Station<T>& st : L.stations)
235 if (st.sched == qn::SchedStrategy::FCFSPRPRIO) {
236 m.push_back("priomva");
237 m.push_back("amva.priomva");
238 break;
239 }
240 // amva.mapqn: the horizontal-cut MVA for one exponential delay and one FCFS MAP
241 // queue (mapqn_amva); offered only on that shape, which mva_mapqn_reason
242 // judges for the list and the run alike.
243 if (mva_mapqn_reason(L, "mapqn").empty()) {
244 m.push_back("amva.mapqn");
245 }
246
247 m.push_back("lin");
248 m.push_back("egflin");
249 m.push_back("gflin");
250 m.push_back("amva.lin");
251
252 // The queueing-system closed forms, for an open single-class two-station
253 // model, which is exactly the shape the qsys analyzer serves.
254 if (open && L.nstations == 2 && L.nclasses == 1) {
255 const char* qsys[] = {"mm1", "mmk", "mg1",
256 "mgi1", "gm1", "gig1",
257 "gim1", "gig1.kingman", "gigk",
258 "gigk.kingman_approx", "gig1.gelenbe",
259 "gig1.heyman", "gig1.kimura", "gig1.allen",
260 "gig1.kobayashi", "gig1.klb", "gig1.marchal",
261 "gigk.whitt", "qed", "gig1.extremal", "gigk.diffusion"};
262 for (const char* q : qsys) m.push_back(q);
263 // The two abandonment methods are listed only when the station actually
264 // reneges: they have nothing to say about a queue nobody walks away
265 // from, and listing them there would name a method that cannot run.
266 const std::size_t qi = detail::station_of_type(L, qn::NodeType::Queue);
267 if (qi != 0 && api::sn_patience_handles(L, qi - 1, 0).present) {
268 m.push_back("erlanga");
269 m.push_back("mgisrgi");
270 }
271 }
272
273 // A LISTED NAME MUST ACTUALLY RUN, and the rules the feature registry cannot
274 // express have to be applied HERE: a product form, a class count and a server
275 // count have no registry name, and `auto_family_refusal` reaches them only
276 // through this list. Each predicate is the one the analyzer itself raises on,
277 // so a listed row is a row that runs and a withheld one is one that would have
278 // errored -- or, for the closed-population family on an open model, answered
279 // with a table of zeros.
280 std::size_t n_inf = 0;
281 for (const qn::Station<T>& st : L.stations)
282 if (st.sched == qn::SchedStrategy::INF) ++n_inf;
283 // The schmidt-ext arm recurs on the CHAIN populations, so the gate asks about
284 // those and not about the class ones; see mva_schmidt_ext_reason. Built lazily
285 // and once: it is the only rule here that costs a chain aggregation, and every
286 // other name in the list would pay for it.
287 std::vector<double> sx_n;
288 std::vector<bool> sx_fcfs;
289 bool sx_ready = false;
290 std::vector<std::string> keep;
291 keep.reserve(m.size());
292 for (const std::string& name : m) {
293 if (!mva_closed_population_reason(L, name).empty()) continue;
294 if (!mva_single_class_open_reason(L, name).empty()) continue;
295 if (!mva_mvac_reason(L, name).empty()) continue;
296 if (!mva_mapqn_reason(L, name).empty()) continue;
297 if (qn::mva_base_method(name) == "schmidt-ext") {
298 if (!sx_ready) {
300 for (std::size_t c = 0; c < L.nchains; ++c) sx_n.push_back(cd.Nchain[c]);
301 for (const qn::Station<T>& st : L.stations)
302 if (st.sched != qn::SchedStrategy::INF && st.sched != qn::SchedStrategy::EXT)
303 sx_fcfs.push_back(st.sched == qn::SchedStrategy::FCFS);
304 sx_ready = true;
305 }
306 if (!mva_schmidt_ext_reason(sx_n, sx_fcfs, name).empty()) continue;
307 }
308 // pfqn_sqni is a closed form for ONE queueing station with a delay; listing
309 // it elsewhere named a method solver_amva refuses by name.
310 if (qn::mva_base_method(name) == "sqni" && !(L.nstations == 2 && n_inf == 1)) continue;
311 keep.push_back(name);
312 }
313 m.swap(keep);
314 return m;
315}
316
317/**
318 * Port of `runAnalyzerChecks`' method gate: a method the solver does not list
319 * is refused before any analyzer sees it.
320 */
321template <class T>
322void check_method(const qn::NetworkStruct<T>& L, const std::string& method) {
323 const std::vector<std::string> valid = list_valid_methods(L);
324 if (std::find(valid.begin(), valid.end(), method) != valid.end()) return;
325 throw UnsupportedError("SolverMVA: the '" + method + "' method is unsupported by this solver");
326}
327
328/**
329 * Port of `SolverMVA.resolveMethod`: the feature-driven `default` -> `rqna`
330 * upgrade for a bursty single-class open network.
331 *
332 * The reference computes this ONLY to pick the method feature set
333 * (`getMethodFeatureSet`), never writing it back to options; `mva_dispatch`
334 * makes the same decision again on its own terminal branch. Without it the gate
335 * would read the base envelope, which declares neither MAP nor MMPP2, and would
336 * refuse the very models `solver_rqna` exists to solve.
337 */
338template <class T>
339std::string resolve_method(const qn::NetworkStruct<T>& L, const std::string& method) {
340 if (method != "default" || L.nclasses != 1) return method;
341 // JobClass::population is a plain double, as mva_dispatch.h:854 reads it; a
342 // num_traits<T> round trip would throw on an infinite one at T = Rational.
343 for (std::size_t r = 0; r < L.nclasses; ++r)
344 if (std::isfinite(L.classes[r].population)) return method;
345 if (api::sn_has_bursty_arrival(L)) return std::string("rqna");
346 if (L.nstations == 2) {
347 // A single-class open station customers ABANDON is a different model,
348 // not a correction to a G/G/k one: the resolution has to happen here as
349 // well as in mva_dispatch, because the feature gate runs on the
350 // resolved name and Reneging is admitted for these two methods only.
351 const std::size_t qi = detail::station_of_type(L, qn::NodeType::Queue);
352 if (qi != 0) {
354 if (h.present) return h.isExponential ? std::string("erlanga") : std::string("mgisrgi");
355 }
356 }
357 return method;
358}
359
360namespace detail {
361
362/**
363 * The visit ratios summed over chains, at STATION level.
364 *
365 * `sn.visits` is indexed by stateful node and by chain; both conversions below
366 * need one (nstations x nclasses) matrix, which is what `cellsum` followed by
367 * the statefulToStation mapping produces in the reference.
368 */
369template <class T>
370Matrix<T> station_visits(const qn::NetworkStruct<T>& L) {
371 const T zero = num_traits<T>::from_int(0);
372 Matrix<T> V(L.nstations, L.nclasses, zero);
373 for (std::size_t c = 0; c < L.nchains; ++c)
374 for (std::size_t i = 0; i < L.nstations; ++i) {
375 const std::size_t sf = L.stateful_of_station(i + 1);
376 for (std::size_t k = 0; k < L.nclasses; ++k)
377 V(i, k) = T(V(i, k) + L.visits[c](sf - 1, k));
378 }
379 return V;
380}
381
382} // namespace detail
383
384/**
385 * Port of `sn_get_residt_from_respt`: the per-JOB residence time.
386 *
387 * `RN` is per visit; multiplying by this station's visits and dividing by the
388 * reference station's visits of the chain's reference class turns it into the
389 * time a job spends here per completion. A response time already below FineTol
390 * (an Immediate class) is passed through unscaled, since scaling noise is
391 * still noise.
392 */
393template <class T>
395 const T zero = num_traits<T>::from_int(0);
396 const std::size_t M = L.nstations, K = L.nclasses;
397 const Matrix<T> V = detail::station_visits(L);
398 Matrix<T> WN(M, K, zero);
399 for (std::size_t i = 0; i < M; ++i)
400 for (std::size_t k = 0; k < K; ++k) {
401 // A PLACE HAS NO SERVICE PROCESS, SO `disabled` IS NOT ABSENCE --
402 // the same caveat `refresh_capacity` already carries. The reference
403 // gates this loop on the metric HANDLE (`WH{ist,k}.disabled`), which
404 // a Place holding a class does have; `sn.disabled` is only a
405 // stand-in for it, and on an SPN it is true at every Place, which
406 // zeroed the whole ResidT column (`-s jmt` reported ResidT 0 beside
407 // RespT 0.35308 on spn_basic_closed while every other codebase
408 // reported both).
409 const bool is_place = L.stations[i].nodetype == qn::NodeType::Place;
410 if ((L.disabled[i][k] && !is_place) || !(RN(i, k) > zero)) continue;
412 WN(i, k) = RN(i, k);
413 continue;
414 }
415 std::size_t c = L.nchains;
416 for (std::size_t cc = 0; cc < L.nchains; ++cc)
417 if (L.chains[cc][k]) c = cc;
418 if (c == L.nchains) continue;
419 const std::size_t rs = L.classes[k].refstat;
420 T den = zero;
421 if (L.refclass[c] > 0) {
422 den = V(rs - 1, L.refclass[c] - 1);
423 } else {
424 for (std::size_t r : L.inchain[c]) den += V(rs - 1, r - 1);
425 }
426 if (den > zero) WN(i, k) = T(RN(i, k) * V(i, k) / den);
427 }
428 for (std::size_t i = 0; i < M; ++i)
429 for (std::size_t k = 0; k < K; ++k)
431 WN(i, k) = zero;
432 return WN;
433}
434
435/**
436 * Port of `sn_get_arvr_from_tput`: the arrival rate each station sees, from the
437 * throughputs and the class-expanded routing `sn.rt`.
438 *
439 * A Source's row is left as the routing computes it and zeroed by the caller's
440 * mask, which is where `getAvg` does it.
441 *
442 * A NON-STATION STATEFUL NODE (Router, Cache) has no throughput of its own, so
443 * the throughput routed into it is found by solving the flow balance over all
444 * of them at once. The hit/miss split rides in the effective routing as a
445 * SELF-LOOP at the cache node (`set_route_effective(r, hitclass[r], ci, ci,
446 * hitprob)`), which is one more term of that system. A sweep in node order,
447 * as this used to be, left a node at zero whenever its feeder came later: a
448 * Router ahead of the Cache zeroed both Delay stations of
449 * cache_replc_routing (ArvR 0 against the reference's 0.4 and 0.6), and a
450 * Router fed by a later-declared Router zeroed the Delay after it.
451 */
452/**
453 * A Join reports the PER-SIBLING waiting time, QLen over the SIBLING ARRIVAL
454 * RATE, and not QLen over its own firing rate.
455 *
456 * The two differ by exactly the fork degree: a Join fires once per parent job
457 * while it takes in one sibling per branch, so Little's law applied with the
458 * firing rate answers a question about parents with a queue length measured in
459 * siblings. On the closed two-branch fork-join with N=2 that is 1.44444 where
460 * the settled JMT convention is 0.72222.
461 *
462 * Called by every engine that folds sibling classes back (CTMC and SSA), after
463 * their own `Q/T` pass, so the override lands on the same table the caller
464 * reads. A model with no Join leaves the matrix untouched.
465 */
466template <class T>
468 Matrix<T>& RN) {
469 const T zero = num_traits<T>::from_int(0);
470 for (std::size_t nd = 0; nd < L.nodes.size(); ++nd) {
471 if (L.nodes[nd].nodetype != qn::NodeType::Join) continue;
472 const std::size_t ist = L.nodes[nd].station;
473 if (ist == 0 || ist > RN.rows()) continue;
474 for (std::size_t r = 0; r < RN.cols(); ++r)
475 if (AN(ist - 1, r) > zero) RN(ist - 1, r) = QN(ist - 1, r) / AN(ist - 1, r);
476 }
477}
478
479template <class T>
481 const T zero = num_traits<T>::from_int(0);
482 const std::size_t M = L.nstations, R = L.nclasses;
483 const std::size_t S = L.nof_stateful();
484 Matrix<T> AN(M, R, zero);
485 if (L.rt.rows() != S * R) return AN; // no routing was refreshed
486
487 // Throughput per stateful node: a station contributes its own, and any
488 // other stateful node inherits what the routing carries into it.
489 Matrix<T> TS(S, R, zero);
490 for (std::size_t sf = 0; sf < S; ++sf) {
491 const std::size_t nd = L.stateful_nodes[sf];
492 const std::size_t ist = L.nodes[nd - 1].station;
493 if (ist == 0) continue;
494 for (std::size_t r = 0; r < R; ++r) TS(sf, r) = TN(ist - 1, r);
495 }
496 // Every non-station stateful node (Router, Cache) takes the flow balance x_u = x_k*P_ku + x_u*P_uu, solved
497 // exactly: a sweep in node order left a Router fed by a later-declared Router at zero, and the Cache's hit/miss
498 // self-loop is just one more term of P_uu.
499 std::vector<std::size_t> ucols;
500 std::vector<bool> is_u(S * R, false);
501 for (std::size_t sf = 0; sf < S; ++sf) {
502 if (L.nodes[L.stateful_nodes[sf] - 1].station != 0) continue;
503 for (std::size_t k = 0; k < R; ++k) {
504 ucols.push_back(sf * R + k);
505 is_u[sf * R + k] = true;
506 }
507 }
508 if (!ucols.empty()) {
509 const std::size_t nU = ucols.size();
510 Matrix<T> At(nU, nU, zero); // (I - P_uu)^T
511 std::vector<T> b(nU, zero);
512 for (std::size_t j = 0; j < nU; ++j) {
513 T acc = zero;
514 for (std::size_t col = 0; col < S * R; ++col)
515 if (!is_u[col]) acc += T(TS(col / R, col % R) * L.rt(col, ucols[j]));
516 b[j] = acc;
517 for (std::size_t i = 0; i < nU; ++i)
518 At(j, i) = T((i == j ? num_traits<T>::from_int(1) : zero) - L.rt(ucols[i], ucols[j]));
519 }
520 const std::vector<T> xu = line::solve(At, b);
521 for (std::size_t j = 0; j < nU; ++j) TS(ucols[j] / R, ucols[j] % R) = xu[j];
522 }
523 for (std::size_t ist = 0; ist < M; ++ist) {
524 const std::size_t sf_i = L.stateful_of_station(ist + 1) - 1;
525 for (std::size_t k = 0; k < R; ++k) {
526 T acc = zero;
527 for (std::size_t sf2 = 0; sf2 < S; ++sf2)
528 for (std::size_t r = 0; r < R; ++r)
529 acc += T(TS(sf2, r) * L.rt(sf2 * R + r, sf_i * R + k));
530 AN(ist, k) = acc;
531 }
532 }
533 return AN;
534}
535
536/**
537 * Which metric is being filtered. `@@MNetwork/getAvgHandles.m` disables a metric
538 * handle per KIND, not per station, and `filterMetric` turns a disabled handle
539 * into a zero -- so the kind is what decides which rows survive:
540 *
541 * Q, R, W disabled at a Source and at a Sink
542 * U disabled at a Source, a Sink, a Fork AND A JOIN -- a Join is an
543 * infinite server whose "utilization" would just restate its queue
544 * length, and the reference declines to report it
545 * T, A never disabled by station kind, only by an undefined service
546 *
547 * Every kind is additionally disabled where the class has no service defined.
548 */
549enum class MetricKind { QLen, Util, RespT, ResidT, Tput, ArvR };
550
551/**
552 * Port of `filterMetric`: what `@@NetworkSolver/getAvg` does between the
553 * analyzer and the caller.
554 *
555 * `zero_mask` carries the RN < 10 FineTol test the queue length and the
556 * utilization take and the other metrics do not.
557 */
558template <class T>
560 const std::vector<std::vector<bool>>* zero_mask) {
561 const T zero = num_traits<T>::from_int(0);
562 const std::size_t M = L.nstations, K = L.nclasses;
563 // the fork-join, cache and spawn-fed exemptions from the reachability test
564 // below: those three constructs are exactly the ones whose visit equations
565 // do not describe how a job actually reaches a station (a Join is entered by
566 // the parent class, which has no routing visit there), so a metric the
567 // analyzer reports as positive is trusted over the visit ratio
568 // ... and the STOCHASTIC PETRI NET, the fourth such construct and for the
569 // same reason: where a token sits is the MARKING, and a Place is reached by
570 // a transition firing rather than by a routing visit, so `sn.visits` is
571 // identically zero over every Place. Without the exemption this loop erased
572 // the whole table an analyzer had just computed -- `spnlp2.upper` on a
573 // fork-join net reported four zeros in place of the token bounds, and
574 // `r.QN >= tokens` then failed against a row the bound had got right.
575 bool has_fj = false, has_cache = false, has_spn = false;
576 for (const qn::NodeDef& nd : L.nodes) {
577 if (nd.nodetype == qn::NodeType::Fork || nd.nodetype == qn::NodeType::Join) has_fj = true;
578 if (nd.nodetype == qn::NodeType::Cache) has_cache = true;
579 if (nd.nodetype == qn::NodeType::Transition) has_spn = true;
580 }
581 // The Tput/ArvR HANDLE is disabled iff `~isServiceDefined && ~isCacheClass`
582 // (getAvgHandles.m): a service-undefined pair keeps its throughput only when
583 // the class is a cache hit/miss class -- NOT for fork-join. The fork-join
584 // exemption belongs to the reachability test (part 2 below), not here.
585 std::vector<bool> is_cache_class(K, false);
586 for (const auto& kv : L.nodeparam) {
587 const qn::CacheParam<T>& cp = kv.second;
588 for (std::size_t r = 0; r < cp.hitclass.size(); ++r) {
589 if (cp.hitclass[r] != 0 && cp.hitclass[r] <= K) is_cache_class[cp.hitclass[r] - 1] = true;
590 if (r < cp.missclass.size() && cp.missclass[r] != 0 && cp.missclass[r] <= K)
591 is_cache_class[cp.missclass[r] - 1] = true;
592 }
593 }
594
595 Matrix<T> out(M, K, zero);
596 for (std::size_t i = 0; i < M; ++i) {
597 const qn::NodeType nt = L.stations[i].nodetype;
598 const bool src_or_sink = (nt == qn::NodeType::Source || nt == qn::NodeType::Sink);
599 bool kind_disabled = false;
600 switch (kind) {
601 case MetricKind::QLen:
604 kind_disabled = src_or_sink;
605 break;
606 case MetricKind::Util:
607 kind_disabled = src_or_sink || nt == qn::NodeType::Fork || nt == qn::NodeType::Join;
608 break;
609 case MetricKind::Tput:
610 case MetricKind::ArvR:
611 kind_disabled = false;
612 break;
613 }
614 if (kind_disabled) continue;
615 // `hasServiceTunnel`, the wrapper this port was missing. Every metric in
616 // `getAvgHandles.m` gates its service test on `if ~hasServiceTunnel(ist)`
617 // -- a station whose SERVER is a ServiceTunnel is never disabled for
618 // having no service law, only by the kind-specific rules just above
619 // (which already match, arm for arm).
620 //
621 // Those stations are Source, Fork, Join and an ORDINARY Place. A Place is
622 // the one that has to be decided rather than looked up: `installQueueServer`
623 // REPLACES the tunnel on a queueing Place, so `hasServiceTunnel` is false
624 // there, and this struct carries no queueing flag. It is recovered the way
625 // MATLAB defines it, PER STATION: an ordinary Place has a law for no class
626 // at all, a queueing one has a real server. Deciding it per class instead
627 // would keep a queueing Place's explicitly Disabled class, which MATLAB
628 // disables.
629 //
630 // The Place arm is what made a pure Petri net print an EMPTY AvgTable:
631 // a non-queueing Place has NaN rates, which is `disabled` here, so every
632 // metric was zeroed and the printer drops an all-zero row -- while MATLAB
633 // reports e.g. P1 QLen 0.25966 on spn_basic_open. Source and Join change
634 // little in practice (a Source's ungenerated class and a Join both compute
635 // 0 or are already kept), but they are MATLAB's rule and are encoded here
636 // rather than left to coincidence.
637 bool service_tunnel = (nt == qn::NodeType::Source || nt == qn::NodeType::Fork ||
638 nt == qn::NodeType::Join);
639 if (nt == qn::NodeType::Place) {
640 service_tunnel = true;
641 for (std::size_t k = 0; k < K && k < L.service[i].size(); ++k)
642 if (!L.service[i][k].disabled) { service_tunnel = false; break; }
643 }
644 for (std::size_t k = 0; k < K; ++k) {
645 if (!L.disabled[i][k] || service_tunnel) {
646 out(i, k) = metric(i, k);
647 } else if (is_cache_class[k] &&
648 (kind == MetricKind::Tput || kind == MetricKind::ArvR)) {
649 // a cache over-routing sends a hit/miss class through a station
650 // that never serves it (a pass-through); its Tput/ArvR handle is
651 // not disabled, so the reported flow is kept
652 out(i, k) = metric(i, k);
653 }
654 }
655 }
656 if (zero_mask)
657 for (std::size_t i = 0; i < M; ++i)
658 for (std::size_t k = 0; k < K; ++k)
659 if ((*zero_mask)[i][k]) out(i, k) = zero;
660 for (std::size_t i = 0; i < M; ++i)
661 for (std::size_t k = 0; k < K; ++k)
662 if (num_traits<T>::to_double(out(i, k)) < GlobalConstants::FineTol) out(i, k) = zero;
663 // a class with no visit at a station holds nothing there
664 for (std::size_t k = 0; k < K; ++k) {
665 std::size_t c = L.nchains;
666 for (std::size_t cc = 0; cc < L.nchains; ++cc)
667 if (L.chains[cc][k]) c = cc;
668 if (c == L.nchains) continue;
669 for (std::size_t i = 0; i < M; ++i) {
670 if (L.visits[c](L.stateful_of_station(i + 1) - 1, k) != zero) continue;
671 if ((has_fj || has_cache || has_spn) &&
673 continue;
674 out(i, k) = zero;
675 }
676 }
677 return out;
678}
679
680/**
681 * The saturated open-station rule of `@@NetworkSolver/getAvg.m:221-308`.
682 *
683 * A finite-server queueing station whose open-class offered load
684 * `rho = sum_r TN(i,r) / (c * lldpeak * rate(i,r))` reaches 1 is saturated:
685 * its utilization is reported normalized to a station total of 1.0 and the
686 * open classes' queue length and response time as Inf. Throughput is NOT
687 * rescaled -- the offered rate is what the reference reports.
688 *
689 * IT LIVES HERE, NEXT TO `filter_metric`, BECAUSE EVERY SOLVER OWES IT. The
690 * reference applies it once in `getAvg.m`, above the analyzer, so MVA, NC and
691 * MAM all report a saturated station the same way. Keeping a private copy in
692 * the MVA runner is what let SolverNC report `QLen 3.05, Util 61.8` and
693 * SolverMAM the clamped `QLen 9.99e7` for the very same station MVA called
694 * Inf (chaos seed 215).
695 *
696 * CALL IT AFTER `out.WN` IS ASSEMBLED. `getAvg.m` computes the residence times
697 * off `RNclass` BEFORE the Inf overwrite, so ResidT keeps the pre-saturation
698 * `RN*V` value and does not become Inf with the response time.
699 *
700 * THE THREE EXCLUSIONS ARE NOT OPTIONAL once the rule is universal, because
701 * each names a station whose queue is bounded at any offered load and whose
702 * exact answer would otherwise be overwritten with Inf:
703 * - a BINDING BUFFER (`cap`, `classcap`, or a finite capacity region), where
704 * the excess is blocked or lost and `rho` sits exactly on 1;
705 * - RENEGING, where the excess abandons instead of accumulating (the Erlang A
706 * queue length `qsys_erlanga` computes exactly);
707 * - LOAD DEPENDENCE, where the nominal rate understates the server and
708 * `lldpeak` is the peak multiplier the reference divides by.
709 *
710 * Skipped under exact arithmetic, where infinity is not representable.
711 */
712template <class T>
714 Matrix<T>& RN, const Matrix<T>& TN) {
715 if constexpr (!num_traits<T>::has_transcendental) {
716 (void)L; (void)QN; (void)UN; (void)RN; (void)TN;
717 return false;
718 } else {
719 const std::size_t M = L.nstations, K = L.nclasses;
720 bool any_open = false;
721 for (std::size_t r = 0; r < K; ++r)
722 if (std::isinf(L.classes[r].population)) any_open = true;
723 if (!any_open) return false;
724
725 const double dinf = std::numeric_limits<double>::infinity();
726 bool any_unstable = false;
727 for (std::size_t i = 0; i < M; ++i) {
728 const double c = num_traits<T>::to_double(L.stations[i].nservers);
729 const qn::NodeType nt = L.stations[i].nodetype;
730 const qn::SchedStrategy sch = L.stations[i].sched;
731 if (!std::isfinite(c) || c <= 0.0) continue; // delay / infinite server
732 if (nt == qn::NodeType::Source || nt == qn::NodeType::Sink) continue;
733 if (sch == qn::SchedStrategy::INF || sch == qn::SchedStrategy::EXT) continue;
734
735 bool bounded = (i < L.cap.size() && std::isfinite(L.cap[i]));
736 if (!bounded && i < L.classcap.size())
737 for (double cc : L.classcap[i])
738 if (std::isfinite(cc)) { bounded = true; break; }
739 if (!bounded)
740 for (const auto& rg : L.regions)
741 if (i < rg.members.size() && rg.members[i]) { bounded = true; break; }
742 if (bounded) continue;
743
744 bool reneges = false;
745 for (lang::ImpatienceType it : L.stations[i].impatience)
746 if (it == lang::ImpatienceType::RENEGING) { reneges = true; break; }
747 if (reneges) continue;
748
749 double lldpeak = 1.0;
750 for (const T& v : L.stations[i].lldscaling) {
751 const double d = num_traits<T>::to_double(v);
752 if (std::isfinite(d) && d > 0.0 && d > lldpeak) lldpeak = d;
753 }
754
755 std::vector<double> rho(K, 0.0);
756 double rho_open = 0.0, rho_tot = 0.0;
757 for (std::size_t r = 0; r < K; ++r) {
758 const double rate = num_traits<T>::to_double(L.rates(i, r));
759 const double tp = num_traits<T>::to_double(TN(i, r));
760 if (rate > 0.0 && tp > 0.0) rho[r] = tp / (c * lldpeak * rate);
761 rho_tot += rho[r];
762 if (std::isinf(L.classes[r].population)) rho_open += rho[r];
763 }
764 if (rho_open < 1.0 || rho_tot <= 0.0) continue;
765 any_unstable = true;
766 for (std::size_t r = 0; r < K; ++r) {
767 UN(i, r) = num_traits<T>::from_double(rho[r] / rho_tot);
768 if (std::isinf(L.classes[r].population) && rho[r] > 0.0) {
769 QN(i, r) = num_traits<T>::from_double(dinf);
770 RN(i, r) = num_traits<T>::from_double(dinf);
771 }
772 }
773 }
774 return any_unstable;
775 }
776}
777
778/** The warning `getAvg.m` raises when cap_unstable_open_stations() fires. */
779inline const char* unstable_open_warning() {
780 return "The model has unstable queues (utilization >= 1); station utilization is "
781 "reported capped at 1.0, queue length and response time as Inf.";
782}
783
784/**
785 * Port of `SolverMVA.supportsFiniteCapacity` (SolverMVA.m:158-186), the
786 * structural capacity gate SolverMVA.supportsModelMethod (SolverMVA.m:143-153)
787 * lays on top of the universal feature gate.
788 *
789 * WHY IT IS SEPARATE FROM `feature_gate`. Finite capacity has no
790 * LINE_QN_FEATURE_LIST enumerator: it is a NUMBER on a station, not a
791 * construct, so no declared feature set can express it. Without this check a
792 * model built with setCapacity is solved as if uncapacitated and returns a
793 * WRONG NUMBER instead of a refusal (BUG-39).
794 *
795 * WHY IT RUNS AFTER THE FEATURE GATE. The reference calls
796 * supportsModelMethod@@NetworkSolver first and only tests capacity `if bool`, so
797 * a model that fails both is refused by the FEATURE, whose message names the
798 * construct to remove. The order is therefore load-bearing, not incidental.
799 *
800 * THE TWO EXEMPTIONS.
801 * - A BAS model, because solver_mva_analyzer routes it to solver_sqd under
802 * `default` too, so the finite buffers ARE honoured on every MVA path. The
803 * predicate is the api/sn one (single CLASS), not the analyzer's dispatch
804 * test mva_is_bas_model (single CHAIN); see solver_mva.h.
805 * - A single-station M/M/1/K with tail drop under any method but `exact`,
806 * which the mg1k.mgs branch of solver_mva_qsys_analyzer solves. That branch
807 * is an approximation away from scv=1, so `exact` is NOT exempted and must
808 * refuse here. `method` is the RESOLVED method, which is what
809 * runAnalyzerChecks (NetworkSolver.m:184) passes on.
810 */
811template <class T>
812void mva_check_finite_capacity(const qn::NetworkStruct<T>& L, const std::string& method) {
813 if (sn_is_bas_model(L)) return;
814 if (method != "exact" && sn_is_mm1k_loss(L)) return;
815 qn::check_binding_capacity("SolverMVA", L);
816}
817
818/**
819 * Port of `@@SolverMVA/runAnalyzer.m` for the `lang='matlab'` path: gate, solve,
820 * convert, filter.
821 *
822 * A model with a Fork is solved through the shared fork-join fixed point
823 * (fj_driver.h), which drives `mva_dispatch` as its inner solve on the
824 * transformed (auxiliary-expanded) model; a model without one runs the dispatch
825 * exactly once. The filtering below is common to both paths.
826 */
827template <class T>
829 const Matrix<T>& init_sol) {
830 check_method(L, opt_in.method);
831
832 // Finite Capacity Region: MVA does not enforce the aggregate per-region job
833 // limit and would otherwise silently return the unconstrained product-form
834 // answer. Port of `runAnalyzer.m:21-25`.
835 if (!L.regions.empty())
836 throw UnsupportedError(
837 "SolverMVA: this model uses a Finite Capacity Region (addRegion), which is not "
838 "supported by SolverMVA. Use SolverJMT, or setCapacity for a single-station limit.");
839
840 // resolveMethod (SolverMVA.resolveMethod) computes the bursty default->rqna
841 // upgrade only for the feature gate; the reference never writes it back to
842 // options. The substitution itself lives in mva_dispatch's terminal
843 // ordinary-QN branch, so the structural special cases (e.g. a 3-node
844 // MAP/M/1, a G/M/1 the qsys analyzer solves) keep method='default' rather
845 // than being forced into rqna and refused.
846 // runAnalyzerChecks' universal feature gate, AFTER the method gate so the
847 // specific message wins wherever one exists. Gated on `L`, before fj_mmt
848 // rewrites the Fork into a Router that MVA does not declare.
849 // The resolved method is passed so that a `default` -> `rqna` refusal names
850 // rqna, which is NetworkSolver.m:184-190's second message form: rqna's set
851 // withdraws ClosedClass, and nothing the user typed mentions rqna.
852 const std::string rm = resolve_method(L, opt_in.method);
853 // The MODEL-aware overload, because one grant in getMethodFeatureSet reads
854 // the model: every method but 'exact' carries FiniteCapacity on a
855 // single-station M/M/1/K with tail drop, the shape mva_dispatch answers
856 // under any name. `mva_check_finite_capacity` below is the same rule as a
857 // structural refusal, and the two must agree.
858 qn::feature_gate("SolverMVA", qn::mva_feature_set(rm, L), L, opt_in.method, rm);
860 const MvaOptions& opt = opt_in;
861
863 std::string actualmethod, warning;
864 // The integrated cacheqn analyzer returns a refreshed struct whose routing
865 // carries the actual hit/miss probabilities; ArvR and ResidT are read from
866 // it. Empty for every other model, in which case the base struct is used.
867 std::shared_ptr<qn::NetworkStruct<T>> refreshed;
868 // What a cache branch measured; see AvgResult::cache. Left empty on the
869 // fork-join path, whose inner solves are of a TRANSFORMED network.
871 if (L.has_fork()) {
872 // The transform turns the fork into a router, the join into a delay and
873 // the branches into auxiliary open classes; the driver iterates the
874 // auxiliary arrival rates to their fixed point and merges the auxiliary
875 // columns back before returning. `mva_dispatch` sees a plain mixed
876 // network, so its own fork checks never fire.
877 FjMmt<T> tr = fj_fork_join_transform(L, opt.fork_join);
878 std::vector<T> lam(tr.V.classes.size() + 1,
880 MvaOptions inner = opt;
881 inner.base_has_fork = true; // the transform removes the fork; see MvaOptions
882 std::string am, wn;
883 s = fj_fixed_point(L, tr, lam, opt, [&inner, &am, &wn](qn::NetworkStruct<T>& V) {
884 const DispatchResult<T> d = mva_dispatch(V, inner, Matrix<T>());
885 am = d.actualmethod;
886 // the last inner solve is the one the returned metrics came from
887 wn = d.warning;
888 return d.sol;
889 });
890 actualmethod = am;
891 warning = wn;
892 } else {
893 const DispatchResult<T> dr = mva_dispatch(L, opt, init_sol);
894 s = dr.sol;
895 actualmethod = dr.actualmethod;
896 warning = dr.warning;
897 refreshed = dr.refreshed_struct;
898 cache = dr.cache;
899 }
900 // Values (not topology) for ArvR/ResidT come from the refreshed struct when
901 // the cacheqn analyzer supplied one; filter_metric still keys on the base L
902 // so its cache pass-through masking is unaffected by the ClassSwitch relabel.
903 const qn::NetworkStruct<T>& refL = refreshed ? *refreshed : L;
904
905 const std::size_t M = L.nstations, K = L.nclasses;
906 std::vector<std::vector<bool>> mask(M, std::vector<bool>(K, false));
907 for (std::size_t i = 0; i < M; ++i)
908 for (std::size_t k = 0; k < K; ++k)
909 mask[i][k] = num_traits<T>::to_double(s.R(i, k)) < 10.0 * GlobalConstants::FineTol;
910
911 // `getAvg` zeroes the arrival rate at a Source explicitly: a Source is not
912 // fed by anything, and the routing would otherwise report whatever the
913 // stochastic complement carries back into it.
914 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(K, false));
915 for (std::size_t i = 0; i < M; ++i)
916 if (L.stations[i].nodetype == qn::NodeType::Source)
917 for (std::size_t k = 0; k < K; ++k) srcmask[i][k] = true;
918
919 AvgResult<T> out;
920 // Carried, not consumed and dropped: the node table needs the same visits
921 // the station table just used, or the two disagree on the cache columns.
922 out.refreshed_struct = refreshed;
923 // The cache answer a cache branch measured. Empty on every other model, and
924 // on the fork-join path, whose inner solves are of a TRANSFORMED network.
925 out.cache = cache;
926 out.QN = filter_metric(L, s.Q, MetricKind::QLen, &mask);
927 out.UN = filter_metric(L, s.U, MetricKind::Util, &mask);
928 out.RN = filter_metric(L, s.R, MetricKind::RespT, nullptr);
929 out.TN = filter_metric(L, s.Tp, MetricKind::Tput, nullptr);
930 // Both ArvR and ResidT come from the refreshed struct when the cacheqn
931 // analyzer supplied one: its cache self-switch is normalized at the ACTUAL
932 // hit/miss split, so the visits back the carried rate without the over-route
933 // inflation. filter_metric still keys on the base L for its topology masking.
934 out.WN = filter_metric(L, sn_get_residt_from_respt(refL, out.RN), MetricKind::ResidT, nullptr);
935 out.AN = filter_metric(L, sn_get_arvr_from_tput(refL, out.TN), MetricKind::ArvR, &srcmask);
936
937 // The saturated open-station rule, applied AFTER out.WN so the residence
938 // times keep their pre-saturation value, exactly as getAvg.m orders it.
939 if (cap_unstable_open_stations(L, out.QN, out.UN, out.RN, out.TN)) {
940 if (!warning.empty()) warning += " ";
941 warning += unstable_open_warning();
942 }
943
944 out.CN = s.C;
945 out.XN = s.X;
946 out.method = opt.method;
947 out.actualmethod = actualmethod;
948 // Immediate feedback is APPROXIMATED, not refused: a fed-back job holds its
949 // server, and mean-value analysis has no way to express that, so the visit
950 // it makes is counted as an ordinary re-entry. The reference warns and
951 // solves (`runAnalyzer.m:26-27`); refusing here would reject a model MATLAB
952 // and the JAR both answer. Appended rather than assigned, so a warning the
953 // dispatch already raised is not swallowed by this one.
954 if (L.has_immediate_feedback()) {
955 if (!warning.empty()) warning += " ";
956 warning +=
957 "SolverMVA does not handle immediate feedback (immfeed); the solver will treat "
958 "self-loops as class-switching with re-queueing.";
959 }
960 // Verbatim: a user must be able to match it against the reference output.
961 out.warning = warning;
962 out.iter = s.iter;
963 out.converged = s.converged;
964 return out;
965}
966
967} // namespace mva
968} // namespace line
969
970#endif // LINE_SOLVERS_MVA_SOLVER_MVA_RUNNER_H
What a solver observed about the Cache nodes of a model.
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
bool has_immediate_feedback() const
Whether immediate feedback is EFFECTIVE anywhere in the model.
std::size_t stateful_of_station(std::size_t st) const
std::size_t nof_nodes() const
std::size_t nof_stateful() const
std::vector< std::size_t > stateful_nodes
1-based node indices, ascending
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< std::vector< bool > > chains
(nchains x nclasses)
std::vector< std::size_t > refclass
(nchains) 1-based class, 0 = none
std::vector< std::vector< bool > > disabled
Matrix< T > rt
sn.rt and sn.rtnodes: the class-expanded routing.
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 ...
T route_eff(std::size_t r, std::size_t s, std::size_t i, std::size_t j) const
The routing actually in force: the expansion when there is one, else P.
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
std::vector< NodeDef > nodes
every node, in creation order
std::vector< std::vector< double > > classcap
std::vector< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
std::vector< Region > regions
The fork-join fixed point that drives one inner MVA solve.
The Heidelberger-Trivedi fork-join transform, options.config.fork_join='ht'.
The fork-join transform SolverMVA applies before solving a layer that contains a Fork.
LU factorization with partial pivoting, templated on the number type.
Port of @@SolverMVA/mvaDispatch.m: one inner solve, choosing the analyzer that fits the model.
The option and result types every MVA analyzer shares.
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)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
ImpatienceType
Impatience kinds, with the values of MATLAB ImpatienceType.
Definition lang_types.h:444
Matrix< T > sn_get_residt_from_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &RN)
Port of sn_get_residt_from_respt: the per-JOB residence time.
Matrix< T > filter_metric(const qn::NetworkStruct< T > &L, const Matrix< T > &metric, MetricKind kind, const std::vector< std::vector< bool > > *zero_mask)
Port of filterMetric: what @@NetworkSolver/getAvg does between the analyzer and the caller.
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::string mva_single_class_open_reason(const qn::NetworkStruct< T > &L, const std::string &method)
RQNA and RQT decompose an open network into GI/G/1 queues and build one uncertainty set per flow out ...
Definition mva_types.h:178
bool cap_unstable_open_stations(const qn::NetworkStruct< T > &L, Matrix< T > &QN, Matrix< T > &UN, Matrix< T > &RN, const Matrix< T > &TN)
The saturated open-station rule of @@NetworkSolver/getAvg.m:221-308.
std::string mva_schmidt_ext_reason(const std::vector< double > &njobs, const std::vector< bool > &fcfs, const std::string &method)
The extended Schmidt method needs a customer of every class to tag.
Definition mva_types.h:219
bool sn_is_mm1k_loss(const qn::NetworkStruct< T > &L)
Port of matlab/src/api/sn/sn_is_mm1k_loss.m: a single-class open Source-Queue-Sink system whose queue...
MetricKind
Which metric is being filtered.
std::string resolve_method(const qn::NetworkStruct< T > &L, const std::string &method)
Port of SolverMVA.resolveMethod: the feature-driven default -> rqna upgrade for a bursty single-class...
std::vector< std::string > list_valid_methods(const qn::NetworkStruct< T > &L)
Port of SolverMVA.listValidMethods.
bool sn_is_bas_model(const qn::NetworkStruct< T > &L)
Port of matlab/src/api/sn/sn_is_bas_model.m: a closed single-CLASS model with a BAS drop rule.
std::string mva_mvac_reason(const qn::NetworkStruct< T > &L, const std::string &method)
MVAC is the exact chain recursion over single-server fixed-rate (SSFR) queues and infinite-server cen...
Definition mva_types.h:245
const char * unstable_open_warning()
The warning getAvg.m raises when cap_unstable_open_stations() fires.
void mva_check_finite_capacity(const qn::NetworkStruct< T > &L, const std::string &method)
Port of SolverMVA.supportsFiniteCapacity (SolverMVA.m:158-186), the structural capacity gate SolverMV...
std::string mva_mapqn_reason(const qn::NetworkStruct< T > &L, const std::string &method)
The reason 'amva.mapqn' cannot solve L, or "" when it can (and "" for any other method).
FjMmt< T > fj_fork_join_transform(const qn::NetworkStruct< T > &L, const std::string &method)
options.config.fork_join -> the transform it names.
Definition fj_ht.h:389
void sn_apply_join_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &QN, const Matrix< T > &AN, Matrix< T > &RN)
Port of sn_get_arvr_from_tput: the arrival rate each station sees, from the throughputs and the class...
std::string mva_closed_population_reason(const qn::NetworkStruct< T > &L, const std::string &method)
Per-method structural gates, shared by list_valid_methods and by the analyzers themselves.
Definition mva_types.h:145
Matrix< T > sn_get_arvr_from_tput(const qn::NetworkStruct< T > &L, const Matrix< T > &TN)
void check_method(const qn::NetworkStruct< T > &L, const std::string &method)
Port of runAnalyzerChecks' method gate: a method the solver does not list is refused before any analy...
MvaSolution< T > fj_fixed_point(const qn::NetworkStruct< T > &L, FjMmt< T > &tr, std::vector< T > &lam, const MvaOptions &opt, InnerSolve inner)
Drive the fork-join fixed point of a transformed model to convergence.
Definition fj_driver.h:435
AvgResult< T > solver_mva_run_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt_in, const Matrix< T > &init_sol)
Port of @@SolverMVA/runAnalyzer.m for the lang='matlab' path: gate, solve, convert,...
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
void check_binding_capacity(const std::string &solver, const NetworkStruct< T > &sn)
FeatureSet mva_feature_set(const std::string &raw_method)
void feature_gate(const std::string &solver, const FeatureSet &declared, const NetworkStruct< T > &sn, const std::string &requested_method="", const std::string &resolved_method="")
runAnalyzerChecks: refuse a model the solver does not declare, by name.
std::string mva_base_method(const std::string &method)
SolverMVA.getFeatureSet, plus getMethodFeatureSet's per-method deltas.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
A queueing network and its refreshed NetworkStruct.
Chain aggregation and de-aggregation.
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.
Closed networks with shortest-job-next (SJF) stations, ladder branch 0.
The patience law of one station-class pair, in the forms the solvers consume.
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.
The metrics getAvg returns, after filtering.
std::shared_ptr< qn::NetworkStruct< T > > refreshed_struct
The struct whose cache self-switch carries the CONVERGED hit/miss split, filled by the cacheqn branch...
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
std::string warning
The reference's own warning text, verbatim, empty when it did not warn.
Matrix< T > UN
utilization
std::vector< T > listcost
(h) mean storage cost held by each cache list, K_j = sum_i sigma_i pi_ij, filled only by the NC cache...
std::optional< double > lognormconst
@@SolverNC/getProbNormConstAggr, i.e.
std::optional< bool > converged
Whether the fixed point met its tolerance, empty when the handler reports none.
Matrix< T > WN
residence time, per job
std::string method
the method asked for
std::string actualmethod
the algorithm that ran
Matrix< T > QN
queue length
std::vector< T > CN
system response time per class
std::vector< T > XN
system throughput per class
solvers::CacheMetrics< T > cache
What the cache branches observed, EMPTY on a model with no Cache node and on every solver that does n...
Matrix< T > AN
arrival rate
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
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 transformed layer and the bookkeeping the fixed point needs to drive it and to merge its results ...
Definition fj_mmt.h:113
static constexpr double FineTol
Definition lang_types.h:760
The options SolverMVA reads.
Definition mva_types.h:31
bool base_has_fork
Whether the model this solve came from has a Fork, which the model handed to the analyzer no longer d...
Definition mva_types.h:65
std::string method
Definition mva_types.h:32
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
std::optional< bool > converged
Whether the fixed point met its tolerance, or empty when the handler reports none.
Definition mva_types.h:109
std::vector< std::size_t > missclass
std::vector< std::size_t > hitclass
One job class of the network.
double population
infinite for an open class
A node of the network.
One station of the network.
std::vector< int > droprule
Per-class blocking rule as an INT, with 0 meaning "not set".
SchedStrategy sched
Every Cache node of the model, in node order; empty on a model with none.