LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
auto_methods.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_AUTO_AUTO_METHODS_H
6#define LINE_SOLVERS_AUTO_AUTO_METHODS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * `SolverAUTO.listValidMethods`: the method names THIS MODEL can actually run.
12 *
13 * WHY IT IS A SEPARATE HEADER. `solver_auto.h` is deliberately light -- a
14 * struct, a ranking table and the feature sets -- and it is included by the CLI,
15 * the facade, the examples and `test_all_headers`. Answering this question needs
16 * every family's method registry, i.e. the MVA, NC, CTMC, fluid, MAM, BA, SSA,
17 * LDES, JMT and QNS runners, so putting it there would make a heavy include of a
18 * cheap one for every caller that only wants the ranking.
19 *
20 * THE GATE IS THE FEATURE SET, asked of every candidate rather than of the first
21 * feasible one. `auto_supports` already asks it for the seven ranked slots, and
22 * it is the same question `chooseSolverRanked` asks before delegating: is every
23 * feature the model uses declared by the solver that would run this method? A
24 * family whose feature set refuses the model contributes nothing, and a method
25 * whose own set refuses it is not offered. Both matter, because a per-method set
26 * is where the deltas live -- MVA's `rqna` consumes MAP arrivals that the rest of
27 * the envelope does not, MAM's LoadDependence holds only for the methods that
28 * read `lldscaling`.
29 *
30 * WHAT A FLAT FEATURE SET CANNOT SAY, and is therefore asked separately, exactly
31 * as the reference's `supportsModelMethod` overrides do:
32 * product form 'exact' is not exact without one (SolverMVA's OI/PAS
33 * stations excepted); folded into `auto_supports`
34 * a chain that fits the CTMC slot is screened for state-space size
35 * a binding buffer setCapacity/classCap, which MVA, NC, FLD and QNS refuse
36 * through `check_binding_capacity` and no feature name
37 * describes
38 * the model SHAPE `mva::list_valid_methods(L)` and `ba::list_valid_methods(L)`
39 * are themselves model-aware: the queueing-system closed
40 * forms are offered on a two-station open model and nowhere
41 * else, the QRF reduction bounds on a single-class closed
42 * network of single servers, the three open-network bounds
43 * on a fully open one. Asking the registry for the model is
44 * what keeps that rule in one place.
45 *
46 * A FAMILY WHOSE EVERY METHOD IS REFUSED LOSES ITS BARE TOKEN TOO: `nc` alone
47 * delegates to SolverNC, which is exactly the rejection the per-method gate just
48 * returned.
49 *
50 * WHAT IS NOT LISTED, and why each is a token this port would refuse anyway:
51 * ln, env, lqns, uq take a LayeredNetwork, an Environment or an inner-solver
52 * factory rather than a Network. The reference's own
53 * `buildFamilySolver` cannot construct them from here
54 * either, and its `familyAcceptsModelClass` drops them.
55 * ldes without an engine `auto_supports` folds `auto_solver_is_available` in,
56 * and the CLI refuses `--method ldes` by name when no
57 * engine is found beside the binary. Listing it there
58 * would be a claim the very next call denies.
59 * JMT IS LISTED although it is not a ranked candidate. It has no slot in
60 * `AutoSolver` -- the reference removed it as one, LDES subsuming its feature
61 * set -- but `-s auto --method jmt` reaches it as an explicit method name, and
62 * `listValidMethods` answers about the method names the caller may ask for, not about
63 * the ones the ranking would pick.
64 *
65 * THERE IS NO `list_all_methods` TWIN. In the reference the model-independent
66 * list exists so that a method-NAME check can reject an unknown method name with the
67 * family's own explanation rather than a flat "unsupported"; `auto_resolve_token`
68 * here performs no such check -- it splits the method name and lets the family refuse
69 * the submethod by name -- so a second list would have no caller.
70 */
71
72#include <algorithm>
73#include <array>
74#include <cctype>
75#include <cmath>
76#include <cstddef>
77#include <initializer_list>
78#include <stdexcept>
79#include <string>
80#include <vector>
81
83#include "line/util/error.h"
90// The two shape predicates the report asks for, which the analyzer header above
91// does not pull in: cftp and mdd each build no generator and are restricted to a
92// shape the state-space guard says nothing about.
103
104namespace line {
105namespace autosolver {
106
107/**
108 * The method families that solve a flat Network, in `SolverAUTO.familyNames`'
109 * own order -- which is also the order an unqualified algorithm name is looked
110 * up in, so it is not arbitrary.
111 *
112 * "ag" SITS AFTER "mam", whose RCAT names it took over, and is a family for the
113 * METHOD NAME and REPORT tables only: `--method ag.inap` and the model help
114 * reach SolverAG through it. It is deliberately NOT one of the ranked
115 * `AutoSolver` slots and NOT in the feature-set union the automatic ranking
116 * takes, so a G-network is still refused by "default" and has to be asked for
117 * by name; see _kb/06-solver-catalog.md, "SolverAG owns the RCAT methods".
118 */
119inline std::vector<std::string> auto_network_family_names() {
120 return {"mva", "nc", "ctmc", "fluid", "mam", "ag", "ba", "ssa", "ldes", "jmt", "qns"};
121}
122
123/**
124 * The prefixes under which a family advertises a SECOND SPELLING of a method it
125 * already declares plainly.
126 *
127 * THIS IS A DECLARATION, not a derivation, and belongs beside
128 * `auto_family_metrics` and `auto_method_class` for the same reason: the
129 * knowledge lives in the solver's own dispatch (the mva registry strips a
130 * leading "amva." before selecting an algorithm) and nothing exposes it, so a
131 * family that gains or loses an alias spelling must be edited into all four
132 * copies in the SAME change. An omission does not fail; it puts the same
133 * algorithm in the report twice.
134 */
135inline std::vector<std::string> auto_method_alias_prefixes(const std::string& family) {
136 if (family == "mva") return {"amva."};
137 return {};
138}
139
140/**
141 * Is `name` a second spelling of another method this family declares?
142 *
143 * The remainder has to be declared too, which is what keeps the rule from eating
144 * a genuine method that merely starts with the prefix: it is an alias only when
145 * the thing it aliases is there beside it.
146 */
147inline bool auto_is_method_alias(const std::string& family, const std::string& name,
148 const std::vector<std::string>& declared) {
149 for (const std::string& prefix : auto_method_alias_prefixes(family)) {
150 if (name.size() > prefix.size() && name.compare(0, prefix.size(), prefix) == 0 &&
151 std::find(declared.begin(), declared.end(), name.substr(prefix.size())) !=
152 declared.end())
153 return true;
154 }
155 return false;
156}
157
158namespace detail {
159
160/** The ranked slot a family occupies, or false when it has none. */
161inline bool auto_slot_of_family(const std::string& family, AutoSolver& out) {
162 if (family == "mva") { out = AutoSolver::MVA; return true; }
163 if (family == "nc") { out = AutoSolver::NC; return true; }
164 if (family == "mam") { out = AutoSolver::MAM; return true; }
165 if (family == "fluid") { out = AutoSolver::FLUID; return true; }
166 if (family == "ssa") { out = AutoSolver::SSA; return true; }
167 if (family == "ctmc") { out = AutoSolver::CTMC; return true; }
168 if (family == "ldes") { out = AutoSolver::LDES; return true; }
169 return false;
170}
171
172/**
173 * The finite-buffer gate as a predicate.
174 *
175 * `has_binding_capacity` is the gate's own question asked without raising, which
176 * is what this needs: on the solve path a capped station the solver cannot
177 * honour has to stop the run rather than be reported unconstrained, but here the
178 * same verdict is a yes or a no. The families are the ones whose own runners ask
179 * it -- SolverMVA, SolverNC, SolverFLD and SolverQNS -- and no feature name
180 * describes a capacity, which is why it cannot ride in the feature set.
181 *
182 * NC EXEMPTS ITS MEM ALGORITHM, which solves the censored GE/GE/c/0;N queue and
183 * therefore DOES honour the buffer. The exemption is on the LITERAL method name
184 * that `nc_dispatch` branches on, not on a resolved one: nothing resolves
185 * 'default' into 'mem', so exempting a default run would advertise a token that
186 * dispatches somewhere the buffer is ignored.
187 */
188template <class T>
189bool capacity_admits(const std::string& family, const qn::NetworkStruct<T>& sn,
190 const std::string& method) {
191 if (family != "mva" && family != "nc" && family != "fluid" && family != "qns") return true;
192 if (!qn::has_binding_capacity(sn)) return true;
193 if (family == "nc" && method == "mem") {
194 const nc::MemSupport ms = nc::solver_nc_mem_supports(sn);
195 return ms.supported && ms.blocking;
196 }
197 return false;
198}
199
200/**
201 * The feature set a family declares for a method, and the label its refusals
202 * are reported under; false when the family solves no flat Network.
203 *
204 * It is the switch `auto_supports` opens on, lifted out so that the bool gate
205 * and the reason-returning one read the same table. A second copy of it is how
206 * a family gains a feature in one answer and not in the other.
207 */
208inline bool declared_feature_set(const std::string& family, const std::string& method,
209 qn::FeatureSet& declared, std::string& label) {
210 if (family == "mva") { declared = qn::mva_feature_set(method); label = "SolverMVA"; return true; }
211 if (family == "nc") { declared = qn::nc_feature_set(method); label = "SolverNC"; return true; }
212 if (family == "mam") { declared = qn::mam_feature_set(method); label = "SolverMAM"; return true; }
213 if (family == "fluid") { declared = qn::fluid_feature_set(method); label = "SolverFLD"; return true; }
214 if (family == "ssa") { declared = qn::ssa_feature_set(method); label = "SolverSSA"; return true; }
215 if (family == "ctmc") { declared = qn::ctmc_feature_set(method); label = "SolverCTMC"; return true; }
216 if (family == "ldes") { declared = qn::ldes_feature_set(method); label = "SolverLDES"; return true; }
217 if (family == "ag") { declared = qn::ag_feature_set(method); label = "SolverAG"; return true; }
218 if (family == "ba") { declared = qn::ba_feature_set(method); label = "SolverBA"; return true; }
219 if (family == "jmt") { declared = qn::jmt_feature_set(method); label = "SolverJMT"; return true; }
220 if (family == "qns") { declared = qn::qns_feature_set(method); label = "SolverQNS"; return true; }
221 return false;
222}
223
224} // namespace detail
225
226/**
227 * The methods a family declares ON THIS MODEL, empty when it declares none.
228 *
229 * The registries are asked rather than copied: `mva::list_valid_methods` and
230 * `ba::list_valid_methods` take the struct and narrow themselves, and a second
231 * copy of either shape rule here is how the two drift apart.
232 */
233template <class T>
234std::vector<std::string> auto_family_methods(const std::string& family,
235 const qn::NetworkStruct<T>& sn) {
236 if (family == "mva") return mva::list_valid_methods(sn);
237 if (family == "nc") return nc::list_valid_methods();
238 if (family == "ctmc") return ctmc::list_valid_methods();
239 if (family == "fluid") return fluid::fluid_list_valid_methods();
240 if (family == "mam") return mam::list_valid_methods();
241 if (family == "ag") return ag::list_valid_methods();
242 if (family == "ba") return ba::list_valid_methods(sn);
243 if (family == "ssa") return ssa::list_valid_methods();
244 if (family == "ldes") return ldes::list_valid_methods();
245 if (family == "jmt") return jmt::jmt_list_valid_methods();
246 if (family == "qns") return qns::list_valid_methods();
247 return std::vector<std::string>();
248}
249
250/**
251 * `Solver.supportsModelMethod` for a family method name: may THIS model run THIS
252 * method of THIS family?
253 *
254 * The seven ranked slots defer to `auto_supports`, which is the same call
255 * `chooseSolverRanked` makes and already carries the product-form rule, the
256 * CTMC state-space screen and the LDES engine probe. The three families with no
257 * slot are gated on their own declared set, since they are reachable by explicit
258 * token and by no ranking.
259 */
260template <class T>
261std::string auto_family_refusal(const std::string& family, const qn::NetworkStruct<T>& sn,
262 const std::string& method) {
263 if (!detail::capacity_admits(family, sn, method))
264 return "SolverAUTO: this method ignores the finite station capacity this model sets "
265 "(setCapacity / classCap), so it would report a capped station as unbounded.";
266
267 qn::FeatureSet declared;
268 std::string label;
269 if (!detail::declared_feature_set(family, method, declared, label))
270 return "SolverAUTO: the '" + family + "' method family does not solve a flat Network.";
271
273 if (!r.ok) return r.reason;
274
275 // NC's structural per-method rules, the ones no feature name can carry:
276 // whether the method has a route on THIS model at all. `nc_method_refusal`
277 // is the single copy of them, asked here and by `solver_nc_solve`, so a pair
278 // this report offers is a pair the run accepts. Asked BEFORE `auto_supports`
279 // because it names the offending thing, where the fallback below can only
280 // say that some structural check refused the model.
281 if (family == "nc") {
282 const std::string ncr = nc::nc_method_refusal(sn, method);
283 if (!ncr.empty()) return ncr;
284 }
285
286 // THE STRUCTURAL RULES OF THE TWO WRAPPER-ADJACENT FAMILIES, asked of the
287 // families themselves rather than restated here. `jmt` has no ranked slot,
288 // so the branch below never reaches it, and `auto_supports` asks the MAM
289 // slot for its feature set only -- which is how findSolver came to offer
290 // jmt.jmva.<alg> on a multi-server model, jmt.replication with no horizon
291 // and mam.retrial on a model that declares no orbit, each of which then
292 // raised when it was run. The two calls are the solvers' OWN predicates,
293 // the same ones their runners raise, taken with the default options a probe
294 // solver carries.
295 if (family == "jmt") {
296 const std::string why = jmt::jmt_method_refusal(sn, method, jmt::JmtOptions());
297 if (!why.empty()) return why;
298 }
299 if (family == "mam") {
300 const std::string why = mam::mam_model_method_refusal(sn, method);
301 if (!why.empty()) return why;
302 }
303 // AG's own structural gate, on the same terms: RCAT needs a Markovian
304 // (D0,D1) service law, state-independent routing, one server per station and
305 // no binding buffer, and none of the four is a feature name. It has no
306 // ranked slot, so nothing below reaches it.
307 if (family == "ag") {
308 const std::string why = ag::runner_detail::method_refusal(sn, method);
309 if (!why.empty()) return why;
310 }
311 // QNS on the same terms: immediate feedback is outside BOTH conversions,
312 // and the multiserver approximations `qnsolver -m` does not offer are a
313 // rule about the method rather than about the model.
314 if (family == "qns") {
315 const std::string why = qns::method_refusal(sn, method);
316 if (!why.empty()) return why;
317 }
318
319 // THE REMAINING THREE FAMILIES, on the same terms. `ba` has no ranked slot
320 // either, so nothing below reaches it; `ctmc` and `fluid` do have one, but
321 // `auto_supports` asks each for a rule about the SOLVER (state-space size,
322 // product form) and never about the METHOD, which is where a class count
323 // and a horizon live. Each call is the family's own predicate, the same one
324 // its analyzer raises, so a pair this report offers is a pair the run
325 // accepts and the two cannot drift.
326 if (family == "ba") {
327 const std::string why = ba::method_refusal(sn, method);
328 if (!why.empty()) return "SolverBA: " + why;
329 }
330 if (family == "ctmc") {
331 // Only the two shape-restricted methods have one; the rest are gated by
332 // the state-space size below, which is a question about the solver.
333 const std::string why =
334 (method == "cftp" || method == "cftp.approx")
336 : (method == "mdd" ? ctmc::solver_ctmc_mdd_supports(sn) : std::string());
337 if (!why.empty()) return why;
338 }
339 // THE FORK-JOIN MODEL CLASS, which is a question about the MODEL and not
340 // about the method, so both exact families have to clear it whichever name
341 // was asked for: each tag-augments through `fj_tag`, whose first line is
342 // `sn_fj_validate`. The feature set cannot state it -- Fork and Join are
343 // declared, and the rules are about how they are WIRED -- and the pairing in
344 // particular is a DECLARATION on the Join rather than something the routing
345 // implies, so a Join that names no fork leaves an unmatched Fork behind.
346 if (family == "ctmc" || family == "ssa") {
347 const std::string why = qn::sn_fj_supports(sn);
348 if (!why.empty()) return why;
349 }
350 // `nrm` IS THE ONE SSA METHOD WITH A MODEL CLASS OF ITS OWN, and the test
351 // already existed: `ssa_nrm_eligible` decides whether the dispatch may
352 // PREFER the NRM, while nothing decided whether the name could be OFFERED.
353 // So `ssa.nrm` was reported runnable on every model and an explicit request
354 // then raised from the analyzer. `ssa_nrm_supports` is the same six checks
355 // read as a sentence.
356 if (family == "ssa" && method == "nrm") {
357 const std::string why = ssa::detail::ssa_nrm_supports(sn);
358 if (!why.empty()) return why;
359 }
360 if (family == "fluid") {
361 // The horizon is an OPTION, not a model feature, so it cannot be a
362 // feature-set delta. A probe carries the defaults, under which the
363 // time-varying methods have no finite end and the refusal says so.
364 const std::string why = fluid::fluid_qsys_horizon_supports(method, fluid::FluidOptions());
365 if (!why.empty()) return why;
366 // Fork AND open is a CONJUNCTION of two declared names, which no feature
367 // set can state; `dae` is the one method the MMT fixed point has no
368 // route for on an open model.
369 const std::string fj = fluid::detail::fluid_forkjoin_supports(sn, method);
370 if (!fj.empty()) return fj;
371 }
372
373 // WHAT auto_supports STILL REFUSES once the feature set has passed is one of
374 // its two residual rules, and each is named rather than reported as a bare
375 // no. Asking auto_supports rather than repeating the rules keeps it the one
376 // authority; the branches below only turn its verdict into a sentence.
378 if (detail::auto_slot_of_family(family, slot)) {
379 if (!auto_solver_is_available(slot))
380 return std::string(auto_solver_name(slot)) +
381 ": no engine for this solver is available in this build.";
382 if (!auto_supports(slot, sn, method)) {
383 if (method == "exact" && (slot == AutoSolver::MVA || slot == AutoSolver::NC))
384 return std::string(auto_solver_name(slot)) +
385 ": 'exact' needs a product-form model, and this one has no product-form "
386 "solution.";
387 if (slot == AutoSolver::CTMC)
388 return "SolverCTMC: the state space of this model is too large to enumerate.";
389 return label + ": this solver refuses the model through its own structural check.";
390 }
391 }
392 return "";
393}
394
395template <class T>
396bool auto_family_supports(const std::string& family, const qn::NetworkStruct<T>& sn,
397 const std::string& method) {
398 return auto_family_refusal(family, sn, method).empty();
399}
400
401// ---------------------------------------------------------------------------
402// findSolver: which solvers and methods can analyze this model
403// Port of @SolverAUTO/findSolver.m and its native python and JAR twins.
404// ---------------------------------------------------------------------------
405
406/**
407 * One row of `auto_find_solver`: a (family, method) pair this model can be
408 * asked for, whether it runs, what kind of answer it returns and which
409 * measures it can report.
410 *
411 * The fields are the columns of the MATLAB table, of the native python frame
412 * and of the JAR's SolverCandidate, under the same names, so a report can be
413 * compared across the four codebases row for row.
414 */
416 std::string solver; ///< the method family: "mva", "ctmc", "ldes", ...
417 std::string method; ///< the method name to pass, "mva.exact"
418 bool runnable = false; ///< the model passes this method's own support gate
419 std::string method_class; ///< "exact", "approx", "bound" or "simulation"
420 std::string metrics; ///< the measure groups the family answers, comma-joined
421 std::string reason; ///< why a refused pair was refused, "" when runnable
422};
423
424/**
425 * The measure groups `auto_find_solver` reports on, in report order.
426 *
427 * A group is a family of accessors that stand or fall together: a solver that
428 * returns getCdfRespT returns getCdfPassT and getPerctRespT as well, because
429 * all three read the same passage time, so listing the three separately would
430 * say nothing extra.
431 */
432inline std::vector<std::string> auto_metric_groups() {
433 return {"avg", "tran", "cdf", "prob", "tranprob", "sample",
434 "cache", "loss", "orbit", "moment", "sens"};
435}
436
437namespace detail {
438
439inline bool name_in(const std::string& n, std::initializer_list<const char*> names) {
440 for (const char* c : names)
441 if (n == c) return true;
442 return false;
443}
444
445inline bool starts_with(const std::string& s, const std::string& p) {
446 return s.size() >= p.size() && s.compare(0, p.size(), p) == 0;
447}
448
449} // namespace detail
450
451/**
452 * The measure group an accessor belongs to, "" when the name belongs to none.
453 *
454 * A group name maps to itself, so `auto_find_solver(sn, "cdf")` and
455 * `auto_find_solver(sn, "getCdfRespT")` ask the same question.
456 *
457 * THIS IS NOT `auto_choose_solver`'S TABLE, although both are keyed by accessor
458 * name. That one maps an accessor to a RANKING, i.e. which candidate should be
459 * preferred; this one maps it to a CAPABILITY question, i.e. which candidates
460 * can answer it at all. The two differ wherever a family can serve a measure
461 * but is never the one AUTO would pick for it.
462 */
463inline std::string auto_metric_group_of(const std::string& name) {
464 if (name.empty()) return "";
465 for (const std::string& g : auto_metric_groups())
466 if (g == name) return g;
467 if (name == "any" || name == "all") return "";
468 if (detail::name_in(name, {"getTranAvg", "getTranAvgVar", "tranAvg"})) return "tran";
469 if (detail::name_in(name, {"getCdfRespT", "getCdfPassT", "getPerctRespT", "getTranCdfPassT",
470 "getTranCdfRespT", "getCdfSysRespT"}))
471 return "cdf";
472 if (detail::name_in(name, {"getTranProb", "getTranProbSys", "getTranProbAggr",
473 "getTranProbSysAggr"}))
474 return "tranprob";
475 if (detail::name_in(name, {"getProb", "getProbAggr", "getProbSys", "getProbSysAggr",
476 "getProbMarg", "getProbNormConstAggr"}))
477 return "prob";
478 if (detail::name_in(name, {"sample", "sampleSys", "sampleAggr", "sampleSysAggr"}))
479 return "sample";
480 if (detail::name_in(name, {"getAvgCacheTable", "getAvgCacheT", "getAvgItemTable",
481 "getAvgItemT", "cacheAvgT", "itemAvgT", "aCaT", "aIT"}))
482 return "cache";
483 if (detail::name_in(name, {"getAvgLossTable", "getAvgLossT", "getAvgRegionLossTable",
484 "getAvgRegionLossT", "lossAvgT", "regionLossAvgT", "aLT", "aRLT"}))
485 return "loss";
486 if (detail::name_in(name, {"getAvgOrbitTable", "getAvgOrbitT", "getAvgOrbit", "orbitAvgT",
487 "aOT"}))
488 return "orbit";
489 if (detail::name_in(name, {"getMomentTable", "getMomentChainTable", "getMomentStationTable",
490 "getMomentT", "getMomentChainT", "getMomentStationT", "momentT",
491 "momentChainT", "momentStationT", "mT", "mCT", "mST"}))
492 return "moment";
493 if (detail::name_in(name, {"getSensitivityTable", "getSensitivityT", "sensitivityT", "sT",
494 "getSensitivity", "getSensitivityRanking"}))
495 return "sens";
496 // Everything else in the accessor surface is a mean measure: getAvg, its
497 // chain, node and system forms, their handles and their short aliases.
498 if (detail::starts_with(name, "getAvg") || detail::starts_with(name, "avg") ||
499 detail::name_in(name, {"getAvgSysRespT", "getAvgSysTput", "aT", "aNT", "aCT", "aST",
500 "aNCT"}))
501 return "avg";
502 return "";
503}
504
505/**
506 * The measure groups a method family can answer.
507 *
508 * Every family answers "avg", which is what a solver is for; the rest is the
509 * capability declaration this header owns.
510 *
511 * SOURCES, so that a claim here can be checked rather than trusted: "tran" is
512 * the reference's supportsTransientAnalysis, which FLD, CTMC, LDES and JMT
513 * override to true and no one else does. "cdf", "prob", "tranprob" and "sample"
514 * are the families that carry an implementation of the corresponding accessor
515 * rather than inheriting the base refusal. The remaining five groups are
516 * computed from a solver's own results, so no per-solver entry point marks
517 * them: their lists are the reference chooseSolverHeur's rankings for the same
518 * accessors, which is where AUTO already records who can serve them.
519 *
520 * A family that gains or loses a measure must be edited here in the same
521 * change, the way a solver that gains a feature is edited into its feature set:
522 * an omission here does not fail, it silently hides the family from a caller
523 * asking for that measure.
524 */
525inline std::vector<std::string> auto_family_metrics(const std::string& family) {
526 if (family == "mva") return {"avg", "prob", "cache", "orbit", "moment", "sens"};
527 if (family == "nc") return {"avg", "cdf", "prob", "cache", "moment", "sens"};
528 if (family == "ctmc")
529 return {"avg", "tran", "cdf", "prob", "tranprob", "sample", "cache", "loss", "orbit",
530 "moment"};
531 if (family == "fluid") return {"avg", "tran", "cdf", "prob", "cache", "sens"};
532 if (family == "mam") return {"avg", "cdf"};
533 // The RCAT fixed point reports means only; the passage-time law it answers
534 // is the base exponential fit, not its own.
535 if (family == "ag") return {"avg"};
536 // A bound brackets the mean measures and nothing else.
537 if (family == "ba") return {"avg"};
538 if (family == "ssa") return {"avg", "cdf", "prob", "sample", "loss"};
539 if (family == "ldes")
540 return {"avg", "tran", "cdf", "prob", "sample", "cache", "loss", "orbit"};
541 if (family == "jmt") return {"avg", "tran", "cdf", "prob", "tranprob", "sample"};
542 return {"avg"};
543}
544
545/**
546 * `Solver.isStochasticMethod` for a family method name: does this method return
547 * seed-dependent estimates?
548 *
549 * SSA and LDES are simulators outright; JMT is one except through its
550 * analytical JMVA engine, whose own sampling variants are stochastic again; NC
551 * has the Monte Carlo, importance-sampling and MCMC routes, and answers for
552 * itself through `nc::is_stochastic_method` rather than through a second copy
553 * of that token list here.
554 */
555inline bool auto_is_stochastic_method(const std::string& family, const std::string& method) {
556 if (family == "ssa" || family == "ldes") return true;
557 if (family == "nc") return nc::is_stochastic_method(method);
558 if (family == "jmt") {
559 std::string tok;
560 std::vector<std::string> toks;
561 for (char ch : method) {
562 if (ch == '.' || ch == '/') {
563 toks.push_back(tok);
564 tok.clear();
565 } else {
566 tok += static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
567 }
568 }
569 toks.push_back(tok);
570 bool jmva = false;
571 for (const std::string& t : toks)
572 if (t == "jmva") jmva = true;
573 if (!jmva) return true;
574 for (const std::string& t : toks)
575 if (t == "ls" || t == "mci" || t == "imci" || t == "sampling") return true;
576 return false;
577 }
578 return false;
579}
580
581namespace detail {
582inline std::string exact_if(bool cond) { return cond ? "exact" : "approx"; }
583} // namespace detail
584
585/**
586 * What KIND of answer a method returns: "exact", "approx", "bound" or
587 * "simulation".
588 *
589 * "simulation" is not decided here: `is_stochastic` is the solver's own verdict,
590 * which already tokenizes qualified and runtime-resolved names.
591 *
592 * "exact" IS CLAIMED ONLY WHERE IT IS TRUE OF THIS MODEL, never of the
593 * algorithm in the abstract. Exactness of a normalizing constant or of mean
594 * value analysis is a property of the product-form model it is computed on, and
595 * of the QBD shape for the matrix analytic methods, so both conditions are
596 * passed in and a method that needs one reports "approx" without it. The bias
597 * is deliberate: an under-claimed "approx" costs a user a better method they
598 * could have had, an over-claimed "exact" costs them a wrong number they
599 * trusted.
600 *
601 * A CACHE IS THE THIRD CONDITION, and it was the over-claim the bias above
602 * exists to prevent. `has_product_form` answers about the QUEUEING network and
603 * knows nothing of a cache: the hit/miss split is a class switch whose
604 * probabilities are not routing data but the output of a cache model, so a
605 * network holding one reads as product form and "mva.exact" was labelled exact
606 * on it. Measured on the tut06 shape with an LRU cache: exact MVA returns QLen
607 * 0.2516 at the hit station where the CTMC returns 0.3022 and simulation 0.3023,
608 * a 17% error under a label that says there is none. The analytic families are
609 * conditioned on it; SolverCTMC is NOT, because its state space carries the
610 * cache contents and it is exact there, which is what the two numbers show.
611 */
612inline std::string auto_method_class(const std::string& family, const std::string& method,
613 bool is_stochastic, bool is_product_form,
614 bool is_qbd_shape, bool has_cache = false) {
615 // Bounds are what SolverBA is for; every one of its methods returns a
616 // bracket rather than an estimate.
617 if (family == "ba") return "bound";
618 if (is_stochastic) return "simulation";
619 if (family == "ctmc") {
620 // The generator is solved as written, so every state-space route is
621 // exact. "cftp.approx" says in its own name that it is not, and "mdd"
622 // is exact on a product-form model and an approximation otherwise.
623 if (method == "cftp.approx") return "approx";
624 if (method == "mdd") return detail::exact_if(is_product_form);
625 return "exact";
626 }
627 if (family == "nc") {
628 // The normalizing-constant routes that evaluate G exactly rather than
629 // expanding or estimating it. The asymptotic expansions (pana, le,
630 // kt, bk, gm, ...) and the non-product-form "morrison" are
631 // approximations by construction and are left out.
632 if (detail::name_in(method, {"exact", "divdiff", "ca", "comom", "comomld", "rec", "ms",
633 "cub", "rgf"}))
634 return detail::exact_if(is_product_form && !has_cache);
635 return "approx";
636 }
637 if (family == "mva") {
638 // Exact MVA; every "amva.*" arm is an approximation, and so are the
639 // open-network QNA transforms.
640 if (detail::name_in(method, {"exact", "mva"}))
641 return detail::exact_if(is_product_form && !has_cache);
642 return "approx";
643 }
644 if (family == "jmt") {
645 // JMVA's exact algorithms. "jsim" and "replication" are simulation and
646 // never reach here.
647 if (detail::name_in(method, {"jmva.mva", "jmva.recal", "jmva.comom", "jmva.treeconv"}))
648 return detail::exact_if(is_product_form && !has_cache);
649 return "approx";
650 }
651 if (family == "mam") {
652 // The QBD is solved exactly on the shape it is stated for, one queueing
653 // station fed by a Source. Everything named "dec.*" is a decomposition
654 // of a larger network into such queues, hence an approximation of it.
655 if (detail::name_in(method, {"default", "mna", "ldqbd", "bgchain", "retrial"}))
656 return detail::exact_if(is_qbd_shape);
657 return "approx";
658 }
659 if (family == "ag") {
660 // Every RCAT arm estimates the reversed rate of each synchronising
661 // action and iterates to a fixed point, an approximation by
662 // construction; SolverAG's "exact" is a vestigial alias that warns and
663 // runs "inap", so nothing here is claimed exact.
664 return "approx";
665 }
666 return "approx";
667}
668
669/**
670 * `SolverAUTO.findSolver`: which solvers and solver methods can analyze this
671 * model, and for the ones that cannot, why not.
672 *
673 * THE GATE IS NOT A SECOND ONE. It is `auto_family_supports`, the gate
674 * `auto_choose_solver` applies before delegating, asked of every candidate
675 * instead of of the first feasible one -- which is exactly what
676 * `auto_list_valid_methods` already did; that function is now the method column
677 * of the runnable rows, so the two cannot disagree. What is new is that the
678 * REASON is kept rather than discarded, and that the answer carries the two
679 * facts a caller needs in order to choose among the survivors: whether the
680 * method is exact on this model, and which measures it can report.
681 *
682 * `metric` narrows the report to the pairs that answer one measure, named
683 * either by its group ("cdf") or by the accessor that returns it
684 * ("getCdfRespT"); "" or "any" keeps every pair. `show_all` keeps the refused
685 * pairs too; by default only the runnable ones are listed, since a caller
686 * asking what it can run has no use for the rows that say it cannot.
687 */
688template <class T>
689std::vector<SolverCandidate> auto_find_solver(const qn::NetworkStruct<T>& sn,
690 const std::string& metric = std::string(),
691 bool show_all = false) {
692 const std::string group = auto_metric_group_of(metric);
693 if (group.empty() && !metric.empty() && metric != "any" && metric != "all") {
694 std::string groups;
695 for (const std::string& g : auto_metric_groups()) {
696 if (!groups.empty()) groups += ", ";
697 groups += g;
698 }
699 // InputError and not a bare runtime_error: this is a CALLER mistake, and
700 // the CLI reports the two differently -- an input error prints
701 // "line-cli: <message>" and exits 2, anything else prints "unexpected
702 // failure" and exits 3, which is what a typo'd measure was getting.
703 throw InputError("'" + metric + "' names no measure. Pass a group (" + groups +
704 ") or the accessor that returns it, e.g. 'getCdfRespT'.");
705 }
706
707 // The two model properties an exactness claim can rest on, evaluated once:
708 // a method whose exactness needs one of them reports "approx" without it.
709 const bool is_product_form = sn.has_product_form();
710 std::size_t nsources = 0;
711 for (const qn::Station<T>& st : sn.stations)
712 if (st.nodetype == lang::NodeType::Source) ++nsources;
713 const std::vector<double> njobs = sn.njobs();
714 bool all_open = !njobs.empty();
715 for (double n : njobs)
716 if (!std::isinf(n)) all_open = false;
717 const bool is_qbd_shape = all_open && (sn.nstations - nsources) == 1;
718 bool has_cache = false;
719 for (const qn::NodeDef& nd : sn.nodes)
720 if (nd.nodetype == qn::NodeType::Cache) has_cache = true;
721
722 std::vector<SolverCandidate> rows;
723 for (const std::string& family : auto_network_family_names()) {
724 const std::vector<std::string> groups = auto_family_metrics(family);
725 if (!group.empty() &&
726 std::find(groups.begin(), groups.end(), group) == groups.end())
727 continue;
728 std::string metric_list;
729 for (const std::string& g : groups) {
730 if (!metric_list.empty()) metric_list += ",";
731 metric_list += g;
732 }
733 const std::vector<std::string> declared = auto_family_methods(family, sn);
734 for (const std::string& name : declared) {
735 if (detail::starts_with(name, family + ".")) {
736 // A spelling already qualified with its own family. The fluid
737 // registry declares both "dae" and "fluid.dae" so that its own
738 // gate takes either, and prefixing the family again yields
739 // "fluid.fluid.dae": a token that does resolve, but that names
740 // the same method twice and would double every fluid row.
741 continue;
742 }
743 if (auto_is_method_alias(family, name, declared)) {
744 // The same duplication under a different prefix. The mva
745 // registry advertises every AMVA name twice, plain and
746 // "amva."-prefixed, and the dispatch strips the prefix, so the
747 // two spellings are one algorithm; that alone was 20 of the 49
748 // mva rows of a report. The plain spelling is the one kept.
749 continue;
750 }
751 const std::string reason = auto_family_refusal(family, sn, name);
752 const bool ok = reason.empty();
753 if (!ok && !show_all) continue;
754 SolverCandidate row;
755 row.solver = family;
756 row.method = family + "." + name;
757 row.runnable = ok;
758 row.method_class = auto_method_class(
759 family, name, auto_is_stochastic_method(family, name), is_product_form,
760 is_qbd_shape, has_cache);
761 row.metrics = metric_list;
762 row.reason = ok ? std::string() : reason;
763 rows.push_back(row);
764 }
765 }
766 return rows;
767}
768
769/** Alias of `auto_find_solver`: the same table, asked for by method. */
770template <class T>
771std::vector<SolverCandidate> auto_find_method(const qn::NetworkStruct<T>& sn,
772 const std::string& metric = std::string(),
773 bool show_all = false) {
774 return auto_find_solver(sn, metric, show_all);
775}
776
777/** Alias of `auto_find_solver`: what can this model be solved with? */
778template <class T>
779std::vector<SolverCandidate> auto_help(const qn::NetworkStruct<T>& sn,
780 const std::string& metric = std::string(),
781 bool show_all = false) {
782 return auto_find_solver(sn, metric, show_all);
783}
784
785/**
786 * The rows as an aligned text table, the form the CLI and a console caller
787 * want. The reason column is last and unpadded, since it is the only one whose
788 * width is unbounded.
789 */
790inline std::string auto_find_solver_table(const std::vector<SolverCandidate>& rows) {
791 if (rows.empty()) return "No solver method can analyze this model.\n";
792 std::vector<std::array<std::string, 6>> cells;
793 cells.push_back({"Solver", "Method", "Runnable", "Class", "Metrics", "Reason"});
794 for (const SolverCandidate& r : rows)
795 cells.push_back({r.solver, r.method, r.runnable ? "true" : "false", r.method_class,
796 r.metrics, r.reason});
797 std::array<std::size_t, 6> width{};
798 for (const auto& row : cells)
799 for (std::size_t c = 0; c < 5; ++c) width[c] = std::max(width[c], row[c].size());
800 std::string out;
801 for (const auto& row : cells) {
802 std::string line;
803 for (std::size_t c = 0; c < 5; ++c) line += row[c] + std::string(width[c] - row[c].size() + 2, ' ');
804 line += row[5];
805 while (!line.empty() && line.back() == ' ') line.pop_back();
806 out += line + "\n";
807 }
808 return out;
809}
810
811/**
812 * `SolverAUTO.listValidMethods`: every method name this model can be asked for.
813 *
814 * The selection intents come first and unconditionally: they name a RANKING and
815 * not an algorithm, and finding a family that supports the model is the
816 * ranking's own job. `bound` is the one that is not quite a ranking -- it names
817 * SolverBA with method 'auto' -- and it is listed unconditionally all the same,
818 * because MATLAB and the JAR list it that way and a caller reading 'bound' as
819 * "give me bounds" should get SolverBA's own refusal rather than a missing
820 * method name. Then each family in `auto_network_family_names` order, bare method name
821 * first and its qualified methods after it.
822 */
823template <class T>
824std::vector<std::string> auto_list_valid_methods(const qn::NetworkStruct<T>& sn) {
825 // IT IS THE RUNNABLE ROWS OF `auto_find_solver`, projected onto their
826 // method name. The narrowing used to be written out a second time here,
827 // and a second copy of one gate is how two answers to one question start to
828 // differ; `auto_find_solver` owns it now, and this adds only the method names
829 // that name no single method: the selection intents and each family's bare
830 // token.
831 //
832 // A family that declares methods and keeps none loses its bare method name too:
833 // it delegates to the solver the per-method gate just refused. That falls
834 // out of the projection, since such a family contributes no row to name.
835 std::vector<std::string> out{"accurate", "auto", "bound", "default",
836 "exact", "fast", "heur", "sim"};
837 for (const SolverCandidate& row : auto_find_solver(sn)) {
838 out.push_back(row.method);
839 out.push_back(row.solver);
840 }
841 std::sort(out.begin(), out.end());
842 out.erase(std::unique(out.begin(), out.end()), out.end());
843 return out;
844}
845
846} // namespace autosolver
847} // namespace line
848
849#endif // LINE_SOLVERS_AUTO_AUTO_METHODS_H
InputError(const std::string &what)
Definition error.h:39
A subset of the registry: MATLAB's SolverFeatureSet, whose list is a flag per field.
A network plus its refreshed NetworkStruct.
The exception types the port throws.
The language-feature gate: what a MODEL uses against what a SOLVER declares.
The fluid solver's outermost entry point: @@SolverFLD/runAnalyzer.m's method resolution over solver_f...
The option and result records of SolverLDES, the discrete-event simulator.
std::vector< std::string > list_valid_methods()
Every method SolverAG serves, as the other families expose theirs.
bool auto_supports(AutoSolver s, const qn::NetworkStruct< T > &sn, const std::string &token)
Solver.supports(model) for a candidate slot, tightened by the two method-level rules a flat feature s...
AutoSolver
The Network candidate slots, in the reference's slot order (SolverAUTO.m:41-50), which is also the or...
std::string auto_method_class(const std::string &family, const std::string &method, bool is_stochastic, bool is_product_form, bool is_qbd_shape, bool has_cache=false)
What KIND of answer a method returns: "exact", "approx", "bound" or "simulation".
std::vector< std::string > auto_network_family_names()
The method families that solve a flat Network, in SolverAUTO.familyNames' own order – which is also t...
std::string auto_metric_group_of(const std::string &name)
The measure group an accessor belongs to, "" when the name belongs to none.
bool auto_solver_is_available(AutoSolver s)
True when this port has an engine behind the slot at all.
std::string auto_family_refusal(const std::string &family, const qn::NetworkStruct< T > &sn, const std::string &method)
Solver.supportsModelMethod for a family method name: may THIS model run THIS method of THIS family?
std::string auto_find_solver_table(const std::vector< SolverCandidate > &rows)
The rows as an aligned text table, the form the CLI and a console caller want.
bool auto_is_stochastic_method(const std::string &family, const std::string &method)
Solver.isStochasticMethod for a family method name: does this method return seed-dependent estimates?
std::vector< std::string > auto_family_metrics(const std::string &family)
The measure groups a method family can answer.
std::vector< SolverCandidate > auto_find_method(const qn::NetworkStruct< T > &sn, const std::string &metric=std::string(), bool show_all=false)
Alias of auto_find_solver: the same table, asked for by method.
std::vector< std::string > auto_metric_groups()
The measure groups auto_find_solver reports on, in report order.
bool auto_family_supports(const std::string &family, const qn::NetworkStruct< T > &sn, const std::string &method)
const char * auto_solver_name(AutoSolver s)
std::vector< SolverCandidate > auto_find_solver(const qn::NetworkStruct< T > &sn, const std::string &metric=std::string(), bool show_all=false)
SolverAUTO.findSolver: which solvers and solver methods can analyze this model, and for the ones that...
std::vector< SolverCandidate > auto_help(const qn::NetworkStruct< T > &sn, const std::string &metric=std::string(), bool show_all=false)
Alias of auto_find_solver: what can this model be solved with?
std::vector< std::string > auto_list_valid_methods(const qn::NetworkStruct< T > &sn)
SolverAUTO.listValidMethods: every method name this model can be asked for.
std::vector< std::string > auto_family_methods(const std::string &family, const qn::NetworkStruct< T > &sn)
The methods a family declares ON THIS MODEL, empty when it declares none.
std::vector< std::string > auto_method_alias_prefixes(const std::string &family)
The prefixes under which a family advertises a SECOND SPELLING of a method it already declares plainl...
bool auto_is_method_alias(const std::string &family, const std::string &name, const std::vector< std::string > &declared)
Is name a second spelling of another method this family declares?
std::string method_refusal(const qn::NetworkStruct< T > &L, const std::string &method)
The STRUCTURAL premises of the SolverBA bound families, in one place: the reason METHOD cannot bound ...
std::vector< std::string > list_valid_methods()
Port of SolverBA.listValidMethods.
std::vector< std::string > list_valid_methods()
Port of SolverCTMC.listValidMethods.
std::string solver_ctmc_mdd_supports(const NetworkStruct< T > &sn)
Can the mdd decision-diagram method be asked for this model?
std::string solver_ctmc_cftp_supports(const NetworkStruct< T > &sn)
The cftp model-class gate as a public predicate.
std::string fluid_qsys_horizon_supports(const std::string &method, const FluidOptions &opt)
The horizon rule the time-varying limits impose, as a public predicate a REPORT can ask: empty when m...
Definition fluid_qsys.h:150
std::vector< std::string > fluid_list_valid_methods()
Port of SolverFLD.listValidMethods.
std::vector< std::string > jmt_list_valid_methods()
Port of SolverJMT.listValidMethods.
Definition solver_jmt.h:838
std::string jmt_method_refusal(const qn::NetworkStruct< T > &sn, const std::string &method, const JmtOptions &opt)
The structural half of SolverJMT's method gate; empty when admissible.
Definition solver_jmt.h:870
std::vector< std::string > list_valid_methods()
Port of SolverLDES.listValidMethods.
std::vector< std::string > list_valid_methods()
Port of SolverMAM.listValidMethods.
std::string mam_model_method_refusal(const qn::NetworkStruct< T > &L, const std::string &method)
check_model_method asked WITHOUT raising: the same verdict as a sentence.
std::vector< std::string > list_valid_methods(const qn::NetworkStruct< T > &L)
Port of SolverMVA.listValidMethods.
MemSupport solver_nc_mem_supports(const qn::NetworkStruct< T > &sn)
Port of solver_nc_mem_supports.m.
std::vector< std::string > list_valid_methods()
Port of SolverNC.listValidMethods.
bool is_stochastic_method(const std::string &method)
Port of SolverNC.isStochasticMethod.
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?
FeatureSet ssa_feature_set(const std::string &)
SolverSSA.getFeatureSet, 98 MATLAB names.
FeatureSet ldes_feature_set(const std::string &)
SolverLDES.getFeatureSet, transcribed WHOLE.
FeatureSet fluid_feature_set(const std::string &method)
SolverFLD.getFeatureSet, transcribed, MINUS what the requested method cannot evaluate – the port of @...
FeatureSet jmt_feature_set(const std::string &method)
FeatureSet used_lang_features(const NetworkStruct< T > &sn)
FeatureSet nc_feature_set(const std::string &method)
SolverNC.getFeatureSet, 48 names, transcribed unchanged.
FeatureSet ba_feature_set(const std::string &method)
std::string sn_fj_supports(const NetworkStruct< T > &sn)
Can the exact fork-join construction be asked for this model?
Definition fj_tag.h:282
FeatureSet qns_feature_set(const std::string &)
SolverQNS.getFeatureSet, transcribed WHOLE.
bool has_binding_capacity(const NetworkStruct< T > &sn)
getUsedLangFeatures: the features the MODEL uses.
FeatureSet ctmc_feature_set(const std::string &method)
SolverCTMC.getFeatureSet, the reference's 104 MATLAB names in full.
SupportResult feature_set_supports(const std::string &solver, const FeatureSet &declared, const FeatureSet &used)
SolverFeatureSet.supports: is every feature the model uses declared?
FeatureSet mam_feature_set(const std::string &method)
SolverMAM.getFeatureSet, the union of its four setTrue calls: 55 MATLAB names, WIDENED for 'default'/...
FeatureSet mva_feature_set(const std::string &raw_method)
FeatureSet ag_feature_set(const std::string &)
SolverAG.getFeatureSet: what the RCAT decomposition can represent.
std::string method_refusal(const qn::NetworkStruct< T > &L, const std::string &method)
SolverQNS.supportsModelMethod's structural rules, as the REASON they refuse, empty when the pair is s...
Definition solver_qns.h:105
std::vector< std::string > list_valid_methods()
Port of SolverQNS.listValidMethods.
Definition solver_qns.h:81
std::vector< std::string > list_valid_methods()
Port of SolverSSA.listValidMethods.
A queueing network and its refreshed NetworkStruct.
The gates and the dispatch of the agent-based (RCAT) solver.
The SolverAUTO chooser: which solver a model is handed to.
The SolverBA class surface: @@SolverBA/runAnalyzer.m, listValidMethods, getBounds and getBoundsTable.
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
The cftp and cftp.approx methods of SolverCTMC: stationary analysis of a closed single-class product-...
The mdd method of SolverCTMC: stationary analysis of a closed single-class network whose state space ...
The DECLARED side of the gate: one feature set per solver.
Port of SolverJMT, the Java Modelling Tools client.
The SolverMAM class surface: @@SolverMAM/runAnalyzer.m and the gates around it.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The SolverNC class surface: @@SolverNC/runAnalyzer.m and the gates around it.
Port of @@SolverQNS, the wrapper around qnsolver of the RADS/LQNS distribution.
The SolverSSA entry surface: a port of @@SolverSSA/runAnalyzer.m's method whitelist,...
One row of auto_find_solver: a (family, method) pair this model can be asked for, whether it runs,...
std::string method
the method name to pass, "mva.exact"
bool runnable
the model passes this method's own support gate
std::string metrics
the measure groups the family answers, comma-joined
std::string reason
why a refused pair was refused, "" when runnable
std::string solver
the method family: "mva", "ctmc", "ldes", ...
std::string method_class
"exact", "approx", "bound" or "simulation"
Controls, defaulting to SolverOptions('Fluid') in the reference.
The options of one JMT solve, SolverOptions('JMT') restricted to what is read.
Definition solver_jmt.h:86
A node of the network.
One station of the network.
The outcome of the gate: the verdict, the offending features, the message.
std::string reason
empty when ok