LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_nc.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_API_PFQN_NC_H
6#define LINE_API_PFQN_NC_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Normalizing constant of a product-form queueing network: the dispatcher.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_nc.m. The routine is two things
14 * at once, and they are kept separate here:
15 *
16 * 1. a MODEL REDUCTION, which folds the open classes into rescaled demands,
17 * drops empty and demand-free classes and stations, normalizes the demands
18 * per class, and closes the degenerate cases (single station, M identical
19 * replicas, delay only) in closed form. This is where most of the file is,
20 * and it is exact.
21 * 2. a DISPATCH to one of the normalizing-constant algorithms.
22 *
23 * OPEN CLASSES. MATLAB marks an open class by N_r = Inf. There is no infinity
24 * in an exact field, so this port marks it by a NEGATIVE population, which is
25 * also what the .qn interchange format uses. Each station's demands are
26 * inflated by 1/(1 - sum_r lambda_r L(i,r)) and the open queue lengths are
27 * read off directly, exactly as in the reference. Note that the reference
28 * leaves lGopen at zero (the line accumulating sum log Ut is commented out),
29 * so the returned constant is the CLOSED-CONDITIONAL one; that convention is
30 * preserved, because changing it would silently rescale every caller's result.
31 *
32 * ARITHMETIC, NOT ALGORITHMS, IS WHAT IS REFUSED. The whole ladder of
33 * `compute_norm_const` is dispatched here: ca, clw, cub/gm, kt, bkt, lekt, le, ble, dir, aghq, ls, is,
34 * mci, imci, sampling, mmint2/gleint, pana, propfair, rgf, mva, exact, comom
35 * and recal. Every one of them but ca / exact / recal / mva / comom is an
36 * asymptotic or Monte Carlo estimator formed in logarithms, so it is compiled
37 * only when `num_traits<T>::has_transcendental` and refused BY NAME otherwise
38 * rather than silently redirected to the convolution: a normalizing constant
39 * computed by an algorithm the caller did not ask for is indistinguishable
40 * from the right answer until it is wrong. NcMethod::Default's multi-station
41 * branch (cub for sum(N) < 1e3, le above it) is refused on the same grounds in
42 * exact arithmetic. `rgf` is the one two-sided name: on a SINGLE-CLASS model it
43 * is the log-domain generating-function recursion and is refused in an exact
44 * field, while a multiclass request reduces to ca and stays exact, reported as
45 * 'rgf/ca'.
46 *
47 * Arithmetic: EXACT-CAPABLE for methods Ca, Exact, Recal, Mva, Comom and for
48 * the whole reduction. The reference's tolerance-based filters
49 * (options.tol, GlobalConstants.FineTol) become exact zero tests, for the same
50 * reason as in pfqn_unique: a tolerant filter perturbs the constant and has no
51 * meaning in the rational field. Pass a positive atol to recover the tolerant
52 * behaviour in any arithmetic.
53 *
54 * WHY THE INEXACT BRANCHES REPORT lG AND NOT G. An estimator returns the LOG
55 * constant; exponentiating it and taking the log again loses the answer
56 * outright once lG passes ~709. Those branches therefore accumulate lG
57 * additively (log Gscale + log Gzdem + lG_core) and derive G from it, while
58 * the exact branches keep the exact product and take its log, as before.
59 */
60
61#include <cmath>
62#include <cstddef>
63#include <string>
64#include <vector>
65
92#include "line/num/number.h"
93#include "line/util/error.h"
94#include "line/util/matrix.h"
96
97namespace line {
98namespace pfqn {
99
100/** The methods this port dispatches, one per `compute_norm_const` case. */
101enum class NcMethod {
102 Default,
103 Adaptive, ///< the reference groups 'adaptive' with 'default'
105 Exact,
111 Gm, ///< the reference's alias of 'cub'
113 Bkt, ///< KT minus the exact Stirling remainder of each Laplaced class (BKT)
114 Lekt, ///< the estimator Ble and Bkt both compute, on the cheaper side
115 Bk, ///< Birman-Kogan saddle point with bottleneck detection
116 Bkue, ///< Birman-Kogan uniform (van der Waerden) expansion, single chain
117 Lc, ///< Birman-Kogan Algorithm 2, single chain subproblems by MVA
118 LcUe, ///< Algorithm 2 with the uniform expansion as the single chain solver
120 Ble, ///< LE plus the empirical eps->0 correction
121 Aghq, ///< adaptive Gauss-Hermite over the simplex; q=1 is Le
126 Mcmc, ///< Chen-O'Cinneide regularization; supplies X and Q, never a constant
129 Gleint, ///< the reference's alias of 'mmint2'
132 Rgf, ///< recursion by generating functions; residues beyond one class
133 Divdiff, ///< divided-difference closed form; no think time, no load dependence
134 Ger ///< residue closed form; free in the eliminated class populations
135};
136
137inline const char* nc_method_name(NcMethod m) {
138 switch (m) {
139 case NcMethod::Default: return "default";
140 case NcMethod::Adaptive: return "adaptive";
141 case NcMethod::Ca: return "ca";
142 case NcMethod::Exact: return "exact";
143 case NcMethod::Recal: return "recal";
144 case NcMethod::Mva: return "mva";
145 case NcMethod::Comom: return "comom";
146 case NcMethod::Clw: return "clw";
147 case NcMethod::Cub: return "cub";
148 case NcMethod::Gm: return "gm";
149 case NcMethod::Kt: return "kt";
150 case NcMethod::Bkt: return "bkt";
151 case NcMethod::Lekt: return "lekt";
152 case NcMethod::Bk: return "bk";
153 case NcMethod::Bkue: return "bkue";
154 case NcMethod::Lc: return "lc";
155 case NcMethod::LcUe: return "lc.ue";
156 case NcMethod::Le: return "le";
157 case NcMethod::Ble: return "ble";
158 case NcMethod::Aghq: return "aghq";
159 case NcMethod::Ls: return "ls";
160 case NcMethod::Is: return "is";
161 case NcMethod::Mci: return "mci";
162 case NcMethod::Imci: return "imci";
163 case NcMethod::Mcmc: return "mcmc";
164 case NcMethod::Sampling: return "sampling";
165 case NcMethod::Mmint2: return "mmint2";
166 case NcMethod::Gleint: return "gleint";
167 case NcMethod::Pana: return "pana";
168 case NcMethod::Propfair: return "propfair";
169 case NcMethod::Rgf: return "rgf";
170 case NcMethod::Divdiff: return "divdiff";
171 case NcMethod::Ger: return "ger";
172 }
173 return "default";
174}
175
176/** Map a method name to its enum; throws UnsupportedError on an unknown one. */
177inline NcMethod nc_method_of(const std::string& s) {
178 if (s == "default") return NcMethod::Default;
179 if (s == "adaptive") return NcMethod::Adaptive;
180 if (s == "ca") return NcMethod::Ca;
181 if (s == "exact") return NcMethod::Exact;
182 if (s == "recal") return NcMethod::Recal;
183 if (s == "mva") return NcMethod::Mva;
184 if (s == "comom") return NcMethod::Comom;
185 if (s == "clw") return NcMethod::Clw;
186 if (s == "cub") return NcMethod::Cub;
187 if (s == "gm") return NcMethod::Gm;
188 if (s == "kt") return NcMethod::Kt;
189 if (s == "bkt") return NcMethod::Bkt;
190 if (s == "lekt") return NcMethod::Lekt;
191 if (s == "bk") return NcMethod::Bk;
192 if (s == "bkue") return NcMethod::Bkue;
193 if (s == "lc") return NcMethod::Lc;
194 if (s == "lc.ue") return NcMethod::LcUe;
195 if (s == "le") return NcMethod::Le;
196 if (s == "ble") return NcMethod::Ble;
197 if (s == "aghq") return NcMethod::Aghq;
198 if (s == "ls") return NcMethod::Ls;
199 if (s == "is") return NcMethod::Is;
200 if (s == "mci") return NcMethod::Mci;
201 if (s == "imci") return NcMethod::Imci;
202 if (s == "mcmc") return NcMethod::Mcmc;
203 if (s == "sampling") return NcMethod::Sampling;
204 if (s == "mmint2") return NcMethod::Mmint2;
205 if (s == "gleint") return NcMethod::Gleint;
206 if (s == "pana") return NcMethod::Pana;
207 if (s == "propfair") return NcMethod::Propfair;
208 if (s == "rgf") return NcMethod::Rgf;
209 if (s == "divdiff") return NcMethod::Divdiff;
210 if (s == "ger") return NcMethod::Ger;
211 throw UnsupportedError("pfqn_nc: unrecognized method '" + s + "'");
212}
213
214/** The `options` fields `compute_norm_const` reads beyond the method itself. */
215struct NcOptions {
216 std::size_t samples = 100000; ///< SolverOptions('NC').samples
217 unsigned long seed = 23000; ///< SolverOptions('NC').seed
218 double tol = 1e-6; ///< handed to pfqn_comomrm
219 /// options.config.aghq_nodes: nodes per simplex direction of the adaptive
220 /// Gauss-Hermite rule. q = 1 reproduces pfqn_le and the rule costs q^(M-1)
221 /// evaluations, so the default stays small.
222 std::size_t aghq_nodes = 3;
223 /// options.config.mcmc_batches: batches pfqn_mcmc splits its run into
225 /// options.config.mcmc_burnin: warm-up fraction pfqn_mcmc discards
227};
228
229/**
230 * Refuse a method in an arithmetic it has no meaning in.
231 * Kept as a function so the message is identical wherever it is raised.
232 */
233inline void pfqn_nc_refuse(const std::string& method) {
234 throw UnsupportedError("pfqn_nc: method '" + method +
235 "' is an asymptotic or Monte Carlo estimator formed in logarithms and "
236 "needs transcendental arithmetic. "
237 "Use 'ca', 'exact', 'recal', 'mva' or 'comom' for an exact constant.");
238}
239
240template <class T>
242 T G; ///< normalizing constant, exact when the method is
243 double lG; ///< its logarithm
244 std::vector<T> X; ///< (R) per-class throughput, empty when not produced
245 Matrix<T> Q; ///< (M x R) queue lengths, empty when not produced
246 std::string method; ///< the algorithm actually used
247 /**
248 * False when the METHOD DECLINED THE MODEL, which is the reference's
249 * `lG = []`: pana outside normal usage, or mmint2/gleint on a model with
250 * more than one queueing station. MATLAB warns and returns empty, and
251 * `getAvg` then renders a table of ZEROS while still reporting a completed
252 * analysis; the caller reproduces that by returning an all-zero solution.
253 *
254 * This is NOT the same as a refusal. A refusal throws, and the reference
255 * throws too -- `comom` on a multi-queue model is `line_error` in
256 * `pfqn_nc.m`, added deliberately with the comment that "a silently zeroed
257 * result is worse than no result". Only the two branches above decline.
258 * Ruled by the user on 2026-07-25 (register row N1).
259 */
260 bool valid = true;
261};
262
263/**
264 * @brief Normalizing constant of a product-form queueing network: the
265 * dispatcher.
266 *
267 * @param lambda (R) arrival rates; zero on the closed classes, may be empty
268 * @param L (M x R) service demands
269 * @param N (R) populations; a NEGATIVE entry marks an open class
270 * @param Z (K x R) think times, summed over rows
271 * @param method requested algorithm
272 * @param atol threshold below which a demand counts as zero; 0 for exact
273 * @param nopt sample count, seed and tolerance the estimators read
274 */
275template <class T>
276NcDispatchResult<T> pfqn_nc(const std::vector<T>& lambda, const Matrix<T>& L,
277 const std::vector<int>& N, const Matrix<T>& Z, NcMethod method,
278 const T& atol, const NcOptions& nopt) {
279 const std::size_t R = N.size();
280 const T zero = num_traits<T>::from_int(0);
281 const T one = num_traits<T>::from_int(1);
282
284 res.G = one;
285 res.lG = 0.0;
286 // Every return BEFORE the dispatch reports 'exact', because every one of
287 // them is a closed form rather than an algorithm: the reference sets this
288 // up front and lets the dispatch overwrite it.
289 res.method = "exact";
290
291 if (R == 0) {
292 res.G = zero;
293 res.lG = -std::numeric_limits<double>::infinity();
294 return res;
295 }
296 if (!L.empty() && L.cols() != R)
297 throw InputError("pfqn_nc: L and N disagree on the class count");
298
299 const std::size_t M0 = L.empty() ? 0 : L.rows();
300 std::vector<T> lam(R, zero);
301 for (std::size_t r = 0; r < R && r < lambda.size(); ++r) lam[r] = lambda[r];
302
303 // ---- open classes: inflate the demands, read off the open queue lengths --
304 Matrix<T> Lw = L;
305 Matrix<T> Qopen(M0, R, zero);
306 std::vector<std::size_t> ocl;
307 for (std::size_t r = 0; r < R; ++r)
308 if (N[r] < 0) ocl.push_back(r);
309 for (std::size_t i = 0; i < M0; ++i) {
310 T u = one;
311 for (std::size_t r = 0; r < R; ++r) u -= lam[r] * L(i, r);
312 if (u == zero) {
313 // A station the open classes fill exactly. This is not a pathology:
314 // it is what a SOURCE row looks like, whose chain demand is 1/lambda
315 // by construction. The reference divides by the zero, gets Inf and
316 // NaN, and its demand filter (Lmax./Lsum > FineTol, false on NaN)
317 // drops the row a few lines later. Dropping it here reaches the same
318 // constant without carrying non-finite values through the reduction.
319 for (std::size_t r = 0; r < R; ++r) Lw(i, r) = zero;
320 continue;
321 }
322 for (std::size_t r = 0; r < R; ++r) {
323 Lw(i, r) = L(i, r) / u;
324 Qopen(i, r) = lam[r] * Lw(i, r) / u;
325 }
326 }
327
328 // Closed populations, with the open classes zeroed out.
329 std::vector<int> Nc(R, 0);
330 for (std::size_t r = 0; r < R; ++r) Nc[r] = N[r] > 0 ? N[r] : 0;
331 long Ntot = 0;
332 for (int v : Nc) Ntot += v;
333 if (Ntot == 0) return res; // lG = 0, G = 1
334
335 const auto colsum = [&](const Matrix<T>& A, std::size_t r) {
336 T s = zero;
337 for (std::size_t i = 0; i < A.rows(); ++i) s += A(i, r);
338 return s;
339 };
340
341 // ---- drop the empty classes ---------------------------------------------
342 std::vector<std::size_t> nnz;
343 for (std::size_t r = 0; r < R; ++r)
344 if (Nc[r] > 0) nnz.push_back(r);
345 const std::size_t R1 = nnz.size();
346
347 // per-class rescaling-order rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
348 std::vector<T> scalevec(R1, one);
349 Matrix<T> L1(M0, R1), Z1(Z.empty() ? 0 : Z.rows(), R1);
350 for (std::size_t k = 0; k < R1; ++k) {
351 const std::size_t r = nnz[k];
352 T mx = zero;
353 for (std::size_t i = 0; i < M0; ++i)
354 if (Lw(i, r) > mx) mx = Lw(i, r);
355 for (std::size_t i = 0; i < Z1.rows(); ++i)
356 if (Z(i, r) > mx) mx = Z(i, r);
357 if (mx > zero) scalevec[k] = mx;
358 for (std::size_t i = 0; i < M0; ++i) L1(i, k) = Lw(i, r) / scalevec[k];
359 for (std::size_t i = 0; i < Z1.rows(); ++i) Z1(i, k) = Z(i, r) / scalevec[k];
360 }
361 // Gscale is the factor the rescaling removed: prod_r scalevec_r^{N_r}.
362 T Gscale = one;
363 for (std::size_t k = 0; k < R1; ++k)
364 Gscale *= num_pow_int(scalevec[k], static_cast<unsigned>(Nc[nnz[k]]));
365 // The LOG path must never take the log of that product: scalevec = 3 with
366 // N = 600 leaves double range and turned a perfectly good lG = 862 into inf.
367 // Accumulate sum_r N_r log(scalevec_r) directly instead. The exact branches
368 // keep using Gscale itself, where the product is representable by
369 // construction.
370 double lGscale = 0.0;
372 for (std::size_t k = 0; k < R1; ++k)
373 lGscale += static_cast<double>(Nc[nnz[k]]) *
374 std::log(num_traits<T>::to_double(scalevec[k]));
375 }
376
377 // ---- drop the stations with no demand at all -----------------------------
378 std::vector<std::size_t> demSt;
379 for (std::size_t i = 0; i < M0; ++i) {
380 T rs = zero;
381 for (std::size_t k = 0; k < R1; ++k) rs += L1(i, k);
382 if (rs > atol) demSt.push_back(i);
383 }
384 Matrix<T> L2(demSt.size(), R1);
385 for (std::size_t a = 0; a < demSt.size(); ++a)
386 for (std::size_t k = 0; k < R1; ++k) L2(a, k) = L1(demSt[a], k);
387 const std::size_t M = demSt.size();
388
389 std::vector<int> N2(R1, 0);
390 for (std::size_t k = 0; k < R1; ++k) N2[k] = Nc[nnz[k]];
391
392 // Aggregate think times, one row.
393 Matrix<T> Zag(Z1.rows() == 0 ? 0 : 1, R1);
394 T Ztot = zero;
395 for (std::size_t k = 0; k < R1; ++k) {
396 T s = zero;
397 for (std::size_t i = 0; i < Z1.rows(); ++i) s += Z1(i, k);
398 if (Zag.rows() > 0) Zag(0, k) = s;
399 Ztot += s;
400 }
401
402 // Delay-only constant prod_r Z_r^{N_r}/N_r!, needed by several branches.
403 const auto delayG = [&](const std::vector<std::size_t>& cls) {
404 T g = one;
405 for (std::size_t k : cls) {
406 T zs = zero;
407 for (std::size_t i = 0; i < Z1.rows(); ++i) zs += Z1(i, k);
408 g *= num_pow_int(zs, static_cast<unsigned>(N2[k])) /
409 num_factorial<T>(static_cast<unsigned>(N2[k]));
410 }
411 return g;
412 };
413 // The open-class measures ride along with the CLOSED ones: the reference
414 // assembles X and Q only when the dispatched method produced them as a
415 // by-product (which is `mva` alone) and returns both empty otherwise, so a
416 // caller can test emptiness to decide whether it must derive the measures
417 // itself. Attaching Qopen to an otherwise empty result would answer that
418 // test wrongly and silence the caller's own open-chain formula.
419 bool have_measures = false;
420 const auto attach_open = [&]() {
421 if (ocl.empty() || !have_measures) return;
422 res.Q = Matrix<T>(M0, R, zero);
423 for (std::size_t i = 0; i < M0; ++i)
424 for (std::size_t r : ocl) res.Q(i, r) = Qopen(i, r);
425 res.X.assign(R, zero);
426 for (std::size_t r : ocl) res.X[r] = lam[r];
427 };
428 const auto finish = [&](const T& gcore) {
429 res.G = Gscale * gcore;
431 attach_open();
432 return res;
433 };
434 // The estimator finish: lG is the answer and G is derived from it, so a
435 // constant past the double range still reports a usable logarithm.
436 const auto finish_log = [&](double lgcore) {
437 res.lG = lGscale + lgcore;
438 res.G = num_traits<T>::from_double(std::exp(res.lG));
439 attach_open();
440 return res;
441 };
442
443 // ---- degenerate cases, in closed form ------------------------------------
444 T Lsum = zero;
445 for (std::size_t i = 0; i < M; ++i)
446 for (std::size_t k = 0; k < R1; ++k) Lsum += L2(i, k);
447
448 if (M == 0 || !(Lsum > atol)) {
449 // All demands zero: the whole population sits in the delay.
450 std::vector<std::size_t> all(R1);
451 for (std::size_t k = 0; k < R1; ++k) all[k] = k;
452 return finish(Ztot > atol ? delayG(all) : one);
453 }
454 if (M == 1 && !(Ztot > atol)) {
455 // Single station, no delay: G = (sum N)! / prod N_r! * prod L_r^{N_r}.
456 long tot = 0;
457 for (int v : N2) tot += v;
458 T g = num_factorial<T>(static_cast<unsigned>(tot));
459 for (std::size_t k = 0; k < R1; ++k)
460 g *= num_pow_int(L2(0, k), static_cast<unsigned>(N2[k])) /
461 num_factorial<T>(static_cast<unsigned>(N2[k]));
462 return finish(g);
463 }
464 if (!(Ztot > atol)) {
465 // M identical replicas, no delay: the multiset-count closed form.
466 bool identical = true;
467 for (std::size_t i = 1; i < M && identical; ++i)
468 for (std::size_t k = 0; k < R1; ++k)
469 if (L2(i, k) != L2(0, k)) {
470 identical = false;
471 break;
472 }
473 if (identical) {
474 long tot = 0;
475 for (int v : N2) tot += v;
476 T g = num_factorial<T>(static_cast<unsigned>(tot + M - 1)) /
477 num_factorial<T>(static_cast<unsigned>(M - 1));
478 for (std::size_t k = 0; k < R1; ++k)
479 g *= num_pow_int(L2(0, k), static_cast<unsigned>(N2[k])) /
480 num_factorial<T>(static_cast<unsigned>(N2[k]));
481 return finish(g);
482 }
483 }
484
485 // ---- classes whose jobs never leave the delay -----------------------------
486 std::vector<std::size_t> zdem, nzdem;
487 for (std::size_t k = 0; k < R1; ++k) {
488 T s = zero;
489 for (std::size_t i = 0; i < M; ++i) s += L2(i, k);
490 (s > atol ? nzdem : zdem).push_back(k);
491 }
492 const T Gzdem = zdem.empty() ? one : delayG(zdem);
493
494 Matrix<T> L3(M, nzdem.size());
495 Matrix<T> Z3(Zag.rows(), nzdem.size());
496 std::vector<int> N3(nzdem.size(), 0);
497 for (std::size_t a = 0; a < nzdem.size(); ++a) {
498 for (std::size_t i = 0; i < M; ++i) L3(i, a) = L2(i, nzdem[a]);
499 for (std::size_t i = 0; i < Zag.rows(); ++i) Z3(i, a) = Zag(i, nzdem[a]);
500 N3[a] = N2[nzdem[a]];
501 }
502
503 // Mean values produced as a by-product come back in the REDUCED, SCALED and
504 // REORDERED problem: demand-bearing stations only, nonzero-demand classes
505 // only, on demands divided by scalevec. Undo all three, or the caller reads
506 // a permuted throughput of the wrong magnitude.
507 const auto attach_measures = [&](const std::vector<T>& Xr, const Matrix<T>& Qr) {
508 res.X.assign(R, num_traits<T>::from_int(0));
509 res.Q = Matrix<T>(M0, R, num_traits<T>::from_int(0));
510 for (std::size_t a = 0; a < nzdem.size() && a < Xr.size(); ++a) {
511 const std::size_t r = nnz[nzdem[a]];
512 res.X[r] = Xr[a] / scalevec[nzdem[a]];
513 for (std::size_t i = 0; i < M && i < Qr.rows(); ++i) res.Q(demSt[i], r) = Qr(i, a);
514 }
515 };
516
517 const std::size_t Rc = nzdem.size();
518 T Z3tot = zero;
519 for (std::size_t i = 0; i < Z3.rows(); ++i)
520 for (std::size_t a = 0; a < Rc; ++a) Z3tot += Z3(i, a);
521
522 // Chain-level views the estimators want: Z summed over its rows, and the
523 // population as field elements rather than ints.
524 std::vector<T> Zv(Rc, zero);
525 for (std::size_t i = 0; i < Z3.rows(); ++i)
526 for (std::size_t a = 0; a < Rc; ++a) Zv[a] += Z3(i, a);
527 std::vector<T> Nv(Rc, zero);
528 for (std::size_t a = 0; a < Rc; ++a) Nv[a] = num_traits<T>::from_int(N3[a]);
529 long Nsum3 = 0;
530 for (int v : N3) Nsum3 += v;
531
532 // ---- the estimator ladder -------------------------------------------------
533 // These are the branches of `compute_norm_const` that answer with a log:
534 // asymptotic expansions, quadratures and Monte Carlo estimators. They are
535 // separated from the exact switch below because they must return through
536 // finish_log, and because the whole group is meaningless -- not merely
537 // inaccurate -- in an exact field, where it is refused by name.
538 const bool default_multi =
539 (method == NcMethod::Default || method == NcMethod::Adaptive) && M > 1;
540 const bool default_big_repairman = (method == NcMethod::Default ||
541 method == NcMethod::Adaptive) &&
542 M == 1 && Z3tot > atol && Nsum3 >= 10000;
543 const bool estimator =
544 default_multi || default_big_repairman || method == NcMethod::Clw ||
545 method == NcMethod::Cub || method == NcMethod::Gm || method == NcMethod::Kt ||
546 method == NcMethod::Bkt || method == NcMethod::Lekt ||
547 method == NcMethod::Bk || method == NcMethod::Bkue ||
548 method == NcMethod::Lc || method == NcMethod::LcUe ||
549 method == NcMethod::Le || method == NcMethod::Ble || method == NcMethod::Ls ||
550 method == NcMethod::Aghq ||
551 method == NcMethod::Is ||
552 method == NcMethod::Mci || method == NcMethod::Imci ||
553 method == NcMethod::Mcmc || method == NcMethod::Sampling ||
554 method == NcMethod::Mmint2 || method == NcMethod::Gleint ||
555 method == NcMethod::Pana || method == NcMethod::Propfair ||
556 // 'rgf' answers with a log in both arities: the grouped convolution at
557 // one class, the residue recursion of Harrison-Coury Thm 1 beyond it.
558 method == NcMethod::Rgf ||
559 // 'divdiff' evaluates alternating sums as signed log-sum-exps, so it
560 // answers with a log like the estimators do, exact though it is.
561 method == NcMethod::Divdiff;
562 if (estimator) {
563 if constexpr (!num_traits<T>::has_transcendental) {
564 pfqn_nc_refuse(default_multi || default_big_repairman
565 ? std::string("default (the multi-station cub / le branch)")
566 : std::string(nc_method_name(method)));
567 } else {
568 const double lgz = num_traits<T>::log_as_double(Gzdem);
569 McRng rng(static_cast<std::uint64_t>(nopt.seed));
570 const T fineTol = num_traits<T>::from_double(1e-8); // GlobalConstants.FineTol
571 // The order the reference raises as far as a fixed cost budget allows.
572 const auto cub_budget_order = [&]() {
573 const double Cmax = static_cast<double>(M * Rc) * 125000.0;
574 const int maxorder =
575 static_cast<int>(std::min<double>(std::ceil((Nsum3 - 1) / 2.0), 16.0));
576 double tot = 0.0;
577 int order = 0;
578 while (order < maxorder) {
579 const double next =
580 static_cast<double>(Rc) *
581 nck(static_cast<int>(M) + 2 * (order + 1), static_cast<int>(M) - 1);
582 if (tot + next > Cmax) break;
583 ++order;
584 tot += next;
585 }
586 // Cmax prices neither the Grundmann-Moeller node count nor the
587 // think-time v-integration, so the order is re-priced against
588 // the true evaluation count and lowered until it fits.
589 double Zsum = 0.0;
590 for (std::size_t a = 0; a < Rc; ++a) Zsum += num_traits<T>::to_double(Zv[a]);
591 while (order > 0 &&
592 pfqn_cub_evals(static_cast<int>(M), order, Zsum) > CUB_MAX_EVALS)
593 --order;
594 return order;
595 };
596 switch (method) {
599 // ONE ESTIMATOR ANSWERS THE WHOLE FAMILY. The
600 // divided-difference closed form of Casale (SIGMETRICS 2017)
601 // is exact here and was briefly tried first on
602 // M>1 && Rc==1 && sum(Z)==0, but the default route does not
603 // serve a single constant: the analyzer differences it at
604 // N-e_r for X and at the AUGMENTED shape for Q, one extra
605 // class holding one job at station i. That shape has Rc+1
606 // classes, which the closed form refuses at any sizeable
607 // population (the outer sum's cancellation), so it kept the
608 // cubature while G(N) turned exact. Mixing the two costs more
609 // than either: on mqn_singleserver_ps the closed-form G(N)
610 // under cubature numerators left sum_i Q_i at 99.500 of
611 // N=100, and the conservation rescale then moved the entire
612 // cubature error into X, 0.5% against the 0.06% the cubature
613 // ratio carries on its own. 'divdiff' stays a NAMED method,
614 // where the caller owns the whole family.
615 if (M > 1 && Nsum3 < 1000) {
616 res.method = "cub";
617 return finish_log(
619 pfqn_cub(L3, N3, Zv, cub_budget_order(), fineTol).lG));
620 }
621 // BLE on the default path: strictly better on lG and it
622 // cancels in G(N-e_r)/G(N). "le" stays the published form.
623 res.method = "ble";
624 // Birman-Kogan Algorithm 2 supplies the MEAN VALUES here.
625 // The caller's fallback differences lG at R+M*R reduced
626 // populations, which on many stations is both dearer and
627 // ~300x less accurate than the load concealment fixed point. Gated
628 // on the station count, since load concealment is mean
629 // field in M: see _kb/06-solver-catalog.md.
630 if (L3.rows() >= 10 && L3.cols() > 1) {
631 bool closed = !Nv.empty();
632 T tot = num_traits<T>::from_int(0);
633 for (std::size_t r = 0; r < Nv.size(); ++r) {
634 if (Nv[r] < num_traits<T>::from_int(0)) closed = false;
635 tot = tot + Nv[r];
636 }
637 if (closed && tot > num_traits<T>::from_int(0)) {
638 BkLcResult<T> thin = pfqn_bklc(L3, Nv, Zv, "mva", 1e-10, 1000);
639 attach_measures(thin.X, thin.Q);
640 res.method = "ble/lc";
641 }
642 }
643 return finish_log(lgz +
644 num_traits<T>::to_double(pfqn_ble(L3, Nv, Zv).lG));
645 case NcMethod::Clw:
646 res.method = "clw";
647 return finish_log(lgz + num_traits<T>::to_double(pfqn_clw(L3, N3, Zv).lG));
648 case NcMethod::Cub:
649 case NcMethod::Gm: {
650 // The reference's exact order for the Z = 0 branch.
651 const int order = static_cast<int>(std::ceil((Nsum3 - 1) / 2.0));
652 res.method = nc_method_name(method);
653 return finish_log(
654 lgz + num_traits<T>::to_double(pfqn_cub(L3, N3, Zv, order, fineTol).lG));
655 }
656 case NcMethod::Kt:
657 res.method = "kt";
658 return finish_log(lgz + num_traits<T>::to_double(pfqn_kt(L3, Nv, Zv).lG));
659 case NcMethod::Bkt:
660 // KT minus the exact Stirling remainder of each Laplaced class
661 res.method = "bkt";
662 return finish_log(lgz + num_traits<T>::to_double(pfqn_bkt(L3, Nv, Zv).lG));
663 case NcMethod::Lekt:
664 // the estimator ble and bkt both compute, on the cheaper side
665 res.method = "lekt";
666 return finish_log(lgz + num_traits<T>::to_double(pfqn_lekt(L3, Nv, Zv).lG));
667 case NcMethod::Bk:
668 res.method = "bk";
669 return finish_log(lgz + num_traits<T>::to_double(pfqn_bk(L3, Nv, Zv).lG));
670 case NcMethod::Bkue: {
671 // The uniform expansion is single chain by construction; the
672 // multichain fallback is the saddle point of the same paper,
673 // which is also how the analyzer reaches this branch, since it
674 // conditions on a station population with an auxiliary class.
675 if (L3.cols() > 1) {
676 res.method = "bkue/bk";
677 return finish_log(lgz + num_traits<T>::to_double(pfqn_bk(L3, Nv, Zv).lG));
678 }
679 std::vector<T> Dv(L3.rows());
680 for (std::size_t i = 0; i < L3.rows(); ++i) Dv[i] = L3(i, 0);
681 res.method = "bkue";
682 return finish_log(
684 pfqn_bkue(Dv, Nv[0], Zv.empty() ? num_traits<T>::from_int(0) : Zv[0]).lG));
685 }
686 case NcMethod::Lc:
687 case NcMethod::LcUe: {
688 // Algorithm 2 returns mean values, not a multichain constant;
689 // the saddle point that seeds it supplies lG on the same
690 // asymptotics. The fixed point converges linearly and slowly,
691 // so it is iterated to the method's own accuracy rather than to
692 // a solver-level reporting tolerance.
694 L3, Nv, Zv, method == NcMethod::LcUe ? "ue" : "mva", 1e-10, 1000);
695 attach_measures(thin.X, thin.Q);
696 res.method = nc_method_name(method);
697 return finish_log(lgz + num_traits<T>::to_double(pfqn_bk(L3, Nv, Zv).lG));
698 }
699 case NcMethod::Le:
700 res.method = "le";
701 return finish_log(lgz + num_traits<T>::to_double(pfqn_le(L3, Nv, Zv).lG));
702 case NcMethod::Ble:
703 // LE plus the empirical eps->0 correction; see _kb/03-api-layer.md
704 res.method = "ble";
705 return finish_log(lgz + num_traits<T>::to_double(pfqn_ble(L3, Nv, Zv).lG));
706 case NcMethod::Aghq:
707 // adaptive Gauss-Hermite over the simplex; q=1 would be "le";
708 // options.config.aghq_nodes overrides the node count.
709 res.method = "aghq";
710 return finish_log(lgz + num_traits<T>::to_double(
711 pfqn_aghq(L3, Nv, Zv,
712 nopt.aghq_nodes < 1 ? 1 : nopt.aghq_nodes).lG));
713 case NcMethod::Ls:
714 res.method = "ls";
715 return finish_log(
716 lgz + num_traits<T>::to_double(pfqn_ls(L3, Nv, Zv, nopt.samples, rng).lG));
717 case NcMethod::Is:
718 res.method = "is";
719 return finish_log(lgz + pfqn_is(L3, N3, Zv, nopt.samples, rng).lG);
720 case NcMethod::Mci:
721 res.method = "mci";
722 return finish_log(
723 lgz + pfqn_mci(L3, N3, Zv, nopt.samples, MciVariant::Mci, rng).lG);
724 case NcMethod::Imci:
725 res.method = "imci";
726 return finish_log(
727 lgz + pfqn_mci(L3, N3, Zv, nopt.samples, MciVariant::Imci, rng).lG);
728 case NcMethod::Mcmc: {
729 // Chen-O'Cinneide REGULARIZATION (TOMACS 8(3), 1998). The chain is
730 // simulated on the regularized network, which shares the steady-state
731 // distribution of the original one, so X and Q come back through the
732 // X/Q channel like 'lc'. What it does NOT return is the constant
733 // itself: the algorithm estimates the RATIOS G(N-e_r)/G(N), never G,
734 // so the lG below is the BLE expansion and is not part of the paper.
735 // It cancels out of every mean value the analyzer reports; only
736 // getProbNormConstAggr reads it.
737 const McmcResult<T> mc = pfqn_mcmc(L3, N3, Zv, std::vector<double>(),
738 nopt.samples, nopt.mcmc_batches,
739 nopt.mcmc_burnin, rng);
740 attach_measures(mc.X, mc.Q);
741 res.method = "mcmc";
742 if (L3.rows() > 1)
743 return finish_log(lgz + num_traits<T>::to_double(pfqn_ble(L3, Nv, Zv).lG));
744 return finish_log(lgz + pfqn_comomrm(L3, N3, Z3, 1).lG);
745 }
747 // The reference picks by shape: one station is a repairman
748 // integral, more stations than classes favour the Monte
749 // Carlo integral, and otherwise the logistic sampler.
750 if (M == 1) {
751 res.method = "sampling";
752 return finish_log(
753 lgz + pfqn_mmsample2(L3, N3, Zv, nopt.samples, rng).lG);
754 }
755 if (M > Rc) {
756 res.method = "imci";
757 return finish_log(
758 lgz + pfqn_mci(L3, N3, Zv, nopt.samples, MciVariant::Imci, rng).lG);
759 }
760 res.method = "ls";
761 return finish_log(
762 lgz + num_traits<T>::to_double(pfqn_ls(L3, Nv, Zv, nopt.samples, rng).lG));
763 case NcMethod::Mmint2:
764 case NcMethod::Gleint: {
765 if (M > 1) {
766 // The reference warns and returns lG = []; see `valid`.
767 res.method = nc_method_name(method);
768 res.valid = false;
769 res.lG = 0.0;
770 res.G = zero;
771 return res;
772 }
773 std::vector<T> Lrow(Rc, zero);
774 for (std::size_t a = 0; a < Rc; ++a) Lrow[a] = L3(0, a);
775 res.method = nc_method_name(method);
776 return finish_log(
778 pfqn_mmint2_gausslegendre(Lrow, Nv, Zv).lG));
779 }
780 case NcMethod::Pana: {
781 const PanaceaResult<T> pa = pfqn_panacea(L3, N3, Zv);
782 res.method = "pana";
783 if (!pa.normalUsage) {
784 // The reference warns and returns lG = []; see the
785 // `valid` field. Deliberate, user-ruled 2026-07-25.
786 res.valid = false;
787 res.lG = 0.0;
788 res.G = zero;
789 return res;
790 }
791 return finish_log(lgz + num_traits<T>::to_double(pa.lG));
792 }
794 res.method = "propfair";
795 return finish_log(lgz +
797 case NcMethod::Divdiff: {
798 // Divided-difference closed form, Eqs. (15) and (16).
799 // Load-independent single-server queues only: a think time
800 // needs the integral form of Corollary 3.4, which is not
801 // implemented. Unlike the default route this one keeps
802 // whatever the expression returns, since a caller that named
803 // the method has no fallback.
804 if (Z3tot > zero)
805 throw InputError(
806 "pfqn_nc: the 'divdiff' method requires a model without think time, "
807 "which needs the integral form of Corollary 3.4. Use 'ca' or "
808 "'default'");
809 const ExplicitResult<T> ex = pfqn_explicit(L3, N3);
810 res.method = "divdiff/" + ex.method;
811 return finish_log(lgz + num_traits<T>::to_double(ex.lG));
812 }
813 case NcMethod::Rgf: {
814 if (Rc == 1) {
815 std::vector<T> Lcol(M, zero);
816 for (std::size_t i = 0; i < M; ++i) Lcol[i] = L3(i, 0);
817 res.method = "rgf";
818 return finish_log(
819 lgz + num_traits<T>::to_double(pfqn_rgf(Lcol, N3[0], Zv[0]).lG));
820 }
821 // Multiclass: the residue recursion, with think times carried
822 // by the Bertozzi-McKenna truncation neither RGF paper has.
823 // That sum is ALTERNATING, so pfqn_rgfmc refuses rather than
824 // return a wrong lG; the exact convolution answers those and
825 // the reported method says so.
826 try {
827 const T lg = pfqn_rgfmc<T>(L3, N3, Zv).lG;
828 res.method = "rgf";
829 return finish_log(lgz + num_traits<T>::to_double(lg));
830 } catch (const InputError&) {
831 Matrix<T> Zm(1, Rc);
832 for (std::size_t r = 0; r < Rc; ++r) Zm(0, r) = Zv[r];
833 res.method = "rgf/ca";
834 return finish_log(
835 lgz + num_traits<T>::to_double(pfqn_ca<T>(L3, N3, Zm).lG));
836 }
837 }
838 default:
839 break;
840 }
841 }
842 }
843
844 // ---- dispatch -------------------------------------------------------------
845 T gcore = one;
846 switch (method) {
847 case NcMethod::Ca:
848 gcore = pfqn_ca(L3, N3, Z3).G;
849 res.method = "ca";
850 break;
851 case NcMethod::Rgf:
852 // Multiclass only: the single-class recursion returned above. The
853 // aggregate marginals reach this branch too, since the analyzer
854 // augments a single-class model with an auxiliary class to
855 // condition on a station population.
856 gcore = pfqn_ca(L3, N3, Z3).G;
857 res.method = "rgf/ca";
858 break;
859 case NcMethod::Ger:
860 // Residue closed form of the same generating function 'clw' inverts
861 // numerically. It sits in the EXACT switch, not among the estimators,
862 // because every operation is a product, a quotient or a binomial
863 // coefficient: in the exact backend it agrees with pfqn_ca bit for bit.
864 // A class eliminated by residues enters only as a pole ORDER, so its
865 // population is free; the term count grows as C(S+M-1,M-1) per further
866 // elimination instead, and the maxterms cap REFUSES rather than
867 // truncating.
868 gcore = pfqn_gerasimov(L3, N3, Z3).G;
869 res.method = "ger";
870 break;
871 case NcMethod::Mva: {
872 // MVA by-product means rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
873 const MvaResult<T> mva = pfqn_mva(L3, N3, Z3);
874 gcore = mva.G;
875 res.X.assign(R, zero);
876 res.Q = Matrix<T>(M0, R, zero);
877 for (std::size_t a = 0; a < Rc; ++a) {
878 const std::size_t r = nnz[nzdem[a]];
879 res.X[r] = mva.XN[a] / scalevec[nzdem[a]];
880 for (std::size_t i = 0; i < M; ++i) res.Q(demSt[i], r) = mva.QN(i, a);
881 }
882 res.method = "mva";
883 have_measures = true;
884 break;
885 }
886 case NcMethod::Recal:
887 if (Z3tot > atol)
888 throw UnsupportedError(
889 "pfqn_nc: RECAL is available only for models with zero think time; this model "
890 "has a delay");
891 gcore = pfqn_recal(L3, N3, Z3).G;
892 res.method = "recal";
893 break;
894 case NcMethod::Exact: {
895 // RECAL routing rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
896 long tot = 0;
897 for (int v : N3) tot += v;
898 if (M >= Rc || tot > 10 || Z3tot > atol) {
899 gcore = pfqn_ca(L3, N3, Z3).G;
900 res.method = "exact/ca";
901 } else {
902 gcore = pfqn_recal(L3, N3, Z3).G;
903 res.method = "exact/recal";
904 }
905 break;
906 }
907 case NcMethod::Comom:
908 // THIS THROW IS THE REFERENCE'S OWN, and it sits a few lines from
909 // branches that DECLINE by returning an empty lG (see `valid`).
910 // The rationale is quoted verbatim from pfqn_nc.m:278-281, so that
911 // a reader who sees zeros next door does not "harmonise" it away:
912 //
913 // "This used to emit a warning gated on options.verbose and then
914 // return lG = [], which the caller turned into all-zero queue
915 // lengths while still reporting a completed analysis. A silently
916 // zeroed result is worse than no result: refuse instead."
917 //
918 // Declining and refusing are different, and the reference does both
919 // on purpose. The 2026-07-25 empty-result ruling covers the first.
920 if (Rc > 1 && M > 1)
921 throw InputError(
922 "pfqn_nc: the 'comom' method supports a single queueing station, but this "
923 "model has more. Use 'default', 'ca' or 'exact'.");
924 if (Rc > 1) {
925 gcore = pfqn_comomrm(L3, N3, Z3).G;
926 res.method = "comom";
927 } else {
928 gcore = pfqn_ca(L3, N3, Z3).G;
929 res.method = "ca";
930 }
931 break;
934 if (M > 1) {
935 // unported cub/le routing rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
936 pfqn_nc_refuse("default (multi-station branch: cub / le)");
937 } else if (!(Z3tot > atol)) {
938 // REFERENCE DEFECT (single-queue-no-delay lG): see _kb/03-api-layer.md (cpp port notes: pfqn)
939 long tot = 0;
940 for (int v : N3) tot += v;
941 T g = num_factorial<T>(static_cast<unsigned>(tot));
942 for (std::size_t a = 0; a < Rc; ++a)
943 g *= num_pow_int(L3(0, a), static_cast<unsigned>(N3[a])) /
944 num_factorial<T>(static_cast<unsigned>(N3[a]));
945 gcore = g;
946 res.method = "exact";
947 } else {
948 gcore = pfqn_comomrm(L3, N3, Z3).G;
949 res.method = "comom";
950 }
951 break;
952 default:
953 // Every remaining name is an estimator and returned above.
955 }
956
957 return finish(Gzdem * gcore);
958}
959
960/** Overload with the reference's default sample count, seed and tolerance. */
961template <class T>
962NcDispatchResult<T> pfqn_nc(const std::vector<T>& lambda, const Matrix<T>& L,
963 const std::vector<int>& N, const Matrix<T>& Z, NcMethod method,
964 const T& atol) {
965 return pfqn_nc(lambda, L, N, Z, method, atol, NcOptions());
966}
967
968/** Overload with the exact (zero-tolerance) filters and no open classes. */
969template <class T>
970NcDispatchResult<T> pfqn_nc(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
971 NcMethod method) {
972 return pfqn_nc(std::vector<T>(), L, N, Z, method, num_traits<T>::from_int(0), NcOptions());
973}
974
975} // namespace pfqn
976} // namespace line
977
978#endif // LINE_API_PFQN_NC_H
Malformed or inconsistent input (dimensions, negative populations, ...).
Definition error.h:37
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
Dense matrix and non-owning view.
NcResult< T > pfqn_mci(const Matrix< T > &D, const std::vector< int > &N, const std::vector< T > &Z, std::size_t samples, MciVariant variant, McRng &rng)
Monte Carlo Integration estimate of the normalizing constant of a closed product-form network (Ross,...
Definition pfqn_mci.h:94
NcResult< T > pfqn_recal(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &m0)
RECAL (REcursive CALculation) for the exact normalizing constant of a closed product-form network (Co...
Definition pfqn_recal.h:188
BktResult< T > pfqn_bkt(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Knessl-Tier expansion with the Stirling-remainder correction (BKT).
Definition pfqn_bkt.h:78
RgfmcResult< T > pfqn_rgfmc(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const T &tol, std::size_t maxterms, const T &maxcancel)
Multiclass Recursion by Generating Functions (RGF), with think times.
Definition pfqn_rgfmc.h:465
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
MvaResult< T > pfqn_mva(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &mi)
Exact Mean Value Analysis for closed product-form networks (Reiser and Lavenberg 1980).
Definition pfqn_mva.h:71
AghqResult< T > pfqn_aghq(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, std::size_t q)
Definition pfqn_aghq.h:59
NcResult< T > pfqn_mmsample2(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, std::size_t samples, McRng &rng)
Sampled McKenna-Mitra integral form of the normalizing constant of a repairman (single-queue plus del...
const char * nc_method_name(NcMethod m)
Definition pfqn_nc.h:137
PropfairResult< T > pfqn_propfair(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Proportionally fair allocation estimate of the normalizing constant (Schweitzer 1979; Walton,...
LektResult< T > pfqn_lekt(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
The common corrected asymptotic expansion (LE-KT), computed on the cheaper side.
Definition pfqn_lekt.h:87
NcMethod nc_method_of(const std::string &s)
Map a method name to its enum; throws UnsupportedError on an unknown one.
Definition pfqn_nc.h:177
BkResult< T > pfqn_bk(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Birman-Kogan saddle point normalizing constant with bottleneck detection.
Definition pfqn_bk.h:162
LsResult< T > pfqn_ls(const Matrix< T > &L0, const std::vector< T > &N, const std::vector< T > &Z, std::size_t I, McRng &rng)
Logistic-sampling estimate of the normalizing constant of a closed product-form network.
Definition pfqn_ls.h:118
BkLcResult< T > pfqn_bklc(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::string &method="mva", double tol=1e-10, int maxiter=1000)
Birman-Kogan load concealment algorithm (Algorithm 2).
Definition pfqn_bk.h:578
constexpr double CUB_MAX_EVALS
GlobalConstants.CubMaxEvals: the integrand-evaluation budget above which pfqn_nc lowers the cubature ...
ComomResult< T > pfqn_comomrm(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, int m)
CoMoM (class-oriented method of moments) for the finite repairman model: one queueing station of mult...
void pfqn_nc_refuse(const std::string &method)
Refuse a method in an arithmetic it has no meaning in.
Definition pfqn_nc.h:233
constexpr double MCMC_DEFAULT_BURNIN
Warm-up fraction discarded before accumulation starts.
Definition pfqn_mcmc.h:139
MmintResult< T > pfqn_mmint2_gausslegendre(const std::vector< T > &L, const std::vector< T > &N, const std::vector< T > &Z, int m, std::size_t nodecap)
Gauss-Legendre form on [0, 1e6] (MATLAB pfqn_mmint2_gausslegendre).
BleResult< T > pfqn_ble(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Logistic expansion estimate of the normalizing constant, bias-corrected.
Definition pfqn_ble.h:56
NcResult< T > pfqn_ca(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z)
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Definition pfqn_ca.h:120
PanaceaResult< T > pfqn_panacea(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, int terms)
PANACEA normal-usage asymptotic expansion of the normalizing constant (Ramakrishnan and Mitra,...
@ Imci
gamma = max(0.01, 1 - U), the MonteQueue 2.0 recommendation
Definition pfqn_mci.h:77
@ Mci
gamma = 1/sqrt(max N) where U > 0.9, else 1 - U
Definition pfqn_mci.h:78
RgfResult< T > pfqn_rgf(const std::vector< T > &L, int N, const T &Z)
Recursion by Generating Functions (RGF) for the normalizing constant of a SINGLE-CLASS closed product...
Definition pfqn_rgf.h:91
ClwResult< T > pfqn_clw(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< long > &m, const ClwOptions &opt)
Choudhury-Leung-Whitt normalization constant by numerical inversion of the generating function (JACM ...
Definition pfqn_clw.h:608
constexpr std::size_t MCMC_DEFAULT_BATCHES
Schmeiser (1982), the batch count used in the tables of the paper.
Definition pfqn_mcmc.h:137
BkResult< T > pfqn_bkue(const std::vector< T > &L, const T &N, const T &Z)
Birman-Kogan uniform (van der Waerden) expansion for a single chain.
Definition pfqn_bk.h:494
NcMethod
The methods this port dispatches, one per compute_norm_const case.
Definition pfqn_nc.h:101
@ Rgf
recursion by generating functions; residues beyond one class
Definition pfqn_nc.h:132
@ Adaptive
the reference groups 'adaptive' with 'default'
Definition pfqn_nc.h:103
@ Gm
the reference's alias of 'cub'
Definition pfqn_nc.h:111
@ Bkt
KT minus the exact Stirling remainder of each Laplaced class (BKT).
Definition pfqn_nc.h:113
@ Bkue
Birman-Kogan uniform (van der Waerden) expansion, single chain.
Definition pfqn_nc.h:116
@ Aghq
adaptive Gauss-Hermite over the simplex; q=1 is Le
Definition pfqn_nc.h:121
@ Lekt
the estimator Ble and Bkt both compute, on the cheaper side
Definition pfqn_nc.h:114
@ Gleint
the reference's alias of 'mmint2'
Definition pfqn_nc.h:129
@ Ger
residue closed form; free in the eliminated class populations
Definition pfqn_nc.h:134
@ Mcmc
Chen-O'Cinneide regularization; supplies X and Q, never a constant.
Definition pfqn_nc.h:126
@ Bk
Birman-Kogan saddle point with bottleneck detection.
Definition pfqn_nc.h:115
@ Ble
LE plus the empirical eps->0 correction.
Definition pfqn_nc.h:120
@ Divdiff
divided-difference closed form; no think time, no load dependence
Definition pfqn_nc.h:133
@ Lc
Birman-Kogan Algorithm 2, single chain subproblems by MVA.
Definition pfqn_nc.h:117
@ LcUe
Algorithm 2 with the uniform expansion as the single chain solver.
Definition pfqn_nc.h:118
McmcResult< T > pfqn_mcmc(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< double > &s, std::size_t samples, std::size_t nbatches, double burnin, McRng &rng)
Chen-O'Cinneide REGULARIZATION: a Markov chain Monte Carlo estimator of the class throughputs X(r) = ...
Definition pfqn_mcmc.h:158
KtResult< T > pfqn_kt(const Matrix< T > &L0, const std::vector< T > &N0, const std::vector< T > &Z0)
Knessl-Tier asymptotic expansion of the normalizing constant.
Definition pfqn_kt.h:71
ExplicitResult< T > pfqn_explicit(const Matrix< T > &L, const std::vector< int > &N, double tol=std::numeric_limits< double >::epsilon(), const std::string &method="auto", double maxloss=std::numeric_limits< double >::infinity())
Explicit closed-form normalizing constant of a multiclass closed network.
LeResult< T > pfqn_le(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Logistic expansion estimate of the normalizing constant.
Definition pfqn_le.h:245
double pfqn_cub_evals(int M, int order, double Zsum)
Integrand-evaluation count of pfqn_cub, and the budget pfqn_nc prices it against.
NcResult< T > pfqn_is(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, std::size_t samples, McRng &rng)
Importance-sampling estimate of the normalizing constant of a closed LOAD-INDEPENDENT product-form ne...
Definition pfqn_is.h:50
NcResult< T > pfqn_gerasimov(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, double tol=1e-12, std::size_t maxterms=200000)
Exact normalizing constant of a closed multiclass product-form network by ITERATED RESIDUES of its ra...
CubResult< T > pfqn_cub(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, int order, const T &atol)
Normalizing constant by Grundmann-Moeller cubature over the simplex.
Definition pfqn_cub.h:131
NcDispatchResult< T > pfqn_nc(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, NcMethod method, const T &atol, const NcOptions &nopt)
Normalizing constant of a product-form queueing network: the dispatcher.
Definition pfqn_nc.h:276
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
double nck(int n, int k)
Binomial coefficient with a thread-local memo table (mp_pfqn util/nck.c).
Definition population.h:69
Number-type abstraction for the templated API port.
Adaptive Gauss-Hermite quadrature of the simplex factor of the McKenna-Mitra integral.
Birman-Kogan asymptotic evaluation of closed networks with many stations.
Knessl-Tier expansion with the Stirling-remainder correction (BKT).
Logistic expansion with the eps->0 bias correction (BLE).
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Choudhury-Leung-Whitt normalization constant by numerical inversion of the generating function (JACM ...
CoMoM (class-oriented method of moments) for the finite repairman model: one queueing station of mult...
Normalizing constant by Grundmann-Moeller cubature over the simplex.
Integrand-evaluation count of pfqn_cub, and the budget pfqn_nc prices it against.
Explicit closed-form normalizing constant of a multiclass closed network.
Gerasimov's residue (closed-form) normalizing constant, generalized to R classes.
Importance-sampling estimate of the normalizing constant of a closed LOAD-INDEPENDENT product-form ne...
Knessl-Tier asymptotic expansion of the normalizing constant.
Logistic expansion (LE) asymptotic approximation of the normalizing constant of a closed product-form...
The common corrected asymptotic expansion (LE-KT), computed on the cheaper side.
Logistic-sampling estimate of the normalizing constant of a closed product-form network.
Monte Carlo Integration estimate of the normalizing constant of a closed product-form network (Ross,...
Chen-O'Cinneide REGULARIZATION: a Markov chain Monte Carlo estimator of the class throughputs X(r) = ...
McKenna-Mitra integral form of the normalizing constant of a repairman model (one queueing station,...
Sampled McKenna-Mitra integral form of the normalizing constant of a repairman (single-queue plus del...
Exact Mean Value Analysis for closed product-form networks (Reiser and Lavenberg 1980).
PANACEA normal-usage asymptotic expansion of the normalizing constant (Ramakrishnan and Mitra,...
Proportionally fair allocation estimate of the normalizing constant (Schweitzer 1979; Walton,...
RECAL (REcursive CALculation) for the exact normalizing constant of a closed product-form network (Co...
Recursion by Generating Functions (RGF) for the normalizing constant of a SINGLE-CLASS closed product...
Multiclass Recursion by Generating Functions (RGF), with think times.
Population-vector enumeration and combinatorics.
Return value of pfqn_bklc.
Definition pfqn_bk.h:66
std::vector< T > X
Definition pfqn_bk.h:67
Return value of pfqn_explicit, mirroring [lG, G, method, lossDigits].
std::string method
expression used, "distinct" (Eq. 15) or "repeated" (Eq. 16)
T lG
logarithm of the normalizing constant
Estimates of pfqn_mcmc together with their batch-means intervals.
Definition pfqn_mcmc.h:122
std::vector< T > X
(R) per-class throughput, empty when not produced
Definition pfqn_nc.h:244
std::string method
the algorithm actually used
Definition pfqn_nc.h:246
Matrix< T > Q
(M x R) queue lengths, empty when not produced
Definition pfqn_nc.h:245
bool valid
False when the METHOD DECLINED THE MODEL, which is the reference's lG = []: pana outside normal usage...
Definition pfqn_nc.h:260
T G
normalizing constant, exact when the method is
Definition pfqn_nc.h:242
double lG
its logarithm
Definition pfqn_nc.h:243
The options fields compute_norm_const reads beyond the method itself.
Definition pfqn_nc.h:215
double mcmc_burnin
options.config.mcmc_burnin: warm-up fraction pfqn_mcmc discards
Definition pfqn_nc.h:226
unsigned long seed
SolverOptions('NC').seed.
Definition pfqn_nc.h:217
std::size_t mcmc_batches
options.config.mcmc_batches: batches pfqn_mcmc splits its run into
Definition pfqn_nc.h:224
std::size_t aghq_nodes
options.config.aghq_nodes: nodes per simplex direction of the adaptive Gauss-Hermite rule.
Definition pfqn_nc.h:222
std::size_t samples
SolverOptions('NC').samples.
Definition pfqn_nc.h:216
double tol
handed to pfqn_comomrm
Definition pfqn_nc.h:218
Return value of pfqn_panacea, mirroring [Gn, lGn].
bool normalUsage
false where MATLAB returns NaN (min alpha < 0)