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