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