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
50
51namespace line {
52namespace mva {
53
54/** The metrics `getAvg` returns, after filtering. */
55template <class T>
56struct AvgResult {
57 Matrix<T> QN; ///< queue length
58 Matrix<T> UN; ///< utilization
59 Matrix<T> RN; ///< response time, per visit
60 Matrix<T> TN; ///< throughput
61 Matrix<T> AN; ///< arrival rate
62 Matrix<T> WN; ///< residence time, per job
63 std::vector<T> CN; ///< system response time per class
64 std::vector<T> XN; ///< system throughput per class
65 std::string method; ///< the method asked for
66 std::string actualmethod;///< the algorithm that ran
67 /**
68 * The reference's own warning text, verbatim, empty when it did not warn.
69 *
70 * It belongs beside `actualmethod` because it is the same kind of thing: a
71 * statement about HOW the numbers were produced rather than a metric. It is
72 * carried, not thrown, because the answer is usable -- the SJN starvation
73 * cap leaves the population law exact and only the ACCURACY unwarranted,
74 * which is solver_nc_cdf.h:83's precedent, not solver_mam_retrial.h:437's.
75 * Dropping it silently is worse than a wrong number: the answer then looks
76 * authoritative exactly where the reference declines to stand behind it.
77 */
78 std::string warning;
79 /**
80 * (h) mean storage cost held by each cache list, K_j = sum_i sigma_i pi_ij,
81 * filled only by the NC cache branch on a model with item sizes; EMPTY
82 * otherwise. See ton21cache Sec. IX and [[09-ldes-and-cache]].
83 */
84 std::vector<T> listcost;
85 int iter = 0;
86 /**
87 * Whether the fixed point met its tolerance, empty when the handler reports
88 * none. Carried beside `iter` because the count alone cannot decide it on
89 * the AMVA route, where it aggregates the nested inner sweeps and reaches
90 * the cap on solves whose outer residual is exactly zero. A caller that
91 * reads the count as a convergence test raises false warnings; that is why
92 * the flag exists and why it has to reach the CLI envelope.
93 */
94 std::optional<bool> converged;
95 /**
96 * `@@SolverNC/getProbNormConstAggr`, i.e. the reference's
97 * `result.Prob.logNormConstAggr`, filled by the NC runner alone and EMPTY
98 * for every other solver.
99 *
100 * It sits here for `listcost`'s reason: it is a statement about the solve
101 * that only one solver can make, and the alternative -- calling
102 * `solver_nc_solve` a second time to recover it -- would solve the model
103 * twice for one number. Absent is not zero: log G = 0 is the constant of an
104 * empty network, so a caller must test the option rather than the value.
105 */
106 std::optional<double> lognormconst;
107 /**
108 * What the cache branches observed, EMPTY on a model with no Cache node and
109 * on every solver that does not analyze one. `getAvgCacheTable` and
110 * `getAvgItemTable` are built from it; see `solvers::CacheMetrics`.
111 */
113 /**
114 * The struct whose cache self-switch carries the CONVERGED hit/miss split,
115 * filled by the cacheqn branches and NULL for every other model.
116 *
117 * It has to travel with the result because the split is a SOLVER OUTPUT, not
118 * model structure: `link()` leaves the self-switch at the offered 1/2-1/2,
119 * and every quantity derived from a visit ratio -- ArvR, ResidT, and the
120 * node-level hit and miss throughputs -- is wrong by that ratio until the
121 * solve replaces it. The runner already uses this struct for the STATION
122 * table (see `refL` below); handing the caller the base struct instead makes
123 * `-a avg` and `-a node` disagree on one column of one model, which is the
124 * divergence `node_metrics` exists to avoid. Measured on cache_replc_routing:
125 * Cache/Router/Sink hit and miss read 1/1 off the base struct against 0.8/1.2.
126 */
127 std::shared_ptr<qn::NetworkStruct<T>> refreshed_struct;
128};
129
130/**
131 * Port of `SolverMVA.listValidMethods`.
132 *
133 * A LISTED NAME MUST ACTUALLY RUN. The reference records why: the bound family
134 * was delisted when it moved to SolverBA, because listing names that error made
135 * the list a false claim. `qdaql` is implemented in `solver_amva` and dispatched
136 * by no analyzer, so it stays off the list here too and the gate below refuses
137 * it.
138 */
139template <class T>
140std::vector<std::string> list_valid_methods(const qn::NetworkStruct<T>& L) {
141 std::vector<std::string> m{"default", "mva", "exact", "amva",
142 "sum", "esum", "qdlin", "amva.qdlin",
143 "bs", "amva.bs", "sqni", "qd",
144 "amva.qd", "qli", "amva.qli", "fli",
145 "amva.fli", "ab", "amva.ab", "schmidt",
146 "amva.schmidt", "schmidt-ext", "amva.schmidt-ext",
147 "scat", "amva.scat",
148 "lcp", "amva.lcp", "chow", "amva.chow",
149 "pamb", "amva.pamb", "pami", "amva.pami",
150 "pamt", "amva.pamt", "clust", "amva.clust",
151 "dmlin", "amva.dmlin"};
152
153 // AQL (pfqn_aql), QSA (pfqn_qsa) and Tay (pfqn_tay) reject multiserver
154 // stations in solver_amva, exactly as MATLAB SolverMVA.listValidMethods
155 // does, so all three are advertised for single-server models only.
156 if (!L.has_multi_server()) {
157 m.push_back("aql");
158 m.push_back("amva.aql");
159 m.push_back("qsa");
160 m.push_back("amva.qsa");
161 m.push_back("tay");
162 m.push_back("amva.tay");
163 }
164
165 bool open = true, closed = false;
166 for (const qn::JobClass& c : L.classes) {
167 if (std::isfinite(c.population) && c.population > 0.0) {
168 closed = true;
169 open = false;
170 }
171 }
172 (void)closed;
173 // SQD is the Smith queue-decomposition handler for a closed single-class
174 // Blocking-After-Service model; mva_is_bas_model advertises it only there --
175 // a closed single class with a BAS drop rule -- not for every single-chain
176 // closed model, which would let check_method accept 'sqd' where MATLAB
177 // rejects it by name.
178 bool has_bas = false;
179 for (const qn::Station<T>& st : L.stations)
180 for (int dr : st.droprule)
181 if (dr == static_cast<int>(lang::DropStrategy::BAS)) has_bas = true;
182 if (L.nclasses == 1 && !open && has_bas) m.push_back("sqd");
183
184 // MVAC recurs on the chains and has no population to remove from an open
185 // one, so SolverMVA.m:80 advertises it on ~any(isinf(njobs)) -- FULLY
186 // closed, not merely "has a closed class": a mixed model would otherwise
187 // pass the gate here and be refused again by solver_mvac_analyzer.
188 bool any_open_class = false;
189 for (const qn::JobClass& c : L.classes)
190 if (std::isinf(c.population)) any_open_class = true;
191 if (!any_open_class) m.push_back("mvac");
192
193 // SJN (shortest-job-next, pfqn_mvasjn / pfqn_amvasjn): the conditional
194 // waiting time equation is a population recursion, so the family runs on a
195 // CLOSED model with an SJF station and nowhere else -- mva_dispatch rejects
196 // an open one by name. Advertised only there, for the reason 'sqni' is
197 // gated: a listed name must actually run, and an unlisted one that IS
198 // dispatched is refused by check_method before the analyzer sees it.
199 if (!any_open_class && sn_has_sjn(L)) {
200 m.push_back("sjn.mva");
201 m.push_back("sjn.amva");
202 }
203
204 if (open) {
205 // QNA and RQNA are open-network decomposition analyzers; on a model with
206 // closed chains they have no fixed point and the reference advertises
207 // them only for a fully open model.
208 m.insert(m.begin() + 4, "rqt");
209 m.insert(m.begin() + 4, "rqna");
210 m.insert(m.begin() + 4, "qna");
211 } else {
212 // Marie is a closed-network method, and is withheld when the classes
213 // are not routed alike: measured against CTMC it is exact for a single
214 // class and for classes routed identically, and 6.2 off once they visit
215 // different queues, so the reference withholds it rather than let it
216 // answer wrongly.
217 bool classdep_routing = false;
218 for (std::size_t r = 0; r + 1 < L.nclasses; ++r)
219 for (std::size_t i = 1; i <= L.nof_nodes() && !classdep_routing; ++i)
220 for (std::size_t j = 1; j <= L.nof_nodes(); ++j)
221 if (L.route_eff(r + 1, r + 1, i, j) != L.route_eff(r + 2, r + 2, i, j)) {
222 classdep_routing = true;
223 break;
224 }
225 if (!classdep_routing) {
226 m.push_back("marie");
227 m.push_back("amva.marie");
228 }
229 }
230 // priomva: preemptive-resume priority arm (Chandy-Lakshmi [ChaL83]), offered
231 // only when a station actually uses FCFSPRPRIO. The arm itself lives in
232 // solver_amvald's forward step. Mirrors SolverMVA.m.
233 for (const qn::Station<T>& st : L.stations)
234 if (st.sched == qn::SchedStrategy::FCFSPRPRIO) {
235 m.push_back("priomva");
236 m.push_back("amva.priomva");
237 break;
238 }
239 // amva.mapqn: the horizontal-cut MVA for one exponential delay and one FCFS MAP
240 // queue (mapqn_amva); offered only on that shape, which mva_mapqn_reason
241 // judges for the list and the run alike.
242 if (mva_mapqn_reason(L, "mapqn").empty()) {
243 m.push_back("amva.mapqn");
244 }
245
246 m.push_back("lin");
247 m.push_back("egflin");
248 m.push_back("gflin");
249 m.push_back("amva.lin");
250
251 // The queueing-system closed forms, for an open single-class two-station
252 // model, which is exactly the shape the qsys analyzer serves.
253 if (open && L.nstations == 2 && L.nclasses == 1) {
254 const char* qsys[] = {"mm1", "mmk", "mg1",
255 "mgi1", "gm1", "gig1",
256 "gim1", "gig1.kingman", "gigk",
257 "gigk.kingman_approx", "gig1.gelenbe",
258 "gig1.heyman", "gig1.kimura", "gig1.allen",
259 "gig1.kobayashi", "gig1.klb", "gig1.marchal",
260 "gigk.whitt", "qed", "gig1.extremal", "gigk.diffusion"};
261 for (const char* q : qsys) m.push_back(q);
262 // The two abandonment methods are listed only when the station actually
263 // reneges: they have nothing to say about a queue nobody walks away
264 // from, and listing them there would name a method that cannot run.
265 const std::size_t qi = detail::station_of_type(L, qn::NodeType::Queue);
266 if (qi != 0 && api::sn_patience_handles(L, qi - 1, 0).present) {
267 m.push_back("erlanga");
268 m.push_back("mgisrgi");
269 }
270 }
271
272 // A LISTED NAME MUST ACTUALLY RUN, and the rules the feature registry cannot
273 // express have to be applied HERE: a product form, a class count and a server
274 // count have no registry name, and `auto_family_refusal` reaches them only
275 // through this list. Each predicate is the one the analyzer itself raises on,
276 // so a listed row is a row that runs and a withheld one is one that would have
277 // errored -- or, for the closed-population family on an open model, answered
278 // with a table of zeros.
279 std::size_t n_inf = 0;
280 for (const qn::Station<T>& st : L.stations)
281 if (st.sched == qn::SchedStrategy::INF) ++n_inf;
282 // The schmidt-ext arm recurs on the CHAIN populations, so the gate asks about
283 // those and not about the class ones; see mva_schmidt_ext_reason. Built lazily
284 // and once: it is the only rule here that costs a chain aggregation, and every
285 // other name in the list would pay for it.
286 std::vector<double> sx_n;
287 std::vector<bool> sx_fcfs;
288 bool sx_ready = false;
289 std::vector<std::string> keep;
290 keep.reserve(m.size());
291 for (const std::string& name : m) {
292 if (!mva_closed_population_reason(L, name).empty()) continue;
293 if (!mva_single_class_open_reason(L, name).empty()) continue;
294 if (!mva_mvac_reason(L, name).empty()) continue;
295 if (!mva_mapqn_reason(L, name).empty()) continue;
296 if (qn::mva_base_method(name) == "schmidt-ext") {
297 if (!sx_ready) {
299 for (std::size_t c = 0; c < L.nchains; ++c) sx_n.push_back(cd.Nchain[c]);
300 for (const qn::Station<T>& st : L.stations)
301 if (st.sched != qn::SchedStrategy::INF && st.sched != qn::SchedStrategy::EXT)
302 sx_fcfs.push_back(st.sched == qn::SchedStrategy::FCFS);
303 sx_ready = true;
304 }
305 if (!mva_schmidt_ext_reason(sx_n, sx_fcfs, name).empty()) continue;
306 }
307 // pfqn_sqni is a closed form for ONE queueing station with a delay; listing
308 // it elsewhere named a method solver_amva refuses by name.
309 if (qn::mva_base_method(name) == "sqni" && !(L.nstations == 2 && n_inf == 1)) continue;
310 keep.push_back(name);
311 }
312 m.swap(keep);
313 return m;
314}
315
316/**
317 * Port of `runAnalyzerChecks`' method gate: a method the solver does not list
318 * is refused before any analyzer sees it.
319 */
320template <class T>
321void check_method(const qn::NetworkStruct<T>& L, const std::string& method) {
322 const std::vector<std::string> valid = list_valid_methods(L);
323 if (std::find(valid.begin(), valid.end(), method) != valid.end()) return;
324 throw UnsupportedError("SolverMVA: the '" + method + "' method is unsupported by this solver");
325}
326
327/**
328 * Port of `SolverMVA.resolveMethod`: the feature-driven `default` -> `rqna`
329 * upgrade for a bursty single-class open network.
330 *
331 * The reference computes this ONLY to pick the method feature set
332 * (`getMethodFeatureSet`), never writing it back to options; `mva_dispatch`
333 * makes the same decision again on its own terminal branch. Without it the gate
334 * would read the base envelope, which declares neither MAP nor MMPP2, and would
335 * refuse the very models `solver_rqna` exists to solve.
336 */
337template <class T>
338std::string resolve_method(const qn::NetworkStruct<T>& L, const std::string& method) {
339 if (method != "default" || L.nclasses != 1) return method;
340 // JobClass::population is a plain double, as mva_dispatch.h:854 reads it; a
341 // num_traits<T> round trip would throw on an infinite one at T = Rational.
342 for (std::size_t r = 0; r < L.nclasses; ++r)
343 if (std::isfinite(L.classes[r].population)) return method;
344 if (api::sn_has_bursty_arrival(L)) return std::string("rqna");
345 if (L.nstations == 2) {
346 // A single-class open station customers ABANDON is a different model,
347 // not a correction to a G/G/k one: the resolution has to happen here as
348 // well as in mva_dispatch, because the feature gate runs on the
349 // resolved name and Reneging is admitted for these two methods only.
350 const std::size_t qi = detail::station_of_type(L, qn::NodeType::Queue);
351 if (qi != 0) {
353 if (h.present) return h.isExponential ? std::string("erlanga") : std::string("mgisrgi");
354 }
355 }
356 return method;
357}
358
359namespace detail {
360
361/**
362 * The visit ratios summed over chains, at STATION level.
363 *
364 * `sn.visits` is indexed by stateful node and by chain; both conversions below
365 * need one (nstations x nclasses) matrix, which is what `cellsum` followed by
366 * the statefulToStation mapping produces in the reference.
367 */
368template <class T>
369Matrix<T> station_visits(const qn::NetworkStruct<T>& L) {
370 const T zero = num_traits<T>::from_int(0);
371 Matrix<T> V(L.nstations, L.nclasses, zero);
372 for (std::size_t c = 0; c < L.nchains; ++c)
373 for (std::size_t i = 0; i < L.nstations; ++i) {
374 const std::size_t sf = L.stateful_of_station(i + 1);
375 for (std::size_t k = 0; k < L.nclasses; ++k)
376 V(i, k) = T(V(i, k) + L.visits[c](sf - 1, k));
377 }
378 return V;
379}
380
381} // namespace detail
382
383/**
384 * Port of `sn_get_residt_from_respt`: the per-JOB residence time.
385 *
386 * `RN` is per visit; multiplying by this station's visits and dividing by the
387 * reference station's visits of the chain's reference class turns it into the
388 * time a job spends here per completion. A response time already below FineTol
389 * (an Immediate class) is passed through unscaled, since scaling noise is
390 * still noise.
391 */
392template <class T>
394 const T zero = num_traits<T>::from_int(0);
395 const std::size_t M = L.nstations, K = L.nclasses;
396 const Matrix<T> V = detail::station_visits(L);
397 Matrix<T> WN(M, K, zero);
398 for (std::size_t i = 0; i < M; ++i)
399 for (std::size_t k = 0; k < K; ++k) {
400 // A PLACE HAS NO SERVICE PROCESS, SO `disabled` IS NOT ABSENCE --
401 // the same caveat `refresh_capacity` already carries. The reference
402 // gates this loop on the metric HANDLE (`WH{ist,k}.disabled`), which
403 // a Place holding a class does have; `sn.disabled` is only a
404 // stand-in for it, and on an SPN it is true at every Place, which
405 // zeroed the whole ResidT column (`-s jmt` reported ResidT 0 beside
406 // RespT 0.35308 on spn_basic_closed while every other codebase
407 // reported both).
408 const bool is_place = L.stations[i].nodetype == qn::NodeType::Place;
409 if ((L.disabled[i][k] && !is_place) || !(RN(i, k) > zero)) continue;
411 WN(i, k) = RN(i, k);
412 continue;
413 }
414 std::size_t c = L.nchains;
415 for (std::size_t cc = 0; cc < L.nchains; ++cc)
416 if (L.chains[cc][k]) c = cc;
417 if (c == L.nchains) continue;
418 const std::size_t rs = L.classes[k].refstat;
419 T den = zero;
420 if (L.refclass[c] > 0) {
421 den = V(rs - 1, L.refclass[c] - 1);
422 } else {
423 for (std::size_t r : L.inchain[c]) den += V(rs - 1, r - 1);
424 }
425 if (den > zero) WN(i, k) = T(RN(i, k) * V(i, k) / den);
426 }
427 for (std::size_t i = 0; i < M; ++i)
428 for (std::size_t k = 0; k < K; ++k)
430 WN(i, k) = zero;
431 return WN;
432}
433
434/**
435 * Port of `sn_get_arvr_from_tput`: the arrival rate each station sees, from the
436 * throughputs and the class-expanded routing `sn.rt`.
437 *
438 * A Source's row is left as the routing computes it and zeroed by the caller's
439 * mask, which is where `getAvg` does it.
440 *
441 * A CACHE IS PROPAGATED FIRST, exactly as the reference propagates it in a
442 * loop of its own before the generic one. The hit/miss split rides in the
443 * effective routing as a SELF-LOOP at the cache node (`set_route_effective(r,
444 * hitclass[r], ci, ci, hitprob)`), so the accumulation below fills the cache's
445 * hit and miss rows from its own arrival row -- but only if the cache is
446 * reached before whatever it feeds. In node order a Router usually precedes
447 * the Cache, and taking the nodes in that order left the Router at zero and,
448 * through it, every station downstream: on cache_replc_routing the two Delay
449 * stations reported ArvR 0 against the reference's 0.4 and 0.6, which then
450 * dropped both rows from the table as all-zero.
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 for (int pass = 0; pass < 2; ++pass) {
497 for (std::size_t sf = 0; sf < S; ++sf) {
498 const std::size_t nd = L.stateful_nodes[sf];
499 if (L.nodes[nd - 1].station != 0) continue;
500 const bool is_cache = L.nodes[nd - 1].nodetype == qn::NodeType::Cache;
501 if (is_cache != (pass == 0)) continue;
502 for (std::size_t k = 0; k < R; ++k) {
503 T acc = zero;
504 for (std::size_t sf2 = 0; sf2 < S; ++sf2)
505 for (std::size_t r = 0; r < R; ++r)
506 acc += T(TS(sf2, r) * L.rt(sf2 * R + r, sf * R + k));
507 TS(sf, k) = acc;
508 }
509 }
510 }
511 for (std::size_t ist = 0; ist < M; ++ist) {
512 const std::size_t sf_i = L.stateful_of_station(ist + 1) - 1;
513 for (std::size_t k = 0; k < R; ++k) {
514 T acc = zero;
515 for (std::size_t sf2 = 0; sf2 < S; ++sf2)
516 for (std::size_t r = 0; r < R; ++r)
517 acc += T(TS(sf2, r) * L.rt(sf2 * R + r, sf_i * R + k));
518 AN(ist, k) = acc;
519 }
520 }
521 return AN;
522}
523
524/**
525 * Which metric is being filtered. `@@MNetwork/getAvgHandles.m` disables a metric
526 * handle per KIND, not per station, and `filterMetric` turns a disabled handle
527 * into a zero -- so the kind is what decides which rows survive:
528 *
529 * Q, R, W disabled at a Source and at a Sink
530 * U disabled at a Source, a Sink, a Fork AND A JOIN -- a Join is an
531 * infinite server whose "utilization" would just restate its queue
532 * length, and the reference declines to report it
533 * T, A never disabled by station kind, only by an undefined service
534 *
535 * Every kind is additionally disabled where the class has no service defined.
536 */
537enum class MetricKind { QLen, Util, RespT, ResidT, Tput, ArvR };
538
539/**
540 * Port of `filterMetric`: what `@@NetworkSolver/getAvg` does between the
541 * analyzer and the caller.
542 *
543 * `zero_mask` carries the RN < 10 FineTol test the queue length and the
544 * utilization take and the other metrics do not.
545 */
546template <class T>
548 const std::vector<std::vector<bool>>* zero_mask) {
549 const T zero = num_traits<T>::from_int(0);
550 const std::size_t M = L.nstations, K = L.nclasses;
551 // the fork-join, cache and spawn-fed exemptions from the reachability test
552 // below: those three constructs are exactly the ones whose visit equations
553 // do not describe how a job actually reaches a station (a Join is entered by
554 // the parent class, which has no routing visit there), so a metric the
555 // analyzer reports as positive is trusted over the visit ratio
556 // ... and the STOCHASTIC PETRI NET, the fourth such construct and for the
557 // same reason: where a token sits is the MARKING, and a Place is reached by
558 // a transition firing rather than by a routing visit, so `sn.visits` is
559 // identically zero over every Place. Without the exemption this loop erased
560 // the whole table an analyzer had just computed -- `spnlp.upper` on a
561 // fork-join net reported four zeros in place of the token bounds, and
562 // `r.QN >= tokens` then failed against a row the bound had got right.
563 bool has_fj = false, has_cache = false, has_spn = false;
564 for (const qn::NodeDef& nd : L.nodes) {
565 if (nd.nodetype == qn::NodeType::Fork || nd.nodetype == qn::NodeType::Join) has_fj = true;
566 if (nd.nodetype == qn::NodeType::Cache) has_cache = true;
567 if (nd.nodetype == qn::NodeType::Transition) has_spn = true;
568 }
569 // The Tput/ArvR HANDLE is disabled iff `~isServiceDefined && ~isCacheClass`
570 // (getAvgHandles.m): a service-undefined pair keeps its throughput only when
571 // the class is a cache hit/miss class -- NOT for fork-join. The fork-join
572 // exemption belongs to the reachability test (part 2 below), not here.
573 std::vector<bool> is_cache_class(K, false);
574 for (const auto& kv : L.nodeparam) {
575 const qn::CacheParam<T>& cp = kv.second;
576 for (std::size_t r = 0; r < cp.hitclass.size(); ++r) {
577 if (cp.hitclass[r] != 0 && cp.hitclass[r] <= K) is_cache_class[cp.hitclass[r] - 1] = true;
578 if (r < cp.missclass.size() && cp.missclass[r] != 0 && cp.missclass[r] <= K)
579 is_cache_class[cp.missclass[r] - 1] = true;
580 }
581 }
582
583 Matrix<T> out(M, K, zero);
584 for (std::size_t i = 0; i < M; ++i) {
585 const qn::NodeType nt = L.stations[i].nodetype;
586 const bool src_or_sink = (nt == qn::NodeType::Source || nt == qn::NodeType::Sink);
587 bool kind_disabled = false;
588 switch (kind) {
589 case MetricKind::QLen:
592 kind_disabled = src_or_sink;
593 break;
594 case MetricKind::Util:
595 kind_disabled = src_or_sink || nt == qn::NodeType::Fork || nt == qn::NodeType::Join;
596 break;
597 case MetricKind::Tput:
598 case MetricKind::ArvR:
599 kind_disabled = false;
600 break;
601 }
602 if (kind_disabled) continue;
603 // `hasServiceTunnel`, the wrapper this port was missing. Every metric in
604 // `getAvgHandles.m` gates its service test on `if ~hasServiceTunnel(ist)`
605 // -- a station whose SERVER is a ServiceTunnel is never disabled for
606 // having no service law, only by the kind-specific rules just above
607 // (which already match, arm for arm).
608 //
609 // Those stations are Source, Fork, Join and an ORDINARY Place. A Place is
610 // the one that has to be decided rather than looked up: `installQueueServer`
611 // REPLACES the tunnel on a queueing Place, so `hasServiceTunnel` is false
612 // there, and this struct carries no queueing flag. It is recovered the way
613 // MATLAB defines it, PER STATION: an ordinary Place has a law for no class
614 // at all, a queueing one has a real server. Deciding it per class instead
615 // would keep a queueing Place's explicitly Disabled class, which MATLAB
616 // disables.
617 //
618 // The Place arm is what made a pure Petri net print an EMPTY AvgTable:
619 // a non-queueing Place has NaN rates, which is `disabled` here, so every
620 // metric was zeroed and the printer drops an all-zero row -- while MATLAB
621 // reports e.g. P1 QLen 0.25966 on spn_basic_open. Source and Join change
622 // little in practice (a Source's ungenerated class and a Join both compute
623 // 0 or are already kept), but they are MATLAB's rule and are encoded here
624 // rather than left to coincidence.
625 bool service_tunnel = (nt == qn::NodeType::Source || nt == qn::NodeType::Fork ||
626 nt == qn::NodeType::Join);
627 if (nt == qn::NodeType::Place) {
628 service_tunnel = true;
629 for (std::size_t k = 0; k < K && k < L.service[i].size(); ++k)
630 if (!L.service[i][k].disabled) { service_tunnel = false; break; }
631 }
632 for (std::size_t k = 0; k < K; ++k) {
633 if (!L.disabled[i][k] || service_tunnel) {
634 out(i, k) = metric(i, k);
635 } else if (is_cache_class[k] &&
636 (kind == MetricKind::Tput || kind == MetricKind::ArvR)) {
637 // a cache over-routing sends a hit/miss class through a station
638 // that never serves it (a pass-through); its Tput/ArvR handle is
639 // not disabled, so the reported flow is kept
640 out(i, k) = metric(i, k);
641 }
642 }
643 }
644 if (zero_mask)
645 for (std::size_t i = 0; i < M; ++i)
646 for (std::size_t k = 0; k < K; ++k)
647 if ((*zero_mask)[i][k]) out(i, k) = zero;
648 for (std::size_t i = 0; i < M; ++i)
649 for (std::size_t k = 0; k < K; ++k)
650 if (num_traits<T>::to_double(out(i, k)) < GlobalConstants::FineTol) out(i, k) = zero;
651 // a class with no visit at a station holds nothing there
652 for (std::size_t k = 0; k < K; ++k) {
653 std::size_t c = L.nchains;
654 for (std::size_t cc = 0; cc < L.nchains; ++cc)
655 if (L.chains[cc][k]) c = cc;
656 if (c == L.nchains) continue;
657 for (std::size_t i = 0; i < M; ++i) {
658 if (L.visits[c](L.stateful_of_station(i + 1) - 1, k) != zero) continue;
659 if ((has_fj || has_cache || has_spn) &&
661 continue;
662 out(i, k) = zero;
663 }
664 }
665 return out;
666}
667
668/**
669 * Port of `SolverMVA.supportsFiniteCapacity` (SolverMVA.m:158-186), the
670 * structural capacity gate SolverMVA.supportsModelMethod (SolverMVA.m:143-153)
671 * lays on top of the universal feature gate.
672 *
673 * WHY IT IS SEPARATE FROM `feature_gate`. Finite capacity has no
674 * LINE_QN_FEATURE_LIST enumerator: it is a NUMBER on a station, not a
675 * construct, so no declared feature set can express it. Without this check a
676 * model built with setCapacity is solved as if uncapacitated and returns a
677 * WRONG NUMBER instead of a refusal (BUG-39).
678 *
679 * WHY IT RUNS AFTER THE FEATURE GATE. The reference calls
680 * supportsModelMethod@@NetworkSolver first and only tests capacity `if bool`, so
681 * a model that fails both is refused by the FEATURE, whose message names the
682 * construct to remove. The order is therefore load-bearing, not incidental.
683 *
684 * THE TWO EXEMPTIONS.
685 * - A BAS model, because solver_mva_analyzer routes it to solver_sqd under
686 * `default` too, so the finite buffers ARE honoured on every MVA path. The
687 * predicate is the api/sn one (single CLASS), not the analyzer's dispatch
688 * test mva_is_bas_model (single CHAIN); see solver_mva.h.
689 * - A single-station M/M/1/K with tail drop under any method but `exact`,
690 * which the mg1k.mgs branch of solver_mva_qsys_analyzer solves. That branch
691 * is an approximation away from scv=1, so `exact` is NOT exempted and must
692 * refuse here. `method` is the RESOLVED method, which is what
693 * runAnalyzerChecks (NetworkSolver.m:184) passes on.
694 */
695template <class T>
696void mva_check_finite_capacity(const qn::NetworkStruct<T>& L, const std::string& method) {
697 if (sn_is_bas_model(L)) return;
698 if (method != "exact" && sn_is_mm1k_loss(L)) return;
699 qn::check_binding_capacity("SolverMVA", L);
700}
701
702/**
703 * Port of `@@SolverMVA/runAnalyzer.m` for the `lang='matlab'` path: gate, solve,
704 * convert, filter.
705 *
706 * A model with a Fork is solved through the shared fork-join fixed point
707 * (fj_driver.h), which drives `mva_dispatch` as its inner solve on the
708 * transformed (auxiliary-expanded) model; a model without one runs the dispatch
709 * exactly once. The filtering below is common to both paths.
710 */
711template <class T>
713 const Matrix<T>& init_sol) {
714 check_method(L, opt_in.method);
715
716 // Finite Capacity Region: MVA does not enforce the aggregate per-region job
717 // limit and would otherwise silently return the unconstrained product-form
718 // answer. Port of `runAnalyzer.m:21-25`.
719 if (!L.regions.empty())
720 throw UnsupportedError(
721 "SolverMVA: this model uses a Finite Capacity Region (addRegion), which is not "
722 "supported by SolverMVA. Use SolverJMT, or setCapacity for a single-station limit.");
723
724 // resolveMethod (SolverMVA.resolveMethod) computes the bursty default->rqna
725 // upgrade only for the feature gate; the reference never writes it back to
726 // options. The substitution itself lives in mva_dispatch's terminal
727 // ordinary-QN branch, so the structural special cases (e.g. a 3-node
728 // MAP/M/1, a G/M/1 the qsys analyzer solves) keep method='default' rather
729 // than being forced into rqna and refused.
730 // runAnalyzerChecks' universal feature gate, AFTER the method gate so the
731 // specific message wins wherever one exists. Gated on `L`, before fj_mmt
732 // rewrites the Fork into a Router that MVA does not declare.
733 // The resolved method is passed so that a `default` -> `rqna` refusal names
734 // rqna, which is NetworkSolver.m:184-190's second message form: rqna's set
735 // withdraws ClosedClass, and nothing the user typed mentions rqna.
736 const std::string rm = resolve_method(L, opt_in.method);
737 // The MODEL-aware overload, because one grant in getMethodFeatureSet reads
738 // the model: every method but 'exact' carries FiniteCapacity on a
739 // single-station M/M/1/K with tail drop, the shape mva_dispatch answers
740 // under any name. `mva_check_finite_capacity` below is the same rule as a
741 // structural refusal, and the two must agree.
742 qn::feature_gate("SolverMVA", qn::mva_feature_set(rm, L), L, opt_in.method, rm);
744 const MvaOptions& opt = opt_in;
745
747 std::string actualmethod, warning;
748 // The integrated cacheqn analyzer returns a refreshed struct whose routing
749 // carries the actual hit/miss probabilities; ArvR and ResidT are read from
750 // it. Empty for every other model, in which case the base struct is used.
751 std::shared_ptr<qn::NetworkStruct<T>> refreshed;
752 // What a cache branch measured; see AvgResult::cache. Left empty on the
753 // fork-join path, whose inner solves are of a TRANSFORMED network.
755 if (L.has_fork()) {
756 // The transform turns the fork into a router, the join into a delay and
757 // the branches into auxiliary open classes; the driver iterates the
758 // auxiliary arrival rates to their fixed point and merges the auxiliary
759 // columns back before returning. `mva_dispatch` sees a plain mixed
760 // network, so its own fork checks never fire.
761 FjMmt<T> tr = fj_fork_join_transform(L, opt.fork_join);
762 std::vector<T> lam(tr.V.classes.size() + 1,
764 MvaOptions inner = opt;
765 inner.base_has_fork = true; // the transform removes the fork; see MvaOptions
766 std::string am, wn;
767 s = fj_fixed_point(L, tr, lam, opt, [&inner, &am, &wn](qn::NetworkStruct<T>& V) {
768 const DispatchResult<T> d = mva_dispatch(V, inner, Matrix<T>());
769 am = d.actualmethod;
770 // the last inner solve is the one the returned metrics came from
771 wn = d.warning;
772 return d.sol;
773 });
774 actualmethod = am;
775 warning = wn;
776 } else {
777 const DispatchResult<T> dr = mva_dispatch(L, opt, init_sol);
778 s = dr.sol;
779 actualmethod = dr.actualmethod;
780 warning = dr.warning;
781 refreshed = dr.refreshed_struct;
782 cache = dr.cache;
783 }
784 // Values (not topology) for ArvR/ResidT come from the refreshed struct when
785 // the cacheqn analyzer supplied one; filter_metric still keys on the base L
786 // so its cache pass-through masking is unaffected by the ClassSwitch relabel.
787 const qn::NetworkStruct<T>& refL = refreshed ? *refreshed : L;
788
789 const std::size_t M = L.nstations, K = L.nclasses;
790 std::vector<std::vector<bool>> mask(M, std::vector<bool>(K, false));
791 for (std::size_t i = 0; i < M; ++i)
792 for (std::size_t k = 0; k < K; ++k)
793 mask[i][k] = num_traits<T>::to_double(s.R(i, k)) < 10.0 * GlobalConstants::FineTol;
794
795 // `getAvg` zeroes the arrival rate at a Source explicitly: a Source is not
796 // fed by anything, and the routing would otherwise report whatever the
797 // stochastic complement carries back into it.
798 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(K, false));
799 for (std::size_t i = 0; i < M; ++i)
800 if (L.stations[i].nodetype == qn::NodeType::Source)
801 for (std::size_t k = 0; k < K; ++k) srcmask[i][k] = true;
802
803 AvgResult<T> out;
804 // Carried, not consumed and dropped: the node table needs the same visits
805 // the station table just used, or the two disagree on the cache columns.
806 out.refreshed_struct = refreshed;
807 // The cache answer a cache branch measured. Empty on every other model, and
808 // on the fork-join path, whose inner solves are of a TRANSFORMED network.
809 out.cache = cache;
810 out.QN = filter_metric(L, s.Q, MetricKind::QLen, &mask);
811 out.UN = filter_metric(L, s.U, MetricKind::Util, &mask);
812 out.RN = filter_metric(L, s.R, MetricKind::RespT, nullptr);
813 out.TN = filter_metric(L, s.Tp, MetricKind::Tput, nullptr);
814 // Both ArvR and ResidT come from the refreshed struct when the cacheqn
815 // analyzer supplied one: its cache self-switch is normalized at the ACTUAL
816 // hit/miss split, so the visits back the carried rate without the over-route
817 // inflation. filter_metric still keys on the base L for its topology masking.
818 out.WN = filter_metric(L, sn_get_residt_from_respt(refL, out.RN), MetricKind::ResidT, nullptr);
819 out.AN = filter_metric(L, sn_get_arvr_from_tput(refL, out.TN), MetricKind::ArvR, &srcmask);
820
821 // Unstable open-station cap (@@NetworkSolver/getAvg.m:198-235): a finite-server
822 // queueing station whose open-class offered load sum_r TN/(c*rate) >= 1 is
823 // saturated. MATLAB normalizes its utilization to sum 1.0 and reports queue
824 // length and response time as Inf; it does NOT rescale throughput (Python
825 // native does -- a MATLAB-vs-Python divergence -- and MATLAB is ground truth).
826 // Skipped under exact arithmetic, where infinity is not representable.
827 if constexpr (num_traits<T>::has_transcendental) {
828 const double dinf = std::numeric_limits<double>::infinity();
829 for (std::size_t i = 0; i < M; ++i) {
830 const double c = num_traits<T>::to_double(L.stations[i].nservers);
831 const qn::NodeType nt = L.stations[i].nodetype;
832 const qn::SchedStrategy sch = L.stations[i].sched;
833 if (!std::isfinite(c) || c <= 0.0) continue; // delay / infinite server
834 if (nt == qn::NodeType::Source || nt == qn::NodeType::Sink) continue;
835 if (sch == qn::SchedStrategy::INF || sch == qn::SchedStrategy::EXT) continue;
836 std::vector<double> rho(K, 0.0);
837 double rho_open = 0.0, rho_tot = 0.0;
838 for (std::size_t r = 0; r < K; ++r) {
839 const double rate = num_traits<T>::to_double(L.rates(i, r));
840 const double tp = num_traits<T>::to_double(out.TN(i, r));
841 if (rate > 0.0 && tp > 0.0) rho[r] = tp / (c * rate);
842 rho_tot += rho[r];
843 if (std::isinf(L.classes[r].population)) rho_open += rho[r];
844 }
845 if (rho_open < 1.0 || rho_tot <= 0.0) continue;
846 for (std::size_t r = 0; r < K; ++r) {
847 out.UN(i, r) = num_traits<T>::from_double(rho[r] / rho_tot);
848 if (std::isinf(L.classes[r].population) && rho[r] > 0.0) {
849 out.QN(i, r) = num_traits<T>::from_double(dinf);
850 out.RN(i, r) = num_traits<T>::from_double(dinf);
851 }
852 }
853 }
854 }
855 out.CN = s.C;
856 out.XN = s.X;
857 out.method = opt.method;
858 out.actualmethod = actualmethod;
859 // Immediate feedback is APPROXIMATED, not refused: a fed-back job holds its
860 // server, and mean-value analysis has no way to express that, so the visit
861 // it makes is counted as an ordinary re-entry. The reference warns and
862 // solves (`runAnalyzer.m:26-27`); refusing here would reject a model MATLAB
863 // and the JAR both answer. Appended rather than assigned, so a warning the
864 // dispatch already raised is not swallowed by this one.
865 if (L.has_immediate_feedback()) {
866 if (!warning.empty()) warning += " ";
867 warning +=
868 "SolverMVA does not handle immediate feedback (immfeed); the solver will treat "
869 "self-loops as class-switching with re-queueing.";
870 }
871 // Verbatim: a user must be able to match it against the reference output.
872 out.warning = warning;
873 out.iter = s.iter;
874 out.converged = s.converged;
875 return out;
876}
877
878} // namespace mva
879} // namespace line
880
881#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
any(sn.immfeed(:)): whether any (station, class) pair feeds back.
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< 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< 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.
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:324
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
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
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:380
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:401
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.
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:668
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.