LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc_runner.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_NC_SOLVER_NC_RUNNER_H
6#define LINE_SOLVERS_NC_SOLVER_NC_RUNNER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The SolverNC class surface: `@@SolverNC/runAnalyzer.m` and the gates around it.
12 *
13 * What sits here rather than in the dispatch is everything that happens BEFORE
14 * and AFTER one inner solve: the method whitelist, the structural gates, the
15 * MULTISERVER-TO-LOAD-DEPENDENCE conversion, the conversions from response time
16 * to residence time and from throughput to arrival rate, and the metric filter.
17 *
18 * THE MULTISERVER CONVERSION IS THE INTERESTING PART, and it belongs here and
19 * not in an analyzer. A c-server station is rewritten as the rate lattice
20 * mu(n) = min(n, c), which routes the model to `solver_ncld` and is EXACT,
21 * whereas leaving it alone routes it to `solver_nc` and Seidmann's
22 * approximation. The reference does the rewrite on 'exact' and 'is' always, and
23 * on 'default' only for the two-station Delay-plus-multiserver shape (which is
24 * every SolverLN layer submodel), and only when the model is product-form --
25 * a non-product-form model has no exact load-dependent solution, so it is sent
26 * to 'comom' instead. The server count is deliberately KEPT: utilization is the
27 * fraction of the c servers busy, and c is not recoverable from min(1:Nt, c)
28 * once the population is below it.
29 *
30 * The metric filter is shared with SolverMVA verbatim (`filter_metric`,
31 * `sn_get_residt_from_respt`, `sn_get_arvr_from_tput` in `solver_mva_runner.h`)
32 * because `@@NetworkSolver/getAvg` is solver-independent: it is the same code
33 * path for both solvers in the reference.
34 */
35
37#include <algorithm>
38#include <cctype>
39#include <cmath>
40#include <limits>
41#include <string>
42#include <vector>
43
63#include "line/util/error.h"
64
65namespace line {
66namespace nc {
67
68/**
69 * Port of `SolverNC.listValidMethods`.
70 *
71 * A LISTED NAME MUST ACTUALLY RUN, or be refused with a message that names the
72 * missing analyzer. `erlangfp` and `mci` (the loss-network Erlang fixed point
73 * and its Monte Carlo counterpart) DO run: a Finite Capacity Region is now
74 * representable -- `sn.regions` and `Network::add_region` -- and
75 * `solver_nc_lossn_analyzer` claims the model above the product-form and
76 * multiserver gates. A model that is not a loss network still reaches the
77 * refusal further down, which is the honest outcome and different from silently
78 * solving a model with its region ignored. `comomld` is not in the reference's
79 * list; it is accepted because the reference selects it internally from
80 * 'default'. `ms` names the same lossn_manjunath transform as `exact` on a loss
81 * network (solver_nc_lossn.h's own method name alias) and was missing here, which
82 * blocked it before it ever reached the analyzer that already accepts it.
83 */
84inline std::vector<std::string> list_valid_methods() {
85 // "rayint" and "spm" both name the SPM saddle point on a cache, which serves
86 // cache_spm_size once the items carry storage costs. On a retrieval model
87 // "rayint" is instead the ray/WKB delayed-hit expansion, admissible only with
88 // an infinite-server fetch system; solver_nc_retrieval branches on the method name
89 // and warns and falls back to "exact" anywhere else.
90 return {"default", "exact", "rayint", "spm", "erlangfp", "mci", "imci", "ls", "le",
91 "ble", "aghq", "mmint2", "gleint", "pana", "panald", "ca", "clw", "kt", "bkt", "lekt",
92 "bk", "bkue", "lc", "lc.ue",
93 "sampling", "is", "propfair", "comom", "cub", "rgf",
94 // "divdiff" is the divided-difference closed form of Casale (SIGMETRICS
95 // 2017); it needs no think time, since a delay would ask for the integral
96 // form of Cor. 3.4, and pfqn_nc refuses one by name. Load-dependent rates
97 // ARE served: pfqn_ncld substitutes the limited load-dependent kernel of
98 // Casale-Harrison-Ong (Perform. Eval. 2021), Thm. 1, and reports itself as
99 // "divdiff.ld/...".
100 "divdiff",
101 // Chen-O'Cinneide regularization; a Markov chain Monte Carlo estimator of the
102 // throughput RATIOS G(N-e_r)/G(N), which supplies no constant of its own
103 "mcmc",
104 "ger", "rd", "nrp",
105 "nrl", "nre", "gm", "mem", "comomld", "ms",
106 // Krzesinski state-dependent routing: `solver_nc` intercepts an
107 // sdr model whatever the method says, but the gate runs first, so
108 // omitting the tokens refused the very name the other three
109 // codebases list -- and `sdr.mva` is the only way to reach the
110 // Section 4 MVA arm rather than the eq. (16) enumeration.
111 "sdr", "sdr.mva",
112 // "morrison" is the heavy-usage asymptotic expansion of the generating
113 // function for a closed think+DPS network (npfqn_dps_morrison,
114 // solver_nc_dps_analyzer). It is the DEFAULT on that shape and
115 // inadmissible anywhere else, where the runner refuses it: nothing else
116 // in NC can see the DPS weights. Non-product-form, so it returns no lG.
117 "morrison",
118 // "rec" is the MDD-rec route: the reachable set lives in a decision
119 // diagram and the product form supplies the rates. It is the ONLY
120 // method admissible on a stochastic Petri net (solver_nc_spn.h) and
121 // names the exact loss-network constant in solver_nc_lossn.h. Both
122 // routes sit BELOW check_method, so leaving the token out of this
123 // list refused the very name the SPN branch's own error tells the
124 // caller to use. The other three codebases list it.
125 "rec"};
126}
127
128/**
129 * Port of `SolverNC.isStochasticMethod`.
130 *
131 * NC is deterministic except for the Monte Carlo integrators, the logistic
132 * sampler, the importance-sampling estimators and the Chen-O'Cinneide Markov
133 * chain Monte Carlo method, whose answer depends on the seed. The name is TOKENIZED on `.` and `/` so that a runtime-resolved name
134 * such as `default/imci` and a prefixed one such as `nc.ls` classify alike.
135 */
136inline bool is_stochastic_method(const std::string& method) {
137 std::string tok;
138 std::vector<std::string> toks;
139 for (char ch : method) {
140 if (ch == '.' || ch == '/') {
141 toks.push_back(tok);
142 tok.clear();
143 } else {
144 tok += static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
145 }
146 }
147 toks.push_back(tok);
148 for (const std::string& t : toks)
149 if (t == "mci" || t == "imci" || t == "ls" || t == "sampling" || t == "is" ||
150 t == "mcmc")
151 return true;
152 return false;
153}
154
155/**
156 * Port of `SolverNC.resolveMethod`: the feature-driven resolution of
157 * `method='default'`.
158 *
159 * An open network with any non-unit SCV that MEM can carry resolves to `mem`,
160 * because the normalizing-constant path would silently exponentialize it.
161 *
162 * IT IS INERT WITH RESPECT TO THE ANALYZER, and deliberately so: MATLAB's
163 * `@@SolverNC/runAnalyzer.m` never consults `resolveMethod`, it reads
164 * `options.method` directly. Measured on an M/E2/1 (Source Exp(0.5), FCFS
165 * Erlang mean 0.5 scv 0.5), `resolveMethod` returns `mem` while
166 * `SolverNC(model).getAvg` reports `default/exact`. The function feeds the
167 * feature gate and the AUTO dispatch only, and this port matches that: nothing
168 * here calls it on the solve path. See register row N5.
169 */
170template <class T>
171std::string resolve_method(const qn::NetworkStruct<T>& L, const std::string& method) {
172 if (method != "default") return method;
173 if (!solver_nc_mem_supports(L).supported) return method;
174 for (std::size_t i = 0; i < L.nstations; ++i)
175 for (std::size_t r = 0; r < L.nclasses; ++r) {
176 if (L.disabled[i][r]) continue;
177 const double v = num_traits<T>::to_double(L.scv(i, r));
178 if (std::isfinite(v) && std::fabs(v - 1.0) > GlobalConstants::FineTol) return "mem";
179 }
180 return method;
181}
182
183/** Port of `runAnalyzerChecks`' method gate: an unlisted method is refused. */
184inline void check_method(const std::string& method) {
185 const std::vector<std::string> valid = list_valid_methods();
186 if (std::find(valid.begin(), valid.end(), method) != valid.end()) return;
187 throw UnsupportedError("SolverNC: the '" + method + "' method is unsupported by this solver");
188}
189
190namespace detail {
191
192/** Is there a node of this type? */
193template <class T>
194bool has_node_type(const qn::NetworkStruct<T>& sn, qn::NodeType ty) {
195 for (const qn::NodeDef& nd : sn.nodes)
196 if (nd.nodetype == ty) return true;
197 return false;
198}
199
200/**
201 * The cache-metric assemblers now live in `line/solvers/cache_metrics.h`, beside
202 * the struct they build, because SolverMVA's cache branches need the identical
203 * rule. Re-exported into this namespace so the call sites below read unchanged.
204 */
207
208/**
209 * Rewrite every finite multiserver station as the rate lattice mu(n)=min(n,c).
210 *
211 * @return false when the total closed population is not finite, in which case
212 * the reference leaves the model alone
213 */
214template <class T>
215bool multiserver_to_lld(qn::NetworkStruct<T>& sn) {
216 double Nt = 0.0;
217 for (const qn::JobClass& c : sn.classes) {
218 if (std::isinf(c.population)) return false;
219 Nt += c.population;
220 }
221 const std::size_t n = static_cast<std::size_t>(std::llround(Nt));
222 if (n < 1) return false;
223 for (std::size_t i = 0; i < sn.nstations; ++i) {
224 std::vector<T> lld(n, num_traits<T>::from_int(1));
225 const double c = sn.stations[i].nservers;
226 if (std::isfinite(c) && c > 1.0)
227 for (std::size_t k = 1; k <= n; ++k)
228 lld[k - 1] =
229 num_traits<T>::from_double(std::min<double>(static_cast<double>(k), c));
230 sn.stations[i].lldscaling = lld;
231 }
232 return true;
233}
234
235/**
236 * Resolves `NcSolverOptions::multiserver` into the handling this solver
237 * implements: `default`, `seidmann` or `lld`.
238 *
239 * Unknown values -- the SolverMVA approximations NC has no counterpart for
240 * (`softmin`, `conway`, `krzesinski`, `suri`, `erlang`) -- resolve to `default`
241 * rather than throwing, because one options object is commonly reused across
242 * solvers. They are NOT honoured silently: `warning`, when given, receives the
243 * text the other three codebases pass to `line_warning`. The port has no
244 * `line_warning` channel, so a warning travels on `NcSolution::warning`, which
245 * `line-cli` prints to stderr; see `NcSolution::warning`.
246 */
247inline std::string nc_multiserver_policy(const std::string& requested,
248 std::string* warning = nullptr) {
249 if (requested.empty() || requested == "default") return "default";
250 if (requested == "seidmann") return "seidmann";
251 if (requested == "lld" || requested == "exact" || requested == "loaddep" ||
252 requested == "load-dependent")
253 return "lld";
254 if (warning != nullptr)
255 *warning = "SolverNC does not implement config.multiserver='" + requested +
256 "' (it is a SolverMVA approximation); using 'default'. SolverNC accepts "
257 "'default', 'seidmann' and 'lld'.";
258 return "default";
259}
260
261/**
262 * The per-chain population lattice `prod(1+Nchain)`, which prices the exact
263 * enumeration `solver_ncld` performs.
264 */
265template <class T>
266double nc_population_lattice(const qn::NetworkStruct<T>& sn) {
267 double lattice = 1.0;
268 if (!sn.chains.empty()) {
269 // sn.chains is (nchains x nclasses) as vector<vector<bool>>, not a Matrix
270 for (std::size_t c = 0; c < sn.chains.size(); ++c) {
271 double popc = 0.0;
272 for (std::size_t r = 0; r < sn.nclasses && r < sn.chains[c].size(); ++r)
273 if (sn.chains[c][r]) {
274 const double v = sn.classes[r].population;
275 if (std::isfinite(v)) popc += v;
276 }
277 lattice *= (1.0 + popc);
278 }
279 } else {
280 for (const qn::JobClass& c : sn.classes)
281 if (std::isfinite(c.population)) lattice *= (1.0 + c.population);
282 }
283 return lattice;
284}
285
286} // namespace detail
287
288/**
289 * Is the closed model in NORMAL USAGE, the domain of the Mitra-McKenna PANACEA
290 * asymptotic expansion (J. ACM 33(3), 1986)?
291 *
292 * Normal usage asks that every queueing centre absorb the load the think
293 * stations offer it: with rho_j0 = Ztot(j) the aggregate think demand of chain
294 * j, r_ij = L_ij / rho_j0 and mu_i(Ntot) the saturation rate,
295 *
296 * alpha_i = 1 - (sum_j N_j r_ij) / mu_i(Ntot) > 0 at every centre i.
297 *
298 * Outside it the {phi(n)} series DIVERGES, which is why `pfqn_panaceald` returns
299 * NaN there and `pfqn_ncld` turns that NaN into a refusal rather than a warning.
300 * It is a property of the DEMANDS and not of a declared construct, so it has no
301 * feature-registry name and cannot live in `nc_feature_set`.
302 *
303 * The rates are the ones `solver_ncld` would build: one for an ordinary single
304 * server, min(n, c) for a finite multiserver (the conversion the runner performs
305 * on the 'panald' arm), and the declared lldscaling row when the model sets
306 * one. An infinite server is a think station and feeds Ztot.
307 */
308template <class T>
310 if (sn.has_open_classes()) return true; // no rho_j0 to expand around
311 const std::size_t M = sn.nstations, C = sn.nchains;
312 const std::vector<double> Nchain = detail::chain_population(sn);
313 double NtD = 0.0;
314 for (std::size_t c = 0; c < C; ++c)
315 if (std::isfinite(Nchain[c])) NtD += Nchain[c];
316 const std::size_t Nt = static_cast<std::size_t>(std::llround(NtD));
317 if (Nt < 1) return true; // the empty network: G = 1, nothing to expand
318
320
321 std::vector<double> Ztot(C, 0.0);
322 for (std::size_t i = 0; i < M; ++i)
323 if (std::isinf(sn.stations[i].nservers))
324 for (std::size_t c = 0; c < C; ++c)
325 Ztot[c] += num_traits<T>::to_double(d.Lchain(i, c));
326 for (std::size_t c = 0; c < C; ++c)
327 if (Nchain[c] > 0.0 && !(Ztot[c] > 0.0))
328 return false; // no think station on a populated chain's route
329
330 for (std::size_t i = 0; i < M; ++i) {
331 const double nserv = sn.stations[i].nservers;
332 if (std::isinf(nserv)) continue;
333 const std::vector<T>& lld = sn.stations[i].lldscaling;
334 double muK;
335 if (!lld.empty())
336 muK = num_traits<T>::to_double(lld[std::min(Nt, lld.size()) - 1]);
337 else if (nserv > 1.0)
338 muK = std::min(static_cast<double>(Nt), nserv);
339 else
340 muK = 1.0;
341 if (!(muK > 0.0) || !std::isfinite(muK)) return false;
342 double lambda = 0.0;
343 for (std::size_t c = 0; c < C; ++c)
344 if (Ztot[c] > 0.0)
345 lambda += num_traits<T>::to_double(d.Lchain(i, c)) / Ztot[c] * Nchain[c];
346 if (!(1.0 - lambda / muK > 0.0)) return false;
347 }
348 // Every centre cleared the test; a model with no queueing centre at all
349 // reaches here too, and there the delay-only constant is exact.
350 return true;
351}
352
353/**
354 * How many queueing (non-infinite-server) stations carry demand from a CLOSED
355 * chain?
356 *
357 * That is the row count L reaches `pfqn_nc` and `pfqn_comomrm_ld` with, once the
358 * delay rows have been folded into Z and the zero-demand rows dropped. Zero when
359 * the model has no closed population at all.
360 */
361template <class T>
363 const std::size_t M = sn.nstations, C = sn.nchains;
364 const std::vector<double> Nchain = detail::chain_population(sn);
365 bool any_closed = false;
366 std::vector<bool> closed(C, false);
367 for (std::size_t c = 0; c < C; ++c) {
368 closed[c] = std::isfinite(Nchain[c]) && Nchain[c] > 0.0;
369 if (closed[c]) any_closed = true;
370 }
371 if (!any_closed) return 0;
372
374 std::size_t nq = 0;
375 for (std::size_t i = 0; i < M; ++i) {
376 if (std::isinf(sn.stations[i].nservers)) continue;
377 for (std::size_t c = 0; c < C; ++c)
378 if (closed[c] &&
380 ++nq;
381 break;
382 }
383 }
384 return nq;
385}
386
387/**
388 * May `method` run on this model? "" when it may, otherwise the reason it may
389 * not, in the words the runner refuses with.
390 *
391 * ONE PREDICATE, TWO CALLERS. `solver_nc_solve` asks it once, ahead of the
392 * dispatch, and throws on a non-empty answer; `auto_family_refusal` asks it so
393 * that `auto_find_solver` never offers a (family, method) pair that would throw,
394 * and so that the ranking never delegates to one. Two copies of these rules is
395 * precisely how the report and the run drift apart, which is the failure this
396 * function exists to prevent, so a new rule goes here and not at a call site.
397 *
398 * ONLY WHAT THE FEATURE REGISTRY CANNOT NAME LIVES HERE. A feature set declares
399 * what the method ACCEPTS, so it can refuse a model for HAVING a construct and
400 * never for lacking one: "closed population only" and "no think time" are said
401 * in `qn::nc_feature_set` by dropping OpenClass and SchedStrategy_INF, while
402 * "requires a cache", "requires state-dependent routing", "requires a loss
403 * network", "requires exactly two stations" and "requires normal usage" have no
404 * such form and are decided here.
405 *
406 * `for_report` says WHICH QUESTION IS BEING ASKED, and for two method names the
407 * two questions have different answers:
408 *
409 * true -- "should `auto_find_solver` offer this pair?" A pair that comes back
410 * as a table of zeros must not be offered, so the answer is no.
411 * false -- "what does the reference DO when asked for it by name?" For 'mmint2'
412 * and 'gleint' outside their shape the reference deliberately WARNS AND
413 * RETURNS A ZERO TABLE (pfqn_nc.m, case {'mmint2','gleint'}: lG = [] and
414 * return, unconditionally), and a caller who names the method keeps that
415 * answer -- which is what `test_nc.cpp` pins.
416 *
417 * THE ASYMMETRY IS A RULING, NOT AN OVERSIGHT (2026-07-25, reaffirmed when this
418 * gate was added): the report answers "should this be offered" and the run answers
419 * "what does the reference do". 'comomld' is NOT in that bucket --
420 * `pfqn_comomrm_ld` refuses "The solver accepts at most a single queueing station."
421 * natively -- so it is refused on both paths.
422 *
423 * @param sn the refreshed struct
424 * @param method the concrete method name
425 * @param slotted true on the discrete-time route, which answers for itself
426 * @param for_report true when the caller is the report, false when it is the run
427 */
428template <class T>
429std::string nc_method_refusal(const qn::NetworkStruct<T>& sn, const std::string& method,
430 bool slotted = false, bool for_report = true) {
431 const std::string m = method.empty() ? std::string("default") : method;
432
433 // The discrete-time route answers for itself: `solver_nc_dt` decides
434 // admissibility on the slot lattice, and every gate below is written about a
435 // continuous-time queueing network.
436 if (slotted) return "";
437
438 // -- discriminatory processor sharing ---------------------------------
439 // Morrison's heavy-usage expansion is the ONLY NC route that can see the DPS
440 // weights; every other method builds a product-form normalizing constant
441 // that silently drops them and answers with the egalitarian-PS network,
442 // which is a wrong number rather than a coarse one.
443 if (nc_is_dps_model(sn)) {
444 if (m != "default" && m != "morrison")
445 return "SolverNC: method '" + m +
446 "' cannot represent the DPS weights of a discriminatory processor-sharing "
447 "station; it would return the egalitarian-PS network. Use method 'default' or "
448 "'morrison' (npfqn_dps_morrison), SolverMVA, SolverFLD or SolverCTMC.";
449 return "";
450 }
451 if (sn_has_dps(sn))
452 // A DPS station outside Morrison's shape. SchedStrategy_DPS is declared
453 // in the feature set because a boolean feature cannot express "this shape
454 // only"; this is that imperative half.
455 return "SolverNC analyzes a discriminatory processor-sharing station only in the shape "
456 "Morrison's expansion is derived for: a CLOSED network of exactly two stations, one "
457 "infinite-server (think) station and one single-server DPS station, exponential "
458 "service, each class visiting the two equally often. Use SolverMVA, SolverFLD or "
459 "SolverCTMC for any other DPS model.";
460 if (m == "morrison")
461 // The method named on a model that is not the shape at all -- not even a
462 // DPS station in it. Left ungated it reaches no route of its own and
463 // falls through to the ordinary normalizing-constant path, which would
464 // answer the product-form model UNDER THE CALLER'S LABEL.
465 return "SolverNC: method 'morrison' is the heavy-usage expansion of a CLOSED network of "
466 "exactly two stations, one infinite-server (think) station and one single-server DPS "
467 "station with exponential service, which this model is not. Remove the method option "
468 "to let SolverNC choose, or use SolverMVA, SolverFLD or SolverCTMC.";
469
470 // -- Krzesinski state-dependent routing -------------------------------
471 // An SDR model is intercepted by `solver_nc_sdr` whatever the method says,
472 // so reaching the second test means the model declares none.
473 if (!sn.sdr.branch.empty()) return "";
474 if (m == "sdr" || m == "sdr.mva")
475 return "SolverNC: method '" + m +
476 "' requires state-dependent routing, which this model does not declare.";
477
478 // -- stochastic Petri net ---------------------------------------------
479 // A net is served only by the MDD-rec route, and none of the gates below --
480 // written about stations, capacities and the queueing-network product form --
481 // says anything about a net. `spn_pf` decides its product-form class, by name.
482 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
483 if (sn.nodes[ind].nodetype == lang::NodeType::Place) {
484 if (m != "default" && m != "rec")
485 return "solver_nc: a stochastic Petri net is solved by the MDD-rec route; method '" +
486 m + "' is a normalizing-constant algorithm for queueing networks. Use 'rec' "
487 "or 'default'";
488 return "";
489 }
490
491 // -- order-independent stations ---------------------------------------
492 // Every method other than the four listed reads the single-job rate mu([r])
493 // of an OI station: the rank rate mu(n) is silently dropped and the answer is
494 // that of an ordinary queue.
495 if (nc_is_oi_model(sn)) {
496 if (m != "default" && m != "exact" && m != "is" && m != "sampling")
497 return "SolverNC: method '" + m +
498 "' cannot represent the rank rate mu(n) of an order-independent station; use "
499 "method 'default' or 'exact' (pfqn_ncoi), 'is', SolverMVA, or SolverCTMC.";
500 return "";
501 }
502
503 // -- caches -------------------------------------------------------------
504 // 'rayint' and 'spm' both name the SPM saddle point of a cache (and, on a
505 // retrieval model, the ray/WKB delayed-hit expansion), so they are admissible
506 // here and nowhere else.
507 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
508 if (sn.nodes[ind].nodetype == qn::NodeType::Cache) {
509 if (m == "exact" && nc_is_noreentrant_cache(sn)) {
510 const auto itp = sn.nodeparam.find(ind + 1);
511 // cache_prob_erec is exact for the exchangeable (RR/FIFO) family
512 // only; a recency-based policy would silently receive the
513 // exchangeable answer, so the exact route refuses it.
514 if (itp != sn.nodeparam.end() &&
515 itp->second.replacestrat != lang::ReplacementStrategy::RR &&
516 itp->second.replacestrat != lang::ReplacementStrategy::FIFO)
517 return "solver_nc_cache_analyzer: NC does not support the exact solution of "
518 "this cache replacement policy -- only RR and FIFO are exchangeable, "
519 "and a recency-based policy (LRU, h-LRU, q-LRU, CLIMB) would silently "
520 "receive the exchangeable answer. Use the default (approximate) method "
521 "or SolverCTMC";
522 }
523 return "";
524 }
525 if (m == "rayint" || m == "spm")
526 return "SolverNC: method " + m +
527 " names the SPM saddle point of a cache and, on a retrieval model, the ray/WKB "
528 "delayed-hit expansion; this model declares no Cache node.";
529
530 // -- loss networks and finite capacity regions --------------------------
531 if (nc_is_lossn_model(sn)) return ""; // 'ms', 'erlangfp', 'rec' all run here
533 return "SolverNC: the Finite Capacity Region holds a single infinite server but does not "
534 "apply DROP to every class; holding an arrival back (WAITQ) or blocking the server "
535 "(BAS/BBS/RSRD) keeps the job in the region while it waits, which the Erlang loss "
536 "model has no state for -- use DROP, or SolverCTMC/SolverJMT";
537 if (m == "erlangfp")
538 return "SolverNC: the 'erlangfp' Erlang fixed point applies only to a loss network, which "
539 "is an open model whose single Delay sits inside a Finite Capacity Region under a "
540 "DROP rule; this model declares no such region (see nc_is_lossn_model)";
541 if (m == "ms")
542 return "SolverNC: method 'ms' is admissible only on a loss network (open model, one DROP "
543 "region holding a single Delay).";
544 if (m == "rec")
545 return "SolverNC: method rec is the MDD-rec route, admissible on a stochastic Petri net or "
546 "on a loss network (open model, one DROP region holding a single Delay); this model "
547 "is neither.";
548 if (!sn.regions.empty())
549 return "SolverNC: this model applies a Finite Capacity Region to queueing stations, whose "
550 "aggregate population limit no normalizing-constant algorithm here enforces; only "
551 "the loss network -- one region over a single infinite server, DROP on every "
552 "class -- is solvable. Use SolverCTMC or SolverJMT, or setCapacity for a "
553 "single-station limit";
554
555 // -- PANACEA's domain ----------------------------------------------------
556 // Normal usage is a property of the demands rather than of a declared
557 // construct, so it has no feature name; an open chain is refused earlier by
558 // the closed-population feature set of the load-dependent evaluators.
559 //
560 // BOTH TOKENS ARE GATED, because `pfqn_ncld` evaluates 'pana' and
561 // 'panald' with the SAME `pfqn_panaceald` -- its case label covers both --
562 // so on a model carrying a rate lattice the load-INDEPENDENT name reaches the
563 // load-dependent expansion and throws with it. Off that lattice 'pana'
564 // takes its own `pfqn_nc` arm, which warns and returns an empty constant
565 // rather than throwing, so it is left alone there. CLASS-dependent scaling
566 // diverts the whole model to `solver_nc_conv`, which never reads the method
567 // at all -- and only class-dependent, because that is the one `solver_ncld`
568 // diverts on in this port; a joint-dependent model still reaches the kernel.
569 if ((m == "pana" || m == "panald") && !sn.has_open_classes()) {
570 bool diverted_to_conv = false, any_lld = false;
571 for (std::size_t i = 0; i < sn.nstations; ++i) {
572 if (static_cast<bool>(sn.stations[i].cdscaling)) diverted_to_conv = true;
573 if (!sn.stations[i].lldscaling.empty()) any_lld = true;
574 }
575 const bool reaches_ld_kernel = (m == "panald") || any_lld;
576 if (!diverted_to_conv && reaches_ld_kernel && !nc_is_normal_usage(sn)) {
577 const std::string why =
578 "the model is not in normal usage, so the 'panald' asymptotic expansion does "
579 "not apply. Use 'exact', 'clw' or an approximate load-dependent method instead.";
580 if (m == "pana")
581 return "SolverNC: method 'pana' reaches the load-dependent kernel on this "
582 "model, where pfqn_ncld evaluates it as 'panald', and " + why;
583 return "SolverNC: " + why;
584 }
585 }
586
587 // -- the single-queueing-station recursions --------------------------------
588 // Two families are stated for a model with a delay and ONE queueing station,
589 // and neither can say so with a feature name: it is a COUNT, and a feature
590 // set has no arithmetic. `pfqn_nc` states it for 'mmint2'/'gleint' in those
591 // words and `pfqn_comomrm_ld` refuses with "The solver accepts at most a
592 // single queueing station."
593 //
594 // The count is taken over the CLOSED chains only, and the rule is inactive
595 // without a closed population, because `pfqn_nc` answers an open network with
596 // the exact open formulas BEFORE its method switch -- the method name is never read
597 // there, so a purely open model with three queues runs these names correctly
598 // today and must go on doing so.
599 // 'mmint2' and 'gleint' are gated for the REPORT ONLY: `pfqn_nc` answers them
600 // with an empty constant and the caller renders a table of zeros, which is a
601 // pair the report must not offer and a run the reference nonetheless performs.
602 // See `for_report` above.
603 if (m == "comomld" || (for_report && (m == "mmint2" || m == "gleint"))) {
604 const std::size_t nq = nc_closed_queueing_stations(sn);
605 if (nq > 1) {
606 if (m == "comomld")
607 return "SolverNC: method 'comomld' is the load-dependent CoMoM recursion, and "
608 "pfqn_comomrm_ld accepts at most a single queueing station; this model has " +
609 std::to_string(nq) + ".";
610 return "SolverNC: the '" + m +
611 "' method requires a model with a delay and a single queueing station; this "
612 "model has " + std::to_string(nq) + ".";
613 }
614 }
615
616 // -- 'exact' outside its domain ------------------------------------------
617 if (m == "exact" && sn.has_open_classes()) {
618 for (std::size_t i = 0; i < sn.nstations; ++i)
619 if (std::isfinite(sn.stations[i].nservers) && sn.stations[i].nservers > 1.0)
620 return "solver_nc_analyzer: the NC solver cannot provide exact solutions for open "
621 "or mixed multiserver queueing networks. Remove the 'exact' option";
622 }
623 return "";
624}
625
626
627/**
628 * The gates, the multiserver conversion and the dispatch of
629 * `@@SolverNC/runAnalyzer.m`, without the metric filter.
630 *
631 * Kept separate so that the reported log normalizing constant comes from the
632 * SAME model the metrics do: reaching the dispatch without the conversion above
633 * would evaluate the constant of the Seidmann-approximated network while the
634 * table reported the load-dependent one.
635 *
636 * A model with a Fork is solved through the shared fork-join fixed point
637 * (`fj_driver.h`), which drives `nc_dispatch` as its inner solve on the
638 * transformed model; a model without one runs the dispatch exactly once.
639 */
640template <class T>
642 check_method(opt_in.method);
643 // runAnalyzerChecks' universal feature gate, AFTER the method gate. Gated on
644 // `L_in`, before multiserver_to_lld rewrites lldscaling. NC DECLARES Region,
645 // so a loss network passes straight through to the imperative split below.
646 qn::feature_gate("SolverNC", qn::nc_feature_set(opt_in.method), L_in);
647
648 // THE STRUCTURAL METHOD GATE, asked once and in one place.
649 //
650 // `nc_method_refusal` holds every rule of the form "this method has no route
651 // on this model": the DPS shape, state-dependent routing, the Petri net, the
652 // order-independent rank rate, the cache and loss-network tokens, PANACEA's
653 // normal usage. `auto_family_refusal` asks the SAME function, which is what
654 // keeps `auto_find_solver` from offering a pair that would throw here.
655 {
656 // for_report=false: this is the RUN, and it asks what the reference DOES
657 // rather than what the report should offer. The two answers differ for
658 // 'mmint2'/'gleint', which `pfqn_nc` answers with an empty constant and a
659 // zero table; see `nc_method_refusal`.
660 const std::string refusal =
661 nc_method_refusal(L_in, opt_in.method, opt_in.slotted, /*for_report=*/false);
662 if (!refusal.empty()) throw UnsupportedError(refusal);
663 }
664
665 NcSolverOptions opt = opt_in;
666 qn::NetworkStruct<T> L = L_in;
667 NcSolution<T> res;
668
669 // Discrete time before every other gate: the slot lattice is a property of
670 // the MODEL, not of a method, and the imperative gates below (finite
671 // capacity, the multiserver-to-lldscaling rewrite, the load-dependence
672 // routing) all assume a continuous time scale. `solver_nc_dt` refuses a
673 // model outside the discrete-time product form rather than falling through.
674 if (opt.slotted) return solver_nc_dt(L, opt);
675
676 // THE STRUCTURAL FINITE-CAPACITY GATE, port of `@@SolverNC/SolverNC.m:180-188`.
677 //
678 // It was missing here, and its absence was not a missing message: a closed
679 // two-queue model with setCapacity(1) on the second station SOLVED, and
680 // reported the UNCONSTRAINED product-form answer (QLen 0.852459/1.147541,
681 // the values of the same model with no buffer at all) where MATLAB, the JAR
682 // and python all refuse by name. A wrong number, silently, is the failure
683 // mode `check_binding_capacity` exists to stop -- SolverMVA has called it
684 // since the port (`mva_check_finite_capacity`) and NC never did.
685 //
686 // AFTER THE SLOTTED RETURN, as in the reference: a finite buffer on a
687 // Bernoulli server is the loss system of Daduna's corollary 2.8, which
688 // `solver_nc_dt` solves exactly, so this gate must not see it.
689 //
690 // THE ONE EXEMPTION IS mem.blocking, which represents the buffer as a
691 // censored GE/GE/c/0;N queue and therefore DOES honour it. The reference
692 // exempts one more, the single-station M/M/1/K with tail drop that its
693 // `qsys_mm1k_loss` branch answers exactly; this port has no such branch in
694 // `nc_dispatch`, so that shape is refused here rather than answered
695 // unconstrained. Add the exemption in the same change that ports the branch.
696 //
697 // IT READS `opt.method`, NOT `resolve_method`, and the difference is not
698 // cosmetic. `nc_dispatch` reaches the MEM algorithm on the literal name --
699 // `if (opt.method == "mem")` -- and nothing on the solve path resolves
700 // `default` into it, so exempting a `default` run because `resolve_method`
701 // would have called it `mem` skips the gate and then dispatches somewhere
702 // that does NOT honour the buffer. That is the very failure this gate
703 // exists to stop. (The reference gates on its resolved method and its
704 // analyzer reads `options.method` directly, so MATLAB has the same seam;
705 // do not copy it.)
706 {
707 bool mem_blocking = false;
708 if (opt.method == "mem") {
710 mem_blocking = ms.supported && ms.blocking;
711 }
712 if (!mem_blocking) qn::check_binding_capacity("SolverNC", L);
713 }
714
715 if (L.has_fork()) {
716 // The transform turns the fork into a router, the join into a delay and
717 // the branches into auxiliary open classes. `nc_dispatch` then sees a
718 // plain mixed network, which is why none of the specialised NC routes
719 // has to know about forks.
721 std::vector<T> lam(tr.V.classes.size() + 1,
723 mva::MvaOptions mopt;
724 mopt.method = opt.method;
725 mopt.tol = opt.tol;
726 mopt.iter_tol = opt.iter_tol;
727 mopt.iter_max = opt.iter_max;
728 mopt.fork_join = opt.fork_join;
729 mopt.base_has_fork = true;
730 NcSolverOptions inner = opt;
731 inner.base_has_fork = true;
732 std::string am;
733 res.sol = mva::fj_fixed_point(L, tr, lam, mopt, [&inner, &am](qn::NetworkStruct<T>& V) {
734 const NcSolution<T> d = nc_dispatch(V, inner);
735 am = d.actualmethod;
736 return d.sol;
737 });
738 res.actualmethod = am;
739 return res;
740 } else {
741 // Closed think+DPS network -> Morrison's heavy-usage generating-function
742 // expansion, the DEFAULT for that shape. Intercepted FIRST, ahead of every
743 // other branch: "SchedStrategy_DPS" is now inside the solver's reach, and
744 // each of the branches below would silently drop the weights and answer
745 // with the egalitarian-PS network. Not a product-form route: lG is NaN.
746 if (nc_is_dps_model(L)) return solver_nc_dps_analyzer(L, opt);
747 // The four refusal arms that used to follow -- another method on a DPS
748 // model, a DPS station outside Morrison's shape, "morrison" on a model with
749 // no DPS station at all, and "sdr"/"sdr.mva" on a model declaring no
750 // state-dependent routing -- moved into `nc_method_refusal` above, with
751 // their wording unchanged.
752
753 // Order-independent networks are intercepted BEFORE the
754 // multiserver-to-lldscaling rewrite below: an OI station is multiserver
755 // but is not a plain min(n,c) load-dependent station, so the rewrite
756 // would send it down a path that cannot represent its rate function.
757 if (nc_is_oi_model(L) && (opt.method == "default" || opt.method == "exact"))
758 return solver_nc_oi_analyzer(L, opt);
759
760 // A loss network is intercepted above the gates below: it is judged by
761 // has_product_form on the 'exact' path and its infinite server would be
762 // run through the multiserver-to-lldscaling rewrite, neither of which
763 // applies to a model whose only dynamics are the region's admission rule.
764 if (nc_is_lossn_model(L)) return solver_nc_lossn_analyzer(L, opt).sol;
765
766 // A stochastic Petri net takes the MDD-rec route: the reachable set
767 // lives in a decision diagram and the product form supplies the rates,
768 // so none of the queueing-network gates below say anything about it.
769 // `spn_pf` is where a net's product-form class is decided, by name.
770 for (std::size_t ind = 0; ind < L.nodes.size(); ++ind)
771 if (L.nodes[ind].nodetype == lang::NodeType::Place)
772 return solver_nc_spn_analyzer(L, opt).sol;
773
774 bool multiserver = false;
775 for (const qn::Station<T>& st : L.stations)
776 if (std::isfinite(st.nservers) && st.nservers > 1.0) multiserver = true;
777 bool anyLld = false;
778 for (const qn::Station<T>& st : L.stations)
779 if (!st.lldscaling.empty()) anyLld = true;
780
781 // How this model's finite multiserver stations are represented. The
782 // shipped "default" reproduces the historical dispatch exactly, so no
783 // result moves unless config.multiserver is set.
784 std::string ms_warning;
785 const std::string ms_policy =
786 detail::nc_multiserver_policy(opt.multiserver, &ms_warning);
787
788 if (opt.method == "default") {
789 // The two-station Delay-plus-multiserver shape: every SolverLN layer
790 // submodel. Exact through load-dependent CoMoM when product-form,
791 // Seidmann through 'comom' when not.
792 if (L.nstations == 2 && !detail::has_node_type(L, qn::NodeType::Cache) &&
793 detail::has_node_type(L, qn::NodeType::Delay) && multiserver) {
794 if (L.has_product_form() && !anyLld) {
795 if (detail::multiserver_to_lld(L)) anyLld = true;
796 } else {
797 opt.method = "comom";
798 }
799 } else if (ms_policy == "lld" && multiserver && !anyLld && L.has_product_form() &&
800 detail::nc_population_lattice(L) <= 6000.0) {
801 // config.multiserver="lld" generalizes the exact load-dependent
802 // lattice of the branch above to any closed product-form model,
803 // under the same 6000-state enumeration budget. Off unless asked
804 // for: with the shipped "default" policy this branch never runs
805 // and the model keeps Seidmann's approximation, as it always has.
806 if (detail::multiserver_to_lld(L)) anyLld = true;
807 }
808 } else if (opt.method == "exact" || opt.method == "is" || opt.method == "panald") {
809 // 'is' and 'panald' need the same model as 'exact', i.e. the same
810 // multiserver conversion.
811 //
812 // 'is' on an OI / P&S model is the specialised sampler, not the
813 // generic one: an OI station is multiserver but not min(n,c), and a
814 // P&S tandem has only per-communicating-class product form, so
815 // has_product_form is false and the gate below would reject it.
816 // nc_is_oi_model too, not only nc_is_pas_model: the latter demands
817 // that BOTH stations be OI/PAS, so a Delay + OI cycle -- the canonical
818 // topology pfqn_oi_is exists to sample -- fell to the guard below.
819 if (opt.method == "is" && (nc_is_pas_model(L) || nc_is_oi_model(L))) {
820 // handled by solver_nc_analyzer (pfqn_pas_is / pfqn_oi_is)
821 } else if (nc_is_lossn_model(L)) {
822 // A LOSS NETWORK (open, one DROP region holding a single Delay)
823 // IS product form -- the truncated Poisson law the residue
824 // transform of solver_nc_lossn evaluates exactly under 'exact'
825 // -- but any region reads as blocking, so has_product_form says
826 // no. Exempted here as `nc_method_refusal` already exempts it.
827 } else if (!L.has_product_form())
828 throw UnsupportedError(
829 "SolverNC: the '" + opt.method +
830 "' method requires the model to have a product-form solution, and this model "
831 "does not");
832 // Only a GENUINE multiserver is converted: filling lldscaling with
833 // ones on an all-single-server model is a semantic no-op that forces
834 // the load-dependent path, whose recovery mishandles a
835 // single-station-confined closed chain.
836 // config.multiserver="seidmann" asks for Seidmann's approximation on
837 // this arm too, so the conversion is skipped. Off by default.
838 if (!anyLld && multiserver && ms_policy != "seidmann") {
839 if (detail::multiserver_to_lld(L)) anyLld = true;
840 }
841 }
842
843 // Caches, in the reference's order: a delayed-hit retrieval system
844 // first (open or closed), then the non-reentrant Source-Cache-Sink
845 // model, then any other cache as an integrated caching-queueing network.
846 // The order is the contract -- a retrieval cache is ALSO a cache, and a
847 // Source-Cache-Sink model is also "a model with a cache node".
848 if (nc_has_retrieval(L)) {
849 if (detail::has_node_type(L, qn::NodeType::Source)) {
851 rr.sol.cache = detail::cache_metrics_of(L, rr.hitprob, rr.missprob, rr.delayedprob,
852 rr.latency, rr.hitproblist, rr.itemprob,
853 std::vector<T>());
854 return rr.sol;
855 }
857 cr.sol.cache = detail::cache_metrics_of(L, cr.hitprob, cr.missprob, cr.delayedprob,
858 cr.latency, cr.hitproblist, Matrix<T>(),
859 std::vector<T>());
860 return cr.sol;
861 }
864 // The non-reentrant model reports the per-list hit fractions and the
865 // per-item law but no scalar hit probability of its own: the hit and
866 // miss columns are the row sums of `hitproblist` and its complement,
867 // and `cache_metrics_of` derives them rather than leaving the total
868 // row of getAvgCacheTable empty on the one branch that knows most.
869 cr.sol.cache = detail::cache_metrics_of(L, cr.hitprob, cr.missprob,
870 std::vector<T>(), std::vector<T>(),
871 cr.hitproblist, cr.itemprob, cr.listcost);
872 return cr.sol;
873 }
874 if (nc_is_cacheqn(L)) {
876 res = cr.sol;
878 // hitprob/missprob are (ncaches x nclasses) here, one row per cache,
879 // against the (K) vectors the retrieval branches return.
880 res.cache = detail::cache_metrics_of_matrix(L, cr.hitprob, cr.missprob);
881 // the per-item law rides in separately: this branch has one per cache
882 // rather than one per model
883 for (std::size_t ci = 0; ci < res.cache.caches.size() && ci < cr.itemprob.size(); ++ci)
884 res.cache.caches[ci].itemprob = cr.itemprob[ci];
885 return res;
886 }
887
888 // A region this solver cannot enforce must STOP the solve, not be
889 // dropped from it. Everything below computes an unconstrained answer,
890 // so a model that reaches it with a live region would be reported as
891 // though the region were absent -- numbers that are not wrong for any
892 // model the user described. The reference raises at the same two points
893 // (`runAnalyzer.m:326,334-336`) for the same reason.
894 //
895 // The loss-network SHAPE with a rule other than all-DROP is named
896 // separately from a region on ordinary queueing stations, because the
897 // two have different remedies: the first is asking for blocking, where
898 // switching the rule to DROP makes the model solvable here, while the
899 // second needs a solver that carries the region as state.
900 // The token gates that used to stand here -- the loss-network shape under
901 // a rule other than all-DROP, a Finite Capacity Region on queueing
902 // stations, "erlangfp"/"ms"/"rec" off a loss network and "rayint"/"spm"
903 // with no Cache node -- moved into `nc_method_refusal`, which decides them
904 // from the same struct before the dispatch begins and which
905 // `auto_family_refusal` asks too; their wording is unchanged. The six
906 // load-dependent evaluators on an OPEN chain are now refused by the
907 // feature set instead (`nc_feature_set` drops OpenClass from them):
908 // "closed population only" is a rule the registry CAN name, and naming it
909 // there is what makes `auto_find_solver` drop the row rather than report
910 // it runnable.
911
912 if (anyLld || nc::detail::has_scaling(L)) {
913 res = solver_ncld_analyzer(L, opt);
914 } else if (opt.method == "rd" || opt.method == "nrp" || opt.method == "nrl" ||
915 opt.method == "nre" || opt.method == "comomld" || opt.method == "panald") {
916 res = solver_ncld_analyzer(L, opt);
917 } else {
918 res = solver_nc_analyzer(L, opt);
919 }
920 // A config.multiserver value this solver does not implement was silently
921 // ignored before; it now says so. Joined rather than overwriting, as the
922 // analyzer warnings are.
923 if (!ms_warning.empty())
924 res.warning = res.warning.empty() ? ms_warning : res.warning + " " + ms_warning;
925 return res;
926 }
927}
928
929/**
930 * Port of `@@SolverNC/runAnalyzer.m` for the `lang='matlab'` path: solve, then
931 * apply the metric filter `@@NetworkSolver/getAvg` puts between the analyzer and
932 * the caller.
933 */
934template <class T>
936 const std::string origmethod = opt_in.method;
937 const NcSolution<T> d = solver_nc_solve(L_in, opt_in);
938 const mva::MvaSolution<T>& s = d.sol;
939 const std::string& actualmethod = d.actualmethod;
940 const qn::NetworkStruct<T>& L = L_in;
941
942 const std::size_t M = L.nstations, K = L.nclasses;
943 std::vector<std::vector<bool>> mask(M, std::vector<bool>(K, false));
944 for (std::size_t i = 0; i < M; ++i)
945 for (std::size_t k = 0; k < K; ++k)
946 mask[i][k] = num_traits<T>::to_double(s.R(i, k)) < 10.0 * GlobalConstants::FineTol;
947 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(K, false));
948 for (std::size_t i = 0; i < M; ++i)
949 if (L.stations[i].nodetype == qn::NodeType::Source)
950 for (std::size_t k = 0; k < K; ++k) srcmask[i][k] = true;
951
953 out.QN = mva::filter_metric(L, s.Q, mva::MetricKind::QLen, &mask);
954 out.UN = mva::filter_metric(L, s.U, mva::MetricKind::Util, &mask);
955 out.RN = mva::filter_metric(L, s.R, mva::MetricKind::RespT, nullptr);
956 out.TN = mva::filter_metric(L, s.Tp, mva::MetricKind::Tput, nullptr);
957 // Both ArvR and ResidT come from the refreshed struct when the cacheqn
958 // analyzer supplied one: its cache self-switch is normalized at the ACTUAL
959 // hit/miss split, so the visits back the carried rate without the over-route
960 // inflation. filter_metric still keys on the base L for its topology masking.
963 mva::MetricKind::ResidT, nullptr);
965 &srcmask);
966 out.CN = s.C;
967 out.XN = s.X;
968 out.method = origmethod;
969 // `runAnalyzer` reports 'default/<algorithm>' when the caller asked for the
970 // default and something more specific ran, so the algorithm that produced
971 // the numbers is never lost.
972 out.actualmethod = (origmethod == "default" && !actualmethod.empty() &&
973 actualmethod != "default")
974 ? "default/" + actualmethod
975 : actualmethod;
976 // Approximated, not refused, for the reason SolverMVA gives: a normalizing
977 // constant counts a fed-back visit as an ordinary re-entry, having no way
978 // to express a job that keeps its server. `@@SolverNC/runAnalyzer.m` warns
979 // and solves, and this text is that warning verbatim.
981 out.warning =
982 "SolverNC does not handle immediate feedback (immfeed); the solver will treat "
983 "self-loops as class-switching with re-queueing.";
984 // A warning raised by an analyzer (the cache branch's cost-cap conditions)
985 // must survive the runner; both can fire, so they are joined rather than
986 // one overwriting the other.
987 if (!d.warning.empty())
988 out.warning = out.warning.empty() ? d.warning : out.warning + " " + d.warning;
989 out.listcost = d.listcost;
990 // What the cache branch measured, carried to `getAvgCacheTable` and
991 // `getAvgItemTable`; empty on every model without a Cache node.
992 out.cache = d.cache;
993 // `runAnalyzer.m`'s last line: the log normalizing constant is stored beside
994 // the average table rather than recomputed, so `getProbNormConstAggr` costs
995 // the caller nothing once `getAvg` has run.
996 out.lognormconst = s.lG;
997 out.iter = s.iter;
998 {
999 const double lg = num_traits<T>::to_double(s.lG);
1000 if (std::isfinite(lg))
1001 line::util::LineConsole::step("normalizing constant obtained: log G = %.6g", lg);
1002 }
1003 return out;
1004}
1005
1006/** Port of `@@SolverNC/getProbNormConstAggr.m`: the log normalizing constant. */
1007template <class T>
1009 return solver_nc_solve(L, opt).sol.lG;
1010}
1011
1012} // namespace nc
1013} // namespace line
1014
1015#endif // LINE_SOLVERS_NC_SOLVER_NC_RUNNER_H
What a solver observed about the Cache nodes of a model.
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
bool has_immediate_feedback() const
any(sn.immfeed(:)): whether any (station, class) pair feeds back.
std::vector< std::vector< bool > > disabled
std::vector< Station< T > > stations
stations[k-1] is the k-th station
std::vector< NodeDef > nodes
every node, in creation order
static void step(const char *fmt,...)
Write one progress line.
The exception types the port throws.
The fork-join fixed point that drives one inner MVA solve.
The Heidelberger-Trivedi fork-join transform, options.config.fork_join='ht'.
The fork-join transform SolverMVA applies before solving a layer that contains a Fork.
Running progress log of a LINE solver run (the "solver console").
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
@ FIFO
first in, first out
Definition lang_types.h:380
Matrix< T > sn_get_residt_from_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &RN)
Port of sn_get_residt_from_respt: the per-JOB residence time.
Matrix< T > filter_metric(const qn::NetworkStruct< T > &L, const Matrix< T > &metric, MetricKind kind, const std::vector< std::vector< bool > > *zero_mask)
Port of filterMetric: what @@NetworkSolver/getAvg does between the analyzer and the caller.
FjMmt< T > fj_fork_join_transform(const qn::NetworkStruct< T > &L, const std::string &method)
options.config.fork_join -> the transform it names.
Definition fj_ht.h:380
Matrix< T > sn_get_arvr_from_tput(const qn::NetworkStruct< T > &L, const Matrix< T > &TN)
MvaSolution< T > fj_fixed_point(const qn::NetworkStruct< T > &L, FjMmt< T > &tr, std::vector< T > &lam, const MvaOptions &opt, InnerSolve inner)
Drive the fork-join fixed point of a transformed model to convergence.
Definition fj_driver.h:401
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
mva::AvgResult< T > solver_nc_run_analyzer(const qn::NetworkStruct< T > &L_in, const NcSolverOptions &opt_in)
Port of @@SolverNC/runAnalyzer.m for the lang='matlab' path: solve, then apply the metric filter @@Ne...
bool nc_is_cacheqn(const qn::NetworkStruct< T > &sn)
True when the model has a Cache node and is not the Source-Cache-Sink shape.
bool nc_is_oi_model(const qn::NetworkStruct< T > &sn)
Port of nc_is_oi_model.m: a closed network with at least one OI station and nothing but BCMP product-...
NcSolution< T > solver_nc_dt(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Exact discrete-time analysis of sn.
NcSpnSolution< T > solver_nc_spn_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Analyse a product-form stochastic Petri net.
NcSolution< T > solver_nc_dps_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Analyzes the closed think+DPS network.
MemSupport solver_nc_mem_supports(const qn::NetworkStruct< T > &sn)
Port of solver_nc_mem_supports.m.
NcCacheSolution< T > solver_nc_cache_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_cache_analyzer.m.
bool nc_is_lossn_model(const qn::NetworkStruct< T > &sn)
True when the model is a loss network: the shape above, with EVERY class dropped at the region.
bool nc_is_pas_model(const qn::NetworkStruct< T > &sn)
Port of nc_is_pas_model.m: a closed two-station OI / P&S tandem.
NcSolution< T > solver_nc_oi_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_oi_analyzer.m.
double solver_nc_lognormconst(const qn::NetworkStruct< T > &L, const NcSolverOptions &opt)
Port of @@SolverNC/getProbNormConstAggr.m: the log normalizing constant.
NcCacheqnRetrievalSolution< T > solver_nc_cacheqn_retrieval_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_cacheqn_retrieval_analyzer.m.
void check_method(const std::string &method)
Port of runAnalyzerChecks' method gate: an unlisted method is refused.
std::vector< std::string > list_valid_methods()
Port of SolverNC.listValidMethods.
std::size_t nc_closed_queueing_stations(const qn::NetworkStruct< T > &sn)
How many queueing (non-infinite-server) stations carry demand from a CLOSED chain?
NcRetrievalSolution< T > solver_nc_retrieval_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_retrieval_analyzer.m.
NcSolution< T > solver_nc_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_analyzer.m.
bool is_stochastic_method(const std::string &method)
Port of SolverNC.isStochasticMethod.
NcSolution< T > solver_nc_solve(const qn::NetworkStruct< T > &L_in, const NcSolverOptions &opt_in)
The gates, the multiserver conversion and the dispatch of @@SolverNC/runAnalyzer.m,...
bool nc_has_lossn_shape(const qn::NetworkStruct< T > &sn)
True when the model has the SHAPE of a loss network – open, one region, one member station,...
NcCacheqnSolution< T > solver_nc_cacheqn_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_cacheqn_analyzer.m.
NcSolution< T > solver_ncld_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_ncld_analyzer.m: the load-dependent analyzer, which is solver_ncld plus the same fract...
Definition nc_dispatch.h:96
bool sn_has_dps(const qn::NetworkStruct< T > &sn)
True when any station of the network is scheduled DPS.
bool nc_has_retrieval(const qn::NetworkStruct< T > &sn)
True when the model's Cache carries a delayed-hit retrieval system.
std::string resolve_method(const qn::NetworkStruct< T > &L, const std::string &method)
Port of SolverNC.resolveMethod: the feature-driven resolution of method='default'.
NcLossnSolution< T > solver_nc_lossn_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_lossn_analyzer.m.
bool nc_is_normal_usage(const qn::NetworkStruct< T > &sn)
Is the closed model in NORMAL USAGE, the domain of the Mitra-McKenna PANACEA asymptotic expansion (J.
bool nc_is_noreentrant_cache(const qn::NetworkStruct< T > &sn)
True when the model is exactly a Source, a Cache and a Sink.
std::string nc_method_refusal(const qn::NetworkStruct< T > &sn, const std::string &method, bool slotted=false, bool for_report=true)
May method run on this model?
bool nc_is_dps_model(const qn::NetworkStruct< T > &sn)
True when the model is the closed two-station network Morrison's expansion is derived for: one infini...
NcSolution< T > nc_dispatch(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of @@SolverNC/ncDispatch.m: the inner solve of the fork-join fixed point, which is the load-depe...
FeatureSet nc_feature_set(const std::string &method)
SolverNC.getFeatureSet, 48 names, transcribed unchanged.
void check_binding_capacity(const std::string &solver, const NetworkStruct< T > &sn)
void feature_gate(const std::string &solver, const FeatureSet &declared, const NetworkStruct< T > &sn, const std::string &requested_method="", const std::string &resolved_method="")
runAnalyzerChecks: refuse a model the solver does not declare, by name.
CacheMetrics< T > cache_metrics_of(const qn::NetworkStruct< T > &sn, const std::vector< T > &hitprob, const std::vector< T > &missprob, const std::vector< T > &delayedprob, const std::vector< T > &latency, const Matrix< T > &hitproblist, const Matrix< T > &itemprob, const std::vector< T > &listcost)
Assemble CacheMetrics from what a cache analyzer returned.
CacheMetrics< T > cache_metrics_of_matrix(const qn::NetworkStruct< T > &sn, const Matrix< T > &hitprob, const Matrix< T > &missprob)
The same, for the integrated caching-queueing branch, whose hit and miss probabilities are (ncaches x...
Port of solver_nc_analyzer.m, solver_ncld_analyzer.m and @@SolverNC/ncDispatch.m: one inner solve,...
Controls and result shape shared by the normalizing-constant analyzers.
A queueing network and its refreshed NetworkStruct.
Chain aggregation and de-aggregation.
The DECLARED side of the gate: one feature set per solver.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
Port of solver_nc_cache_analyzer.m: the NON-REENTRANT cache, a model that is exactly a Source,...
Port of solver_nc_cacheqn_analyzer.m: the INTEGRATED caching-queueing network, where a Cache sits ins...
Port of solver_nc_cacheqn_retrieval_analyzer.m: a CLOSED integrated cache-queueing model whose Cache ...
Heavy-usage asymptotic analysis of the closed two-station network with one think (infinite-server) st...
Port of solver_nc_lossn_analyzer.m: the open LOSS NETWORK, which is a Source, ONE multiclass Delay si...
Port of solver_nc_mem.m and solver_nc_mem_supports.m: the Maximum Entropy Method of Kouvatsos (1994).
Order-independent (OI) and pass-and-swap (P&S) normalizing-constant analysis.
Port of solver_nc_retrieval_analyzer.m: the OPEN delayed-hit (retrieval-system) cache.
Stationary analysis of a PRODUCT-FORM stochastic Petri net by MDD-rec.
The metrics getAvg returns, after filtering.
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
std::string warning
The reference's own warning text, verbatim, empty when it did not warn.
Matrix< T > UN
utilization
std::vector< T > listcost
(h) mean storage cost held by each cache list, K_j = sum_i sigma_i pi_ij, filled only by the NC cache...
std::optional< double > lognormconst
@@SolverNC/getProbNormConstAggr, i.e.
Matrix< T > WN
residence time, per job
std::string method
the method asked for
std::string actualmethod
the algorithm that ran
Matrix< T > QN
queue length
std::vector< T > CN
system response time per class
std::vector< T > XN
system throughput per class
solvers::CacheMetrics< T > cache
What the cache branches observed, EMPTY on a model with no Cache node and on every solver that does n...
Matrix< T > AN
arrival rate
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
Matrix< T > Lchain
(M x C) demand
Definition sn_chain.h:47
The transformed layer and the bookkeeping the fixed point needs to drive it and to merge its results ...
Definition fj_mmt.h:113
The options SolverMVA reads.
Definition mva_types.h:31
bool base_has_fork
Whether the model this solve came from has a Fork, which the model handed to the analyzer no longer d...
Definition mva_types.h:65
std::string method
Definition mva_types.h:32
std::string fork_join
options.config.fork_join: which fork-join arm the fixed point takes.
Definition mva_types.h:51
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
double lG
log of the normalizing constant, the reference's lG.
Definition mva_types.h:118
std::vector< T > C
Definition mva_types.h:98
static constexpr double FineTol
Definition lang_types.h:668
The verdict of solver_nc_mem_supports.
bool blocking
the model carries a binding finite station buffer
What the cache analyzer returns beyond the usual metric table.
Matrix< T > hitproblist
(u x h) access-weighted per-list hit probability, NaN if not exact
std::vector< T > missprob
(u) per-class miss and hit PROBABILITIES, missrate divided by the read class's arrival rate and its c...
Matrix< T > itemprob
(n x h+1) the same from the EXACT recursion, NaN above 10 items
std::vector< T > listcost
(h) mean storage cost held by each list, EMPTY without item sizes
What the closed delayed-hit analyzer returns.
std::vector< T > delayedprob
(K) zero on this path, by the reference's convention
std::vector< T > missprob
(K) the delayed fraction is folded in here
std::vector< T > hitprob
(K) P(item cached)
std::vector< T > latency
(K) NaN: not computed on this path
Matrix< T > hitproblist
(K x h) NaN: not computed on this path
What the integrated analyzer returns: the metrics plus the converged split.
Matrix< T > hitprob
(ncaches x nclasses) converged hit probability
Matrix< T > missprob
(ncaches x nclasses)
qn::NetworkStruct< T > refreshed
The struct whose cache self-switch carries the CONVERGED split rather than the offered one,...
std::vector< Matrix< T > > itemprob
per cache, (n x h+1); EMPTY = not computed
What the delayed-hit analyzer returns beyond the metric table.
Matrix< T > itemprob
(n x h+1) column 0 = miss, 1.. = per list
std::vector< T > hitprob
(K) TRUE hit fraction, NaN off the read class
Matrix< T > hitproblist
(K x h) per-list hit fraction
std::vector< T > delayedprob
(K) delayed-hit fraction
std::vector< T > latency
(K) NaN: the latency belongs to SolverMVA
The [Q,U,R,T,C,X,lG] of the reference, plus the algorithm that ran.
Definition nc_types.h:113
std::vector< T > listcost
(h) mean storage cost held by each cache list, K_j = sum_i sigma_i pi_ij.
Definition nc_types.h:139
mva::MvaSolution< T > sol
Definition nc_types.h:114
std::string warning
The reference's warning text, verbatim, empty when it did not warn.
Definition nc_types.h:133
std::shared_ptr< qn::NetworkStruct< T > > refreshed_struct
Set only by the integrated cacheqn branch: the converged struct whose routing carries the ACTUAL hit/...
Definition nc_types.h:123
solvers::CacheMetrics< T > cache
What the cache branches observed, EMPTY on a model with no Cache node.
Definition nc_types.h:148
std::string actualmethod
Definition nc_types.h:115
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
bool base_has_fork
Whether the model this solve came from has a Fork, which the model handed to the analyzer no longer d...
Definition nc_types.h:98
bool slotted
options.config.slotted.
Definition nc_types.h:106
A node of the network.
One station of the network.
double nservers
may be infinite (a Delay, or an inf-scheduled task)
std::vector< T > lldscaling
sn.lldscaling for this station: the multiplier at population 1, 2, ... Empty when the station is not ...