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