LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ba_analyzer.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_ANALYZER_H
6#define LINE_SOLVERS_BA_SOLVER_BA_ANALYZER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `matlab/src/solvers/BA/solver_ba_analyzer.m`, the bound-analysis
12 * handler behind SolverBA.
13 *
14 * Each method returns ONE side of a bracket, optimistic or pessimistic, never a
15 * point estimate. The bound itself is a scalar on the chain throughput; the
16 * per-station [Q,U,R,T,C] around it is reconstructed by the utilization law and
17 * by the same optimistic/pessimistic residence convention the ABA bound uses,
18 * which is why every family agrees on the shape of the answer even where the
19 * literature leaves the per-station response time undefined.
20 *
21 * WHAT A BOUND NEEDS is demands (visits x mean service time) and populations.
22 * No service-time distribution enters, so the featset is deliberately narrow:
23 * closed, product-form-parameterized models. The single-class families reject a
24 * multiclass model by name rather than answer for one class.
25 *
26 * ARITHMETIC. The asymptotic, balanced, proportional and box families are sums,
27 * products, integer powers, minima and divisions, so they are exact in rational
28 * arithmetic. Three paths are not and are gated: the geometric family (gb) and
29 * the successively-improving bounds (sib) solve a quadratic, and the sb lower
30 * bound takes an (N-1)-st root.
31 */
32
33#include <algorithm>
34#include <cmath>
35#include <cstddef>
36#include <limits>
37#include <sstream>
38#include <string>
39#include <vector>
40
63
64namespace line {
65namespace ba {
66
68
69/** The options SolverBA reads. Defaults are `SolverBA.defaultOptions`. */
70struct BaOptions {
71 /** Bound method; `default` resolves to `gb.upper` in the runner. */
72 std::string method = "default";
73 /**
74 * `options.level`: the hierarchy level of pbh/bjbh/cbh/sib. MATLAB's
75 * default is 2 and is NOT the pfqn default,
76 * which is 1 for pbh and 3 for sib -- the solver overrides both.
77 */
78 int level = 2;
79 /**
80 * `options.config.qrf_alpha`, the (nstations x N) load-dependent scaling of
81 * the two load-dependent QRF arms. Empty means all ones.
82 */
84 /**
85 * `options.config.qrf_params`, the blocking tables the BAS and RS-RD arms
86 * need. `sn_to_qrf_params` in the reference refuses rather than defaulting:
87 * assuming no blocking puts the bound ~31x farther from exact.
88 */
89 struct QrfParams {
90 bool supplied = false;
91 int f = 1; ///< finite-capacity queue, 1-based as in the reference
92 int MR = 1; ///< number of blocking configurations
93 std::vector<std::vector<int> > BB; ///< (MR x M) blocking state
94 std::vector<std::vector<int> > MM; ///< (MR x 2) blocking order
95 std::vector<std::vector<int> > MM1; ///< (MR x M) extended order
96 std::vector<int> ZZ; ///< (MR) blocked count per config
97 std::vector<int> F; ///< (M) capacity; empty takes sn.cap
98 };
100};
101
102/** Class-level results, the [Q,U,R,T,C,X] of `solver_ba_analyzer`. */
103template <class T>
106 std::vector<T> C, X;
107 /** `-N log X`, the reference's approximate normalizing constant. */
108 double lG = 0.0;
109 int iter = 1;
110};
111
112namespace detail {
113
114/** Station-indexed visit vector of the single chain of a single-class model. */
115template <class T>
116std::vector<T> ba_station_visits(const qn::NetworkStruct<T>& L) {
117 // THE CONVERSION IS LOAD-BEARING, not a formality: `L.visits` is
118 // STATEFUL-indexed (nstateful x nclasses) while `L.rates`, `L.stations` and
119 // the sched are STATION-indexed. Every station is stateful, but not every
120 // stateful node is a station -- a Transition is stateful without being one,
121 // a Place is both -- so on a Petri net the two lengths differ (4 against 7
122 // on the fork-join SPN). Walking the VISIT rows and indexing the station
123 // arrays with the row is the bug the other three codebases carried into
124 // their degeneracy predicate; do not "simplify" this loop into it.
125 std::vector<T> V(L.nstations, num_traits<T>::from_int(0));
126 for (std::size_t i = 0; i < L.nstations; ++i)
127 V[i] = L.visits[0](L.stateful_of_station(i + 1) - 1, 0);
128 return V;
129}
130
131template <class T>
132bool ba_is_delay(const qn::NetworkStruct<T>& L, std::size_t i) {
133 return L.stations[i].sched == SchedStrategy::INF;
134}
135
136/**
137 * Port of the local `ba_sc_demands`: the visit vector, the aggregate think
138 * time, the per-queue demand vector and the closed population.
139 */
140template <class T>
141struct ScDemands {
142 std::vector<T> V; ///< (M) station-indexed visits
143 std::vector<T> D; ///< (Mq) demands of the queueing stations, in station order
144 T Z; ///< aggregate think time of the delay stations
145 long N; ///< closed population
146};
147
148template <class T>
149ScDemands<T> ba_sc_demands(const qn::NetworkStruct<T>& L) {
150 // No single-class test here: `method_refusal` owns that rule for every
151 // caller, and a second copy is what would let the report and the run
152 // disagree. `solver_ba_analyzer` has already asked it by the time we arrive.
153 ScDemands<T> d;
154 d.V = ba_station_visits(L);
155 d.Z = num_traits<T>::from_int(0);
156 for (std::size_t i = 0; i < L.nstations; ++i) {
157 const T di = T(d.V[i] / L.rates(i, 0));
158 if (ba_is_delay(L, i))
159 d.Z += di;
160 else
161 d.D.push_back(di);
162 }
163 d.N = static_cast<long>(L.nclosedjobs());
164 return d;
165}
166
167/**
168 * The per-station metrics implied by a scalar chain-throughput bound and a
169 * system response time, shared by every family (`ba_fill` in the reference,
170 * inlined in each noniterative branch there).
171 *
172 * The optimistic side charges every queueing station the full-contention
173 * response time N/mu and the pessimistic side the no-contention 1/mu; a delay
174 * station takes 1/mu on both, having no contention to bound.
175 */
176template <class T>
177BaSolution<T> ba_fill_xc(const qn::NetworkStruct<T>& L, const std::vector<T>& V, long N, const T& X,
178 const T& C, bool is_upper) {
179 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
180 const std::size_t M = L.nstations;
181 BaSolution<T> s;
182 s.Q = Matrix<T>(M, 1, zero);
183 s.U = Matrix<T>(M, 1, zero);
184 s.R = Matrix<T>(M, 1, zero);
185 s.Tp = Matrix<T>(M, 1, zero);
186 for (std::size_t i = 0; i < M; ++i) {
187 s.Tp(i, 0) = T(V[i] * X);
188 const T svc = T(one / L.rates(i, 0));
189 s.R(i, 0) = (is_upper && !ba_is_delay(L, i)) ? T(svc * num_traits<T>::from_int(N)) : svc;
190 s.Q(i, 0) = T(s.Tp(i, 0) * s.R(i, 0));
191 if (ba_is_delay(L, i)) {
192 s.U(i, 0) = s.Q(i, 0);
193 } else {
194 // Utilization law per SERVER: without the nservers divisor a
195 // multiserver station reports U > 1 (ssd/ldac/auto reach here)
196 const T c = num_traits<T>::from_double(std::max(1.0, L.stations[i].nservers));
197 s.U(i, 0) = T(s.Tp(i, 0) / T(c * L.rates(i, 0)));
198 }
199 }
200 s.C.assign(1, C);
201 s.X.assign(1, X);
202 s.lG = -static_cast<double>(N) * num_traits<T>::log_as_double(X);
203 return s;
204}
205
206/**
207 * Port of the local `ba_chain_qfill`, the chain-indexed queue length of the two
208 * multiclass families.
209 *
210 * NEITHER RESIDENCE THE BOUND CARRIES YIELDS A QUEUE LENGTH ON THE DECLARED
211 * SIDE: the optimistic no-contention residence understates it and the Theorem-1
212 * residence of mwrbb overstates it, so the reference builds Q from the
213 * utilizations instead. The pessimistic side takes U itself, which is the jobs
214 * in service and so a lower bound on the jobs present; the optimistic side
215 * charges the whole chain population to the station in proportion to its
216 * saturation, capped at one.
217 */
218template <class T>
219Matrix<T> ba_chain_qfill(const Matrix<T>& Uchain, const std::vector<T>& Xchain,
220 const Matrix<T>& Lchain, const std::vector<T>& Nchain,
221 const std::vector<bool>& isdelay, bool is_upper) {
222 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
223 const std::size_t M = Uchain.rows(), C = Uchain.cols();
224 std::vector<T> Utot(M, zero);
225 for (std::size_t i = 0; i < M; ++i) {
226 for (std::size_t c = 0; c < C; ++c) Utot[i] += Uchain(i, c);
227 if (Utot[i] > one) Utot[i] = one;
228 }
229 Matrix<T> Qchain(M, C, zero);
230 for (std::size_t c = 0; c < C; ++c)
231 for (std::size_t i = 0; i < M; ++i) {
232 if (isdelay[i])
233 Qchain(i, c) = T(Xchain[c] * Lchain(i, c));
234 else if (is_upper)
235 Qchain(i, c) = T(Nchain[c] * Utot[i]);
236 else
237 Qchain(i, c) = Uchain(i, c);
238 }
239 return Qchain;
240}
241
242/** Port of the local `ba_fill`, whose cycle time is the ABA one at this side. */
243template <class T>
244BaSolution<T> ba_fill(const qn::NetworkStruct<T>& L, const std::vector<T>& V, long N, const T& X,
245 const T& Z, const std::vector<T>& D, bool is_upper) {
246 T Dsum = num_traits<T>::from_int(0);
247 for (const T& d : D) Dsum += d;
248 const T C = is_upper ? T(Z + num_traits<T>::from_int(N) * Dsum) : T(Z + Dsum);
249 return ba_fill_xc(L, V, N, X, C, is_upper);
250}
251
252/** min, max, sum and mean of the demand vector, which every family needs. */
253template <class T>
254struct DemandStats {
255 T sum, max, mean;
256};
257
258template <class T>
259DemandStats<T> ba_demand_stats(const std::vector<T>& D) {
260 if (D.empty()) throw UnsupportedError(ErrorKind{"Topology"}, "solver_ba_analyzer: the model has no queueing station");
261 DemandStats<T> st;
262 st.sum = num_traits<T>::from_int(0);
263 st.max = D[0];
264 for (const T& d : D) {
265 st.sum += d;
266 if (d > st.max) st.max = d;
267 }
268 st.mean = T(st.sum / num_traits<T>::from_int(static_cast<long>(D.size())));
269 return st;
270}
271
272template <class T>
273T ba_min(const T& a, const T& b) {
274 return a < b ? a : b;
275}
276
277/**
278 * Port of the local `mwrbb_disc_code`.
279 *
280 * The reference additionally maps the priority-scheduling variants of PS/DPS/GPS
281 * and the preemptive-resume priority disciplines; neither family exists in this
282 * port's SchedStrategy, so no input can reach those codes and the default
283 * (ABA full-contention, code 4) covers every remaining work-conserving case.
284 */
285inline pfqn::MwrbbSched mwrbb_disc_code(SchedStrategy s) {
286 switch (s) {
287 case SchedStrategy::FCFS:
289 case SchedStrategy::PS:
290 case SchedStrategy::DPS:
291 case SchedStrategy::GPS:
293 case SchedStrategy::HOL:
295 default:
297 }
298}
299
300/** The chain-level scaffolding the two multiclass families share. */
301template <class T>
302struct ChainView {
303 mva::ChainDemands<T> d;
304 std::vector<bool> isdelay;
305 std::vector<std::size_t> qstat; ///< 0-based station index of each queueing station
306 std::vector<T> Nv, Zc;
307};
308
309template <class T>
310ChainView<T> ba_chain_view(const qn::NetworkStruct<T>& L) {
311 ChainView<T> v;
313 const std::size_t M = L.nstations, C = L.nchains;
314 v.isdelay.assign(M, false);
315 for (std::size_t i = 0; i < M; ++i) {
316 v.isdelay[i] = ba_is_delay(L, i);
317 if (!v.isdelay[i]) v.qstat.push_back(i);
318 }
319 v.Nv.resize(C);
320 v.Zc.assign(C, num_traits<T>::from_int(0));
321 for (std::size_t c = 0; c < C; ++c) {
322 v.Nv[c] = num_traits<T>::from_double(v.d.Nchain[c]);
323 for (std::size_t i = 0; i < M; ++i)
324 if (v.isdelay[i]) v.Zc[c] += v.d.Lchain(i, c);
325 }
326 return v;
327}
328
329
330/**
331 * The families whose bound is a function of the single-chain demand vector
332 * D = V/rates, the think time Z and the population N. A multiclass or open model
333 * simply does not have those, which is why the rule is total.
334 */
335inline bool ba_is_single_class_family(const std::string& fam) {
336 // 'mapamva' is single-class for a different reason from the rest -- its LP
337 // variables QN(i,k)/UN(i,k) are indexed by station and MAP phase, with no
338 // class index at all -- but the premise it fails on is the same one.
339 return fam == "auto" || fam == "aba" || fam == "bjb" || fam == "pb" || fam == "sb" ||
340 fam == "gb" || fam == "harel" || fam == "lr" || fam == "pbh" || fam == "bjbh" ||
341 fam == "cbh" || fam == "ssd" || fam == "sib" || fam == "scb" ||
342 fam == "ldac" || fam == "mapamva" || fam == "qrf";
343}
344
345/**
346 * The multiclass families: they take a per-chain demand MATRIX and a population
347 * VECTOR, so several classes are fine and an infinite population is not.
348 */
349inline bool ba_is_fully_closed_family(const std::string& fam) {
350 return fam == "mw" || fam == "cub" || fam == "mbjb" || fam == "looping";
351}
352
353/**
354 * The single-server families whose alternative on a multiserver model IS 'ssd',
355 * which is why the reason names it. 'ssd' is the multiserver bound itself,
356 * 'ldac' is parameterized by the limiting demand of a load-dependent station
357 * and 'auto' composes whichever candidates survive, so all three are absent.
358 */
359inline bool ba_is_single_server_family(const std::string& fam) {
360 return fam == "aba" || fam == "bjb" || fam == "pb" || fam == "sb" || fam == "gb" ||
361 fam == "harel" || fam == "lr" || fam == "pbh" || fam == "bjbh" || fam == "cbh" ||
362 fam == "sib" || fam == "scb" || fam == "mapamva";
363}
364
365} // namespace detail
366
367/**
368 * The STRUCTURAL premises of the SolverBA bound families, in one place: the
369 * reason METHOD cannot bound the model L, or "" when it can.
370 *
371 * ONE PREDICATE, THREE CALLERS. `solver_ba_analyzer` asks it before dispatching
372 * and throws what it returns, `solver_ba_run_analyzer` asks it on the way in, and
373 * `list_valid_methods(L)` asks it so the name never reaches a report at all,
374 * which is the route `auto_family_methods` takes. A second copy of any rule
375 * below is how the report and the run drift apart: the report offers a pair that throws the moment it is run, which is the
376 * defect this function exists to remove.
377 *
378 * WHAT BELONGS HERE AND WHAT DOES NOT. Only the rules the feature registry
379 * cannot name. `qn::Feature` has no entry for "one class", for a server count or
380 * for a station count, so those are structural and live here. Rules of the form
381 * "this family does not accept a delay station" ARE nameable and belong in
382 * `qn::ba_feature_set`, which unsets SchedStrategy_INF for the offending method
383 * instead: a feature set can refuse a model for HAVING a construct, never for
384 * lacking one.
385 *
386 * METHOD is taken as the caller spells it and resolved through
387 * `qn::ba_resolve_method_name`, the copy of `resolve_method` that lives beside the
388 * feature set (this header is below the runner and cannot reach back into it),
389 * so 'default' is judged as the gb.upper it runs as and the reason names that.
390 * The marking-parameterized spnlp and the QRF reduction bounds carry no rule
391 * here: the QRF premise is the reducibility test `list_valid_methods(L)` already
392 * applies. Of the three OPEN families, 'bpt' and 'bgt' carry none either -- a
393 * closed model is refused by their feature set and their analyzers walk the
394 * routing matrix for the rest -- while 'snc' carries one, the SERVICE law.
395 *
396 * WHY THE SNC SERVICE LAW IS HERE AND THE bpt/bgt ONE IS NOT. All three
397 * analyzers refuse a non-exponential law at a queueing station. For bpt and bgt
398 * that rule extends to the SOURCE and is registry-expressible, so it rides in
399 * `qn::ba_feature_set` as a dropped law: both are invariant to the arrival law
400 * beyond its mean, so a non-exponential source is not something they refuse, it
401 * is something they silently bound as if it were Poisson. snc is the opposite:
402 * it CONSUMES the arrival law and its analyzer branches on a non-exponential
403 * source deliberately. Its rule is about the SERVICE only, and no feature name
404 * can say "Erlang at a Queue but not at a Source", so it is structural.
405 *
406 * Mirrors `matlab/src/solvers/BA/ba_method_refusal.m` and its JAR and native
407 * python twins.
408 */
409template <class T>
410std::string method_refusal(const qn::NetworkStruct<T>& L, const std::string& method) {
411 const std::string resolved = qn::ba_resolve_method_name(method);
412 const std::string fam = qn::ba_family_of(resolved);
413 bool anyOpen = false;
414 for (std::size_t r = 0; r < L.classes.size(); ++r)
415 if (!std::isfinite(L.classes[r].population)) anyOpen = true;
416 const bool anyClosed = L.nclosedjobs() > 0.0;
417 if (detail::ba_is_single_class_family(fam)) {
418 if (L.nclasses != 1 || !anyClosed)
419 return "Method '" + resolved + "' supports single-class closed networks only.";
420 } else if (detail::ba_is_fully_closed_family(fam)) {
421 if (!anyClosed || anyOpen)
422 return "Method '" + resolved + "' supports fully closed networks only.";
423 }
424 bool multiserver = false;
425 for (std::size_t i = 0; i < L.stations.size() && !multiserver; ++i) {
426 if (L.stations[i].sched == SchedStrategy::INF) continue;
427 if (L.stations[i].nservers > 1.0) multiserver = true;
428 }
429 if (multiserver) {
430 if (detail::ba_is_single_server_family(fam))
431 return "Method '" + resolved +
432 "' does not support multi-server stations (use 'ssd').";
433 if (detail::ba_is_fully_closed_family(fam))
434 return "Method '" + resolved + "' does not support multi-server stations.";
435 }
436
437 // The SNC service law. Judged over the pairs a station COULD serve rather
438 // than over the ones that carry traffic: the analyzer restricts to the
439 // latter, which needs the traffic equations solved, and this is their
440 // conservative outer approximation -- it never admits a pair the analyzer
441 // refuses, and can only differ on a pair given a service time at a station
442 // its class never visits. A Source is skipped, which is the whole reason
443 // this is not a feature-set delta.
444 if (fam == "snc") {
445 for (std::size_t i = 0; i < L.nstations; ++i) {
446 if (L.stations[i].nodetype == qn::NodeType::Source) continue;
447 for (std::size_t r = 0; r < L.nclasses; ++r) {
448 const double mu = num_traits<T>::to_double(L.rates(i, r));
449 if (!std::isfinite(mu) || mu <= 0.0) continue;
450 if (L.procid(i + 1, r + 1) != lang::ProcessType::EXP)
451 return "Method '" + resolved + "' requires exponential service: station " +
452 std::to_string(i + 1) + " class " + std::to_string(r + 1) +
453 " is not exponential.";
454 }
455 }
456 }
457 return "";
458}
459
460/**
461 * Whether METHOD APPLIES to L but its bound carries no information there, and
462 * why. Empty when the bound is informative, and empty for every method that has
463 * no such regime.
464 *
465 * THIS IS A DIFFERENT QUESTION FROM `method_refusal`, which is why it is a
466 * different function. That one answers "is this model outside the method's
467 * domain", and its answer is what the analyzer throws. This one answers "inside
468 * the domain, does the formula still say anything", and its answer is NOT
469 * thrown: a degenerate bound is a VALID bound, just a vacuous one, so an
470 * analyzer asked for it by name is entitled to publish it -- which is also what
471 * `tests/test_ba.cpp` pins at the regime boundary. What must not happen is
472 * OFFERING it: `list_valid_methods(L)` names the pairs a caller can act on, and
473 * a table of zeros over a network with jobs circulating in it is not one.
474 *
475 * THE ONE METHOD WITH SUCH A REGIME IS 'ldac.lower'. The Anselmi-Cremonesi
476 * bound is built from the population SURPLUS a = N - Qhat, where Qhat is the
477 * occupancy the non-bottleneck stations and the think time would hold in the
478 * open network fed at the bottleneck's saturation rate. `pfqn_ldbcmp` reports
479 * `applicable = false` below the regime, which the analyzer already throws on;
480 * AT the boundary a = 0 it returns Xlo = 0, which is formally the trivial bound
481 * X >= 0 and propagates into a table whose queue lengths, utilizations and
482 * throughputs are all zero. Every entry of that table is a true lower bound and
483 * none of them is usable, and a caller cannot tell it from a real answer of
484 * zero.
485 *
486 * Mirrors `matlab/src/solvers/BA/ba_method_degenerate.m` and its JAR and native
487 * python twins.
488 */
489template <class T>
490std::string method_degenerate(const qn::NetworkStruct<T>& L, const std::string& method) {
491 if (qn::ba_resolve_method_name(method) != "ldac.lower") return "";
492 // The applicability rules come first and are not restated: a model this
493 // method is outside the domain of has no bound to be degenerate about.
494 if (!method_refusal(L, method).empty()) return "";
495
496 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
497 // NO QUEUEING STATION, NO BOUND. Every station is a delay (or a Place, on a
498 // Petri net, which is an INF station too), so there is no bottleneck to
499 // build Qhat on and `pfqn_ldbcmp` refuses an empty demand vector. A
500 // PREDICATE MUST NOT THROW -- this one is asked once per name by
501 // `list_valid_methods`, before the Petri sieve has had a chance to drop
502 // anything -- so the case is answered rather than propagated.
503 if (sc.D.empty())
504 return "Method 'ldac.lower' has no queueing station to bound here: every station is "
505 "an infinite server, so the bottleneck the open-network occupancy is built on "
506 "does not exist.";
507 const T zero = num_traits<T>::from_int(0);
508 const std::vector<T> cz(sc.D.size(), zero);
510 sc.D, num_traits<T>::from_int(static_cast<int>(sc.N)), sc.Z, cz,
512 if (!b.applicable)
513 return "Method 'ldac.lower' does not apply here: the population is below the "
514 "open-network occupancy Qhat the bound is built from.";
515 const double xlo = num_traits<T>::to_double(b.Xlo);
516 if (!std::isfinite(xlo) || xlo <= 0.0) {
517 std::ostringstream os;
518 os << "Method 'ldac.lower' needs a population strictly above the open-network "
519 "occupancy the bound is built from (Qhat="
520 << num_traits<T>::to_double(b.Qhat) << ", N=" << sc.N
521 << "): with no surplus it degenerates to the trivial bound X >= 0 and reports a "
522 "table of zeros.";
523 return os.str();
524 }
525 return "";
526}
527
528/**
529 * Port of `solver_ba_analyzer`.
530 *
531 * @param L the refreshed struct of a closed model
532 * @param opt the method and, for a hierarchical family, the level
533 * @return the [Q,U,R,T,C,X] of the requested bound
534 */
535template <class T>
537 const std::string& method = opt.method;
538 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
539
540 // The STRUCTURAL premises of every family -- single-class closed, fully
541 // closed, single-server -- are asked here and nowhere else, so this run and
542 // the report give one answer whichever a caller meets first. Asking once at
543 // the top is what replaced the per-branch copies: an applicability test
544 // inlined in each family is how the gate and the run drift apart.
545 //
546 // IT ALSO RETIRED `ba_out_of_domain`. The five noniterative single-class
547 // families and `mw` used to answer a model outside their CLASS domain
548 // with zeroed metrics and an empty C and X, reproducing a reference whose
549 // own branches were wrapped in an applicability test with no else. That was
550 // a deliberate alignment (user ruling 2026-07-25) and it is OVERTURNED
551 // (user ruling 2026-09-04): the reference refuses by name since
552 // `ba_method_refusal.m` landed, and a table a caller cannot tell from a
553 // real answer of zero is the defect class this gate exists to remove.
554 {
555 const std::string refusal = method_refusal(L, method);
556 if (!refusal.empty()) throw UnsupportedError(ErrorKind{"Structure"}, "solver_ba_analyzer: " + refusal);
557 }
558 // The Petri-net split, which list_valid_methods applies as a sieve: no family but spnlp has a
559 // representation of a marking. Asked here rather than in method_refusal so method_degenerate
560 // still reads a net's ldac regime.
561 if (method.compare(0, 5, "spnlp") != 0) {
562 for (std::size_t i = 0; i < L.nodes.size(); ++i)
563 if (L.nodes[i].nodetype == lang::NodeType::Transition)
564 throw UnsupportedError(ErrorKind{"Structure"},
565 "solver_ba_analyzer: Method '" + method + "' is parameterized by demands and a "
566 "population, which the marking of a Petri net is not. Use "
567 "'spnlp2.upper'/'spnlp2.lower' or the 'spnlp1.upper'/'spnlp1.lower' pair on this model.");
568 }
569
570 // AUTO composite: evaluate every noniterative bound and keep the tightest
571 // side. Feasibility is probed by execution -- a candidate that rejects the
572 // model (multiserver, delay station, regime gate) throws and is skipped --
573 // so the list stays correct as families are added.
574 if (method == "auto.upper" || method == "auto.lower") {
575 const bool up = method == "auto.upper";
576 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
577 static const char* const cand_up[] = {"aba.upper", "bjb.upper", "pb.upper", "gb.upper",
578 "sb.upper", "mw.upper", "ssd.upper", "cub.upper"};
579 static const char* const cand_lo[] = {"aba.lower", "bjb.lower", "pb.lower",
580 "gb.lower", "sb.lower", "mw.lower",
581 "ssd.lower", "mbjb.lower", "ldac.lower"};
582 const char* const* cand = up ? cand_up : cand_lo;
583 const std::size_t ncand = up ? sizeof(cand_up) / sizeof(*cand_up)
584 : sizeof(cand_lo) / sizeof(*cand_lo);
585 bool have = false;
586 T Xbest = zero;
587 for (std::size_t ci = 0; ci < ncand; ++ci) {
588 BaOptions oc = opt;
589 oc.method = cand[ci];
591 try {
592 r = solver_ba_analyzer(L, oc);
593 } catch (const std::exception&) {
594 continue;
595 }
596 if (r.X.empty()) continue;
597 const T Xc = r.X[0];
598 if (!std::isfinite(num_traits<T>::to_double(Xc)) || !(Xc > zero)) continue;
599 if (!have || (up && Xc < Xbest) || (!up && Xc > Xbest)) {
600 Xbest = Xc;
601 have = true;
602 }
603 }
604 if (!have)
605 throw UnsupportedError(ErrorKind{"Infeasible"}, "solver_ba_analyzer: method '" + method +
606 "' found no feasible bound for this model");
607 return detail::ba_fill(L, sc.V, sc.N, Xbest, sc.Z, sc.D, up);
608 }
609
610 // The noniterative single-class families (aba, bjb, pb, sb, gb) share the
611 // demand extraction; the reference inlines it in every branch.
612 const bool single_class_family =
613 method == "aba.upper" || method == "aba.lower" || method == "bjb.upper" ||
614 method == "bjb.lower" || method == "pb.upper" || method == "pb.lower" ||
615 method == "sb.upper" || method == "sb.lower" || method == "gb.upper" ||
616 method == "gb.lower";
617
618 if (single_class_family) {
619 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
620 const std::vector<T>& V = sc.V;
621 const std::vector<T>& D = sc.D;
622 const T& Z = sc.Z;
623 const long N = sc.N;
624 const T Nt = num_traits<T>::from_int(N);
625 const detail::DemandStats<T> st = detail::ba_demand_stats(D);
626
627 if (method == "aba.upper") {
628 const T X = detail::ba_min(T(one / st.max), T(Nt / T(Z + st.sum)));
629 return detail::ba_fill_xc(L, V, N, X, T(Z + Nt * st.sum), true);
630 }
631 if (method == "aba.lower") {
632 const T X = T(Nt / T(Z + Nt * st.sum));
633 return detail::ba_fill_xc(L, V, N, X, T(Z + st.sum), false);
634 }
635 // The balanced and proportional families evaluate the ABA bracket at
636 // N-1: both are one exact MVA step taken from a bounded arrival-theorem
637 // queue length, so the population they see is the one a job arriving to
638 // the system leaves behind.
639 const T Nm1 = num_traits<T>::from_int(N - 1);
640 const T xup1 = detail::ba_min(T(one / st.max), T(Nm1 / T(Z + st.sum)));
641 const T xlo1 = T(Nm1 / T(Z + Nm1 * st.sum));
642 if (method == "bjb.upper") {
643 const T C = T(Z + st.sum + st.max * T(Nm1 - Z * xlo1));
644 const T X = detail::ba_min(
645 T(one / st.max), T(Nt / T(Z + st.sum + st.mean * T(Nm1 - Z * xup1))));
646 return detail::ba_fill_xc(L, V, N, X, C, true);
647 }
648 if (method == "bjb.lower") {
649 const T C = T(Z + st.sum + st.mean * T(Nm1 - Z * xup1));
650 const T X = T(Nt / T(Z + st.sum + st.max * T(Nm1 - Z * xlo1)));
651 return detail::ba_fill_xc(L, V, N, X, C, false);
652 }
653 if (method == "pb.upper" || method == "pb.lower") {
654 T d2 = zero, dN = zero, dNm1 = zero;
655 for (const T& d : D) {
656 d2 += T(d * d);
657 dN += num_pow_int(d, static_cast<unsigned>(N));
658 dNm1 += num_pow_int(d, static_cast<unsigned>(N - 1));
659 }
660 const T Dpb2 = T(d2 / st.sum);
661 const T DpbN = T(dN / dNm1);
662 if (method == "pb.upper") {
663 const T C = T(Z + st.sum + DpbN * T(Nm1 - Z * xlo1));
664 const T X = detail::ba_min(
665 T(one / st.max), T(Nt / T(Z + st.sum + Dpb2 * T(Nm1 - Z * xup1))));
666 return detail::ba_fill_xc(L, V, N, X, C, true);
667 }
668 const T C = T(Z + st.sum + Dpb2 * T(Nm1 - Z * xup1));
669 const T X = T(Nt / T(Z + st.sum + DpbN * T(Nm1 - Z * xlo1)));
670 return detail::ba_fill_xc(L, V, N, X, C, false);
671 }
672 if (method == "sb.upper" || method == "sb.lower") {
673 // The power sums of Harel-Namn-Sturm are written for a network with
674 // no terminal population, and the reference refuses a delay station
675 // rather than drop its think time.
676 for (std::size_t i = 0; i < L.nstations; ++i)
677 if (detail::ba_is_delay(L, i))
678 throw UnsupportedError(ErrorKind{"DelayStation"}, "solver_ba_analyzer: method '" + method +
679 "' does not support infinite-server stations");
680 T A1 = zero, A2 = zero, A3 = zero;
681 for (const T& d : D) {
682 A1 += d;
683 A2 += T(d * d);
684 A3 += T(d * d * d);
685 }
686 if (method == "sb.upper") {
687 const T C = T(Z + A1 + Nm1 * T(T(A1 * A2 + A3) / T(A1 * A1 + A2)));
688 const T X = detail::ba_min(T(one / st.max), T(Nt / C));
689 // The power-sum family leaves the per-station response time
690 // undefined, and the reference pairs BOTH sides with the
691 // no-contention residence rather than only the lower one.
692 return detail::ba_fill_xc(L, V, N, X, C, false);
693 }
694 // The (N-1)-st root of the N-th power sum has no field expression.
695 if constexpr (!num_traits<T>::has_transcendental) {
696 throw UnsupportedError(ErrorKind{"Arithmetic"},
697 "solver_ba_analyzer: method 'sb.lower' needs an (N-1)-st root and is "
698 "unavailable in exact arithmetic");
699 } else {
700 using std::pow;
701 T AN = zero;
702 for (const T& d : D) AN += num_pow_int(d, static_cast<unsigned>(N));
703 // pow is taken on T, not through double: a high-precision
704 // backend would otherwise lose every digit past the 17th here.
705 const T root = pow(T(AN / A1), T(one / Nm1));
706 const T C = T(Z + A1 + Nm1 * root);
707 return detail::ba_fill_xc(L, V, N, T(Nt / C), C, false);
708 }
709 }
710 // gb: the geometric bounds solve a quadratic in the throughput.
711 if constexpr (!num_traits<T>::has_transcendental) {
712 throw UnsupportedError(ErrorKind{"Arithmetic"}, "solver_ba_analyzer: the geometric bounds '" + method +
713 "' solve a quadratic and are unavailable in exact arithmetic");
714 } else {
715 const bool up = (method == "gb.upper");
716 const T Xup = pfqn::pfqn_xzgsbup(D, Nt, Z);
717 const T Xlo = pfqn::pfqn_xzgsblow(D, Nt, Z);
718 const T X = up ? detail::ba_min(T(one / st.max), Xup) : Xlo;
719 const T C = T(Nt / (up ? Xlo : Xup));
720 // The queue-length bound is the geometric one, not the residence
721 // convention of ba_fill; the response time follows from it by the
722 // OPPOSITE side of the throughput bracket, which is what keeps
723 // Q = X R consistent with a bound rather than with a point estimate.
724 const T Xden = up ? Xlo : Xup;
726 s.Q = Matrix<T>(L.nstations, 1, zero);
727 s.U = Matrix<T>(L.nstations, 1, zero);
728 s.R = Matrix<T>(L.nstations, 1, zero);
729 s.Tp = Matrix<T>(L.nstations, 1, zero);
730 std::size_t k = 0;
731 for (std::size_t i = 0; i < L.nstations; ++i) {
732 s.Tp(i, 0) = T(V[i] * X);
733 if (detail::ba_is_delay(L, i)) {
734 s.R(i, 0) = T(one / L.rates(i, 0));
735 s.Q(i, 0) = T(X * s.R(i, 0));
736 } else {
737 s.Q(i, 0) = up ? pfqn::pfqn_qzgbup(D, Nt, Z, k) : pfqn::pfqn_qzgblow(D, Nt, Z, k);
738 s.R(i, 0) = T(T(s.Q(i, 0) / Xden) / V[i]);
739 ++k;
740 }
741 s.U(i, 0) = detail::ba_is_delay(L, i) ? s.Q(i, 0) : T(s.Tp(i, 0) / L.rates(i, 0));
742 }
743 s.C.assign(1, C);
744 s.X.assign(1, X);
745 s.lG = -static_cast<double>(N) * num_traits<T>::log_as_double(X);
746 return s;
747 }
748 }
749
750 if (method == "lr.upper" || method == "lr.lower") {
751 // LP linear-reduction bound: one LP per station, each minimizing or
752 // maximizing that station's utilization over a polytope that CONTAINS
753 // the exact stationary solution.
754 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
755 for (std::size_t i = 0; i < L.nstations; ++i)
756 if (detail::ba_is_delay(L, i))
757 throw UnsupportedError(ErrorKind{"DelayStation"}, "solver_ba_analyzer: method '" + method +
758 "' does not support delay (infinite-server) stations");
759 const bool up = (method == "lr.upper");
760 const std::size_t M = L.nstations;
761 const std::vector<T>& V = sc.V;
762
764 par.M = static_cast<int>(M);
765 // MATLAB reads sn.njobs(1), the class population, which for the
766 // single-class model this branch admits is nclosedjobs.
767 par.N = static_cast<int>(sc.N);
768 par.mu.resize(M);
769 T totV = zero;
770 for (std::size_t i = 0; i < M; ++i) {
771 par.mu[i] = L.rates(i, 0);
772 totV += V[i];
773 }
774 // Every row of r is the visit vector normalized by its total: the
775 // reference builds it with repmat, so a job leaves any station for j
776 // with the same probability V_j / sum V.
777 par.r = Matrix<T>(M, M, zero);
778 if (totV == zero) throw NumericError(ErrorKind{"Degenerate"}, "solver_ba_analyzer: the visit ratios are all zero");
779 for (std::size_t i = 0; i < M; ++i)
780 for (std::size_t j = 0; j < M; ++j) par.r(i, j) = T(V[j] / totV);
781
783 std::vector<T> U(M, zero);
784 for (std::size_t ti = 0; ti < M; ++ti) {
785 const mapqn::LrPfResult<T> r =
786 mapqn::mapqn_bnd_lr_pf(par, static_cast<int>(ti) + 1, sense);
787 if (!r.ok)
788 throw NumericError(ErrorKind{"NonConvergence"}, "solver_ba_analyzer: the '" + method +
789 "' linear program did not solve (" + r.status + ")");
790 U[ti] = r.objective;
791 }
792 // The chain throughput implied by the bounded utilizations, U_i = X V_i / mu_i.
793 // MATLAB drops the non-finite candidates (a station with no visit) and
794 // takes the minimum, or zero when none survives.
795 bool any = false;
796 T X = zero;
797 for (std::size_t i = 0; i < M; ++i) {
798 if (V[i] == zero) continue; // MATLAB: the candidate is Inf and isfinite drops it
799 const T cand = T(U[i] * L.rates(i, 0) / V[i]);
800 if (!any || cand < X) {
801 X = cand;
802 any = true;
803 }
804 }
805 if (!any) X = zero;
806 // Q, R, T and C follow the ABA residence convention of ba_fill at this
807 // side, with no think time since a delay station is refused above; only
808 // U comes from the LP rather than from the utilization law.
809 BaSolution<T> s = detail::ba_fill(L, V, sc.N, X, zero, sc.D, up);
810 for (std::size_t i = 0; i < M; ++i) s.U(i, 0) = U[i];
811 s.lG = std::numeric_limits<double>::quiet_NaN(); // the reference leaves it unset
812 return s;
813 }
814
815 if (method == "mapamva.upper" || method == "mapamva.lower") {
816 // MAP-AMVA (Casale-Smirni, DSN 2009): the LP over the EXACT mean-value
817 // balance equations of a closed MAP queueing network. Like the QRF
818 // family, it consumes the CORRELATION between successive services
819 // rather than the service mean alone -- its variables are the per-phase
820 // queue lengths QN(i,k) and utilizations UN(i,k), so a workload whose
821 // burstiness moves the bottleneck between stations is bounded rather
822 // than averaged into a renewal process. That is why 'MAP' and 'MMPP2'
823 // reach this family's feature set (and the QRF and snc ones, which also
824 // read the (D0,D1) law).
825 //
826 // The LP carries phases at ONE queue and requires it to be the LAST --
827 // q(i,j,k,h) reads the scalar muM(i) for i < M and the (D0,D1) pair
828 // muMAP/v for i == M -- so a model whose phase-carrying station sits
829 // elsewhere is PERMUTED rather than refused, and the results are
830 // permuted back before they are returned.
831 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
832 for (std::size_t i = 0; i < L.nstations; ++i)
833 if (detail::ba_is_delay(L, i))
834 throw UnsupportedError(ErrorKind{"DelayStation"},
835 "solver_ba_analyzer: method '" + method +
836 "' does not support delay (infinite-server) stations: the MAP-AMVA program "
837 "of Casale-Smirni (DSN 2009) is written for a network of queues and the "
838 "paper names the delay extension as open work. Use a QRF method, which "
839 "carries the load-dependent rate law");
840 const bool up = (method == "mapamva.upper");
841 const std::size_t M = L.nstations;
842 const std::vector<T>& V = sc.V;
843
844 // Phase order per station. One phase is an exponential server, which
845 // enters the LP as the scalar rate muM(i); more than one is the (D0,D1)
846 // pair, which only queue M can hold.
847 std::vector<std::pair<Matrix<T>, Matrix<T>>> MAPs(M);
848 std::vector<int> kph(M, 1);
849 for (std::size_t i = 0; i < M; ++i) {
850 const mam::Map<T> m = lang::dist_to_map(L.service[i][0]);
851 if (m.D0.rows() == 0) {
852 MAPs[i] = std::make_pair(Matrix<T>(1, 1, num_traits<T>::from_int(-1)),
854 kph[i] = 1;
855 } else {
856 MAPs[i] = std::make_pair(m.D0, m.D1);
857 kph[i] = static_cast<int>(m.D0.rows());
858 }
859 }
860 std::vector<std::size_t> phased;
861 for (std::size_t i = 0; i < M; ++i)
862 if (kph[i] > 1) phased.push_back(i);
863 if (phased.size() > 1) {
864 std::string names;
865 for (std::size_t j = 0; j < phased.size(); ++j)
866 names += (j ? ", " : "") + std::to_string(phased[j] + 1);
867 throw UnsupportedError(ErrorKind{"NonExponential"},
868 "solver_ba_analyzer: method '" + method +
869 "' carries phases at ONE station: the LP gives queue M the (D0,D1) pair and "
870 "every other queue a scalar rate. Stations " + names + " are all "
871 "non-exponential. Use a QRF method, whose q carries a phase at every station");
872 }
873 // Every station exponential: the program is still the right one, it just
874 // degenerates to K = 1, where the per-phase variables collapse and the
875 // balances become the product-form ones of mapqn_bnd_lr_pf.
876 const std::size_t map_idx = phased.empty() ? M - 1 : phased.front();
877 std::vector<std::size_t> perm;
878 perm.reserve(M);
879 for (std::size_t i = 0; i < M; ++i)
880 if (i != map_idx) perm.push_back(i);
881 perm.push_back(map_idx);
882 const int K = kph[map_idx];
883 const std::size_t Ks = static_cast<std::size_t>(K);
884
886 par.M = static_cast<int>(M);
887 par.N = static_cast<int>(sc.N);
888 par.K = K;
889 par.muM.resize(M - 1);
890 for (std::size_t a = 0; a + 1 < M; ++a) par.muM[a] = L.rates(perm[a], 0);
891 // muMAP(k,h) is the completion rate out of phase k landing in phase h,
892 // i.e. D1(k,h); v(k,h) is the background phase change that completes no
893 // job, i.e. D0 off the diagonal. Same (from,to) convention as
894 // qrf_extract_mu_v -- writing either as its transpose is invisible for a
895 // reversible D0 and silently reverses the phase order of an Erlang.
896 par.muMAP = Matrix<T>(Ks, Ks, zero);
897 par.v = Matrix<T>(Ks, Ks, zero);
898 for (std::size_t a = 0; a < Ks; ++a)
899 for (std::size_t b = 0; b < Ks; ++b) {
900 par.muMAP(a, b) = MAPs[map_idx].second(a, b);
901 par.v(a, b) = (a == b) ? zero : MAPs[map_idx].first(a, b);
902 }
903 par.r = Matrix<T>(M, M, zero);
904 for (std::size_t a = 0; a < M; ++a)
905 for (std::size_t b = 0; b < M; ++b) par.r(a, b) = L.rt(perm[a], perm[b]);
906
907 std::vector<T> Vp(M, zero), Sp(M, zero);
908 for (std::size_t a = 0; a < M; ++a) {
909 const std::size_t i = perm[a];
910 Vp[a] = V[i];
911 mam::Map<T> mi;
912 mi.D0 = MAPs[i].first;
913 mi.D1 = MAPs[i].second;
914 Sp[a] = mam::map_mean(mi);
915 }
916
917 // THREE SWEEPS OF THE SAME LP, and the utilization one runs in BOTH
918 // senses on purpose. R_i = Q_i/(V_i*X) rises with Q_i and FALLS with X,
919 // so an upper bound on the response time pairs Q_i^max with X^min;
920 // dividing by X^max on both sides is what would report an upper R below
921 // the exact value and break the bracket.
923 std::vector<T> umax(M, zero), umin(M, zero), qbnd(M, zero);
924 for (std::size_t a = 0; a < M; ++a) {
925 const int ti = static_cast<int>(a);
934 if (!ru.ok || !rl.ok || !rq.ok)
935 throw NumericError(ErrorKind{"NonConvergence"}, "solver_ba_analyzer: the '" + method +
936 "' linear program did not solve (" +
937 (ru.ok ? (rl.ok ? rq.status : rl.status) : ru.status) + ")");
938 umax[a] = ru.objective;
939 umin[a] = rl.objective;
940 qbnd[a] = rq.objective;
941 }
942
943 // Utilization law U_i = X*V_i*S_i, exact at a single server under ANY
944 // service law, so each station turns its own utilization bound into a
945 // throughput bound and the tightest of the M survives. A station with no
946 // visits or no service time carries no information and is skipped.
947 bool any = false;
948 T x_up = zero, x_lo = zero;
949 for (std::size_t a = 0; a < M; ++a) {
950 const T load = T(Vp[a] * Sp[a]);
951 if (load <= zero) continue;
952 const T cu = T(umax[a] / load), cl = T(umin[a] / load);
953 if (!any) {
954 x_up = cu;
955 x_lo = cl;
956 any = true;
957 } else {
958 if (cu < x_up) x_up = cu;
959 if (cl > x_lo) x_lo = cl;
960 }
961 }
962 if (!any)
963 throw NumericError(ErrorKind{"Degenerate"}, "solver_ba_analyzer: method '" + method +
964 "' found no station with both a positive visit ratio and a "
965 "positive mean service time");
966 const T xb = up ? x_up : x_lo;
967 const T xopp = up ? x_lo : x_up;
968
969 // Unpermute: the LP orders the stations with the phase-carrying one last.
971 s.Q = Matrix<T>(M, 1, zero);
972 s.U = Matrix<T>(M, 1, zero);
973 s.R = Matrix<T>(M, 1, zero);
974 s.Tp = Matrix<T>(M, 1, zero);
975 for (std::size_t a = 0; a < M; ++a) {
976 const std::size_t i = perm[a];
977 s.U(i, 0) = up ? umax[a] : umin[a];
978 s.Q(i, 0) = qbnd[a];
979 s.Tp(i, 0) = T(Vp[a] * xb);
980 s.R(i, 0) = (xopp > zero && Vp[a] > zero) ? T(qbnd[a] / (Vp[a] * xopp)) : zero;
981 }
982 // Delay stations are refused above, so the closed-network response time
983 // is N/X exactly and the throughput bracket transfers to it directly.
984 // Summing the per-station R bounds instead would add M separately
985 // attained maxima and report a looser number.
986 s.C.assign(1, xopp > zero
987 ? T(num_traits<T>::from_int(static_cast<long>(sc.N)) / xopp)
988 : zero);
989 s.X.assign(1, xb);
990 s.lG = -static_cast<double>(sc.N) * num_traits<T>::log_as_double(xb);
991 return s;
992 }
993
994 if (method == "mw.upper" || method == "mw.lower") {
995 // Majumdar-Woodside robust box bounds: multiclass, and the only family
996 // that reads the scheduling discipline, since its lower bound is a
997 // per-discipline residence guarantee.
998 const detail::ChainView<T> v = detail::ba_chain_view(L);
999 const std::size_t M = L.nstations, C = L.nchains, Kq = v.qstat.size();
1000 Matrix<T> Vq(Kq, C, zero), Sq(Kq, C, zero);
1001 std::vector<pfqn::MwrbbSched> schedq(Kq);
1002 for (std::size_t k = 0; k < Kq; ++k) {
1003 const std::size_t i = v.qstat[k];
1004 schedq[k] = detail::mwrbb_disc_code(L.stations[i].sched);
1005 for (std::size_t c = 0; c < C; ++c) {
1006 Vq(k, c) = v.d.Vchain(i, c);
1007 Sq(k, c) = v.d.STchain(i, c);
1008 }
1009 }
1010 // Chain priority: the reference class's when the chain has one, else the
1011 // highest priority (lowest value) any class of the chain carries.
1012 std::vector<int> prioc(C, 0);
1013 for (std::size_t c = 0; c < C; ++c) {
1014 if (L.refclass[c] > 0) {
1015 prioc[c] = L.classes[L.refclass[c] - 1].prio;
1016 } else {
1017 int p = L.classes[L.inchain[c][0] - 1].prio;
1018 for (std::size_t r : L.inchain[c]) p = std::min(p, L.classes[r - 1].prio);
1019 prioc[c] = p;
1020 }
1021 }
1022 const pfqn::MwrbbBounds<T> b = pfqn::pfqn_mwrbb(Vq, Sq, v.Nv, v.Zc, schedq, prioc);
1023 const bool up = (method == "mw.upper");
1024 const std::vector<T>& Xchain = up ? b.Xup : b.Xlo;
1025 Matrix<T> Tchain(M, C, zero), Uchain(M, C, zero), Qchain(M, C, zero);
1026 for (std::size_t c = 0; c < C; ++c)
1027 for (std::size_t i = 0; i < M; ++i) {
1028 Tchain(i, c) = T(Xchain[c] * v.d.Vchain(i, c));
1029 Uchain(i, c) = T(Xchain[c] * v.d.Lchain(i, c)); // utilization law
1030 }
1031 Qchain = detail::ba_chain_qfill(Uchain, Xchain, v.d.Lchain, v.Nv, v.isdelay, up);
1033 L, v.d, Qchain, Uchain, Matrix<T>(), Tchain, Xchain);
1034 BaSolution<T> s;
1035 s.Q = cr.Q;
1036 s.U = cr.U;
1037 s.R = cr.R;
1038 s.Tp = cr.Tp;
1039 s.C = cr.C;
1040 s.X = cr.X;
1041 s.lG = std::numeric_limits<double>::quiet_NaN();
1042 return s;
1043 }
1044
1045 // Eager Looping: the multiclass bracket that initializes the multiple-class
1046 // PBH. Pessimistic side from the heap-inflated response time, optimistic
1047 // side from the response-time lower bound.
1048 if (method == "looping.upper" || method == "looping.lower") {
1049 const detail::ChainView<T> v = detail::ba_chain_view(L);
1050 const std::size_t M = L.nstations, C = L.nchains, Kq = v.qstat.size();
1051 Matrix<T> Lq(Kq, C, zero);
1052 for (std::size_t k = 0; k < Kq; ++k)
1053 for (std::size_t c = 0; c < C; ++c) Lq(k, c) = v.d.Lchain(v.qstat[k], c);
1054 const pfqn::LoopingBounds<T> b = pfqn::pfqn_looping(Lq, v.Nv, v.Zc);
1055 const bool up = (method == "looping.upper");
1056 const std::vector<T>& Xchain = up ? b.Xup : b.Xlo;
1057 Matrix<T> Tchain(M, C, zero), Uchain(M, C, zero), Qchain(M, C, zero);
1058 for (std::size_t c = 0; c < C; ++c)
1059 for (std::size_t i = 0; i < M; ++i) {
1060 Tchain(i, c) = T(Xchain[c] * v.d.Vchain(i, c));
1061 Uchain(i, c) = T(Xchain[c] * v.d.Lchain(i, c)); // utilization law
1062 }
1063 Qchain = detail::ba_chain_qfill(Uchain, Xchain, v.d.Lchain, v.Nv, v.isdelay, up);
1065 L, v.d, Qchain, Uchain, Matrix<T>(), Tchain, Xchain);
1066 BaSolution<T> s;
1067 s.Q = cr.Q;
1068 s.U = cr.U;
1069 s.R = cr.R;
1070 s.Tp = cr.Tp;
1071 s.C = cr.C;
1072 s.X = cr.X;
1073 s.lG = std::numeric_limits<double>::quiet_NaN();
1074 return s;
1075 }
1076
1077 // Achievable-region LP relaxation (Bertsimas-Paschalidis-Tsitsiklis 1994).
1078 // The only OPEN-network family here: it lower bounds the mean response
1079 // times attainable by ANY non-idling policy, so it is refused on the closed
1080 // models every other family requires.
1081 if (method == "bpt.lower") {
1082 BaSolution<T> s;
1083 solver_ba_bpt(L, s);
1084 return s;
1085 }
1086
1087 // Piecewise-linear Lyapunov bound (Bertsimas-Gamarnik-Tsitsiklis 2001), the
1088 // OPEN-network upper side. A feasible gamma > 0 both certifies that every
1089 // work-conserving policy is stable and yields the (loose) queue-length bound.
1090 if (method == "bgt.upper") {
1091 BaSolution<T> s;
1092 solver_ba_bgt(L, s);
1093 return s;
1094 }
1095
1096 // Stochastic network calculus (Fidler-Rizk 2015), the third OPEN-network
1097 // family and the only one whose native object is a TAIL: MGF envelopes are
1098 // propagated hop by hop and the delay bound is integrated into a mean.
1099 if (method == "snc.upper") {
1100 BaSolution<T> s;
1101 solver_ba_snc(L, s);
1102 return s;
1103 }
1104
1105 if (method == "cub.upper" || method == "mbjb.lower") {
1106 const detail::ChainView<T> v = detail::ba_chain_view(L);
1107 const std::size_t M = L.nstations, C = L.nchains, Kq = v.qstat.size();
1108 Matrix<T> Lq(Kq, C, zero);
1109 for (std::size_t k = 0; k < Kq; ++k)
1110 for (std::size_t c = 0; c < C; ++c) Lq(k, c) = v.d.Lchain(v.qstat[k], c);
1111 const pfqn::McubBounds<T> b = pfqn::pfqn_mcub(Lq, v.Nv, v.Zc);
1112 const bool up = (method == "cub.upper");
1113 const std::vector<T>& Xchain = up ? b.Xub : b.Xlb;
1114 Matrix<T> Tchain(M, C, zero), Uchain(M, C, zero), Qchain(M, C, zero);
1115 for (std::size_t c = 0; c < C; ++c)
1116 for (std::size_t i = 0; i < M; ++i) {
1117 Tchain(i, c) = T(Xchain[c] * v.d.Vchain(i, c));
1118 Uchain(i, c) = T(Xchain[c] * v.d.Lchain(i, c)); // utilization law
1119 }
1120 Qchain = detail::ba_chain_qfill(Uchain, Xchain, v.d.Lchain, v.Nv, v.isdelay, up);
1122 L, v.d, Qchain, Uchain, Matrix<T>(), Tchain, Xchain);
1123 BaSolution<T> s;
1124 s.Q = cr.Q;
1125 s.U = cr.U;
1126 s.R = cr.R;
1127 s.Tp = cr.Tp;
1128 s.C = cr.C;
1129 s.X = cr.X;
1130 s.lG = std::numeric_limits<double>::quiet_NaN();
1131 return s;
1132 }
1133
1134 // Sharp bounds of Harel-Namn-Sturm, distinct from the 'sb' family of the
1135 // same paper: they extrapolate from the EXACT normalizing constant at
1136 // populations n <= 7 instead of using the first three power sums.
1137 if (method == "harel.upper" || method == "harel.lower") {
1138 const detail::ScDemands<T> sch = detail::ba_sc_demands(L);
1139 if (!(sch.Z == zero))
1140 throw UnsupportedError(ErrorKind{"DelayStation"}, "solver_ba_analyzer: method '" + method +
1141 "' does not support think times (infinite-server stations)");
1142 const int Nh = static_cast<int>(sch.N);
1143 const int maxUB = Nh < 7 ? Nh : 7;
1144 const pfqn::HarelBoundsResult<T> b = pfqn::pfqn_harel_bounds(sch.D, Nh, zero, maxUB);
1145 const bool uph = method == "harel.upper";
1146 T Xb = b.LB;
1147 if (uph) {
1148 T dmax = sch.D[0];
1149 for (const T& d : sch.D)
1150 if (d > dmax) dmax = d;
1151 const T cap = T(num_traits<T>::from_int(1) / dmax);
1152 const T ext = maxUB >= 2 ? b.UB[static_cast<std::size_t>(maxUB)] : b.TH[1];
1153 Xb = ext < cap ? ext : cap;
1154 }
1155 return detail::ba_fill(L, sch.V, sch.N, Xb, sch.Z, sch.D, uph);
1156 }
1157
1158 // The hierarchical / iterative single-class families.
1159 const bool hier = method == "pbh.upper" || method == "pbh.lower" || method == "cbh.upper" ||
1160 method == "cbh.lower" || method == "bjbh.upper" || method == "bjbh.lower" ||
1161 method == "ssd.upper" ||
1162 method == "ssd.lower" || method == "sib.upper" || method == "sib.lower" ||
1163 method == "scb.upper" || method == "scb.lower" ||
1164 method == "ldac.lower";
1165 if (!hier)
1166 throw UnsupportedError(ErrorKind{"Method"}, "solver_ba_analyzer: unknown bound method '" + method + "'");
1167
1168 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
1169 const bool up = method.size() > 6 && method.compare(method.size() - 6, 6, ".upper") == 0;
1170 const T Nt = num_traits<T>::from_int(sc.N);
1171 const int lvl = opt.level;
1172
1173 // ssd and ldac are the two families the reference does NOT gate on server
1174 // count: ssd bounds a multiserver station by disaggregating it into single
1175 // servers, and ldac reads only the limiting demand.
1176 if (method == "ssd.upper" || method == "ssd.lower") {
1177 std::vector<T> cvec;
1178 for (std::size_t i = 0; i < L.nstations; ++i)
1179 if (!detail::ba_is_delay(L, i))
1180 cvec.push_back(num_traits<T>::from_double(L.stations[i].nservers));
1181 const pfqn::SsdBounds<T> b = pfqn::pfqn_ssd(sc.D, Nt, sc.Z, cvec);
1182 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1183 }
1184 if (method == "ldac.lower") {
1185 // Fixed-rate parameterization (Heffes c = 0); the bound holds only in
1186 // the asymptotic regime N >= Qhat, and the reference errors below it
1187 // rather than return the NaN pfqn_ldbcmp produces there.
1188 const std::vector<T> cz(sc.D.size(), zero);
1189 const pfqn::LdBcmpBound<T> b =
1190 pfqn::pfqn_ldbcmp(sc.D, Nt, sc.Z, cz, num_traits<T>::from_double(1e-10));
1191 if (!b.applicable)
1192 throw UnsupportedError(ErrorKind{"Population"},
1193 "solver_ba_analyzer: method 'ldac.lower' requires the asymptotic regime N >= "
1194 "Qhat");
1195 return detail::ba_fill(L, sc.V, sc.N, b.Xlo, sc.Z, sc.D, false);
1196 }
1197 if (method == "scb.upper" || method == "scb.lower") {
1198 // Single-class bounds of Dowdy et al. (1992). THE BRACKETED OBJECT IS NOT
1199 // THIS MODEL: scb brackets the multiclass system that this single-class
1200 // model aggregates, so scb.lower is the EXACT single-class throughput and
1201 // scb.upper adds the demand-free Expression-(3) gap. That is why scb is
1202 // absent from the auto candidate list -- mixing it with families that
1203 // bracket this model's own solution would compare two different quantities.
1204 if (!(sc.Z == zero))
1205 throw UnsupportedError(ErrorKind{"DelayStation"}, "solver_ba_analyzer: method '" + method +
1206 "' supports Z=0 (no delay station) only; Theorem 3 rests on "
1207 "the delay-free balanced-network throughput");
1208 const pfqn::ScbBounds<T> b = pfqn::pfqn_scb(sc.D, sc.N);
1209 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1210 }
1211 if (method == "pbh.upper" || method == "pbh.lower") {
1212 const pfqn::PbhBounds<T> b = pfqn::pfqn_pbh(sc.D, static_cast<int>(sc.N), sc.Z, lvl);
1213 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1214 }
1215 if (method == "bjbh.upper" || method == "bjbh.lower") {
1216 const pfqn::PbhBounds<T> b = pfqn::pfqn_bjbk(sc.D, static_cast<int>(sc.N), sc.Z, lvl);
1217 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1218 }
1219 if (method == "cbh.upper" || method == "cbh.lower") {
1220 const pfqn::CbhBounds<T> b = pfqn::pfqn_cbh(sc.D, static_cast<int>(sc.N), sc.Z, lvl);
1221 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1222 }
1223 // sib
1224 if (!(sc.Z == zero))
1225 throw UnsupportedError(ErrorKind{"DelayStation"}, "solver_ba_analyzer: method '" + method +
1226 "' supports Z=0 (no delay station) only; delay needs the SIB "
1227 "Section-3.2 extension");
1228 if constexpr (!num_traits<T>::has_transcendental) {
1229 throw UnsupportedError(ErrorKind{"Arithmetic"}, "solver_ba_analyzer: method '" + method +
1230 "' solves a quadratic and is unavailable in exact arithmetic");
1231 } else {
1232 const pfqn::SibBounds<T> b = pfqn::pfqn_sib(sc.D, static_cast<int>(sc.N), zero, lvl);
1233 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1234 }
1235}
1236
1237} // namespace ba
1238} // namespace line
1239
1240#endif // LINE_SOLVERS_BA_SOLVER_BA_ANALYZER_H
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
std::size_t stateful_of_station(std::size_t st) const
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< std::size_t > refclass
(nchains) 1-based class, 0 = none
Matrix< T > rt
sn.rt and sn.rtnodes: the class-expanded routing.
Matrix< double > procid() const
sn.procid (M x K): the ProcessType id of each (station, class) pair, as refreshProcessTypes....
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
std::vector< NodeDef > nodes
every node, in creation order
std::vector< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
double nclosedjobs() const
sn.nclosedjobs: the total population of the closed classes.
MVA-shaped linear-reduction bound for a closed network of M - 1 exponential queues and ONE MAP queue.
Linear-reduction (LR) bound on the utilization of one station of a closed product-form network.
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 solver_ba_snc(const qn::NetworkStruct< T > &L, Solution &out)
void solver_ba_bgt(const qn::NetworkStruct< T > &L, Solution &out)
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.
void solver_ba_bpt(const qn::NetworkStruct< T > &L, Solution &out)
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
MapqnBndLrMvaResult< T > mapqn_bnd_lr_mva(const LrMvaParams< T > &p, int objective_queue, int objective_level, MapqnSense sense=MapqnSense::Max, MapqnObjectiveVar objective_var=MapqnObjectiveVar::UN)
Bound UN or QN at (objective_queue, objective_level) over the MVA-shaped LR polytope.
MapqnSense
Which direction the bound is taken in.
LrPfResult< T > mapqn_bnd_lr_pf(const LrPfParams< T > &p, int objective_queue, MapqnSense sense)
Port of mapqn_bnd_lr_pf.
ClassResults< T > sn_deaggregate_chain_results(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, const Matrix< T > &Qchain, const Matrix< T > &Uchain, const Matrix< T > &Rchain, const Matrix< T > &Tchain, const std::vector< T > &Xchain)
Port of sn_deaggregate_chain_results.
Definition sn_chain.h:227
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
ScbBounds< T > pfqn_scb(const std::vector< T > &L, long N)
Bracket on the throughput and the per-device utilizations of the UNKNOWN multiclass system whose sing...
Definition pfqn_scb.h:67
McubBounds< T > pfqn_mcub(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Kerola's multiclass composite bound (Perf.
Definition pfqn_mcub.h:55
MwrbbSched
Station discipline codes, matching the MATLAB sched argument.
Definition pfqn_mwrbb.h:48
T pfqn_xzgsbup(const std::vector< T > &L, const T &N, const T &Z)
X = 2N / (R + sqrt(R^2 - 4 Z Lmax N)), R from the geometric queue bound.
SibBounds< T > pfqn_sib(const std::vector< T > &L, int N, const T &Z, int level)
Successively Improving Bounds (Srinivasan 1985/1987) on the cycle time and throughput of a single-cla...
Definition pfqn_sib.h:84
HarelBoundsResult< T > pfqn_harel_bounds(const std::vector< T > &rho, int N, const T &Z, int maxUB)
Both bounds, plus the exact throughputs the upper bounds extrapolate from.
T pfqn_qzgbup(const std::vector< T > &L, const T &N, const T &Z, std::size_t i)
As the lower bound, with Y from the ABA upper bound and the sigma term.
Definition pfqn_qzgbup.h:34
PbhBounds< T > pfqn_bjbk(const std::vector< T > &L, int N, const T &Z, int k)
BJB(k), the iterative Balanced Job Bound.
Definition pfqn_pbh.h:173
MwrbbBounds< T > pfqn_mwrbb(const Matrix< T > &V, const Matrix< T > &S, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< MwrbbSched > &sched, const std::vector< int > &prio)
Majumdar-Woodside robust box bounds on the per-class throughput of a closed multiclass network with m...
Definition pfqn_mwrbb.h:161
PbhBounds< T > pfqn_pbh(const std::vector< T > &L, int N, const T &Z, int level)
Performance Bound Hierarchy (Eager and Sevcik 1983, ACM TOCS 1(2):99-115) for single-class closed pro...
Definition pfqn_pbh.h:114
LdBcmpBound< T > pfqn_ldbcmp(const std::vector< T > &L, const T &N, const T &Z, const std::vector< T > &c, const T &tol)
Anselmi-Cremonesi (2008) lower throughput bound for a closed single-class BCMP network with load-depe...
Definition pfqn_ldbcmp.h:64
LoopingBounds< T > pfqn_looping(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, double tol=1e-6, std::size_t maxiter=1000)
Eager Looping bounds for closed multiclass product-form networks.
T pfqn_qzgblow(const std::vector< T > &L, const T &N, const T &Z, std::size_t i)
Qgb = y/(1-y) - y^(N+1)/(1-y) with y = N L_i / (Z + sum(L) + Lmax N).
CbhBounds< T > pfqn_cbh(const std::vector< T > &L, int N, const T &Z, int level)
Convolutional Bound Hierarchy (Dowdy, Eager, Gordon and Saxton 1984) on the throughput of a single-cl...
Definition pfqn_cbh.h:118
T pfqn_xzgsblow(const std::vector< T > &L, const T &N, const T &Z)
X = 2N / (R + sqrt(R^2 - 4 Z Lmax (N-1))), R from the geometric queue bound.
SsdBounds< T > pfqn_ssd(const std::vector< T > &L, const T &N, const T &Z, const std::vector< T > &nservers)
Server-Station Disaggregation bounds for a multiserver closed network (Dallery and Suri,...
Definition pfqn_ssd.h:55
std::string ba_family_of(const std::string &method)
The family prefix of a bound method name: everything before the first dot.
std::string ba_resolve_method_name(const std::string &method)
The 'default'/'auto'/'qr'/'lr' aliases, duplicated from ba::resolve_method for the same reason ba_fam...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:204
A queueing network and its refreshed NetworkStruct.
Convolutional Bound Hierarchy (Dowdy, Eager, Gordon and Saxton 1984) on the throughput of a single-cl...
Harel-Namn-Sturm throughput bounds for a single-class closed network.
Anselmi-Cremonesi (2008) lower throughput bound for a closed single-class BCMP network with load-depe...
Eager Looping bounds for closed multiclass product-form networks.
Kerola's multiclass composite bound (Perf.
Majumdar-Woodside robust box bounds on the per-class throughput of a closed multiclass network with m...
Performance Bound Hierarchy (Eager and Sevcik 1983, ACM TOCS 1(2):99-115) for single-class closed pro...
Geometric-bound lower bound on the queue length at station i.
Geometric-bound upper bound on the queue length at station i.
Dowdy-Carlson-Krantz-Tripathi (1992) single-class bounds of multi-class queueing networks,...
Successively Improving Bounds (Srinivasan 1985/1987) on the cycle time and throughput of a single-cla...
Server-Station Disaggregation bounds for a multiserver closed network (Dallery and Suri,...
Geometric-square-root bound, lower bound on throughput.
Geometric-square-root bound, upper bound on throughput.
Chain aggregation and de-aggregation.
Piecewise-linear Lyapunov UPPER bound on the steady-state queue lengths of a multitype open Markovian...
Achievable-region LOWER bound on the mean response times of a multiclass open Markovian network,...
Stochastic network calculus UPPER bound on the mean response times and queue lengths of a feed-forwar...
The DECLARED side of the gate: one feature set per solver.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
options.config.qrf_params, the blocking tables the BAS and RS-RD arms need.
std::vector< std::vector< int > > MM1
(MR x M) extended order
std::vector< std::vector< int > > MM
(MR x 2) blocking order
std::vector< std::vector< int > > BB
(MR x M) blocking state
int MR
number of blocking configurations
int f
finite-capacity queue, 1-based as in the reference
std::vector< int > ZZ
(MR) blocked count per config
std::vector< int > F
(M) capacity; empty takes sn.cap
The options SolverBA reads.
int level
options.level: the hierarchy level of pbh/bjbh/cbh/sib.
std::string method
Bound method; default resolves to gb.upper in the runner.
Matrix< double > qrf_alpha
options.config.qrf_alpha, the (nstations x N) load-dependent scaling of the two load-dependent QRF ar...
Class-level results, the [Q,U,R,T,C,X] of solver_ba_analyzer.
double lG
-N log X, the reference's approximate normalizing constant.
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
Parameters of the MVA-shaped LR bound, mirroring the reference's params.
Matrix< T > r
(M x M) routing probabilities
Matrix< T > muMAP
(K x K) completion rates of the MAP queue
int K
number of levels of the MAP queue
int M
number of queues, the last of which is the MAP
Matrix< T > v
(K x K) level-change rates of the MAP queue
std::vector< T > muM
(M-1) service rates of the exponential queues
int N
closed population
Product-form parameters of the LR bound, mirroring MATLAB's params.
int N
closed population
Matrix< T > r
(M x M) routing probabilities
std::vector< T > mu
(M) service rates
Return value of mapqn_bnd_lr_pf, mirroring the MATLAB result struct.
T objective
the bounded utilization of the objective station
std::string status
simplex status name
Result of an MVA-shaped LR bound solve.
T objective
the bound on UN(objective_queue, objective_level)
std::string status
textual LP status
bool ok
the LP reached an optimal vertex
Class-level results, as sn_deaggregate_chain_results returns them.
Definition sn_chain.h:204
std::vector< T > C
Definition sn_chain.h:206
std::vector< T > X
Definition sn_chain.h:206
Return value of pfqn_cbh, mirroring [Xlo, Xhi].
Definition pfqn_cbh.h:41
Return value of pfqn_harel_bounds, mirroring Ret.pfqnHarelBounds.
std::vector< T > UB
UB[n] for n = 2..maxUB; entries 0 and 1 are unset.
T LB
throughput lower bound at population N
std::vector< T > TH
TH[n] = exact throughput at population n, n = 1..maxUB.
Return value of pfqn_ldbcmp, mirroring [Xlo, Rhi, Qhat].
Definition pfqn_ldbcmp.h:48
bool applicable
false where MATLAB returns NaN (N < Qhat, or rho >= 1)
Definition pfqn_ldbcmp.h:52
Return value of pfqn_looping: the throughput bracket plus its queue lengths.
std::vector< T > Xup
(R) optimistic (upper) throughput bound
std::vector< T > Xlo
(R) pessimistic (lower) throughput bound
Return value of pfqn_mcub, mirroring [Xub, Xlb].
Definition pfqn_mcub.h:42
std::vector< T > Xlb
Definition pfqn_mcub.h:44
std::vector< T > Xub
Definition pfqn_mcub.h:43
Return value of pfqn_mwrbb, mirroring [Xlo, Xup, Wlo].
Definition pfqn_mwrbb.h:52
std::vector< T > Xlo
Definition pfqn_mwrbb.h:53
std::vector< T > Xup
Definition pfqn_mwrbb.h:54
Return value of pfqn_pbh, mirroring [Xlo, Xhi, Qlo, Qhi].
Definition pfqn_pbh.h:43
Return value of pfqn_scb, mirroring [Xlo, Xhi, Ulo, Uhi].
Definition pfqn_scb.h:42
Return value of pfqn_sib, mirroring [Xlo, Xhi, Wlo, Whi].
Definition pfqn_sib.h:67