LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ba_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_BA_SOLVER_BA_RUNNER_H
6#define LINE_SOLVERS_BA_SOLVER_BA_RUNNER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The SolverBA class surface: `@@SolverBA/runAnalyzer.m`, `listValidMethods`,
12 * `getBounds` and `getBoundsTable`.
13 *
14 * What sits here rather than in the analyzer is everything around one bound
15 * evaluation: the closed-model gate, the method aliases, the whitelist, the
16 * arrival-rate conversion and the metric filter `@@NetworkSolver/getAvg` applies
17 * to every solver's output. The result shape is SolverMVA's `AvgResult`, so a
18 * bound and a point estimate are directly comparable -- which is what a caller
19 * checking that a bracket contains the exact answer needs.
20 *
21 * FINITE-BUFFER BLOCKING IS REFUSED, NOT BOUNDED. Needing only demands and a
22 * population is the BCMP parameterization, which presumes UNBOUNDED buffers; a
23 * buffer that binds couples the station occupancies and the resulting numbers
24 * do not bracket the blocked model. `solver_ba_run_analyzer` therefore gates on
25 * `api::sn_has_blocking` and `list_valid_methods(L)` drops every blocking-blind
26 * method. The exceptions are `qrf.bas*`/`qrf.rsrd`, which carry the blocking
27 * tables explicitly. Use SolverMVA method `sqd` for a point estimate.
28 *
29 * THE RESIDENCE TIME IS ZERO BY CONSTRUCTION. `runAnalyzer.m` sets `WN` to a
30 * zero matrix and does not derive it from `RN`: the bound families define no
31 * per-station residence, and the reference declines to invent one.
32 */
33
34#include <algorithm>
35#include <cmath>
36#include <limits>
37#include <string>
38#include <vector>
39
50
51namespace line {
52namespace ba {
53
54using lang::GlobalConstants;
55
56/**
57 * Port of `runAnalyzer`'s method aliases: `default` is the geometric upper
58 * bound, bare `auto` is the AUTO composite's upper side, and `qr`/`lr` are the
59 * friendly names of two QRF sub-methods.
60 */
61inline std::string resolve_method(const std::string& method) {
62 if (method == "default") return "gb.upper";
63 if (method == "auto") return "auto.upper";
64 if (method == "lr") return "lr.upper";
65 if (method == "qr") return "qrf.mmi";
66 return method;
67}
68
69/**
70 * Whether METHOD bounds a model as if its buffers were unbounded.
71 *
72 * Every family but the QRF BLOCKING bounds is parameterized by demands (visits
73 * x service time) and a population alone, which is the BCMP parameterization:
74 * unbounded buffers, and an equilibrium distribution that factorizes. A finite
75 * buffer that BINDS breaks both premises, so the numbers do not bracket the
76 * blocked model -- on `cqn_bas_blocking` (Queue2 capped at 1, N = 2) `gb.upper`
77 * reports QLen 1.28 at a station that can never hold more than one job.
78 * `qrf.bas*` and `qrf.rsrd` carry the blocking tables (MM, MM1, ZZ, ZM, BB, F)
79 * explicitly and are the exceptions.
80 *
81 * METHOD must already be resolved through `resolve_method`.
82 */
83inline bool ignores_blocking(const std::string& method) {
84 // 'spnlp.*' is an exception alongside the QRF blocking bounds: its polytope
85 // is indexed by the marking itself, so a bounded place enters it as a
86 // variable upper bound and as the P-invariant equality that produced the
87 // bound. The buffer is modelled, not assumed away.
88 return !(method.rfind("qrf.bas", 0) == 0 || method.rfind("qrf.rsrd", 0) == 0 ||
89 method.rfind("spnlp", 0) == 0);
90}
91
92/**
93 * Port of `SolverBA.listValidMethods`.
94 *
95 * A LISTED NAME MUST ACTUALLY RUN, OR BE REFUSED BY NAME. The `lr` family IS
96 * listed: its bound is a pure linear program, which `lp::simplex_solve` serves
97 * exactly. The four no-blocking `qrf.*` arms and the supported BAS-blocking
98 * bounds are listed too, served by `solver_ba_qrf_analyzer`.
99 *
100 * `qrf.bas.mmi` was refused by name in the reference between 2026-08-01 and
101 * 2026-09-03, when `solver_ba_qrf_analyzer.m` began serving it again; it is
102 * listed here, as in `SolverBA.listAllMethods`, and served like the other BAS bounds.
103 */
104inline std::vector<std::string> list_valid_methods() {
105 // the names and their order are this solver's row of the method registry (doc/line-methods.json)
106 return ::line::method_registry::methods("BA");
107}
108
109/**
110 * What `default`/`auto` must mean on a model with finite buffers: the QRF BAS
111 * bound, or nothing.
112 *
113 * `default` resolves to the geometric upper bound, which is parameterized by
114 * demands and a population alone and therefore bounds a blocked model as if its
115 * buffers were unbounded. `ignores_blocking` refuses that, which is right; but
116 * refusing is not the whole answer, because `qrf.bas` DOES model the finite
117 * buffer and `sn_to_qrf_blocking` derives its tables from the model, so there
118 * is nothing left for the caller to supply. A blocked model of the right shape
119 * therefore gets `qrf.bas` as its default, exactly as SolverMVA routes a BAS
120 * model to `sqd`.
121 *
122 * The shape is the one `list_valid_methods(L)` calls "reducible" and the QRF
123 * analyzer gates on. On top of it the tables must actually derive, which is
124 * asked of `sn_to_qrf_blocking` rather than re-tested here -- it owns the
125 * single-finite-buffer rule and the size guard, and a second copy of either is
126 * how the two drift apart.
127 *
128 * Only the UPPER side is routed: the analyzer solves qrf.bas in the 'max'
129 * direction alone, so `auto.lower` has no blocking counterpart and keeps
130 * refusing rather than being answered with the wrong side.
131 *
132 * @return {method, why}; method is empty when the routing does not apply, and
133 * why then carries the reason (empty for an unblocked model).
134 */
135template <class T>
136std::pair<std::string, std::string> blocking_default(const qn::NetworkStruct<T>& L) {
137 typedef std::pair<std::string, std::string> R;
138 if (!api::sn_has_blocking(L)) return R("", "");
139 if (L.nclasses != 1)
140 return R("", "the QRF blocking bounds are derived for a single-class closed network,"
141 " which this model is not.");
142 for (std::size_t r = 0; r < L.classes.size(); ++r)
143 if (!std::isfinite(L.classes[r].population))
144 return R("", "the QRF blocking bounds are derived for a single-class closed"
145 " network, which this model is not.");
146 for (std::size_t i = 0; i < L.stations.size(); ++i) {
147 if (L.stations[i].sched == SchedStrategy::INF)
148 return R("", "the QRF blocking bounds model every station as a single server and"
149 " have no infinite-server notion, so a delay station rules them out.");
150 if (L.stations[i].nservers > 1.0)
151 return R("", "the QRF blocking bounds model every station as a single server, so a"
152 " multiserver station rules them out.");
153 }
154 // Same phase count `solver_ba_qrf_analyzer` derives, so the size guard
155 // here judges the LP the analyzer would actually build.
156 int Ktot = 0;
157 for (std::size_t i = 0; i < L.stations.size(); ++i) {
158 if (L.service[i][0].disabled) { Ktot += 1; continue; } // a Join: one phase, as in MATLAB
159 const mam::Map<T> mp = lang::dist_to_map(L.service[i][0]);
160 Ktot += mp.D0.rows() == 0 ? 1 : static_cast<int>(mp.D0.rows());
161 }
162 const sn::QrfBlocking blk = sn::sn_to_qrf_blocking(L, Ktot);
163 if (!blk.msg.empty()) return R("", blk.msg);
164 return R("qrf.bas", "");
165}
166
167/** True for the aliases whose meaning a blocked model is allowed to change. */
168inline bool is_default_request(const std::string& method) {
169 return method == "default" || method == "auto" || method == "auto.upper";
170}
171
172/**
173 * Port of `ba_resolve_model_method.m` (`SolverBA.resolveMethod`): the method
174 * METHOD runs as on THIS model. The model-free aliases of `resolve_method`, then
175 * the finite-buffer routing of `blocking_default`, under which `default`, `auto`
176 * and `auto.upper` run as `qrf.bas` on a blocked model of the right shape -- the
177 * same resolution `solver_ba_solve` dispatches on, so findSolver gates the name
178 * the run serves.
179 */
180template <class T>
181std::string resolve_model_method(const qn::NetworkStruct<T>& L, const std::string& method) {
182 std::string resolved = resolve_method(method);
183 if (is_default_request(method) && ignores_blocking(resolved) && api::sn_has_blocking(L)) {
184 const std::pair<std::string, std::string> routed = blocking_default(L);
185 if (!routed.first.empty()) resolved = routed.first;
186 }
187 return resolved;
188}
189
190/**
191 * The same list, narrowed to what THIS model can run.
192 *
193 * The QR/LR/QRF reduction bounds share one premise -- a single-class closed
194 * network of single-server stations -- which `solver_ba_qrf_analyzer` enforces
195 * and the `lr` family shares. A listed name that always throws is a method a
196 * caller is invited to ask for and cannot have, so the model-aware overload
197 * drops them; `SolverMVA` gates `sqni` the same way. Mirrors `SolverBA.m`,
198 * `SolverBA.java` and the python `list_valid_methods`.
199 */
200template <class T>
201std::vector<std::string> list_valid_methods(const qn::NetworkStruct<T>& L) {
202 std::vector<std::string> all = list_valid_methods();
203 // The STRUCTURAL premises -- single-class closed, fully closed,
204 // single-server -- come from `method_refusal`, the same predicate
205 // `solver_ba_run_analyzer` throws on. Asked first so every later narrowing
206 // works on names this model could actually run, and asked HERE because this
207 // is the list `autosolver::auto_family_methods` reads: before it, a
208 // two-class closed network was offered all 36 demand-parameterized bounds
209 // and 30 of them threw the moment they were run.
210 // ... and `method_degenerate` withholds the second kind of name: one whose
211 // premises this model MEETS but whose formula says nothing here.
212 // 'ldac.lower' at N == Qhat is the only such case: it reports the trivial
213 // X >= 0, which propagates into an all-zero table a caller cannot tell from
214 // an answer. Offering is what stops; asking for it by NAME still runs and
215 // still publishes it, since a vacuous bound is a valid one -- which is what
216 // `tests/test_ba.cpp` pins at that boundary.
217 {
218 std::vector<std::string> structural;
219 for (std::size_t i = 0; i < all.size(); ++i)
220 if (method_refusal(L, all[i]).empty() && method_degenerate(L, all[i]).empty())
221 structural.push_back(all[i]);
222 all = structural;
223 }
224 // 'spnlp.*' is the only family indexed by a MARKING rather than by demands
225 // and a population, and the split is total in both directions: on a Petri
226 // net nothing else has a representation of the model, and off one spnlp has
227 // nothing to read. Two of the gates below already half-cover this by
228 // accident -- a Place is an INF station, so `reducible` and `bptOk` are both
229 // false on any Petri net -- but the demand-parameterized families survive
230 // them and must be dropped by name. Applied first so the open/closed
231 // narrowing cannot reinstate one.
232 bool isPetri = false;
233 for (std::size_t i = 0; i < L.nodes.size() && !isPetri; ++i)
234 isPetri = L.nodes[i].nodetype == lang::NodeType::Transition;
235 {
236 std::vector<std::string> sieved;
237 for (std::size_t i = 0; i < all.size(); ++i)
238 if (is_spnlp_method(all[i]) == isPetri) sieved.push_back(all[i]);
239 all = sieved;
240 if (isPetri) return all;
241 }
242 bool reducible = (L.nclasses == 1);
243 for (std::size_t r = 0; reducible && r < L.classes.size(); ++r)
244 if (!std::isfinite(L.classes[r].population)) reducible = false;
245 bool closedSingleClass = reducible;
246 for (std::size_t i = 0; reducible && i < L.stations.size(); ++i) {
247 if (L.stations[i].sched == SchedStrategy::INF)
248 reducible = false;
249 else if (L.stations[i].nservers > 1.0)
250 reducible = false;
251 }
252 // The LOAD-DEPENDENT arms survive where the rest of the family cannot run:
253 // alpha(i,n) is the rate law of a delay (alpha = n), of a c-server station
254 // (alpha = min(n,c)) and of limited load dependence alike, so
255 // `qrf.mmi.ld` and `qrf.mmi.linear` answer those models on the model's own
256 // chain. `sn_to_qrf_alpha` owns the one restriction that survives,
257 // exponential service wherever a station serves several jobs at once.
258 // Dropping them with the rest would hide from a caller enumerating the list
259 // the only two bound methods such a model has.
260 const bool ldReducible =
261 !reducible && closedSingleClass && sn::sn_to_qrf_alpha(L).msg.empty();
262 // 'bpt', 'bgt' and 'snc' are the mirror image of the reduction bounds: all
263 // three are derived for an OPEN network of single-server exponential
264 // stations, so every closed model, every delay station and every
265 // multiserver station rules them out. Every other family rules OUT the open
266 // model, so on an open network the list narrows to those three.
267 bool fullyOpen = !L.classes.empty();
268 for (std::size_t r = 0; fullyOpen && r < L.classes.size(); ++r)
269 if (std::isfinite(L.classes[r].population)) fullyOpen = false;
270 bool bptOk = fullyOpen;
271 for (std::size_t i = 0; bptOk && i < L.stations.size(); ++i) {
272 if (L.stations[i].nodetype == qn::NodeType::Source) continue;
273 if (L.stations[i].sched == SchedStrategy::INF)
274 bptOk = false;
275 else if (L.stations[i].nservers > 1.0)
276 bptOk = false;
277 }
278 std::vector<std::string> keep;
279 if (fullyOpen) {
280 if (bptOk) {
281 // Each is re-added only if it SURVIVED the structural narrowing
282 // above. Pushing them unconditionally is what silently undid it:
283 // `snc.upper` is dropped there on non-exponential service, and a
284 // blind push put it straight back, so an Erlang-service model was
285 // still offered a bound that throws.
286 const char* const open_fams[] = {"bpt.lower",
287 // 'bgt.upper' additionally needs
288 // deterministic non-merging routes, and
289 // 'snc.upper' a feed-forward station
290 // graph; both are checked by walking the
291 // routing matrix in their own analyzers,
292 // too expensive to repeat here, so they
293 // stay listed and refuse by name.
294 "bgt.upper", "snc.upper"};
295 for (const char* nm : open_fams)
296 if (std::find(all.begin(), all.end(), std::string(nm)) != all.end())
297 keep.push_back(nm);
298 }
299 } else {
300 for (std::size_t i = 0; i < all.size(); ++i) {
301 const std::string& m = all[i];
302 if (m == "bpt.lower" || m == "bgt.upper" || m == "snc.upper") continue;
303 if (reducible) {
304 keep.push_back(m);
305 continue;
306 }
307 if (ldReducible && (m == "qrf.mmi.ld" || m == "qrf.mmi.linear")) {
308 keep.push_back(m);
309 continue;
310 }
311 // 'mapamva' shares that premise exactly -- single-class closed,
312 // single-server, no delay -- so it narrows with them rather than
313 // being offered on a model it would refuse on contact. It is NOT
314 // one of the load-dependent arms: its q carries no population
315 // index, so a delay or a c-server station has nowhere to go.
316 if (m == "qr" || m == "lr" || m.compare(0, 3, "lr.") == 0 ||
317 m.compare(0, 4, "qrf.") == 0 || m.compare(0, 7, "mapamva") == 0)
318 continue;
319 keep.push_back(m);
320 }
321 }
322 // A binding finite buffer rules out everything but the QRF blocking bounds:
323 // the other families presume unbounded buffers, and `solver_ba_run_analyzer` refuses
324 // them by name on such a model. The list can legitimately come back EMPTY
325 // -- a blocked model that is not single-class closed single-server has no
326 // bound method at all, and offering one would be the mis-selection this
327 // gate exists to prevent.
328 if (api::sn_has_blocking(L)) {
329 std::vector<std::string> unblocked;
330 // 'default' is offered back when it now MEANS one of the survivors:
331 // `solver_ba_run_analyzer` routes it to 'qrf.bas' on a blocked model of the right
332 // shape, so a caller enumerating the list would otherwise be told the
333 // model's own default is invalid.
334 if (!blocking_default(L).first.empty()) unblocked.push_back("default");
335 for (std::size_t i = 0; i < keep.size(); ++i)
336 if (!ignores_blocking(resolve_method(keep[i]))) unblocked.push_back(keep[i]);
337 return unblocked;
338 }
339 return keep;
340}
341
342/** Port of `runAnalyzer`'s method gate: an unlisted name is refused by name. */
343inline void check_method(const std::string& method) {
344 // `qrf.bas.mmi` used to be refused here, mirroring the reference. The
345 // reference serves it again since 2026-09-03, so nothing is refused by name
346 // beyond an unlisted one.
347 const std::string m = resolve_method(method);
348 const std::vector<std::string> valid = list_valid_methods();
349 if (std::find(valid.begin(), valid.end(), m) != valid.end()) return;
350 throw UnsupportedError(ErrorKind{"Method"}, "SolverBA: unknown bound method '" + method + "'");
351}
352
353/**
354 * Port of `@@SolverBA/runAnalyzer.m` for the `lang='matlab'` path.
355 *
356 * @param L the refreshed struct of a closed model
357 * @param opt_in the method and, for a hierarchical family, the level
358 */
359template <class T>
361 // Every bound family here is parameterized by demands and a closed
362 // population, except the three OPEN-network families 'bpt', 'bgt' and
363 // 'snc', which are refused on a closed model instead.
364 // ... and the 'spnlp' family, which is parameterized by a MARKING: whether
365 // that marking is bounded is a question about the P-invariants of the net
366 // and not about nclosedjobs, so `spn_lpbnd` decides it rather than this gate.
367 if (!(L.nclosedjobs() > 0.0) && opt_in.method.compare(0, 3, "bpt") != 0 &&
368 opt_in.method.compare(0, 3, "bgt") != 0 && opt_in.method.compare(0, 3, "snc") != 0 &&
369 !is_spnlp_method(opt_in.method))
370 throw UnsupportedError(ErrorKind{"OpenClass"}, "SolverBA: supports closed queueing networks only");
371 check_method(opt_in.method);
372 BaOptions opt = opt_in;
373 opt.method = resolve_method(opt_in.method);
374
375 // Finite-buffer BLOCKING is outside the premises of every family here
376 // except the QRF blocking bounds: the rest are parameterized by demands and
377 // a population alone, which presumes unbounded buffers and a product form
378 // that the truncation destroys. Refusing is not conservatism -- on
379 // `cqn_bas_blocking`, `gb.upper` reports QLen 1.28 at a station capped at 1
380 // job. Gated after the aliases so `default` is judged as the `gb.upper` it
381 // resolves to.
382 std::string blocking_why;
383 if (ignores_blocking(opt.method) && api::sn_has_blocking(L) &&
384 is_default_request(opt_in.method)) {
385 // A blocked model whose shape admits the QRF BAS bound gets it as the
386 // DEFAULT rather than a refusal: 'qrf.bas' models the finite buffer,
387 // and since `sn_to_qrf_blocking` derives its tables from the model
388 // there is nothing left for the caller to supply.
389 const std::pair<std::string, std::string> routed = blocking_default(L);
390 blocking_why = routed.second;
391 if (!routed.first.empty()) opt.method = routed.first;
392 }
393
394 if (ignores_blocking(opt.method) && api::sn_has_blocking(L))
395 throw UnsupportedError(ErrorKind{"FiniteCapacity"},
396 "SolverBA: method '" + opt_in.method +
397 "' does not support finite-buffer blocking: every SolverBA bound family but the"
398 " QRF blocking ones is parameterized by demands and a population alone, so it"
399 " bounds the model as if its buffers were unbounded. Use SolverMVA with method"
400 " 'sqd', an exact solver (CTMC, SSA, JMT, LDES), or the QRF blocking bounds"
401 " 'qrf.bas'/'qrf.rsrd', which model the finite buffer" +
402 (blocking_why.empty()
403 ? std::string()
404 : ". The QRF blocking bounds do not apply here either: " + blocking_why));
405
406 // The STRUCTURAL premises -- single-class closed, fully closed,
407 // single-server -- are NOT re-asked here: `solver_ba_analyzer` asks
408 // `method_refusal` on the way in and throws it, so a second ask would only
409 // duplicate the throw one frame earlier and lose the analyzer's own prefix.
410 // The report is gated by `list_valid_methods(L)`, which projects the same
411 // predicate plus `method_degenerate`.
412
413 const bool qrf = detail::is_qrf_noblo_method(opt.method) ||
414 detail::is_qrf_lp_method(opt.method) ||
415 detail::is_qrf_bas_nlp_method(opt.method);
416 const BaSolution<T> s = is_spnlp_method(opt.method)
419 const T zero = num_traits<T>::from_int(0);
420 const std::size_t M = L.nstations, K = L.nclasses;
421
422 std::vector<std::vector<bool>> mask(M, std::vector<bool>(K, false));
423 for (std::size_t i = 0; i < M; ++i)
424 for (std::size_t k = 0; k < K; ++k)
425 mask[i][k] = num_traits<T>::to_double(s.R(i, k)) < 10.0 * GlobalConstants::FineTol;
426
428 out.QN = mva::filter_metric(L, s.Q, mva::MetricKind::QLen, &mask);
429 out.UN = mva::filter_metric(L, s.U, mva::MetricKind::Util, &mask);
430 out.RN = mva::filter_metric(L, s.R, mva::MetricKind::RespT, nullptr);
431 out.TN = mva::filter_metric(L, s.Tp, mva::MetricKind::Tput, nullptr);
432 // The arrival rate comes from the throughputs through the same helper
433 // SolverMVA and SolverNC use. The bound goldens were recorded under those
434 // solvers before the family moved here and carry a real per station-class
435 // arrival rate; returning zeros silently emptied one column. The Source mask
436 // is getAvg's and is kept although no model that reaches here has a Source:
437 // the gates admit a mixed model, and every family then refuses it.
438 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(K, false));
439 for (std::size_t i = 0; i < M; ++i)
440 if (L.stations[i].nodetype == qn::NodeType::Source)
441 for (std::size_t k = 0; k < K; ++k) srcmask[i][k] = true;
443 &srcmask);
444 out.WN = Matrix<T>(M, K, zero);
445 out.CN = s.C;
446 out.XN = s.X;
447 out.method = opt_in.method;
448 out.actualmethod = opt.method;
449 out.iter = s.iter;
450 return out;
451}
452
453/** Port of `SolverBA.getBounds`: the {lower,upper} bracket of a family. */
454template <class T>
455struct BaBounds {
456 Matrix<T> Qlower, Qupper; ///< (M x K), all-NaN on a side the family lacks
458 bool has_lower = false, has_upper = false;
459 /**
460 * `getBoundsTable`'s row filter, (M x K): whether the (station, class) pair
461 * earns a row.
462 *
463 * This is the only part of `getBoundsTable` that is behaviour rather than
464 * formatting, so it is carried here and the label columns are not ported --
465 * no solver in `cpp/` has a table layer, `getAvgTable` included, and the
466 * CLI formats its own. The rule mirrors `getAvgTable`'s drop of disabled
467 * pairs but is NaN-SAFE: a row survives when any value that is present is
468 * nonzero, so an all-NaN side (a one-sided family) never removes it.
469 */
470 std::vector<std::vector<bool>> keep;
471};
472
473/**
474 * Port of `SolverBA.getBounds`.
475 *
476 * The family is the method's prefix before the first dot, and BOTH sides are
477 * re-run with the caller's full option set: constructing the re-run with only
478 * the method would reset `level` to its default and a hierarchical family would
479 * never tighten as the level is raised. A one-sided family (cub upper-only,
480 * mbjb/ldac lower-only) leaves its missing side as NaN, never as zero.
481 */
482template <class T>
484 const std::string m = resolve_method(opt.method);
485 const std::string fam = m.substr(0, m.find('.'));
486 // `solver_ba_run_analyzer` routes 'default'/'auto' to 'qrf.bas' on a blocked model,
487 // and ba_bounds has to say so rather than let the re-run below fail with
488 // the generic gate -- but it still cannot BRACKET, because the analyzer
489 // solves qrf.bas in the 'max' direction alone.
491 const std::pair<std::string, std::string> routed = blocking_default(L);
492 if (!routed.first.empty())
493 throw UnsupportedError(ErrorKind{"FiniteCapacity"},
494 "SolverBA: '" + opt.method + "' resolves to '" + routed.first +
495 "' on this model, which has a binding finite buffer, and that bound is"
496 " UPPER-only: there is no bracket to return. Call getAvg for the upper"
497 " bound, or SolverMVA with method 'sqd' for a point estimate");
498 }
499 const std::vector<std::string> valid = list_valid_methods();
500 auto listed = [&](const std::string& x) {
501 return std::find(valid.begin(), valid.end(), x) != valid.end();
502 };
503 const double nan = std::numeric_limits<double>::quiet_NaN();
504 BaBounds<T> b;
505 const std::size_t M = L.nstations, K = L.nclasses;
506 const T nanT = num_traits<T>::from_double(nan);
507 b.Qlower = Matrix<T>(M, K, nanT);
508 b.Qupper = Matrix<T>(M, K, nanT);
509 b.Tlower = Matrix<T>(M, K, nanT);
510 b.Tupper = Matrix<T>(M, K, nanT);
511 if (listed(fam + ".lower")) {
512 BaOptions o = opt;
513 o.method = fam + ".lower";
515 b.Qlower = r.QN;
516 b.Tlower = r.TN;
517 b.has_lower = true;
518 }
519 if (listed(fam + ".upper")) {
520 BaOptions o = opt;
521 o.method = fam + ".upper";
523 b.Qupper = r.QN;
524 b.Tupper = r.TN;
525 b.has_upper = true;
526 }
527 b.keep.assign(M, std::vector<bool>(K, true));
528 for (std::size_t i = 0; i < M; ++i)
529 for (std::size_t k = 0; k < K; ++k) {
530 const T v[4] = {b.Qlower(i, k), b.Qupper(i, k), b.Tlower(i, k), b.Tupper(i, k)};
531 bool any_present = false, any_nonzero = false;
532 for (const T& x : v) {
533 if (std::isnan(num_traits<T>::to_double(x))) continue;
534 any_present = true;
535 if (x != num_traits<T>::from_int(0)) any_nonzero = true;
536 }
537 b.keep[i][k] = !any_present || any_nonzero;
538 }
539 return b;
540}
541
542} // namespace ba
543} // namespace line
544
545#endif // LINE_SOLVERS_BA_SOLVER_BA_RUNNER_H
bool empty() const
Definition matrix.h:92
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
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< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
std::vector< NodeDef > nodes
every node, in creation order
double nclosedjobs() const
sn.nclosedjobs: the total population of the closed classes.
The method names of each solver, in listing order.
bool sn_has_blocking(const qn::NetworkStruct< T > &sn)
Defined below, after sn_is_mm1k_loss, which it exempts.
BaBounds< T > ba_bounds(const qn::NetworkStruct< T > &L, const BaOptions &opt)
Port of SolverBA.getBounds.
BaSolution< T > solver_ba_qrf_analyzer(const qn::NetworkStruct< T > &L, const BaOptions &opt)
Port of solver_ba_qrf_analyzer.
bool is_default_request(const std::string &method)
True for the aliases whose meaning a blocked model is allowed to change.
bool ignores_blocking(const std::string &method)
Whether METHOD bounds a model as if its buffers were unbounded.
std::string resolve_model_method(const qn::NetworkStruct< T > &L, const std::string &method)
Port of ba_resolve_model_method.m (SolverBA.resolveMethod): the method METHOD runs as on THIS model.
std::pair< std::string, std::string > blocking_default(const qn::NetworkStruct< T > &L)
What default/auto must mean on a model with finite buffers: the QRF BAS bound, or nothing.
std::string method_refusal(const qn::NetworkStruct< T > &L, const std::string &method)
The STRUCTURAL premises of the SolverBA bound families, in one place: the reason METHOD cannot bound ...
BaSolution< T > solver_ba_analyzer(const qn::NetworkStruct< T > &L, const BaOptions &opt)
Port of solver_ba_analyzer.
void check_method(const std::string &method)
Port of runAnalyzer's method gate: an unlisted name is refused by name.
BaSolution< T > solver_ba_spnlp_analyzer(const qn::NetworkStruct< T > &L, const BaOptions &opt)
Moment-relaxation LP bounds for a stochastic Petri net.
mva::AvgResult< T > solver_ba_run_analyzer(const qn::NetworkStruct< T > &L, const BaOptions &opt_in)
Port of @@SolverBA/runAnalyzer.m for the lang='matlab' path.
std::string method_degenerate(const qn::NetworkStruct< T > &L, const std::string &method)
Whether METHOD APPLIES to L but its bound carries no information there, and why.
std::vector< std::string > list_valid_methods()
Port of SolverBA.listValidMethods.
bool is_spnlp_method(const std::string &method)
Whether a resolved method name belongs to the Petri-net LP family.
std::string resolve_method(const std::string &method)
Port of runAnalyzer's method aliases: default is the geometric upper bound, bare auto is the AUTO com...
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
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.
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...
QrfBlocking sn_to_qrf_blocking(const qn::NetworkStruct< T > &sn, int Ktot, double max_vars=kQrfDefaultMaxVars)
The QRF BAS blocking tables (f, MR, BB, MM, ZZ, MM1), derived from an sn.
QrfAlpha sn_to_qrf_alpha(const qn::NetworkStruct< T > &L)
ld stays TRUE through a refusal: the model IS load dependent, and the caller has to tell "no arm serv...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Ports of the sn_has_* / sn_is_* predicate family of matlab/src/api/sn.
The QRF load-dependent rate scaling alpha(i,n), derived from an sn.
The QRF BAS blocking tables (f, MR, BB, MM, ZZ, MM1), derived from an sn.
Port of matlab/src/solvers/BA/solver_ba_analyzer.m, the bound-analysis handler behind SolverBA.
Port of matlab/src/solvers/BA/solver_ba_qrf_analyzer.m, the adapter that bridges the sn struct to the...
Linear-programming bounds on the mean marking and the throughputs of a stochastic timed Petri net.
The DECLARED side of the gate: one feature set per solver.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Port of SolverBA.getBounds: the {lower,upper} bracket of a family.
std::vector< std::vector< bool > > keep
getBoundsTable's row filter, (M x K): whether the (station, class) pair earns a row.
Matrix< T > Qupper
(M x K), all-NaN on a side the family lacks
The options SolverBA reads.
std::string method
Bound method; default resolves to gb.upper in the runner.
Class-level results, the [Q,U,R,T,C,X] of solver_ba_analyzer.
static constexpr double FineTol
Definition lang_types.h:765
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D0
Definition map_moment.h:54
The metrics getAvg returns, after filtering.
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
Matrix< T > UN
utilization
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
Matrix< T > AN
arrival rate
std::string msg
empty on success
The derived blocking tables, in the reference's 1-based queue indexing.