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
66#include "line/util/error.h"
67
68namespace line {
69namespace nc {
70
71/**
72 * Port of `SolverNC.listValidMethods`.
73 *
74 * A LISTED NAME MUST ACTUALLY RUN, or be refused with a message that names the
75 * missing analyzer. `erlangfp` and `mci` (the loss-network Erlang fixed point
76 * and its Monte Carlo counterpart) DO run: a Finite Capacity Region is now
77 * representable -- `sn.regions` and `Network::add_region` -- and
78 * `solver_nc_lossn_analyzer` claims the model above the product-form and
79 * multiserver gates. A model that is not a loss network still reaches the
80 * refusal further down, which is the honest outcome and different from silently
81 * solving a model with its region ignored. `comomld` is not in the reference's
82 * list; it is accepted because the reference selects it internally from
83 * 'default'. `ms` names the same lossn_manjunath transform as `exact` on a loss
84 * network (solver_nc_lossn.h's own method name alias) and was missing here, which
85 * blocked it before it ever reached the analyzer that already accepts it.
86 */
87inline std::vector<std::string> list_valid_methods() {
88 // "rayint" and "spm" both name the SPM saddle point on a cache, which serves
89 // cache_spm_size once the items carry storage costs. On a retrieval model
90 // "rayint" is instead the ray/WKB delayed-hit expansion, admissible only with
91 // an infinite-server fetch system; solver_nc_retrieval branches on the method name
92 // and warns and falls back to "exact" anywhere else.
93 return {"default", "exact", "rayint", "spm", "erlangfp", "mci", "imci", "ls", "le",
94 "ble", "aghq", "mmint2", "gleint", "pana", "panald", "ca", "clw", "kt", "bkt", "lekt",
95 "bk", "bkue", "lc", "lc.ue",
96 "sampling", "is", "propfair", "comom", "cub", "rgf",
97 // "divdiff" is the divided-difference closed form of Casale (SIGMETRICS
98 // 2017); it needs no think time, since a delay would ask for the integral
99 // form of Cor. 3.4, and pfqn_nc refuses one by name. Load-dependent rates
100 // ARE served: pfqn_ncld substitutes the limited load-dependent kernel of
101 // Casale-Harrison-Ong (Perform. Eval. 2021), Thm. 1, and reports itself as
102 // "divdiff.ld/...".
103 "divdiff",
104 // Chen-O'Cinneide regularization; a Markov chain Monte Carlo estimator of the
105 // throughput RATIOS G(N-e_r)/G(N), which supplies no constant of its own
106 "mcmc",
107 "ger", "rd", "nrp",
108 "nrl", "nre", "gm", "mem", "comomld", "ms",
109 // Krzesinski state-dependent routing: `solver_nc` intercepts an
110 // sdr model whatever the method says, but the gate runs first, so
111 // omitting the tokens refused the very name the other three
112 // codebases list -- and `sdr.mva` is the only way to reach the
113 // Section 4 MVA arm rather than the eq. (16) enumeration.
114 "sdr", "sdr.mva",
115 // "morrison" is the heavy-usage asymptotic expansion of the generating
116 // function for a closed think+DPS network (npfqn_dps_morrison,
117 // solver_nc_dps_analyzer). It is the DEFAULT on that shape and
118 // inadmissible anywhere else, where the runner refuses it: nothing else
119 // in NC can see the DPS weights. Non-product-form, so it returns no lG.
120 "morrison",
121 // "rec" is the MDD-rec route: the reachable set lives in a decision
122 // diagram and the product form supplies the rates. It is the ONLY
123 // method admissible on a stochastic Petri net (solver_nc_spn.h) and
124 // names the exact loss-network constant in solver_nc_lossn.h. Both
125 // routes sit BELOW check_method, so leaving the token out of this
126 // list refused the very name the SPN branch's own error tells the
127 // caller to use. The other three codebases list it.
128 "rec",
129 // Kijima-Matsui perfect sampling of the closed single-class Gordon-Newell
130 // product form from its balance function (solver_nc_cftp.h). It builds no
131 // generator and yields no constant; formerly listed under SolverCTMC.
132 "cftp", "cftp.approx"};
133}
134
135/**
136 * Port of `SolverNC.isStochasticMethod`.
137 *
138 * NC is deterministic except for the Monte Carlo integrators, the logistic
139 * sampler, the importance-sampling estimators and the Chen-O'Cinneide Markov
140 * chain Monte Carlo method, whose answer depends on the seed. The name is TOKENIZED on `.` and `/` so that a runtime-resolved name
141 * such as `default/imci` and a prefixed one such as `nc.ls` classify alike.
142 */
143inline bool is_stochastic_method(const std::string& method) {
144 std::string tok;
145 std::vector<std::string> toks;
146 for (char ch : method) {
147 if (ch == '.' || ch == '/') {
148 toks.push_back(tok);
149 tok.clear();
150 } else {
151 tok += static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
152 }
153 }
154 toks.push_back(tok);
155 for (const std::string& t : toks)
156 if (t == "mci" || t == "imci" || t == "ls" || t == "sampling" || t == "is" ||
157 t == "mcmc")
158 return true;
159 return false;
160}
161
162/**
163 * Port of `SolverNC.resolveMethod`: the feature-driven resolution of
164 * `method='default'`.
165 *
166 * An open network with any non-unit SCV that MEM can carry resolves to `mem`,
167 * because the normalizing-constant path would silently exponentialize it.
168 *
169 * IT IS INERT WITH RESPECT TO THE ANALYZER, and deliberately so: MATLAB's
170 * `@@SolverNC/runAnalyzer.m` never consults `resolveMethod`, it reads
171 * `options.method` directly. Measured on an M/E2/1 (Source Exp(0.5), FCFS
172 * Erlang mean 0.5 scv 0.5), `resolveMethod` returns `mem` while
173 * `SolverNC(model).getAvg` reports `default/exact`. The function feeds the
174 * feature gate and the AUTO dispatch only, and this port matches that: nothing
175 * here calls it on the solve path. See register row N5.
176 */
177template <class T>
178std::string resolve_method(const qn::NetworkStruct<T>& L, const std::string& method) {
179 if (method != "default") return method;
180 if (!solver_nc_mem_supports(L).supported) return method;
181 for (std::size_t i = 0; i < L.nstations; ++i)
182 for (std::size_t r = 0; r < L.nclasses; ++r) {
183 if (L.disabled[i][r]) continue;
184 const double v = num_traits<T>::to_double(L.scv(i, r));
185 if (std::isfinite(v) && std::fabs(v - 1.0) > GlobalConstants::FineTol) return "mem";
186 }
187 return method;
188}
189
190/** Port of `runAnalyzerChecks`' method gate: an unlisted method is refused. */
191inline void check_method(const std::string& method) {
192 const std::vector<std::string> valid = list_valid_methods();
193 if (std::find(valid.begin(), valid.end(), method) != valid.end()) return;
194 throw UnsupportedError("SolverNC: the '" + method + "' method is unsupported by this solver");
195}
196
197namespace detail {
198
199/** Is there a node of this type? */
200template <class T>
201bool has_node_type(const qn::NetworkStruct<T>& sn, qn::NodeType ty) {
202 for (const qn::NodeDef& nd : sn.nodes)
203 if (nd.nodetype == ty) return true;
204 return false;
205}
206
207/**
208 * The cache-metric assemblers now live in `line/solvers/cache_metrics.h`, beside
209 * the struct they build, because SolverMVA's cache branches need the identical
210 * rule. Re-exported into this namespace so the call sites below read unchanged.
211 */
214
215/**
216 * Rewrite every finite multiserver station as the rate lattice mu(n)=min(n,c).
217 *
218 * @return false when the total closed population is not finite, in which case
219 * the reference leaves the model alone
220 */
221template <class T>
222bool multiserver_to_lld(qn::NetworkStruct<T>& sn) {
223 double Nt = 0.0;
224 for (const qn::JobClass& c : sn.classes) {
225 if (std::isinf(c.population)) return false;
226 Nt += c.population;
227 }
228 const std::size_t n = static_cast<std::size_t>(std::llround(Nt));
229 if (n < 1) return false;
230 for (std::size_t i = 0; i < sn.nstations; ++i) {
231 std::vector<T> lld(n, num_traits<T>::from_int(1));
232 const double c = sn.stations[i].nservers;
233 if (std::isfinite(c) && c > 1.0)
234 for (std::size_t k = 1; k <= n; ++k)
235 lld[k - 1] =
236 num_traits<T>::from_double(std::min<double>(static_cast<double>(k), c));
237 sn.stations[i].lldscaling = lld;
238 }
239 return true;
240}
241
242/**
243 * Resolves `NcSolverOptions::multiserver` into the handling this solver
244 * implements: `default`, `seidmann` or `lld`.
245 *
246 * Unknown values -- the SolverMVA approximations NC has no counterpart for
247 * (`softmin`, `conway`, `krzesinski`, `suri`, `erlang`) -- resolve to `default`
248 * rather than throwing, because one options object is commonly reused across
249 * solvers. They are NOT honoured silently: `warning`, when given, receives the
250 * text the other three codebases pass to `line_warning`. The port has no
251 * `line_warning` channel, so a warning travels on `NcSolution::warning`, which
252 * `line-cli` prints to stderr; see `NcSolution::warning`.
253 */
254inline std::string nc_multiserver_policy(const std::string& requested,
255 std::string* warning = nullptr) {
256 if (requested.empty() || requested == "default") return "default";
257 if (requested == "seidmann") return "seidmann";
258 if (requested == "lld" || requested == "exact" || requested == "loaddep" ||
259 requested == "load-dependent")
260 return "lld";
261 if (warning != nullptr)
262 *warning = "SolverNC does not implement config.multiserver='" + requested +
263 "' (it is a SolverMVA approximation); using 'default'. SolverNC accepts "
264 "'default', 'seidmann' and 'lld'.";
265 return "default";
266}
267
268/**
269 * The per-chain population lattice `prod(1+Nchain)`, which prices the exact
270 * enumeration `solver_ncld` performs.
271 */
272template <class T>
273double nc_population_lattice(const qn::NetworkStruct<T>& sn) {
274 double lattice = 1.0;
275 if (!sn.chains.empty()) {
276 // sn.chains is (nchains x nclasses) as vector<vector<bool>>, not a Matrix
277 for (std::size_t c = 0; c < sn.chains.size(); ++c) {
278 double popc = 0.0;
279 for (std::size_t r = 0; r < sn.nclasses && r < sn.chains[c].size(); ++r)
280 if (sn.chains[c][r]) {
281 const double v = sn.classes[r].population;
282 if (std::isfinite(v)) popc += v;
283 }
284 lattice *= (1.0 + popc);
285 }
286 } else {
287 for (const qn::JobClass& c : sn.classes)
288 if (std::isfinite(c.population)) lattice *= (1.0 + c.population);
289 }
290 return lattice;
291}
292
293} // namespace detail
294
295/**
296 * Is the closed model in NORMAL USAGE, the domain of the Mitra-McKenna PANACEA
297 * asymptotic expansion (J. ACM 33(3), 1986)?
298 *
299 * Normal usage asks that every queueing centre absorb the load the think
300 * stations offer it: with rho_j0 = Ztot(j) the aggregate think demand of chain
301 * j, r_ij = L_ij / rho_j0 and mu_i(Ntot) the saturation rate,
302 *
303 * alpha_i = 1 - (sum_j N_j r_ij) / mu_i(Ntot) > 0 at every centre i.
304 *
305 * Outside it the {phi(n)} series DIVERGES, which is why `pfqn_panaceald` returns
306 * NaN there and `pfqn_ncld` turns that NaN into a refusal rather than a warning.
307 * It is a property of the DEMANDS and not of a declared construct, so it has no
308 * feature-registry name and cannot live in `nc_feature_set`.
309 *
310 * The rates are the ones `solver_ncld` would build: one for an ordinary single
311 * server, min(n, c) for a finite multiserver (the conversion the runner performs
312 * on the 'panald' arm), and the declared lldscaling row when the model sets
313 * one. An infinite server is a think station and feeds Ztot.
314 */
315template <class T>
317 if (sn.has_open_classes()) return true; // no rho_j0 to expand around
318 const std::size_t M = sn.nstations, C = sn.nchains;
319 const std::vector<double> Nchain = detail::chain_population(sn);
320 double NtD = 0.0;
321 for (std::size_t c = 0; c < C; ++c)
322 if (std::isfinite(Nchain[c])) NtD += Nchain[c];
323 const std::size_t Nt = static_cast<std::size_t>(std::llround(NtD));
324 if (Nt < 1) return true; // the empty network: G = 1, nothing to expand
325
327
328 std::vector<double> Ztot(C, 0.0);
329 for (std::size_t i = 0; i < M; ++i)
330 if (std::isinf(sn.stations[i].nservers))
331 for (std::size_t c = 0; c < C; ++c)
332 Ztot[c] += num_traits<T>::to_double(d.Lchain(i, c));
333 for (std::size_t c = 0; c < C; ++c)
334 if (Nchain[c] > 0.0 && !(Ztot[c] > 0.0))
335 return false; // no think station on a populated chain's route
336
337 for (std::size_t i = 0; i < M; ++i) {
338 const double nserv = sn.stations[i].nservers;
339 if (std::isinf(nserv)) continue;
340 const std::vector<T>& lld = sn.stations[i].lldscaling;
341 double muK;
342 if (!lld.empty())
343 muK = num_traits<T>::to_double(lld[std::min(Nt, lld.size()) - 1]);
344 else if (nserv > 1.0)
345 muK = std::min(static_cast<double>(Nt), nserv);
346 else
347 muK = 1.0;
348 if (!(muK > 0.0) || !std::isfinite(muK)) return false;
349 double lambda = 0.0;
350 for (std::size_t c = 0; c < C; ++c)
351 if (Ztot[c] > 0.0)
352 lambda += num_traits<T>::to_double(d.Lchain(i, c)) / Ztot[c] * Nchain[c];
353 if (!(1.0 - lambda / muK > 0.0)) return false;
354 }
355 // Every centre cleared the test; a model with no queueing centre at all
356 // reaches here too, and there the delay-only constant is exact.
357 return true;
358}
359
360/**
361 * How many queueing (non-infinite-server) stations carry demand from a CLOSED
362 * chain?
363 *
364 * That is the row count L reaches `pfqn_nc` and `pfqn_comomrm_ld` with, once the
365 * delay rows have been folded into Z and the zero-demand rows dropped. Zero when
366 * the model has no closed population at all.
367 */
368template <class T>
370 const std::size_t M = sn.nstations, C = sn.nchains;
371 const std::vector<double> Nchain = detail::chain_population(sn);
372 bool any_closed = false;
373 std::vector<bool> closed(C, false);
374 for (std::size_t c = 0; c < C; ++c) {
375 closed[c] = std::isfinite(Nchain[c]) && Nchain[c] > 0.0;
376 if (closed[c]) any_closed = true;
377 }
378 if (!any_closed) return 0;
379
381 std::size_t nq = 0;
382 for (std::size_t i = 0; i < M; ++i) {
383 if (std::isinf(sn.stations[i].nservers)) continue;
384 for (std::size_t c = 0; c < C; ++c)
385 if (closed[c] &&
387 ++nq;
388 break;
389 }
390 }
391 return nq;
392}
393
394/**
395 * May `method` run on this model? "" when it may, otherwise the reason it may
396 * not, in the words the runner refuses with.
397 *
398 * ONE PREDICATE, TWO CALLERS. `solver_nc_solve` asks it once, ahead of the
399 * dispatch, and throws on a non-empty answer; `auto_family_refusal` asks it so
400 * that `auto_find_solver` never offers a (family, method) pair that would throw,
401 * and so that the ranking never delegates to one. Two copies of these rules is
402 * precisely how the report and the run drift apart, which is the failure this
403 * function exists to prevent, so a new rule goes here and not at a call site.
404 *
405 * ONLY WHAT THE FEATURE REGISTRY CANNOT NAME LIVES HERE. A feature set declares
406 * what the method ACCEPTS, so it can refuse a model for HAVING a construct and
407 * never for lacking one: "closed population only" and "no think time" are said
408 * in `qn::nc_feature_set` by dropping OpenClass and SchedStrategy_INF, while
409 * "requires a cache", "requires state-dependent routing", "requires a loss
410 * network", "requires exactly two stations" and "requires normal usage" have no
411 * such form and are decided here.
412 *
413 * `for_report` says WHICH QUESTION IS BEING ASKED, and for two method names the
414 * two questions have different answers:
415 *
416 * true -- "should `auto_find_solver` offer this pair?" A pair that comes back
417 * as a table of zeros must not be offered, so the answer is no.
418 * false -- "what does the reference DO when asked for it by name?" For 'mmint2'
419 * and 'gleint' outside their shape the reference deliberately WARNS AND
420 * RETURNS A ZERO TABLE (pfqn_nc.m, case {'mmint2','gleint'}: lG = [] and
421 * return, unconditionally), and a caller who names the method keeps that
422 * answer -- which is what `test_nc.cpp` pins.
423 *
424 * THE ASYMMETRY IS A RULING, NOT AN OVERSIGHT (2026-07-25, reaffirmed when this
425 * gate was added): the report answers "should this be offered" and the run answers
426 * "what does the reference do". 'comomld' is NOT in that bucket --
427 * `pfqn_comomrm_ld` refuses "The solver accepts at most a single queueing station."
428 * natively -- so it is refused on both paths.
429 *
430 * @param sn the refreshed struct
431 * @param method the concrete method name
432 * @param slotted true on the discrete-time route, which answers for itself
433 * @param for_report true when the caller is the report, false when it is the run
434 */
435template <class T>
436std::string nc_method_refusal(const qn::NetworkStruct<T>& sn, const std::string& method,
437 bool slotted = false, bool for_report = true) {
438 const std::string m = method.empty() ? std::string("default") : method;
439
440 // The discrete-time route answers for itself: `solver_nc_dt` decides
441 // admissibility on the slot lattice, and every gate below is written about a
442 // continuous-time queueing network.
443 if (slotted) return "";
444
445 // -- perfect sampling --------------------------------------------------
446 // The sampler's structural predicate is the whole of its envelope: a
447 // closed single-class Gordon-Newell network, which excludes every shape the
448 // rules below route (DPS, SDR, Petri nets, OI, caches, regions).
449 if (m == "cftp" || m == "cftp.approx") return solver_nc_cftp_supports(sn);
450
451 // -- discriminatory processor sharing ---------------------------------
452 // Morrison's heavy-usage expansion is the ONLY NC route that can see the DPS
453 // weights; every other method builds a product-form normalizing constant
454 // that silently drops them and answers with the egalitarian-PS network,
455 // which is a wrong number rather than a coarse one.
456 if (nc_is_dps_model(sn)) {
457 if (m != "default" && m != "morrison")
458 return "SolverNC: method '" + m +
459 "' cannot represent the DPS weights of a discriminatory processor-sharing "
460 "station; it would return the egalitarian-PS network. Use method 'default' or "
461 "'morrison' (npfqn_dps_morrison), SolverMVA, SolverFLD or SolverCTMC.";
462 return "";
463 }
464 if (sn_has_dps(sn))
465 // A DPS station outside Morrison's shape. SchedStrategy_DPS is declared
466 // in the feature set because a boolean feature cannot express "this shape
467 // only"; this is that imperative half.
468 return "SolverNC analyzes a discriminatory processor-sharing station only in the shape "
469 "Morrison's expansion is derived for: a CLOSED network of exactly two stations, one "
470 "infinite-server (think) station and one single-server DPS station, exponential "
471 "service, each class visiting the two equally often. Use SolverMVA, SolverFLD or "
472 "SolverCTMC for any other DPS model.";
473 if (m == "morrison")
474 // The method named on a model that is not the shape at all -- not even a
475 // DPS station in it. Left ungated it reaches no route of its own and
476 // falls through to the ordinary normalizing-constant path, which would
477 // answer the product-form model UNDER THE CALLER'S LABEL.
478 return "SolverNC: method 'morrison' is the heavy-usage expansion of a CLOSED network of "
479 "exactly two stations, one infinite-server (think) station and one single-server DPS "
480 "station with exponential service, which this model is not. Remove the method option "
481 "to let SolverNC choose, or use SolverMVA, SolverFLD or SolverCTMC.";
482
483 // -- Krzesinski state-dependent routing -------------------------------
484 // An SDR model is intercepted by `solver_nc_sdr` whatever the method says,
485 // so reaching the second test means the model declares none.
486 if (!sn.sdr.branch.empty()) return "";
487 if (m == "sdr" || m == "sdr.mva")
488 return "SolverNC: method '" + m +
489 "' requires state-dependent routing, which this model does not declare.";
490
491 // -- stochastic Petri net ---------------------------------------------
492 // A net is served only by the MDD-rec route, and none of the gates below --
493 // written about stations, capacities and the queueing-network product form --
494 // says anything about a net. `spn_pf` decides its product-form class, by name.
495 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
496 if (sn.nodes[ind].nodetype == lang::NodeType::Place) {
497 if (m != "default" && m != "rec")
498 return "solver_nc: a stochastic Petri net is solved by the MDD-rec route; method '" +
499 m + "' is a normalizing-constant algorithm for queueing networks. Use 'rec' "
500 "or 'default'";
501 return "";
502 }
503
504 // -- order-independent stations ---------------------------------------
505 // Every method other than the four listed reads the single-job rate mu([r])
506 // of an OI station: the rank rate mu(n) is silently dropped and the answer is
507 // that of an ordinary queue.
508 if (nc_is_oi_model(sn)) {
509 if (m != "default" && m != "exact" && m != "is" && m != "sampling")
510 return "SolverNC: method '" + m +
511 "' cannot represent the rank rate mu(n) of an order-independent station; use "
512 "method 'default' or 'exact' (pfqn_ncoi), 'is', SolverMVA, or SolverCTMC.";
513 return "";
514 }
515
516 // -- caches -------------------------------------------------------------
517 // 'rayint' and 'spm' both name the SPM saddle point of a cache (and, on a
518 // retrieval model, the ray/WKB delayed-hit expansion), so they are admissible
519 // here and nowhere else.
520 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
521 if (sn.nodes[ind].nodetype == qn::NodeType::Cache) {
522 if (m == "exact" && nc_is_noreentrant_cache(sn)) {
523 const auto itp = sn.nodeparam.find(ind + 1);
524 // cache_prob_erec is exact for the exchangeable (RR/FIFO) family
525 // only; a recency-based policy would silently receive the
526 // exchangeable answer, so the exact route refuses it.
527 if (itp != sn.nodeparam.end() &&
528 itp->second.replacestrat != lang::ReplacementStrategy::RR &&
529 itp->second.replacestrat != lang::ReplacementStrategy::FIFO)
530 return "solver_nc_cache_analyzer: NC does not support the exact solution of "
531 "this cache replacement policy -- only RR and FIFO are exchangeable, "
532 "and a recency-based policy (LRU, h-LRU, q-LRU, CLIMB) would silently "
533 "receive the exchangeable answer. Use the default (approximate) method "
534 "or SolverCTMC";
535 }
536 return "";
537 }
538 if (m == "rayint" || m == "spm")
539 return "SolverNC: method " + m +
540 " names the SPM saddle point of a cache and, on a retrieval model, the ray/WKB "
541 "delayed-hit expansion; this model declares no Cache node.";
542
543 // -- loss networks and finite capacity regions --------------------------
544 if (nc_is_lossn_model(sn)) return ""; // 'ms', 'erlangfp', 'rec' all run here
546 return "SolverNC: the Finite Capacity Region holds a single infinite server but does not "
547 "apply DROP to every class; holding an arrival back (WAITQ) or blocking the server "
548 "(BAS/BBS/RSRD) keeps the job in the region while it waits, which the Erlang loss "
549 "model has no state for -- use DROP, or SolverCTMC/SolverJMT";
550 if (m == "erlangfp")
551 return "SolverNC: the 'erlangfp' Erlang fixed point applies only to a loss network, which "
552 "is an open model whose single Delay sits inside a Finite Capacity Region under a "
553 "DROP rule; this model declares no such region (see nc_is_lossn_model)";
554 if (m == "ms")
555 return "SolverNC: method 'ms' is admissible only on a loss network (open model, one DROP "
556 "region holding a single Delay).";
557 if (m == "rec")
558 return "SolverNC: method rec is the MDD-rec route, admissible on a stochastic Petri net or "
559 "on a loss network (open model, one DROP region holding a single Delay); this model "
560 "is neither.";
561 if (!sn.regions.empty())
562 return "SolverNC: this model applies a Finite Capacity Region to queueing stations, whose "
563 "aggregate population limit no normalizing-constant algorithm here enforces; only "
564 "the loss network -- one region over a single infinite server, DROP on every "
565 "class -- is solvable. Use SolverCTMC or SolverJMT, or setCapacity for a "
566 "single-station limit";
567
568 // -- the maximum-entropy route reads ONE service law ----------------------
569 // `solver_nc_mem` builds Kouvatsos's GE/GE/1/N from the first two moments of a
570 // SINGLE service law -- neither it nor `solver_nc_mem_supports` mentions
571 // lldscaling, cdscaling or jdscaling -- so a station declaring one is answered
572 // with the UNSCALED queue. That is worse than it looks: 'mem' is also the one
573 // name exempt from the binding-capacity gate, so on a finite buffer it is the
574 // last route standing and the silent answer is the one the caller gets. A
575 // method that cannot read a declared field has no feature name for it.
576 if (m == "mem") {
577 for (std::size_t i = 0; i < sn.nstations; ++i)
578 if (!sn.stations[i].lldscaling.empty() ||
579 static_cast<bool>(sn.stations[i].cdscaling) ||
580 static_cast<bool>(sn.stations[i].jdscaling))
581 return "SolverNC: method 'mem' is the maximum-entropy GE/GE/1/N of "
582 "solver_nc_mem, which reads the first two moments of ONE service law and "
583 "cannot apply a declared load-, class- or joint-dependent rate. Use "
584 "SolverMVA, SolverCTMC or SolverLDES";
585 }
586
587 // -- class-dependent rates: closed only -----------------------------------
588 // A cdscaling station diverts the whole model, on every route and whatever
589 // the method, to `solver_nc_conv`, Sauer's multichain convolution. That
590 // recursion is defined on the CLOSED population lattice and holds no position
591 // for an open chain, so the analyzer throws there. The rule is a CONJUNCTION
592 // -- class dependence AND an open chain -- which a boolean feature set cannot
593 // express, so it lives here; said here as well as at the run so the report
594 // and the run agree. BOTH dependences, since 2026-09-13: `solver_ncld` now
595 // diverts on `jdscaling` as well, `solver_nc_conv` folding eta into the same
596 // handle, so the joint-dependent model lands on the same closed lattice.
597 if (sn.has_open_classes()) {
598 for (std::size_t i = 0; i < sn.nstations; ++i)
599 if (static_cast<bool>(sn.stations[i].cdscaling) ||
600 static_cast<bool>(sn.stations[i].jdscaling))
601 return "SolverNC: class- or joint-dependent rates are solved by the convolution "
602 "of solver_nc_conv, Sauer's multichain recursion on the CLOSED population "
603 "lattice, which holds no position for an open chain; this model is open "
604 "or mixed. Use SolverMVA, SolverCTMC or SolverLDES";
605 }
606 // The convolution reads `nservers` only to tell a delay from a queue and
607 // never reads `lldscaling`: a c-server or load-dependent station beside a
608 // class-dependent one would be solved as a single fixed-rate server.
609 // Refused rather than answered wrongly, the same pattern as the residual FCR
610 // above. A COUNT and a CONJUNCTION, so no feature set can say it
611 // (nc_method_refusal.m:347).
612 {
613 bool any_cd = false, any_multi = false, any_lld = false;
614 for (std::size_t i = 0; i < sn.nstations; ++i) {
615 if (static_cast<bool>(sn.stations[i].cdscaling) ||
616 static_cast<bool>(sn.stations[i].jdscaling))
617 any_cd = true;
618 const double c = sn.stations[i].nservers;
619 if (std::isfinite(c) && c > 1.0) any_multi = true;
620 if (!sn.stations[i].lldscaling.empty()) any_lld = true;
621 }
622 if (any_cd && (any_multi || any_lld))
623 return "SolverNC: class- or joint-dependent rates are solved by the convolution of "
624 "solver_nc_conv, which reads no server count above one and no load-dependent "
625 "rate lattice; fold the multiserver capacity into the class-dependent handle, "
626 "or use SolverMVA or SolverCTMC";
627 }
628
629 // -- PANACEA's domain ----------------------------------------------------
630 // Normal usage is a property of the demands rather than of a declared
631 // construct, so it has no feature name; an open chain is refused earlier by
632 // the closed-population feature set of the load-dependent evaluators.
633 //
634 // BOTH TOKENS ARE GATED, because `pfqn_ncld` evaluates 'pana' and
635 // 'panald' with the SAME `pfqn_panaceald` -- its case label covers both --
636 // so on a model carrying a rate lattice the load-INDEPENDENT name reaches the
637 // load-dependent expansion and throws with it. Off that lattice 'pana'
638 // takes its own `pfqn_nc` arm, which warns and returns an empty constant
639 // rather than throwing, so it is left alone there. CLASS-dependent scaling
640 // diverts the whole model to `solver_nc_conv`, which never reads the method
641 // at all -- and only class-dependent, because that is the one `solver_ncld`
642 // diverts on in this port; a joint-dependent model still reaches the kernel.
643 if ((m == "pana" || m == "panald") && !sn.has_open_classes()) {
644 bool diverted_to_conv = false, any_lld = false;
645 for (std::size_t i = 0; i < sn.nstations; ++i) {
646 if (static_cast<bool>(sn.stations[i].cdscaling) ||
647 static_cast<bool>(sn.stations[i].jdscaling))
648 diverted_to_conv = true;
649 if (!sn.stations[i].lldscaling.empty()) any_lld = true;
650 }
651 const bool reaches_ld_kernel = (m == "panald") || any_lld;
652 if (!diverted_to_conv && reaches_ld_kernel && !nc_is_normal_usage(sn)) {
653 const std::string why =
654 "the model is not in normal usage, so the 'panald' asymptotic expansion does "
655 "not apply. Use 'exact', 'clw' or an approximate load-dependent method instead.";
656 if (m == "pana")
657 return "SolverNC: method 'pana' reaches the load-dependent kernel on this "
658 "model, where pfqn_ncld evaluates it as 'panald', and " + why;
659 return "SolverNC: " + why;
660 }
661 }
662
663 // -- the single-queueing-station recursions --------------------------------
664 // Two families are stated for a model with a delay and ONE queueing station,
665 // and neither can say so with a feature name: it is a COUNT, and a feature
666 // set has no arithmetic. `pfqn_nc` states it for 'mmint2'/'gleint' in those
667 // words and `pfqn_comomrm_ld` refuses with "The solver accepts at most a
668 // single queueing station."
669 //
670 // The count is taken over the CLOSED chains only, and the rule is inactive
671 // without a closed population, because `pfqn_nc` answers an open network with
672 // the exact open formulas BEFORE its method switch -- the method name is never read
673 // there, so a purely open model with three queues runs these names correctly
674 // today and must go on doing so.
675 // 'mmint2' and 'gleint' are gated for the REPORT ONLY: `pfqn_nc` answers them
676 // with an empty constant and the caller renders a table of zeros, which is a
677 // pair the report must not offer and a run the reference nonetheless performs.
678 // See `for_report` above.
679 if (m == "comomld" || (for_report && (m == "mmint2" || m == "gleint"))) {
680 const std::size_t nq = nc_closed_queueing_stations(sn);
681 if (nq > 1) {
682 if (m == "comomld")
683 return "SolverNC: method 'comomld' is the load-dependent CoMoM recursion, and "
684 "pfqn_comomrm_ld accepts at most a single queueing station; this model has " +
685 std::to_string(nq) + ".";
686 return "SolverNC: the '" + m +
687 "' method requires a model with a delay and a single queueing station; this "
688 "model has " + std::to_string(nq) + ".";
689 }
690 }
691
692 // -- 'exact' outside its domain ------------------------------------------
693 if (m == "exact" && sn.has_open_classes()) {
694 for (std::size_t i = 0; i < sn.nstations; ++i)
695 if (std::isfinite(sn.stations[i].nservers) && sn.stations[i].nservers > 1.0)
696 return "solver_nc_analyzer: the NC solver cannot provide exact solutions for open "
697 "or mixed multiserver queueing networks. Remove the 'exact' option";
698 }
699 return "";
700}
701
702
703/**
704 * The gates, the multiserver conversion and the dispatch of
705 * `@@SolverNC/runAnalyzer.m`, without the metric filter.
706 *
707 * Kept separate so that the reported log normalizing constant comes from the
708 * SAME model the metrics do: reaching the dispatch without the conversion above
709 * would evaluate the constant of the Seidmann-approximated network while the
710 * table reported the load-dependent one.
711 *
712 * A model with a Fork is solved through the shared fork-join fixed point
713 * (`fj_driver.h`), which drives `nc_dispatch` as its inner solve on the
714 * transformed model; a model without one runs the dispatch exactly once.
715 */
716template <class T>
718 check_method(opt_in.method);
719 // THE PERFECT SAMPLER, intercepted ahead of every other gate and route, as
720 // `@SolverNC/runAnalyzer.m` does right after getStruct. Its own structural
721 // predicate is asked FIRST, so a caller reads "the cftp method supports
722 // closed models only" rather than a bare "(feature: OpenClass)", and it is
723 // asked here in the run as well as in the report, so skipping the report
724 // cannot bypass it. The routes below (DPS, OI, cache, loss network,
725 // multiserver rewrite) have nothing to say about a balance-function draw.
726 if (opt_in.method == "cftp" || opt_in.method == "cftp.approx") {
727 const std::string why = solver_nc_cftp_supports(L_in);
728 if (!why.empty()) throw UnsupportedError(why);
729 qn::feature_gate("SolverNC", qn::nc_feature_set(opt_in.method), L_in);
730 if constexpr (num_traits<T>::has_transcendental) {
731 return solver_nc_cftp_solution(solver_nc_cftp(L_in, opt_in));
732 } else {
733 throw UnsupportedError(
734 "SolverNC(cftp): the cftp methods draw random states and form the station "
735 "balance functions in the log domain, neither of which exists in exact rational "
736 "arithmetic");
737 }
738 }
739 // runAnalyzerChecks' universal feature gate, AFTER the method gate. Gated on
740 // `L_in`, before multiserver_to_lld rewrites lldscaling. NC DECLARES Region,
741 // so a loss network passes straight through to the imperative split below.
742 qn::feature_gate("SolverNC", qn::nc_feature_set(opt_in.method), L_in);
743
744 // THE STRUCTURAL METHOD GATE, asked once and in one place.
745 //
746 // `nc_method_refusal` holds every rule of the form "this method has no route
747 // on this model": the DPS shape, state-dependent routing, the Petri net, the
748 // order-independent rank rate, the cache and loss-network tokens, PANACEA's
749 // normal usage. `auto_family_refusal` asks the SAME function, which is what
750 // keeps `auto_find_solver` from offering a pair that would throw here.
751 {
752 // for_report=false: this is the RUN, and it asks what the reference DOES
753 // rather than what the report should offer. The two answers differ for
754 // 'mmint2'/'gleint', which `pfqn_nc` answers with an empty constant and a
755 // zero table; see `nc_method_refusal`.
756 const std::string refusal =
757 nc_method_refusal(L_in, opt_in.method, opt_in.slotted, /*for_report=*/false);
758 if (!refusal.empty()) throw UnsupportedError(refusal);
759 }
760
761 NcSolverOptions opt = opt_in;
762 qn::NetworkStruct<T> L = L_in;
763 NcSolution<T> res;
764
765 // Discrete time before every other gate: the slot lattice is a property of
766 // the MODEL, not of a method, and the imperative gates below (finite
767 // capacity, the multiserver-to-lldscaling rewrite, the load-dependence
768 // routing) all assume a continuous time scale. `solver_nc_dt` refuses a
769 // model outside the discrete-time product form rather than falling through.
770 if (opt.slotted) return solver_nc_dt(L, opt);
771
772 // THE STRUCTURAL FINITE-CAPACITY GATE, port of `@@SolverNC/SolverNC.m:180-188`.
773 //
774 // It was missing here, and its absence was not a missing message: a closed
775 // two-queue model with setCapacity(1) on the second station SOLVED, and
776 // reported the UNCONSTRAINED product-form answer (QLen 0.852459/1.147541,
777 // the values of the same model with no buffer at all) where MATLAB, the JAR
778 // and python all refuse by name. A wrong number, silently, is the failure
779 // mode `check_binding_capacity` exists to stop -- SolverMVA has called it
780 // since the port (`mva_check_finite_capacity`) and NC never did.
781 //
782 // AFTER THE SLOTTED RETURN, as in the reference: a finite buffer on a
783 // Bernoulli server is the loss system of Daduna's corollary 2.8, which
784 // `solver_nc_dt` solves exactly, so this gate must not see it.
785 //
786 // TWO EXEMPTIONS. mem.blocking represents the buffer as a censored
787 // GE/GE/c/0;N queue and therefore DOES honour it. The single-station
788 // M/M/1/K with tail drop is answered exactly by the `qsys_mm1k_loss` branch
789 // in `nc_dispatch`, ported 2026-09-13; before that this port had no such
790 // branch and the shape was refused here, while `nc_feature_set` advertised
791 // FiniteCapacity for 'default'/'exact' on a route that did not exist.
792 //
793 // IT READS `opt.method`, NOT `resolve_method`, and the difference is not
794 // cosmetic. `nc_dispatch` reaches the MEM algorithm on the literal name --
795 // `if (opt.method == "mem")` -- and nothing on the solve path resolves
796 // `default` into it, so exempting a `default` run because `resolve_method`
797 // would have called it `mem` skips the gate and then dispatches somewhere
798 // that does NOT honour the buffer. That is the very failure this gate
799 // exists to stop. (The reference gates on its resolved method and its
800 // analyzer reads `options.method` directly, so MATLAB has the same seam;
801 // do not copy it.)
802 {
803 bool mem_blocking = false;
804 if (opt.method == "mem") {
806 mem_blocking = ms.supported && ms.blocking;
807 }
808 const bool mm1k_closed_form =
809 (opt.method == "default" || opt.method == "exact") && api::sn_is_mm1k_loss(L);
810 if (!mem_blocking && !mm1k_closed_form) qn::check_binding_capacity("SolverNC", L);
811 }
812
813 if (L.has_fork()) {
814 // The transform turns the fork into a router, the join into a delay and
815 // the branches into auxiliary open classes. `nc_dispatch` then sees a
816 // plain mixed network, which is why none of the specialised NC routes
817 // has to know about forks.
819 std::vector<T> lam(tr.V.classes.size() + 1,
821 mva::MvaOptions mopt;
822 mopt.method = opt.method;
823 mopt.tol = opt.tol;
824 mopt.iter_tol = opt.iter_tol;
825 mopt.iter_max = opt.iter_max;
826 mopt.fork_join = opt.fork_join;
827 mopt.base_has_fork = true;
828 NcSolverOptions inner = opt;
829 inner.base_has_fork = true;
830 std::string am;
831 res.sol = mva::fj_fixed_point(L, tr, lam, mopt, [&inner, &am](qn::NetworkStruct<T>& V) {
832 const NcSolution<T> d = nc_dispatch(V, inner);
833 am = d.actualmethod;
834 return d.sol;
835 });
836 res.actualmethod = am;
837 return res;
838 } else {
839 // Closed think+DPS network -> Morrison's heavy-usage generating-function
840 // expansion, the DEFAULT for that shape. Intercepted FIRST, ahead of every
841 // other branch: "SchedStrategy_DPS" is now inside the solver's reach, and
842 // each of the branches below would silently drop the weights and answer
843 // with the egalitarian-PS network. Not a product-form route: lG is NaN.
844 if (nc_is_dps_model(L)) return solver_nc_dps_analyzer(L, opt);
845 // The four refusal arms that used to follow -- another method on a DPS
846 // model, a DPS station outside Morrison's shape, "morrison" on a model with
847 // no DPS station at all, and "sdr"/"sdr.mva" on a model declaring no
848 // state-dependent routing -- moved into `nc_method_refusal` above, with
849 // their wording unchanged.
850
851 // Single-station M/M/1/K with tail drop: exact probability-based loss
852 // analysis off the M/M/1/K stationary distribution (runAnalyzer.m:397).
853 // Intercepted here for the same reason the loss network is: the buffer
854 // makes `has_product_form` say no, so the 'exact' gate below would
855 // refuse the one truncated shape that does keep a product form over its
856 // single station. 'mem' asked by name never reaches this (it is claimed
857 // above); every other name is refused by `nc_method_refusal`, since the
858 // closed form reads none.
859 if ((opt.method == "default" || opt.method == "exact") && api::sn_is_mm1k_loss(L)) {
860 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
861 const std::size_t M = L.nstations;
862 const std::size_t src = mva::detail::station_of_type(L, qn::NodeType::Source);
863 const std::size_t q = mva::detail::station_of_type(L, qn::NodeType::Queue);
864 const std::size_t qstateful = L.stateful_of_station(q);
865 const T Vq = L.visits[0](qstateful - 1, 0);
866 const T lambda = T(L.rates(src - 1, 0) * Vq);
867 const T mu = L.rates(q - 1, 0);
868 const T rho = T(lambda / mu);
869 const double Kcap = L.cap[q - 1];
870 const unsigned Kint = static_cast<unsigned>(std::llround(Kcap));
871 const T Ploss = qsys::qsys_mm1k_loss(lambda, mu, Kint).lossProbability;
872 const T Tq = T(lambda * (one - Ploss)); // carried throughput
873 T Lsys = zero;
874 if (std::fabs(num_traits<T>::to_double(rho) - 1.0) < 1e-10) {
875 Lsys = num_traits<T>::from_double(Kcap / 2.0); // L'Hopital limit at rho = 1
876 } else {
877 // K is a capacity, so the exponent is an INTEGER: num_pow_int stays exact
878 // at T = Rational, where pow(rational, rational) does not exist at all.
879 const T Kp1 = num_traits<T>::from_int(static_cast<int>(Kint) + 1);
880 const T rKp1 = num_pow_int(rho, Kint + 1);
881 Lsys = T(rho / (one - rho) - Kp1 * rKp1 / (one - rKp1));
882 }
883 NcSolution<T> lout;
884 mva::MvaSolution<T>& ls = lout.sol;
885 ls.Q = Matrix<T>(M, 1, zero);
886 ls.U = Matrix<T>(M, 1, zero);
887 ls.R = Matrix<T>(M, 1, zero);
888 ls.Tp = Matrix<T>(M, 1, zero);
889 ls.X.assign(1, zero);
890 ls.C.assign(1, zero);
891 ls.method = "mm1k.loss";
892 ls.iter = 1;
893 ls.lG = 0.0; // a closed form, not a convolution: no normalizing constant is formed
894 ls.R(q - 1, 0) = T(Lsys / Tq); // per-visit response time, by Little
895 ls.Q(q - 1, 0) = Lsys;
896 ls.U(q - 1, 0) = T(Tq / mu); // single-server utilization
897 ls.Tp(q - 1, 0) = Tq; // carried (effective) rate
898 ls.Tp(src - 1, 0) = lambda; // offered arrival rate
899 ls.X[0] = Tq; // system throughput = carried rate
900 ls.C[0] = T(ls.R(q - 1, 0) * Vq);
901 lout.actualmethod = "mm1k.loss";
902 return lout;
903 }
904
905 // Order-independent networks are intercepted BEFORE the
906 // multiserver-to-lldscaling rewrite below: an OI station is multiserver
907 // but is not a plain min(n,c) load-dependent station, so the rewrite
908 // would send it down a path that cannot represent its rate function.
909 if (nc_is_oi_model(L) && (opt.method == "default" || opt.method == "exact"))
910 return solver_nc_oi_analyzer(L, opt);
911
912 // A loss network is intercepted above the gates below: it is judged by
913 // has_product_form on the 'exact' path and its infinite server would be
914 // run through the multiserver-to-lldscaling rewrite, neither of which
915 // applies to a model whose only dynamics are the region's admission rule.
916 if (nc_is_lossn_model(L)) return solver_nc_lossn_analyzer(L, opt).sol;
917
918 // A stochastic Petri net takes the MDD-rec route: the reachable set
919 // lives in a decision diagram and the product form supplies the rates,
920 // so none of the queueing-network gates below say anything about it.
921 // `spn_pf` is where a net's product-form class is decided, by name.
922 for (std::size_t ind = 0; ind < L.nodes.size(); ++ind)
923 if (L.nodes[ind].nodetype == lang::NodeType::Place)
924 return solver_nc_spn_analyzer(L, opt).sol;
925
926 bool multiserver = false;
927 for (const qn::Station<T>& st : L.stations)
928 if (std::isfinite(st.nservers) && st.nservers > 1.0) multiserver = true;
929 bool anyLld = false;
930 for (const qn::Station<T>& st : L.stations)
931 if (!st.lldscaling.empty()) anyLld = true;
932
933 // How this model's finite multiserver stations are represented. The
934 // shipped "default" reproduces the historical dispatch exactly, so no
935 // result moves unless config.multiserver is set.
936 std::string ms_warning;
937 const std::string ms_policy =
938 detail::nc_multiserver_policy(opt.multiserver, &ms_warning);
939
940 if (opt.method == "default") {
941 // The two-station Delay-plus-multiserver shape: every SolverLN layer
942 // submodel. Exact through load-dependent CoMoM when product-form,
943 // Seidmann through 'comom' when not.
944 if (L.nstations == 2 && !detail::has_node_type(L, qn::NodeType::Cache) &&
945 detail::has_node_type(L, qn::NodeType::Delay) && multiserver) {
946 if (L.has_product_form() && !anyLld) {
947 if (detail::multiserver_to_lld(L)) anyLld = true;
948 } else {
949 opt.method = "comom";
950 }
951 } else if (ms_policy == "lld" && multiserver && !anyLld && L.has_product_form() &&
952 detail::nc_population_lattice(L) <= 6000.0) {
953 // config.multiserver="lld" generalizes the exact load-dependent
954 // lattice of the branch above to any closed product-form model,
955 // under the same 6000-state enumeration budget. Off unless asked
956 // for: with the shipped "default" policy this branch never runs
957 // and the model keeps Seidmann's approximation, as it always has.
958 if (detail::multiserver_to_lld(L)) anyLld = true;
959 }
960 } else if (opt.method == "exact" || opt.method == "is" || opt.method == "panald") {
961 // 'is' and 'panald' need the same model as 'exact', i.e. the same
962 // multiserver conversion.
963 //
964 // 'is' on an OI / P&S model is the specialised sampler, not the
965 // generic one: an OI station is multiserver but not min(n,c), and a
966 // P&S tandem has only per-communicating-class product form, so
967 // has_product_form is false and the gate below would reject it.
968 // nc_is_oi_model too, not only nc_is_pas_model: the latter demands
969 // that BOTH stations be OI/PAS, so a Delay + OI cycle -- the canonical
970 // topology pfqn_oi_is exists to sample -- fell to the guard below.
971 if (opt.method == "is" && (nc_is_pas_model(L) || nc_is_oi_model(L))) {
972 // handled by solver_nc_analyzer (pfqn_pas_is / pfqn_oi_is)
973 } else if (nc_is_lossn_model(L)) {
974 // A LOSS NETWORK (open, one DROP region holding a single Delay)
975 // IS product form -- the truncated Poisson law the residue
976 // transform of solver_nc_lossn evaluates exactly under 'exact'
977 // -- but any region reads as blocking, so has_product_form says
978 // no. Exempted here as `nc_method_refusal` already exempts it.
979 } else if (!L.has_product_form())
980 throw UnsupportedError(
981 "SolverNC: the '" + opt.method +
982 "' method requires the model to have a product-form solution, and this model "
983 "does not");
984 // Only a GENUINE multiserver is converted: filling lldscaling with
985 // ones on an all-single-server model is a semantic no-op that forces
986 // the load-dependent path, whose recovery mishandles a
987 // single-station-confined closed chain.
988 // config.multiserver="seidmann" asks for Seidmann's approximation on
989 // this arm too, so the conversion is skipped. Off by default.
990 if (!anyLld && multiserver && ms_policy != "seidmann") {
991 if (detail::multiserver_to_lld(L)) anyLld = true;
992 }
993 }
994
995 // Caches, in the reference's order: a delayed-hit retrieval system
996 // first (open or closed), then the non-reentrant Source-Cache-Sink
997 // model, then any other cache as an integrated caching-queueing network.
998 // The order is the contract -- a retrieval cache is ALSO a cache, and a
999 // Source-Cache-Sink model is also "a model with a cache node".
1000 if (nc_has_retrieval(L)) {
1001 if (detail::has_node_type(L, qn::NodeType::Source)) {
1003 rr.sol.cache = detail::cache_metrics_of(L, rr.hitprob, rr.missprob, rr.delayedprob,
1004 rr.latency, rr.hitproblist, rr.itemprob,
1005 std::vector<T>());
1006 return rr.sol;
1007 }
1009 cr.sol.cache = detail::cache_metrics_of(L, cr.hitprob, cr.missprob, cr.delayedprob,
1010 cr.latency, cr.hitproblist, Matrix<T>(),
1011 std::vector<T>());
1012 return cr.sol;
1013 }
1014 if (nc_is_noreentrant_cache(L)) {
1016 // The non-reentrant model reports the per-list hit fractions and the
1017 // per-item law but no scalar hit probability of its own: the hit and
1018 // miss columns are the row sums of `hitproblist` and its complement,
1019 // and `cache_metrics_of` derives them rather than leaving the total
1020 // row of getAvgCacheTable empty on the one branch that knows most.
1021 cr.sol.cache = detail::cache_metrics_of(L, cr.hitprob, cr.missprob,
1022 std::vector<T>(), std::vector<T>(),
1023 cr.hitproblist, cr.itemprob, cr.listcost);
1024 return cr.sol;
1025 }
1026 if (nc_is_cacheqn(L)) {
1028 res = cr.sol;
1030 // hitprob/missprob are (ncaches x nclasses) here, one row per cache,
1031 // against the (K) vectors the retrieval branches return.
1032 res.cache = detail::cache_metrics_of_matrix(L, cr.hitprob, cr.missprob);
1033 // the per-item law rides in separately: this branch has one per cache
1034 // rather than one per model
1035 for (std::size_t ci = 0; ci < res.cache.caches.size() && ci < cr.itemprob.size(); ++ci)
1036 res.cache.caches[ci].itemprob = cr.itemprob[ci];
1037 return res;
1038 }
1039
1040 // A region this solver cannot enforce must STOP the solve, not be
1041 // dropped from it. Everything below computes an unconstrained answer,
1042 // so a model that reaches it with a live region would be reported as
1043 // though the region were absent -- numbers that are not wrong for any
1044 // model the user described. The reference raises at the same two points
1045 // (`runAnalyzer.m:326,334-336`) for the same reason.
1046 //
1047 // The loss-network SHAPE with a rule other than all-DROP is named
1048 // separately from a region on ordinary queueing stations, because the
1049 // two have different remedies: the first is asking for blocking, where
1050 // switching the rule to DROP makes the model solvable here, while the
1051 // second needs a solver that carries the region as state.
1052 // The token gates that used to stand here -- the loss-network shape under
1053 // a rule other than all-DROP, a Finite Capacity Region on queueing
1054 // stations, "erlangfp"/"ms"/"rec" off a loss network and "rayint"/"spm"
1055 // with no Cache node -- moved into `nc_method_refusal`, which decides them
1056 // from the same struct before the dispatch begins and which
1057 // `auto_family_refusal` asks too; their wording is unchanged. The six
1058 // load-dependent evaluators on an OPEN chain are now refused by the
1059 // feature set instead (`nc_feature_set` drops OpenClass from them):
1060 // "closed population only" is a rule the registry CAN name, and naming it
1061 // there is what makes `auto_find_solver` drop the row rather than report
1062 // it runnable.
1063
1064 if (anyLld || nc::detail::has_scaling(L)) {
1065 res = solver_ncld_analyzer(L, opt);
1066 } else if (opt.method == "rd" || opt.method == "nrp" || opt.method == "nrl" ||
1067 opt.method == "nre" || opt.method == "comomld" || opt.method == "panald") {
1068 res = solver_ncld_analyzer(L, opt);
1069 } else {
1070 res = solver_nc_analyzer(L, opt);
1071 }
1072 // A config.multiserver value this solver does not implement was silently
1073 // ignored before; it now says so. Joined rather than overwriting, as the
1074 // analyzer warnings are.
1075 if (!ms_warning.empty())
1076 res.warning = res.warning.empty() ? ms_warning : res.warning + " " + ms_warning;
1077 return res;
1078 }
1079}
1080
1081/**
1082 * Port of `@@SolverNC/runAnalyzer.m` for the `lang='matlab'` path: solve, then
1083 * apply the metric filter `@@NetworkSolver/getAvg` puts between the analyzer and
1084 * the caller.
1085 */
1086template <class T>
1087mva::AvgResult<T> solver_nc_avg_table(const qn::NetworkStruct<T>& L_in, const NcSolution<T>& d,
1088 const std::string& origmethod);
1089
1090template <class T>
1092 return solver_nc_avg_table(L_in, solver_nc_solve(L_in, opt_in), opt_in.method);
1093}
1094
1095/**
1096 * The metric filter half of `solver_nc_run_analyzer`, for a caller that already
1097 * holds the `NcSolution` and needs more of it than the table (the CLI's cftp
1098 * banner reads the sampled states and horizons off the same solve).
1099 */
1100template <class T>
1102 const std::string& origmethod) {
1103 const mva::MvaSolution<T>& s = d.sol;
1104 const std::string& actualmethod = d.actualmethod;
1105 const qn::NetworkStruct<T>& L = L_in;
1106
1107 const std::size_t M = L.nstations, K = L.nclasses;
1108 std::vector<std::vector<bool>> mask(M, std::vector<bool>(K, false));
1109 for (std::size_t i = 0; i < M; ++i)
1110 for (std::size_t k = 0; k < K; ++k)
1111 mask[i][k] = num_traits<T>::to_double(s.R(i, k)) < 10.0 * GlobalConstants::FineTol;
1112 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(K, false));
1113 for (std::size_t i = 0; i < M; ++i)
1114 if (L.stations[i].nodetype == qn::NodeType::Source)
1115 for (std::size_t k = 0; k < K; ++k) srcmask[i][k] = true;
1116
1118 out.QN = mva::filter_metric(L, s.Q, mva::MetricKind::QLen, &mask);
1119 out.UN = mva::filter_metric(L, s.U, mva::MetricKind::Util, &mask);
1120 out.RN = mva::filter_metric(L, s.R, mva::MetricKind::RespT, nullptr);
1121 out.TN = mva::filter_metric(L, s.Tp, mva::MetricKind::Tput, nullptr);
1122 // Both ArvR and ResidT come from the refreshed struct when the cacheqn
1123 // analyzer supplied one: its cache self-switch is normalized at the ACTUAL
1124 // hit/miss split, so the visits back the carried rate without the over-route
1125 // inflation. filter_metric still keys on the base L for its topology masking.
1128 mva::MetricKind::ResidT, nullptr);
1130 &srcmask);
1131 // The saturated open-station rule of getAvg.m, applied AFTER out.WN so the
1132 // residence times keep their pre-saturation value. Without it SolverNC
1133 // reported the mixed formula's raw `QLen 3.05, Util 61.8` for a station
1134 // MATLAB and SolverMVA both call saturated (chaos seed 215).
1135 std::string satwarn;
1136 if (mva::cap_unstable_open_stations(L, out.QN, out.UN, out.RN, out.TN))
1137 satwarn = mva::unstable_open_warning();
1138 out.CN = s.C;
1139 out.XN = s.X;
1140 out.method = origmethod;
1141 // `runAnalyzer` reports 'default/<algorithm>' when the caller asked for the
1142 // default and something more specific ran, so the algorithm that produced
1143 // the numbers is never lost.
1144 out.actualmethod = (origmethod == "default" && !actualmethod.empty() &&
1145 actualmethod != "default")
1146 ? "default/" + actualmethod
1147 : actualmethod;
1148 // Approximated, not refused, for the reason SolverMVA gives: a normalizing
1149 // constant counts a fed-back visit as an ordinary re-entry, having no way
1150 // to express a job that keeps its server. `@@SolverNC/runAnalyzer.m` warns
1151 // and solves, and this text is that warning verbatim.
1152 if (L.has_immediate_feedback())
1153 out.warning =
1154 "SolverNC does not handle immediate feedback (immfeed); the solver will treat "
1155 "self-loops as class-switching with re-queueing.";
1156 // A warning raised by an analyzer (the cache branch's cost-cap conditions)
1157 // must survive the runner; both can fire, so they are joined rather than
1158 // one overwriting the other.
1159 if (!d.warning.empty())
1160 out.warning = out.warning.empty() ? d.warning : out.warning + " " + d.warning;
1161 if (!satwarn.empty())
1162 out.warning = out.warning.empty() ? satwarn : out.warning + " " + satwarn;
1163 out.listcost = d.listcost;
1164 // What the cache branch measured, carried to `getAvgCacheTable` and
1165 // `getAvgItemTable`; empty on every model without a Cache node.
1166 out.cache = d.cache;
1167 // `runAnalyzer.m`'s last line: the log normalizing constant is stored beside
1168 // the average table rather than recomputed, so `getProbNormConstAggr` costs
1169 // the caller nothing once `getAvg` has run.
1170 out.lognormconst = s.lG;
1171 out.iter = s.iter;
1172 {
1173 const double lg = num_traits<T>::to_double(s.lG);
1174 if (std::isfinite(lg))
1175 line::util::LineConsole::step("normalizing constant obtained: log G = %.6g", lg);
1176 }
1177 return out;
1178}
1179
1180/** Port of `@@SolverNC/getProbNormConstAggr.m`: the log normalizing constant. */
1181template <class T>
1183 return solver_nc_solve(L, opt).sol.lG;
1184}
1185
1186} // namespace nc
1187} // namespace line
1188
1189#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
Whether immediate feedback is EFFECTIVE anywhere in the model.
std::size_t stateful_of_station(std::size_t st) const
std::vector< std::vector< bool > > disabled
std::vector< double > cap
sn.cap and sn.classcap: the total and per-class buffers.
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< NodeDef > nodes
every node, in creation order
std::vector< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
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").
bool sn_is_mm1k_loss(const qn::NetworkStruct< T > &sn)
sn_is_mm1k_loss: the three-node Source/Queue/Sink model of an M/M/1/K with loss, the shape MVA answer...
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
@ FIFO
first in, first out
Definition lang_types.h:382
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.
bool cap_unstable_open_stations(const qn::NetworkStruct< T > &L, Matrix< T > &QN, Matrix< T > &UN, Matrix< T > &RN, const Matrix< T > &TN)
The saturated open-station rule of @@NetworkSolver/getAvg.m:221-308.
const char * unstable_open_warning()
The warning getAvg.m raises when cap_unstable_open_stations() fires.
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:389
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:435
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
NcCftpSolution< T > solver_nc_cftp(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const NcCftpOptions &cftpopt)
Solve with the cftp / cftp.approx method.
mva::AvgResult< T > solver_nc_run_analyzer(const qn::NetworkStruct< T > &L_in, const NcSolverOptions &opt_in)
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.
NcSolution< T > solver_nc_cftp_solution(const NcCftpSolution< T > &s)
A cftp solve in the shape every other SolverNC analyzer returns, so the runner's metric filter applie...
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...
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?
mva::AvgResult< T > solver_nc_avg_table(const qn::NetworkStruct< T > &L_in, const NcSolution< T > &d, const std::string &origmethod)
Port of @@SolverNC/runAnalyzer.m for the lang='matlab' path: solve, then apply the metric filter @@Ne...
std::string solver_nc_cftp_supports(const qn::NetworkStruct< T > &sn)
The cftp model-class gate as a public predicate.
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.
Mm1kLossResult< T > qsys_mm1k_loss(const T &lambda, const T &mu, unsigned K)
Blocking probability of the M/M/1/K queue.
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...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:218
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.
Blocking probability of the M/M/1/K queue.
Chain aggregation and de-aggregation.
Ports of the sn_has_* / sn_is_* predicate family of matlab/src/api/sn.
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 ...
The cftp and cftp.approx methods of SolverNC: stationary analysis of a closed single-class product-fo...
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:760
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 ...