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/cbh/sib and the iteration
75 * count k of pbk/bjbk. MATLAB's 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/ldbcmp/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("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 == "cbh" ||
341 fam == "pbk" || fam == "bjbk" || fam == "ssd" || fam == "sib" || fam == "scb" ||
342 fam == "ldbcmp" || fam == "mapamva";
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 == "mwba" || 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 * 'ldbcmp' 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 == "cbh" || fam == "pbk" ||
362 fam == "bjbk" || 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 'ldbcmp.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) != "ldbcmp.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 'ldbcmp.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 'ldbcmp.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 'ldbcmp.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 `mwba` 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("solver_ba_analyzer: " + refusal);
557 }
558
559 // AUTO composite: evaluate every noniterative bound and keep the tightest
560 // side. Feasibility is probed by execution -- a candidate that rejects the
561 // model (multiserver, delay station, regime gate) throws and is skipped --
562 // so the list stays correct as families are added.
563 if (method == "auto.upper" || method == "auto.lower") {
564 const bool up = method == "auto.upper";
565 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
566 static const char* const cand_up[] = {"aba.upper", "bjb.upper", "pb.upper", "gb.upper",
567 "sb.upper", "mwba.upper", "ssd.upper", "cub.upper"};
568 static const char* const cand_lo[] = {"aba.lower", "bjb.lower", "pb.lower",
569 "gb.lower", "sb.lower", "mwba.lower",
570 "ssd.lower", "mbjb.lower", "ldbcmp.lower"};
571 const char* const* cand = up ? cand_up : cand_lo;
572 const std::size_t ncand = up ? sizeof(cand_up) / sizeof(*cand_up)
573 : sizeof(cand_lo) / sizeof(*cand_lo);
574 bool have = false;
575 T Xbest = zero;
576 for (std::size_t ci = 0; ci < ncand; ++ci) {
577 BaOptions oc = opt;
578 oc.method = cand[ci];
580 try {
581 r = solver_ba_analyzer(L, oc);
582 } catch (const std::exception&) {
583 continue;
584 }
585 if (r.X.empty()) continue;
586 const T Xc = r.X[0];
587 if (!std::isfinite(num_traits<T>::to_double(Xc)) || !(Xc > zero)) continue;
588 if (!have || (up && Xc < Xbest) || (!up && Xc > Xbest)) {
589 Xbest = Xc;
590 have = true;
591 }
592 }
593 if (!have)
594 throw UnsupportedError("solver_ba_analyzer: method '" + method +
595 "' found no feasible bound for this model");
596 return detail::ba_fill(L, sc.V, sc.N, Xbest, sc.Z, sc.D, up);
597 }
598
599 // The noniterative single-class families (aba, bjb, pb, sb, gb) share the
600 // demand extraction; the reference inlines it in every branch.
601 const bool single_class_family =
602 method == "aba.upper" || method == "aba.lower" || method == "bjb.upper" ||
603 method == "bjb.lower" || method == "pb.upper" || method == "pb.lower" ||
604 method == "sb.upper" || method == "sb.lower" || method == "gb.upper" ||
605 method == "gb.lower";
606
607 if (single_class_family) {
608 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
609 const std::vector<T>& V = sc.V;
610 const std::vector<T>& D = sc.D;
611 const T& Z = sc.Z;
612 const long N = sc.N;
613 const T Nt = num_traits<T>::from_int(N);
614 const detail::DemandStats<T> st = detail::ba_demand_stats(D);
615
616 if (method == "aba.upper") {
617 const T X = detail::ba_min(T(one / st.max), T(Nt / T(Z + st.sum)));
618 return detail::ba_fill_xc(L, V, N, X, T(Z + Nt * st.sum), true);
619 }
620 if (method == "aba.lower") {
621 const T X = T(Nt / T(Z + Nt * st.sum));
622 return detail::ba_fill_xc(L, V, N, X, T(Z + st.sum), false);
623 }
624 // The balanced and proportional families evaluate the ABA bracket at
625 // N-1: both are one exact MVA step taken from a bounded arrival-theorem
626 // queue length, so the population they see is the one a job arriving to
627 // the system leaves behind.
628 const T Nm1 = num_traits<T>::from_int(N - 1);
629 const T xup1 = detail::ba_min(T(one / st.max), T(Nm1 / T(Z + st.sum)));
630 const T xlo1 = T(Nm1 / T(Z + Nm1 * st.sum));
631 if (method == "bjb.upper") {
632 const T C = T(Z + st.sum + st.max * T(Nm1 - Z * xlo1));
633 const T X = detail::ba_min(
634 T(one / st.max), T(Nt / T(Z + st.sum + st.mean * T(Nm1 - Z * xup1))));
635 return detail::ba_fill_xc(L, V, N, X, C, true);
636 }
637 if (method == "bjb.lower") {
638 const T C = T(Z + st.sum + st.mean * T(Nm1 - Z * xup1));
639 const T X = T(Nt / T(Z + st.sum + st.max * T(Nm1 - Z * xlo1)));
640 return detail::ba_fill_xc(L, V, N, X, C, false);
641 }
642 if (method == "pb.upper" || method == "pb.lower") {
643 T d2 = zero, dN = zero, dNm1 = zero;
644 for (const T& d : D) {
645 d2 += T(d * d);
646 dN += num_pow_int(d, static_cast<unsigned>(N));
647 dNm1 += num_pow_int(d, static_cast<unsigned>(N - 1));
648 }
649 const T Dpb2 = T(d2 / st.sum);
650 const T DpbN = T(dN / dNm1);
651 if (method == "pb.upper") {
652 const T C = T(Z + st.sum + DpbN * T(Nm1 - Z * xlo1));
653 const T X = detail::ba_min(
654 T(one / st.max), T(Nt / T(Z + st.sum + Dpb2 * T(Nm1 - Z * xup1))));
655 return detail::ba_fill_xc(L, V, N, X, C, true);
656 }
657 const T C = T(Z + st.sum + Dpb2 * T(Nm1 - Z * xup1));
658 const T X = T(Nt / T(Z + st.sum + DpbN * T(Nm1 - Z * xlo1)));
659 return detail::ba_fill_xc(L, V, N, X, C, false);
660 }
661 if (method == "sb.upper" || method == "sb.lower") {
662 // The power sums of Harel-Namn-Sturm are written for a network with
663 // no terminal population, and the reference refuses a delay station
664 // rather than drop its think time.
665 for (std::size_t i = 0; i < L.nstations; ++i)
666 if (detail::ba_is_delay(L, i))
667 throw UnsupportedError("solver_ba_analyzer: method '" + method +
668 "' does not support infinite-server stations");
669 T A1 = zero, A2 = zero, A3 = zero;
670 for (const T& d : D) {
671 A1 += d;
672 A2 += T(d * d);
673 A3 += T(d * d * d);
674 }
675 if (method == "sb.upper") {
676 const T C = T(Z + A1 + Nm1 * T(T(A1 * A2 + A3) / T(A1 * A1 + A2)));
677 const T X = detail::ba_min(T(one / st.max), T(Nt / C));
678 // The power-sum family leaves the per-station response time
679 // undefined, and the reference pairs BOTH sides with the
680 // no-contention residence rather than only the lower one.
681 return detail::ba_fill_xc(L, V, N, X, C, false);
682 }
683 // The (N-1)-st root of the N-th power sum has no field expression.
684 if constexpr (!num_traits<T>::has_transcendental) {
685 throw UnsupportedError(
686 "solver_ba_analyzer: method 'sb.lower' needs an (N-1)-st root and is "
687 "unavailable in exact arithmetic");
688 } else {
689 using std::pow;
690 T AN = zero;
691 for (const T& d : D) AN += num_pow_int(d, static_cast<unsigned>(N));
692 // pow is taken on T, not through double: a high-precision
693 // backend would otherwise lose every digit past the 17th here.
694 const T root = pow(T(AN / A1), T(one / Nm1));
695 const T C = T(Z + A1 + Nm1 * root);
696 return detail::ba_fill_xc(L, V, N, T(Nt / C), C, false);
697 }
698 }
699 // gb: the geometric bounds solve a quadratic in the throughput.
700 if constexpr (!num_traits<T>::has_transcendental) {
701 throw UnsupportedError("solver_ba_analyzer: the geometric bounds '" + method +
702 "' solve a quadratic and are unavailable in exact arithmetic");
703 } else {
704 const bool up = (method == "gb.upper");
705 const T Xup = pfqn::pfqn_xzgsbup(D, Nt, Z);
706 const T Xlo = pfqn::pfqn_xzgsblow(D, Nt, Z);
707 const T X = up ? detail::ba_min(T(one / st.max), Xup) : Xlo;
708 const T C = T(Nt / (up ? Xlo : Xup));
709 // The queue-length bound is the geometric one, not the residence
710 // convention of ba_fill; the response time follows from it by the
711 // OPPOSITE side of the throughput bracket, which is what keeps
712 // Q = X R consistent with a bound rather than with a point estimate.
713 const T Xden = up ? Xlo : Xup;
715 s.Q = Matrix<T>(L.nstations, 1, zero);
716 s.U = Matrix<T>(L.nstations, 1, zero);
717 s.R = Matrix<T>(L.nstations, 1, zero);
718 s.Tp = Matrix<T>(L.nstations, 1, zero);
719 std::size_t k = 0;
720 for (std::size_t i = 0; i < L.nstations; ++i) {
721 s.Tp(i, 0) = T(V[i] * X);
722 if (detail::ba_is_delay(L, i)) {
723 s.R(i, 0) = T(one / L.rates(i, 0));
724 s.Q(i, 0) = T(X * s.R(i, 0));
725 } else {
726 s.Q(i, 0) = up ? pfqn::pfqn_qzgbup(D, Nt, Z, k) : pfqn::pfqn_qzgblow(D, Nt, Z, k);
727 s.R(i, 0) = T(T(s.Q(i, 0) / Xden) / V[i]);
728 ++k;
729 }
730 s.U(i, 0) = detail::ba_is_delay(L, i) ? s.Q(i, 0) : T(s.Tp(i, 0) / L.rates(i, 0));
731 }
732 s.C.assign(1, C);
733 s.X.assign(1, X);
734 s.lG = -static_cast<double>(N) * num_traits<T>::log_as_double(X);
735 return s;
736 }
737 }
738
739 if (method == "lr.upper" || method == "lr.lower") {
740 // LP linear-reduction bound: one LP per station, each minimizing or
741 // maximizing that station's utilization over a polytope that CONTAINS
742 // the exact stationary solution.
743 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
744 for (std::size_t i = 0; i < L.nstations; ++i)
745 if (detail::ba_is_delay(L, i))
746 throw UnsupportedError("solver_ba_analyzer: method '" + method +
747 "' does not support delay (infinite-server) stations");
748 const bool up = (method == "lr.upper");
749 const std::size_t M = L.nstations;
750 const std::vector<T>& V = sc.V;
751
753 par.M = static_cast<int>(M);
754 // MATLAB reads sn.njobs(1), the class population, which for the
755 // single-class model this branch admits is nclosedjobs.
756 par.N = static_cast<int>(sc.N);
757 par.mu.resize(M);
758 T totV = zero;
759 for (std::size_t i = 0; i < M; ++i) {
760 par.mu[i] = L.rates(i, 0);
761 totV += V[i];
762 }
763 // Every row of r is the visit vector normalized by its total: the
764 // reference builds it with repmat, so a job leaves any station for j
765 // with the same probability V_j / sum V.
766 par.r = Matrix<T>(M, M, zero);
767 if (totV == zero) throw NumericError("solver_ba_analyzer: the visit ratios are all zero");
768 for (std::size_t i = 0; i < M; ++i)
769 for (std::size_t j = 0; j < M; ++j) par.r(i, j) = T(V[j] / totV);
770
772 std::vector<T> U(M, zero);
773 for (std::size_t ti = 0; ti < M; ++ti) {
774 const mapqn::LrPfResult<T> r =
775 mapqn::mapqn_bnd_lr_pf(par, static_cast<int>(ti) + 1, sense);
776 if (!r.ok)
777 throw NumericError("solver_ba_analyzer: the '" + method +
778 "' linear program did not solve (" + r.status + ")");
779 U[ti] = r.objective;
780 }
781 // The chain throughput implied by the bounded utilizations, U_i = X V_i / mu_i.
782 // MATLAB drops the non-finite candidates (a station with no visit) and
783 // takes the minimum, or zero when none survives.
784 bool any = false;
785 T X = zero;
786 for (std::size_t i = 0; i < M; ++i) {
787 if (V[i] == zero) continue; // MATLAB: the candidate is Inf and isfinite drops it
788 const T cand = T(U[i] * L.rates(i, 0) / V[i]);
789 if (!any || cand < X) {
790 X = cand;
791 any = true;
792 }
793 }
794 if (!any) X = zero;
795 // Q, R, T and C follow the ABA residence convention of ba_fill at this
796 // side, with no think time since a delay station is refused above; only
797 // U comes from the LP rather than from the utilization law.
798 BaSolution<T> s = detail::ba_fill(L, V, sc.N, X, zero, sc.D, up);
799 for (std::size_t i = 0; i < M; ++i) s.U(i, 0) = U[i];
800 s.lG = std::numeric_limits<double>::quiet_NaN(); // the reference leaves it unset
801 return s;
802 }
803
804 if (method == "mapamva.upper" || method == "mapamva.lower") {
805 // MAP-AMVA (Casale-Smirni, DSN 2009): the LP over the EXACT mean-value
806 // balance equations of a closed MAP queueing network. It is the only
807 // family here that consumes the CORRELATION between successive services
808 // rather than the service mean alone -- its variables are the per-phase
809 // queue lengths QN(i,k) and utilizations UN(i,k), so a workload whose
810 // burstiness moves the bottleneck between stations is bounded rather
811 // than averaged into a renewal process. That is why 'MAP' and 'MMPP2'
812 // reach this family's feature set and no other.
813 //
814 // The LP carries phases at ONE queue and requires it to be the LAST --
815 // q(i,j,k,h) reads the scalar muM(i) for i < M and the (D0,D1) pair
816 // muMAP/v for i == M -- so a model whose phase-carrying station sits
817 // elsewhere is PERMUTED rather than refused, and the results are
818 // permuted back before they are returned.
819 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
820 for (std::size_t i = 0; i < L.nstations; ++i)
821 if (detail::ba_is_delay(L, i))
822 throw UnsupportedError(
823 "solver_ba_analyzer: method '" + method +
824 "' does not support delay (infinite-server) stations: the MAP-AMVA program "
825 "of Casale-Smirni (DSN 2009) is written for a network of queues and the "
826 "paper names the delay extension as open work. Use a QRF method, which "
827 "carries the load-dependent rate law");
828 const bool up = (method == "mapamva.upper");
829 const std::size_t M = L.nstations;
830 const std::vector<T>& V = sc.V;
831
832 // Phase order per station. One phase is an exponential server, which
833 // enters the LP as the scalar rate muM(i); more than one is the (D0,D1)
834 // pair, which only queue M can hold.
835 std::vector<std::pair<Matrix<T>, Matrix<T>>> MAPs(M);
836 std::vector<int> kph(M, 1);
837 for (std::size_t i = 0; i < M; ++i) {
838 const mam::Map<T> m = lang::dist_to_map(L.service[i][0]);
839 if (m.D0.rows() == 0) {
840 MAPs[i] = std::make_pair(Matrix<T>(1, 1, num_traits<T>::from_int(-1)),
842 kph[i] = 1;
843 } else {
844 MAPs[i] = std::make_pair(m.D0, m.D1);
845 kph[i] = static_cast<int>(m.D0.rows());
846 }
847 }
848 std::vector<std::size_t> phased;
849 for (std::size_t i = 0; i < M; ++i)
850 if (kph[i] > 1) phased.push_back(i);
851 if (phased.size() > 1) {
852 std::string names;
853 for (std::size_t j = 0; j < phased.size(); ++j)
854 names += (j ? ", " : "") + std::to_string(phased[j] + 1);
855 throw UnsupportedError(
856 "solver_ba_analyzer: method '" + method +
857 "' carries phases at ONE station: the LP gives queue M the (D0,D1) pair and "
858 "every other queue a scalar rate. Stations " + names + " are all "
859 "non-exponential. Use a QRF method, whose q carries a phase at every station");
860 }
861 // Every station exponential: the program is still the right one, it just
862 // degenerates to K = 1, where the per-phase variables collapse and the
863 // balances become the product-form ones of mapqn_bnd_lr_pf.
864 const std::size_t map_idx = phased.empty() ? M - 1 : phased.front();
865 std::vector<std::size_t> perm;
866 perm.reserve(M);
867 for (std::size_t i = 0; i < M; ++i)
868 if (i != map_idx) perm.push_back(i);
869 perm.push_back(map_idx);
870 const int K = kph[map_idx];
871 const std::size_t Ks = static_cast<std::size_t>(K);
872
874 par.M = static_cast<int>(M);
875 par.N = static_cast<int>(sc.N);
876 par.K = K;
877 par.muM.resize(M - 1);
878 for (std::size_t a = 0; a + 1 < M; ++a) par.muM[a] = L.rates(perm[a], 0);
879 // muMAP(k,h) is the completion rate out of phase k landing in phase h,
880 // i.e. D1(k,h); v(k,h) is the background phase change that completes no
881 // job, i.e. D0 off the diagonal. Same (from,to) convention as
882 // qrf_extract_mu_v -- writing either as its transpose is invisible for a
883 // reversible D0 and silently reverses the phase order of an Erlang.
884 par.muMAP = Matrix<T>(Ks, Ks, zero);
885 par.v = Matrix<T>(Ks, Ks, zero);
886 for (std::size_t a = 0; a < Ks; ++a)
887 for (std::size_t b = 0; b < Ks; ++b) {
888 par.muMAP(a, b) = MAPs[map_idx].second(a, b);
889 par.v(a, b) = (a == b) ? zero : MAPs[map_idx].first(a, b);
890 }
891 par.r = Matrix<T>(M, M, zero);
892 for (std::size_t a = 0; a < M; ++a)
893 for (std::size_t b = 0; b < M; ++b) par.r(a, b) = L.rt(perm[a], perm[b]);
894
895 std::vector<T> Vp(M, zero), Sp(M, zero);
896 for (std::size_t a = 0; a < M; ++a) {
897 const std::size_t i = perm[a];
898 Vp[a] = V[i];
899 mam::Map<T> mi;
900 mi.D0 = MAPs[i].first;
901 mi.D1 = MAPs[i].second;
902 Sp[a] = mam::map_mean(mi);
903 }
904
905 // THREE SWEEPS OF THE SAME LP, and the utilization one runs in BOTH
906 // senses on purpose. R_i = Q_i/(V_i*X) rises with Q_i and FALLS with X,
907 // so an upper bound on the response time pairs Q_i^max with X^min;
908 // dividing by X^max on both sides is what would report an upper R below
909 // the exact value and break the bracket.
911 std::vector<T> umax(M, zero), umin(M, zero), qbnd(M, zero);
912 for (std::size_t a = 0; a < M; ++a) {
913 const int ti = static_cast<int>(a);
922 if (!ru.ok || !rl.ok || !rq.ok)
923 throw NumericError("solver_ba_analyzer: the '" + method +
924 "' linear program did not solve (" +
925 (ru.ok ? (rl.ok ? rq.status : rl.status) : ru.status) + ")");
926 umax[a] = ru.objective;
927 umin[a] = rl.objective;
928 qbnd[a] = rq.objective;
929 }
930
931 // Utilization law U_i = X*V_i*S_i, exact at a single server under ANY
932 // service law, so each station turns its own utilization bound into a
933 // throughput bound and the tightest of the M survives. A station with no
934 // visits or no service time carries no information and is skipped.
935 bool any = false;
936 T x_up = zero, x_lo = zero;
937 for (std::size_t a = 0; a < M; ++a) {
938 const T load = T(Vp[a] * Sp[a]);
939 if (load <= zero) continue;
940 const T cu = T(umax[a] / load), cl = T(umin[a] / load);
941 if (!any) {
942 x_up = cu;
943 x_lo = cl;
944 any = true;
945 } else {
946 if (cu < x_up) x_up = cu;
947 if (cl > x_lo) x_lo = cl;
948 }
949 }
950 if (!any)
951 throw NumericError("solver_ba_analyzer: method '" + method +
952 "' found no station with both a positive visit ratio and a "
953 "positive mean service time");
954 const T xb = up ? x_up : x_lo;
955 const T xopp = up ? x_lo : x_up;
956
957 // Unpermute: the LP orders the stations with the phase-carrying one last.
959 s.Q = Matrix<T>(M, 1, zero);
960 s.U = Matrix<T>(M, 1, zero);
961 s.R = Matrix<T>(M, 1, zero);
962 s.Tp = Matrix<T>(M, 1, zero);
963 for (std::size_t a = 0; a < M; ++a) {
964 const std::size_t i = perm[a];
965 s.U(i, 0) = up ? umax[a] : umin[a];
966 s.Q(i, 0) = qbnd[a];
967 s.Tp(i, 0) = T(Vp[a] * xb);
968 s.R(i, 0) = (xopp > zero && Vp[a] > zero) ? T(qbnd[a] / (Vp[a] * xopp)) : zero;
969 }
970 // Delay stations are refused above, so the closed-network response time
971 // is N/X exactly and the throughput bracket transfers to it directly.
972 // Summing the per-station R bounds instead would add M separately
973 // attained maxima and report a looser number.
974 s.C.assign(1, xopp > zero
975 ? T(num_traits<T>::from_int(static_cast<long>(sc.N)) / xopp)
976 : zero);
977 s.X.assign(1, xb);
978 s.lG = -static_cast<double>(sc.N) * num_traits<T>::log_as_double(xb);
979 return s;
980 }
981
982 if (method == "mwba.upper" || method == "mwba.lower") {
983 // Majumdar-Woodside robust box bounds: multiclass, and the only family
984 // that reads the scheduling discipline, since its lower bound is a
985 // per-discipline residence guarantee.
986 const detail::ChainView<T> v = detail::ba_chain_view(L);
987 const std::size_t M = L.nstations, C = L.nchains, Kq = v.qstat.size();
988 Matrix<T> Vq(Kq, C, zero), Sq(Kq, C, zero);
989 std::vector<pfqn::MwrbbSched> schedq(Kq);
990 for (std::size_t k = 0; k < Kq; ++k) {
991 const std::size_t i = v.qstat[k];
992 schedq[k] = detail::mwrbb_disc_code(L.stations[i].sched);
993 for (std::size_t c = 0; c < C; ++c) {
994 Vq(k, c) = v.d.Vchain(i, c);
995 Sq(k, c) = v.d.STchain(i, c);
996 }
997 }
998 // Chain priority: the reference class's when the chain has one, else the
999 // highest priority (lowest value) any class of the chain carries.
1000 std::vector<int> prioc(C, 0);
1001 for (std::size_t c = 0; c < C; ++c) {
1002 if (L.refclass[c] > 0) {
1003 prioc[c] = L.classes[L.refclass[c] - 1].prio;
1004 } else {
1005 int p = L.classes[L.inchain[c][0] - 1].prio;
1006 for (std::size_t r : L.inchain[c]) p = std::min(p, L.classes[r - 1].prio);
1007 prioc[c] = p;
1008 }
1009 }
1010 const pfqn::MwrbbBounds<T> b = pfqn::pfqn_mwrbb(Vq, Sq, v.Nv, v.Zc, schedq, prioc);
1011 const bool up = (method == "mwba.upper");
1012 const std::vector<T>& Xchain = up ? b.Xup : b.Xlo;
1013 Matrix<T> Tchain(M, C, zero), Uchain(M, C, zero), Qchain(M, C, zero);
1014 for (std::size_t c = 0; c < C; ++c)
1015 for (std::size_t i = 0; i < M; ++i) {
1016 Tchain(i, c) = T(Xchain[c] * v.d.Vchain(i, c));
1017 Uchain(i, c) = T(Xchain[c] * v.d.Lchain(i, c)); // utilization law
1018 }
1019 Qchain = detail::ba_chain_qfill(Uchain, Xchain, v.d.Lchain, v.Nv, v.isdelay, up);
1021 L, v.d, Qchain, Uchain, Matrix<T>(), Tchain, Xchain);
1022 BaSolution<T> s;
1023 s.Q = cr.Q;
1024 s.U = cr.U;
1025 s.R = cr.R;
1026 s.Tp = cr.Tp;
1027 s.C = cr.C;
1028 s.X = cr.X;
1029 s.lG = std::numeric_limits<double>::quiet_NaN();
1030 return s;
1031 }
1032
1033 // Eager Looping: the multiclass bracket that initializes the multiple-class
1034 // PBH. Pessimistic side from the heap-inflated response time, optimistic
1035 // side from the response-time lower bound.
1036 if (method == "looping.upper" || method == "looping.lower") {
1037 const detail::ChainView<T> v = detail::ba_chain_view(L);
1038 const std::size_t M = L.nstations, C = L.nchains, Kq = v.qstat.size();
1039 Matrix<T> Lq(Kq, C, zero);
1040 for (std::size_t k = 0; k < Kq; ++k)
1041 for (std::size_t c = 0; c < C; ++c) Lq(k, c) = v.d.Lchain(v.qstat[k], c);
1042 const pfqn::LoopingBounds<T> b = pfqn::pfqn_looping(Lq, v.Nv, v.Zc);
1043 const bool up = (method == "looping.upper");
1044 const std::vector<T>& Xchain = up ? b.Xup : b.Xlo;
1045 Matrix<T> Tchain(M, C, zero), Uchain(M, C, zero), Qchain(M, C, zero);
1046 for (std::size_t c = 0; c < C; ++c)
1047 for (std::size_t i = 0; i < M; ++i) {
1048 Tchain(i, c) = T(Xchain[c] * v.d.Vchain(i, c));
1049 Uchain(i, c) = T(Xchain[c] * v.d.Lchain(i, c)); // utilization law
1050 }
1051 Qchain = detail::ba_chain_qfill(Uchain, Xchain, v.d.Lchain, v.Nv, v.isdelay, up);
1053 L, v.d, Qchain, Uchain, Matrix<T>(), Tchain, Xchain);
1054 BaSolution<T> s;
1055 s.Q = cr.Q;
1056 s.U = cr.U;
1057 s.R = cr.R;
1058 s.Tp = cr.Tp;
1059 s.C = cr.C;
1060 s.X = cr.X;
1061 s.lG = std::numeric_limits<double>::quiet_NaN();
1062 return s;
1063 }
1064
1065 // Achievable-region LP relaxation (Bertsimas-Paschalidis-Tsitsiklis 1994).
1066 // The only OPEN-network family here: it lower bounds the mean response
1067 // times attainable by ANY non-idling policy, so it is refused on the closed
1068 // models every other family requires.
1069 if (method == "bpt.lower") {
1070 BaSolution<T> s;
1071 solver_ba_bpt(L, s);
1072 return s;
1073 }
1074
1075 // Piecewise-linear Lyapunov bound (Bertsimas-Gamarnik-Tsitsiklis 2001), the
1076 // OPEN-network upper side. A feasible gamma > 0 both certifies that every
1077 // work-conserving policy is stable and yields the (loose) queue-length bound.
1078 if (method == "bgt.upper") {
1079 BaSolution<T> s;
1080 solver_ba_bgt(L, s);
1081 return s;
1082 }
1083
1084 // Stochastic network calculus (Fidler-Rizk 2015), the third OPEN-network
1085 // family and the only one whose native object is a TAIL: MGF envelopes are
1086 // propagated hop by hop and the delay bound is integrated into a mean.
1087 if (method == "snc.upper") {
1088 BaSolution<T> s;
1089 solver_ba_snc(L, s);
1090 return s;
1091 }
1092
1093 if (method == "cub.upper" || method == "mbjb.lower") {
1094 const detail::ChainView<T> v = detail::ba_chain_view(L);
1095 const std::size_t M = L.nstations, C = L.nchains, Kq = v.qstat.size();
1096 Matrix<T> Lq(Kq, C, zero);
1097 for (std::size_t k = 0; k < Kq; ++k)
1098 for (std::size_t c = 0; c < C; ++c) Lq(k, c) = v.d.Lchain(v.qstat[k], c);
1099 const pfqn::McubBounds<T> b = pfqn::pfqn_mcub(Lq, v.Nv, v.Zc);
1100 const bool up = (method == "cub.upper");
1101 const std::vector<T>& Xchain = up ? b.Xub : b.Xlb;
1102 Matrix<T> Tchain(M, C, zero), Uchain(M, C, zero), Qchain(M, C, zero);
1103 for (std::size_t c = 0; c < C; ++c)
1104 for (std::size_t i = 0; i < M; ++i) {
1105 Tchain(i, c) = T(Xchain[c] * v.d.Vchain(i, c));
1106 Uchain(i, c) = T(Xchain[c] * v.d.Lchain(i, c)); // utilization law
1107 }
1108 Qchain = detail::ba_chain_qfill(Uchain, Xchain, v.d.Lchain, v.Nv, v.isdelay, up);
1110 L, v.d, Qchain, Uchain, Matrix<T>(), Tchain, Xchain);
1111 BaSolution<T> s;
1112 s.Q = cr.Q;
1113 s.U = cr.U;
1114 s.R = cr.R;
1115 s.Tp = cr.Tp;
1116 s.C = cr.C;
1117 s.X = cr.X;
1118 s.lG = std::numeric_limits<double>::quiet_NaN();
1119 return s;
1120 }
1121
1122 // Sharp bounds of Harel-Namn-Sturm, distinct from the 'sb' family of the
1123 // same paper: they extrapolate from the EXACT normalizing constant at
1124 // populations n <= 7 instead of using the first three power sums.
1125 if (method == "harel.upper" || method == "harel.lower") {
1126 const detail::ScDemands<T> sch = detail::ba_sc_demands(L);
1127 if (!(sch.Z == zero))
1128 throw UnsupportedError("solver_ba_analyzer: method '" + method +
1129 "' does not support think times (infinite-server stations)");
1130 const int Nh = static_cast<int>(sch.N);
1131 const int maxUB = Nh < 7 ? Nh : 7;
1132 const pfqn::HarelBoundsResult<T> b = pfqn::pfqn_harel_bounds(sch.D, Nh, zero, maxUB);
1133 const bool uph = method == "harel.upper";
1134 T Xb = b.LB;
1135 if (uph) {
1136 T dmax = sch.D[0];
1137 for (const T& d : sch.D)
1138 if (d > dmax) dmax = d;
1139 const T cap = T(num_traits<T>::from_int(1) / dmax);
1140 const T ext = maxUB >= 2 ? b.UB[static_cast<std::size_t>(maxUB)] : b.TH[1];
1141 Xb = ext < cap ? ext : cap;
1142 }
1143 return detail::ba_fill(L, sch.V, sch.N, Xb, sch.Z, sch.D, uph);
1144 }
1145
1146 // The hierarchical / iterative single-class families.
1147 const bool hier = method == "pbh.upper" || method == "pbh.lower" || method == "cbh.upper" ||
1148 method == "cbh.lower" || method == "pbk.upper" || method == "pbk.lower" ||
1149 method == "bjbk.upper" || method == "bjbk.lower" || method == "ssd.upper" ||
1150 method == "ssd.lower" || method == "sib.upper" || method == "sib.lower" ||
1151 method == "scb.upper" || method == "scb.lower" ||
1152 method == "ldbcmp.lower";
1153 if (!hier)
1154 throw UnsupportedError("solver_ba_analyzer: unknown bound method '" + method + "'");
1155
1156 const detail::ScDemands<T> sc = detail::ba_sc_demands(L);
1157 const bool up = method.size() > 6 && method.compare(method.size() - 6, 6, ".upper") == 0;
1158 const T Nt = num_traits<T>::from_int(sc.N);
1159 const int lvl = opt.level;
1160
1161 // ssd and ldbcmp are the two families the reference does NOT gate on server
1162 // count: ssd bounds a multiserver station by disaggregating it into single
1163 // servers, and ldbcmp reads only the limiting demand.
1164 if (method == "ssd.upper" || method == "ssd.lower") {
1165 std::vector<T> cvec;
1166 for (std::size_t i = 0; i < L.nstations; ++i)
1167 if (!detail::ba_is_delay(L, i))
1168 cvec.push_back(num_traits<T>::from_double(L.stations[i].nservers));
1169 const pfqn::SsdBounds<T> b = pfqn::pfqn_ssd(sc.D, Nt, sc.Z, cvec);
1170 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1171 }
1172 if (method == "ldbcmp.lower") {
1173 // Fixed-rate parameterization (Heffes c = 0); the bound holds only in
1174 // the asymptotic regime N >= Qhat, and the reference errors below it
1175 // rather than return the NaN pfqn_ldbcmp produces there.
1176 const std::vector<T> cz(sc.D.size(), zero);
1177 const pfqn::LdBcmpBound<T> b =
1178 pfqn::pfqn_ldbcmp(sc.D, Nt, sc.Z, cz, num_traits<T>::from_double(1e-10));
1179 if (!b.applicable)
1180 throw UnsupportedError(
1181 "solver_ba_analyzer: method 'ldbcmp.lower' requires the asymptotic regime N >= "
1182 "Qhat");
1183 return detail::ba_fill(L, sc.V, sc.N, b.Xlo, sc.Z, sc.D, false);
1184 }
1185 if (method == "scb.upper" || method == "scb.lower") {
1186 // Single-class bounds of Dowdy et al. (1992). THE BRACKETED OBJECT IS NOT
1187 // THIS MODEL: scb brackets the multiclass system that this single-class
1188 // model aggregates, so scb.lower is the EXACT single-class throughput and
1189 // scb.upper adds the demand-free Expression-(3) gap. That is why scb is
1190 // absent from the auto candidate list -- mixing it with families that
1191 // bracket this model's own solution would compare two different quantities.
1192 if (!(sc.Z == zero))
1193 throw UnsupportedError("solver_ba_analyzer: method '" + method +
1194 "' supports Z=0 (no delay station) only; Theorem 3 rests on "
1195 "the delay-free balanced-network throughput");
1196 const pfqn::ScbBounds<T> b = pfqn::pfqn_scb(sc.D, sc.N);
1197 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1198 }
1199 if (method == "pbh.upper" || method == "pbh.lower") {
1200 const pfqn::PbhBounds<T> b = pfqn::pfqn_pbh(sc.D, static_cast<int>(sc.N), sc.Z, lvl);
1201 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1202 }
1203 if (method == "pbk.upper" || method == "pbk.lower") {
1204 const pfqn::PbhBounds<T> b = pfqn::pfqn_pbk(sc.D, static_cast<int>(sc.N), sc.Z, lvl);
1205 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1206 }
1207 if (method == "bjbk.upper" || method == "bjbk.lower") {
1208 const pfqn::PbhBounds<T> b = pfqn::pfqn_bjbk(sc.D, static_cast<int>(sc.N), sc.Z, lvl);
1209 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1210 }
1211 if (method == "cbh.upper" || method == "cbh.lower") {
1212 const pfqn::CbhBounds<T> b = pfqn::pfqn_cbh(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 // sib
1216 if (!(sc.Z == zero))
1217 throw UnsupportedError("solver_ba_analyzer: method '" + method +
1218 "' supports Z=0 (no delay station) only; delay needs the SIB "
1219 "Section-3.2 extension");
1220 if constexpr (!num_traits<T>::has_transcendental) {
1221 throw UnsupportedError("solver_ba_analyzer: method '" + method +
1222 "' solves a quadratic and is unavailable in exact arithmetic");
1223 } else {
1224 const pfqn::SibBounds<T> b = pfqn::pfqn_sib(sc.D, static_cast<int>(sc.N), zero, lvl);
1225 return detail::ba_fill(L, sc.V, sc.N, up ? b.Xhi : b.Xlo, sc.Z, sc.D, up);
1226 }
1227}
1228
1229} // namespace ba
1230} // namespace line
1231
1232#endif // LINE_SOLVERS_BA_SOLVER_BA_ANALYZER_H
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
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.
std::vector< JobClass > classes
ProcessType procid(std::size_t ist, std::size_t r) const
sn.procid(i,r): the process type of a (station, class) pair.
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< 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)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
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:214
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
PbhBounds< T > pfqn_pbk(const std::vector< T > &L, int N, const T &Z, int k)
PB(k), the iterative Eager-Sevcik proportional bound.
Definition pfqn_pbh.h:151
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:162
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:103
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:57
std::string ba_family_of(const std::string &method)
SolverBA.getFeatureSet, transcribed name for name.
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...
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
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.
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/cbh/sib and the iteration count k of pbk/bjbk.
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:191
std::vector< T > C
Definition sn_chain.h:193
std::vector< T > X
Definition sn_chain.h:193
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
T Xlo
lower bound on multiclass throughput X_R (= exact X_1)
Definition pfqn_scb.h:43
T Xhi
upper bound on X_R
Definition pfqn_scb.h:44
Return value of pfqn_sib, mirroring [Xlo, Xhi, Wlo, Whi].
Definition pfqn_sib.h:67