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