LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_feature_sets.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_LANG_QN_SOLVER_FEATURE_SETS_H
6#define LINE_LANG_QN_SOLVER_FEATURE_SETS_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * The DECLARED side of the gate: one feature set per solver.
12 *
13 * SOURCE. Each function transcribes that solver's MATLAB declaration:
14 * MVA matlab/src/solvers/MVA/@@SolverMVA/SolverMVA.m:188-217
15 * per-method deltas SolverMVA.m:126-141
16 * NC matlab/src/solvers/NC/@@SolverNC/SolverNC.m:180-209
17 * CTMC matlab/src/solvers/CTMC/@@SolverCTMC/SolverCTMC.m:126-168
18 * SSA matlab/src/solvers/SSA/@@SolverSSA/SolverSSA.m:67-110
19 * FLUID matlab/src/solvers/FLD/@@SolverFLD/SolverFLD.m:88-119
20 * MAM matlab/src/solvers/MAM/@@SolverMAM/SolverMAM.m:141-184 (four setTrue calls, unioned)
21 * ENV matlab/src/solvers/@@SolverENV/SolverENV.m:1399-1444 (inline, inside supports)
22 * LDES matlab/src/solvers/wrappers/LDES/@@SolverLDES/SolverLDES.m:108-186
23 * BA matlab/src/solvers/BA/@@SolverBA/SolverBA.m (getFeatureSet)
24 * LQNS matlab/src/solvers/wrappers/LQNS/@@SolverLQNS/SolverLQNS.m (getFeatureSet, flat Network)
25 *
26 * WHY `method`. MATLAB's gate is METHOD-aware, not solver-aware:
27 * NetworkSolver.runAnalyzerChecks resolves the method first and then reads
28 * getMethodFeatureSet(method), which only SolverMVA specialises in the
29 * reference (SolverMVA.m:126-141). The other five take the argument and ignore
30 * it. MAM's C++ port is the one exception: mam_feature_set reads `method`
31 * because its LoadDependence declaration is a WIDENING over MATLAB (see below)
32 * and only holds for the methods that actually consume st.lldscaling; MATLAB
33 * itself has no such delta to transcribe.
34 *
35 * WHY THE SETS ARE NOT ALWAYS THE MATLAB ONES. feature_set.h:30-33: a feature
36 * DECLARED but not implemented yields a wrong number, an undeclared one yields
37 * a clean refusal. So each divergence below narrows the MATLAB set to what the
38 * C++ code actually handles, and carries the C++ refusal site that proves it.
39 * The one WIDENING left is MAM's LoadDependence, method-gated as above, which is
40 * a false refusal otherwise.
41 *
42 * LDES AND LQNS ARE THE EXCEPTIONS TO THAT PARAGRAPH, and only because neither is
43 * an engine here: SolverLDES forwards the model document to the SSJ engine, and
44 * SolverLQNS on a flat Network writes the JMVA document `qnsolver` reads or
45 * converts the model for `lqns`. In both the code that implements a declared
46 * feature is the external binary the reference ALSO drives, so `ldes_feature_set`
47 * and `lqns_network_feature_set` are the MATLAB declarations transcribed WHOLE;
48 * narrowing either would refuse models the reference answers with that binary.
49 *
50 * MATLAB FEATURE NAMES WITH NO LINE_QN_FEATURE_LIST ENUMERATOR: none. Every
51 * name in all twelve declarations, and in the MVA per-method deltas, has an
52 * exact 1:1 registry entry, so the mapping is the identity and needs no alias.
53 * `Normal` is the one name dropped on purpose rather than for want of an
54 * enumerator; `lqns_network_feature_set` records why.
55 */
56
57#include <cctype>
58#include <string>
59
61
62namespace line {
63namespace qn {
64
65/**
66 * The method name with any leading "amva." alias stripped.
67 *
68 * The mva registry strips the prefix before it selects an algorithm, so every
69 * rule keyed on a method name has to see the canonical spelling or it would
70 * hold for "bs" and not for "amva.bs".
71 */
72inline std::string mva_base_method(const std::string& method) {
73 return (method.compare(0, 5, "amva.") == 0) ? method.substr(5) : method;
74}
75
76/**
77 * The AMVA algorithms whose recursion is over a CLOSED population vector.
78 *
79 * Each approximates the arrival-instant queue length E[Q(N-1_r)] from E[Q(N)]
80 * and is handed (L, N, Z) alone, with no arrival rate and no rate function, so
81 * an open chain gives it nothing to recur on and the recursion falls out with
82 * every metric at zero. ONE list: `mva_feature_set` drops OpenClass for these,
83 * `mva::mva_closed_population_reason` refuses them by name at solve time, and
84 * `mva::list_valid_methods` withholds them where they cannot run.
85 */
86inline bool mva_is_closed_population_method(const std::string& method) {
87 static const char* kNames[] = {"bs", "aql", "qsa", "sqni", "tay", "scat", "lcp", "chow",
88 "pamb", "pami", "pamt", "clust", "dmlin", "ab",
89 "schmidt", "schmidt-ext"};
90 const std::string base = mva_base_method(method);
91 for (const char* n : kNames)
92 if (base == n) return true;
93 return false;
94}
95
96/**
97 * The method names `solver_mva_qsys_analyzer` has an arm for.
98 *
99 * ONE LIST FOR THE INTERCEPTION AND THE GATE, as `qsys_serves_method.m` is in
100 * the reference. `mva_dispatch` claims the single-class open Source-Queue-Sink
101 * shape for exactly these names -- `detail::qsys_serves_method` forwards here,
102 * and carries the long note on which names are deliberately absent -- while
103 * `mva_feature_set` below withdraws the state-dependence features from the ones
104 * that are NOT also general network entry points. The list lives in lang/qn
105 * because a feature set cannot include a solver header.
106 *
107 * Judged on the ALIASED name, the same `amva_method_alias` the analyzer applies
108 * on the way in.
109 */
110inline bool mva_qsys_serves_method(const std::string& method) {
111 static const char* const kServed[] = {
112 "default", "exact", "erlanga", "mgisrgi", "gigk.diffusion",
113 "mm1", "mmk", "mg1", "mgi1", "gigk",
114 "gigk.kingman_approx", "gigk.whitt", "gig1", "gig1.allen",
115 "gig1.kingman", "gig1.heyman", "gig1.kobayashi", "gig1.klb", "gig1.marchal",
116 "gig1.gelenbe", "gig1.kimura", "gig1.extremal", "qed", "rqna",
117 "rqt", "gm1", "gim1"};
118 for (std::size_t i = 0; i < sizeof(kServed) / sizeof(*kServed); ++i)
119 if (method == kServed[i]) return true;
120 return false;
121}
122
123/**
124 * Drops every scheduling name OUTSIDE the BCMP set {INF, PS, FCFS, SIRO,
125 * LCFS-PR} that the base MVA envelope declares.
126 *
127 * The chain algorithms that walk the stations one by one -- the summation
128 * method, MVAC and QNA -- accept the BCMP set and refuse the rest, so each drops
129 * these from its own envelope rather than answering a station it has no arm for.
130 */
132 f.unset(Feature::SchedStrategy_HOL);
133 f.unset(Feature::SchedStrategy_DPS);
134 f.unset(Feature::SchedStrategy_FCFSPRPRIO);
135 f.unset(Feature::SchedStrategy_LCFS);
136 f.unset(Feature::SchedStrategy_POLLING);
137 f.unset(Feature::SchedStrategy_SJF);
138 f.unset(Feature::SchedStrategy_SRPT);
139 f.unset(Feature::SchedStrategy_PSJF);
140 f.unset(Feature::SchedStrategy_FB);
141 f.unset(Feature::SchedStrategy_LRPT);
142 f.unset(Feature::SchedStrategy_SETF);
143 f.unset(Feature::SchedStrategy_OI);
144 f.unset(Feature::SchedStrategy_PAS);
145}
146
147/**
148 * SolverMVA.getFeatureSet, plus getMethodFeatureSet's per-method deltas.
149 *
150 * SolverMVA.m:189-213 declares 59 names; this set is those 59, name for name.
151 * RQNA natively consumes non-renewal MAP/MMPP/RAP processes but is open-network
152 * only; QNA is a two-moment open-network method.
153 *
154 * When the reference set moves, this one must move with it.
155 *
156 * Each declared name and the code that consumes it:
157 * Cache, CacheClassSwitcher mva_dispatch.h:754, :765
158 * CacheRetrieval mva_dispatch.h:741 (open); the CLOSED variant
159 * is refused by name at :734-740, a shape
160 * restriction the boolean cannot express
161 * ReplacementStrategy_LRU, _HLRU solver_mva_cache.h:144-165
162 * SchedStrategy_OI, _PAS mva_dispatch.h, branch 1, solver_mva_oi.h
163 * SchedStrategy_POLLING mva_dispatch.h, branch 5, solver_mva_polling.h
164 * SchedStrategy_SJF mva_dispatch.h, branch 0, solver_mva_sjn.h; the
165 * OPEN case is refused by name there, a shape
166 * restriction the boolean cannot express
167 * SchedStrategy_SRPT, _PSJF, _FB, mva_dispatch.h, branch 3,
168 * _LRPT, _SETF solver_mva_qsys_sizebased
169 * Fork, Forker, Join, Joiner solver_mva_runner.h:414-426, fj_mmt + fj_fixed_point
170 * LoadDependence solver_mva.h:543-561, :655-662, :1486-1487
171 * ClassDependence solver_mva.h:553-560, :671-684 (pfqn_cdfun)
172 * Pareto, Weibull, Lognormal, Uniform, Det, Replayer, BMAP and MMAP carry no
173 * consumer of their own by design: there is no `ProcessType::` reference
174 * anywhere under cpp/include/line/solvers/mva/, because MVA is a two-moment
175 * method that reads only rates and SCVs. That is exactly how MATLAB's MVA
176 * treats them, which is why it declares them. BMAP is the weakest of the eight:
177 * the batch structure is dropped to two moments rather than refused, in both
178 * codebases.
179 */
180inline FeatureSet mva_feature_set(const std::string& raw_method) {
181 const std::string method = mva_base_method(raw_method);
182 FeatureSet f;
183 f.set({Feature::Sink, Feature::Source});
184 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue});
185 f.set({Feature::APH, Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp,
186 Feature::HyperExp, Feature::BMAP});
187 f.set({Feature::Pareto, Feature::Weibull, Feature::Lognormal, Feature::Uniform, Feature::Det});
188 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
189 Feature::Buffer, Feature::Dispatcher});
190 f.set({Feature::CacheClassSwitcher, Feature::Cache});
191 f.set(Feature::CacheRetrieval);
192 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::ServiceTunnel});
193 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS});
194 f.set({Feature::SchedStrategy_DPS, Feature::SchedStrategy_FCFS, Feature::SchedStrategy_SIRO,
195 Feature::SchedStrategy_HOL, Feature::SchedStrategy_FCFSPRPRIO});
196 f.set({Feature::SchedStrategy_LCFS, Feature::SchedStrategy_LCFSPR,
197 Feature::SchedStrategy_POLLING});
198 f.set({Feature::SchedStrategy_OI, Feature::SchedStrategy_PAS});
199 // Closed models only, solver_mva_sjn.h; the open case is refused by name.
200 f.set(Feature::SchedStrategy_SJF);
201 // Size-based M/G/1, mva_dispatch.h branch 3, solver_mva_qsys_sizebased
202 // (Wierman and Harchol-Balter, SIGMETRICS 2003)
203 f.set({Feature::SchedStrategy_SRPT, Feature::SchedStrategy_PSJF,
204 Feature::SchedStrategy_FB, Feature::SchedStrategy_LRPT,
205 Feature::SchedStrategy_SETF});
206 // JoinPartial: the MMT fixed point charges the k-th branch completion
207 // (fj_ordstat_exp), not the maximum
208 f.set({Feature::Fork, Feature::Forker, Feature::Join, Feature::Joiner,
209 Feature::JoinPartial});
210 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
211 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO,
212 Feature::ReplacementStrategy_LRU, Feature::ReplacementStrategy_HLRU});
213 f.set({Feature::MMAP, Feature::MPH});
214 f.set({Feature::ClosedClass, Feature::SelfLoopingClass, Feature::OpenClass, Feature::Replayer});
215 // lldscaling: solver_mva.h:543-561 and :1486-1487; cdscaling: :553-560 via pfqn_cdfun.
216 f.set({Feature::LoadDependence, Feature::ClassDependence});
217 f.set(Feature::JointDependence);
218 // c-server stations, SolverMVA.m:1214-1221: the exact recursion, every AMVA
219 // kernel, qna/rqt and the M/M/k and G/G/k closed forms carry the count; the
220 // per-method block below withdraws it from the single-server names.
221 // FiniteCapacity is deliberately NOT here, as in the reference: it is
222 // granted per method, and on an M/M/1/K loss model per MODEL.
223 f.set(Feature::MultiServer);
225 // The closed-population AMVA family estimates the arrival-instant queue
226 // length as a function of the population vector N and is handed (L, N, Z)
227 // alone, so an open chain gives it nothing to recur on: solver_amva has no
228 // arm for any of these outside its closed product-form branch, and falling
229 // through returned the qd-family answer under their name. Strict product
230 // form has no registry name and is applied by
231 // mva::mva_closed_population_reason instead.
232 f.unset(Feature::OpenClass);
233 }
234 // THE QUEUEING-SYSTEM CLOSED FORMS READ ONE SERVICE RATE. mm1, mmk, mg1,
235 // gig1 and the rest take a scalar mu (or one service law's first two
236 // moments) from sn.rates, so a station carrying a lattice of rates cannot be
237 // answered by them: mva_dispatch now stands aside for such a model, and this
238 // withdrawal is what turns the interception that used to answer it with the
239 // UNSCALED queue into a refusal by name. 'default' and 'exact' are exempt
240 // because they are general entry points too, and fall through to the
241 // load-dependent analyzer on exactly this model.
242 if (mva_qsys_serves_method(method) && method != "default" && method != "exact") {
243 f.unset(Feature::LoadDependence);
244 f.unset(Feature::ClassDependence);
245 f.unset(Feature::JointDependence);
246 }
247 // The load-dependent analyzer serves a load-, class- or joint-dependent model
248 // through "exact"/"mva" (load dependence only, it has no class- or
249 // joint-dependent recursion) and through the default/amva/qd/lin/qdlin arms,
250 // and refuses every other name by name.
251 {
252 static const char* kLdRefused[] = {"sum", "esum", "mvac", "qli", "fli",
253 "gflin", "egflin", "qna", "rqna", "rqt", "mvablo"};
254 bool ld_refused = mva_is_closed_population_method(method);
255 for (const char* n : kLdRefused)
256 if (method == n) ld_refused = true;
257 if (ld_refused) {
258 f.unset(Feature::LoadDependence);
259 f.unset(Feature::ClassDependence);
260 f.unset(Feature::JointDependence);
261 } else if (method == "mva" || method == "exact") {
262 f.unset(Feature::ClassDependence);
263 f.unset(Feature::JointDependence);
264 }
265 }
266 if (method != "default" && method != "exact") {
267 // An order-independent or pass-and-swap station is served by solver_mva_oi
268 // alone, which mva_dispatch reaches only under "default" or "exact"; every
269 // other name is refused there by name, so it must not be advertised here.
270 f.unset(Feature::SchedStrategy_OI);
271 f.unset(Feature::SchedStrategy_PAS);
272 }
273 if (method == "sum" || method == "esum") {
274 // solver_mva_sum passes each station to the summation kernel as an INF, PS,
275 // LCFS-PR, FCFS or SIRO centre and refuses every other discipline by name.
277 } else if (method == "mvac") {
278 // pfqn_mvac recurs on the closed chains over single-server fixed-rate
279 // (SSFR) and infinite-server centres; solver_mvac refuses the rest by name.
280 f.unset(Feature::OpenClass);
282 }
283 if (method == "exact") {
284 // The exact cache recursion (cache_mva) is the RR/FIFO one: the cache analyzer refuses LRU and h-LRU by
285 // name and the cache-in-network miss callback runs it whatever the policy (SolverMVA.m, case 'exact').
286 f.unset(Feature::ReplacementStrategy_LRU);
287 f.unset(Feature::ReplacementStrategy_HLRU);
288 } else if (method == "marie") {
289 // solver_mva_marie folds INF stations into the think time and isolates FCFS, PS and LCFS-PR queues from
290 // chain demands and SCVs alone: closed networks, no cache, no fork-join (SolverMVA.m, case 'marie').
291 for (Feature x : {Feature::OpenClass, Feature::Source, Feature::Sink, Feature::Fork, Feature::Forker,
292 Feature::Join, Feature::Joiner, Feature::JoinPartial, Feature::Cache,
293 Feature::CacheClassSwitcher, Feature::CacheRetrieval, Feature::SchedStrategy_SIRO})
294 f.unset(x);
296 } else if (method == "sjn.mva" || method == "sjn.amva") {
297 // solver_mva_sjn: a closed model whose SJF station and other single-server BCMP stations enter Kant's
298 // recursion through chain demands and SCVs (SolverMVA.m, case {'sjn.mva','sjn.amva'}); SJF stays.
299 for (Feature x : {Feature::OpenClass, Feature::Source, Feature::Sink, Feature::Fork, Feature::Forker,
300 Feature::Join, Feature::Joiner, Feature::JoinPartial, Feature::Cache,
301 Feature::CacheClassSwitcher, Feature::CacheRetrieval})
302 f.unset(x);
304 f.set(Feature::SchedStrategy_SJF);
305 }
306 if (method == "mapqn") {
307 // The horizontal-cut MVA consumes a MAP service natively (a closed delay +
308 // FCFS queue model, see mva::mva_mapqn_reason); MAP is granted here alone.
309 f.set({Feature::MAP, Feature::MMPP2});
310 for (Feature x : {Feature::OpenClass, Feature::Source, Feature::Sink, Feature::Fork, Feature::Forker,
311 Feature::Join, Feature::Joiner, Feature::JoinPartial, Feature::ClassSwitch,
312 Feature::StatelessClassSwitcher, Feature::Cache, Feature::CacheClassSwitcher,
313 Feature::CacheRetrieval, Feature::LoadDependence, Feature::ClassDependence,
314 Feature::JointDependence, Feature::SchedStrategy_PS, Feature::SchedStrategy_SIRO,
315 Feature::SchedStrategy_LCFSPR, Feature::SchedStrategy_SRPT, Feature::SchedStrategy_PSJF,
316 Feature::SchedStrategy_FB, Feature::SchedStrategy_LRPT, Feature::SchedStrategy_SETF,
317 Feature::SchedStrategy_OI, Feature::SchedStrategy_PAS})
318 f.unset(x);
320 }
321 if (method == "rqna") {
322 // RAP is INERT: ProcessType carries no RAP, so it can never be emitted.
323 f.set({Feature::MAP, Feature::MMPP2, Feature::MMAP, Feature::MPH, Feature::RAP});
324 f.unset(Feature::ClosedClass);
325 f.unset(Feature::SelfLoopingClass);
326 } else if (method == "rqt") {
327 // Robust queueing theory: single-class open networks, the primitives
328 // entering the uncertainty sets are two moments.
329 f.unset(Feature::ClosedClass);
330 f.unset(Feature::SelfLoopingClass);
331 }
332 if (method == "qna") {
333 // Round-robin dispatching enters as a deterministic traffic split
334 // (npfqn_traffic_split_rr), which SolverMVA.m:149 sets true for this
335 // method and the exact-MVA paths have no counterpart for; the C++ set
336 // omitted this and only carried the two setFalse calls below.
337 f.set(Feature::RoutingStrategy_RROBIN);
338 f.unset(Feature::ClosedClass);
339 f.unset(Feature::SelfLoopingClass);
340 // solver_qna's station loop has an arm for INF, PS and FCFS and none for
341 // anything else, so on a SIRO, LCFS-PR, HOL or priority station it left
342 // that row of Q, U, R and T at zero and reported the table as a solution.
343 f.unset(Feature::SchedStrategy_SIRO);
344 f.unset(Feature::SchedStrategy_LCFSPR);
346 } else if (method == "erlanga" || method == "mgisrgi") {
347 // The only analytical methods in LINE that carry an abandonment rate
348 // (qsys_erlanga, qsys_mgisrgi_whitt). Reneging stays OUT of the base
349 // MVA envelope: every other method here would silently ignore the
350 // patience law and report the no-abandonment answer.
351 f.set(Feature::Reneging);
352 f.unset(Feature::ClosedClass);
353 f.unset(Feature::SelfLoopingClass);
354 }
355 if (method == "rqna" || method == "rqt") {
356 // A Join is a synchronisation node, not a queue: it carries no service
357 // process, so the index-of-dispersion curve these two read off every
358 // station does not exist for it, and neither analyzer has a
359 // synchronisation term to put in its place. QNA keeps Fork/Join -- its
360 // station loop has an explicit Join arm.
361 f.unset(Feature::Fork);
362 f.unset(Feature::Forker);
363 f.unset(Feature::Join);
364 f.unset(Feature::Joiner);
365 f.unset(Feature::JoinPartial);
366 // Both decompose the network into FCFS queues (GI/G/1 workload for RQNA, the worst-case G/G/k system time
367 // for RQT) and read no discipline, and neither has a cache arm: the cache-in-network branch would hand them
368 // the relabelled network and they would refuse it by class count.
369 for (Feature x : {Feature::SchedStrategy_PS, Feature::SchedStrategy_SIRO, Feature::SchedStrategy_LCFSPR,
370 Feature::Cache, Feature::CacheClassSwitcher, Feature::CacheRetrieval})
371 f.unset(x);
373 }
374 // THE SINGLE-SERVER RECURSIONS, SolverMVA.m:378-382. AQL, QSA, Tay, SQNI
375 // (mva_closed_population_reason), MVAC's SSFR chain recursion, RQNA's
376 // GI/G/1 workload, Kant's SJN recursion and the single-server closed forms
377 // of the queueing-system analyzer, i.e. every M/G/1, G/M/1 and G/G/1 name.
378 // Each structural predicate stays and words the refusal for the run; the
379 // delta is what makes it nameable. RQT, QNA, M/M/k, G/G/k and the rest of
380 // the envelope carry a server count.
381 {
382 static const char* kSingleServer[] = {"aql", "qsa", "tay", "sqni", "mvac", "rqna", "sjn.mva",
383 "sjn.amva", "mm1", "mg1", "mgi1", "gm1", "gim1"};
384 bool single = method.compare(0, 4, "gig1") == 0;
385 for (const char* n : kSingleServer)
386 if (method == n) single = true;
387 if (single) f.unset(Feature::MultiServer);
388 }
389 // FINITECAPACITY, SolverMVA.m:384-399, is NOT in the base envelope: the
390 // product-form recursions solve a buffer away, which is what
391 // `mva_check_finite_capacity` refuses. 'default' and 'sqd' reach solver_sqd,
392 // and 'mvablo' reaches solver_mvablo, the two Blocking-After-Service arms.
393 // The single-station M/M/1/K with tail drop is served by the qsys_mg1k_loss_mgs branch of the queueing-system
394 // analyzer, which mva_dispatch reaches under EVERY name on that shape,
395 // 'exact' excepted since the branch is exact at scv=1 only; that grant is
396 // judged on the MODEL and lives in the overload below, because no name
397 // carries it.
398 if (method == "default" || method == "sqd" || method == "mvablo") f.set(Feature::FiniteCapacity);
399 return f;
400}
401
402/**
403 * `mva_feature_set` on a MODEL, i.e. the whole of SolverMVA.getMethodFeatureSet.
404 *
405 * The reference's last arm reads `self.model` (SolverMVA.m:396-399): every
406 * method but 'exact' is granted FiniteCapacity on a single-station M/M/1/K with
407 * tail drop, because mva_dispatch ORs `sn_is_mm1k_loss` into its single-station
408 * test and answers that shape whatever the caller named. A name cannot carry
409 * the grant, so a caller that HAS the struct asks this overload and one that
410 * does not gets the static table above.
411 */
412template <class T>
413FeatureSet mva_feature_set(const std::string& raw_method, const NetworkStruct<T>& sn) {
414 FeatureSet f = mva_feature_set(raw_method);
415 if (mva_base_method(raw_method) != "exact" && sn.is_mm1k_loss())
416 f.set(Feature::FiniteCapacity);
417 return f;
418}
419
420/**
421 * SolverNC.getFeatureSet, 48 names, transcribed unchanged.
422 *
423 * `Region` is declared DELIBERATELY (SolverNC.m:185-190): NC solves the open
424 * single-Delay loss network exactly and a boolean cannot express that split, so
425 * the queueing-station case stays as the imperative refusal at
426 * solver_nc_runner.h:315-332. DPS, GPS and HOL appear under cpp/.../nc/ but are
427 * absent here because MATLAB's NC refuses them and MATLAB is ground truth.
428 */
429inline FeatureSet nc_feature_set(const std::string& method) {
430 FeatureSet f;
431 f.set({Feature::Sink, Feature::Source});
432 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue});
433 f.set({Feature::APH, Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Det,
434 Feature::Exp, Feature::HyperExp});
435 // Geometric is admitted for the discrete-time route only (opt.slotted,
436 // solver_nc_dt); on the continuous-time routes it is treated by its mean
437 // and SCV like any other renewal law.
438 f.set(Feature::Geometric);
439 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer});
440 f.set({Feature::SharedServer, Feature::Buffer, Feature::Dispatcher});
441 f.set(Feature::Region);
442 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::ServiceTunnel});
443 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_SIRO});
444 f.set({Feature::SchedStrategy_LCFS, Feature::SchedStrategy_LCFSPR});
445 // DPS is served ONLY in Morrison's closed think+DPS shape (nc_is_dps_model
446 // -> solver_nc_dps). A boolean feature cannot express that restriction, so
447 // the runner keeps an imperative check for every other DPS model, the same
448 // pattern as Region above.
449 f.set(Feature::SchedStrategy_DPS);
450 // SDR is state dependent yet keeps a product form of its own (eq. 16),
451 // which solver_nc_sdr evaluates exactly; see _kb/16-state-dependent-routing.md
452 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND,
453 Feature::RoutingStrategy_SDR});
454 f.set({Feature::SchedStrategy_FCFS, Feature::SchedStrategy_OI, Feature::SchedStrategy_PAS});
455 // JoinPartial: the MMT fixed point charges the k-th branch completion
456 // (fj_ordstat_exp), not the maximum
457 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner,
458 Feature::JoinPartial});
459 f.set({Feature::ClosedClass, Feature::SelfLoopingClass});
460 f.set({Feature::Cache, Feature::CacheClassSwitcher, Feature::OpenClass});
461 f.set(Feature::CacheRetrieval);
462 f.set(Feature::CacheItemSize); // exact and sampling methods only
463 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO});
464 f.set(Feature::ReplacementStrategy_HLRU);
465 // Petri nets: the 'rec' route (solver_nc_spn) walks the reachable set in a
466 // decision diagram, so a Place is a token container rather than a station
467 // with a service process. A queueing Place is refused by spn_pf, which is
468 // where the product-form class is decided.
469 f.set({Feature::Place, Feature::Transition, Feature::Linkage, Feature::Enabling,
470 Feature::Inhibiting, Feature::Timing, Feature::Firing, Feature::Storage});
471 f.set({Feature::LoadDependence, Feature::ClassDependence});
472 f.set(Feature::JointDependence);
473 // c-server stations, SolverNC.m:454-460: every route but 'divdiff' folds the
474 // count into its own kernel. FiniteCapacity is deliberately NOT here, as in
475 // the reference: 'mem', 'default' and 'exact' are granted it below and the
476 // rest solve a buffer away.
477 f.set(Feature::MultiServer);
478
479 // PER-METHOD DELTAS. A feature set declares what the method ACCEPTS, so it
480 // can refuse a model for HAVING a construct and never for lacking one:
481 // "closed population only" and "no think time" are sayable here, while
482 // "requires a cache" or "requires a loss network" are not and live in
483 // `nc::nc_method_refusal`, which the runner and `auto_family_refusal` ask.
484 // CLASS- AND JOINT-DEPENDENT RATES, SolverNC.m:193-203: they divert the model,
485 // on every route, to `solver_nc_conv`, Sauer's multichain convolution, which
486 // never reads the method -- so any other name would be ANSWERED by that
487 // recursion under a name that says something else. It is exact for the
488 // product-form beta_{i,r}(n) but a joint eta_i(n) breaks the BCMP recurrence
489 // it runs on, so 'exact' keeps the former only. Missing here until
490 // 2026-09-13, which is why C++ accepted 'exact' on a joint-dependent model.
491 if (method != "default" && method != "exact") {
492 f.unset(Feature::ClassDependence);
493 f.unset(Feature::JointDependence);
494 } else if (method == "exact") {
495 f.unset(Feature::JointDependence);
496 }
497 // NO `LoadDependence` DELTA HERE, DELIBERATELY. `SolverNC.m:184-191` drops the
498 // feature for every name outside {default, exact, is, clw, pana, panald,
499 // divdiff, rd, nrp, nrl, nre, comomld, sdr, sdr.mva}, because `pfqn_ncld` has
500 // an arm for those names only. THE OTHER THREE PORTS ARE WIDER ON PURPOSE: a
501 // rate lattice sends the model to the load-dependent kernel and the run
502 // reports itself as 'exact/gld', so 'ca' or 'comom' on a lattice is an
503 // HONEST DOWNGRADE to the right answer rather than a wrong one under the
504 // wrong name (measured: all 13 names return the CTMC queue 2.389302/1.610698
505 // on the `loaddep` fixture). Both gate suites pin that breadth --
506 // `line-test.git/cpp/tests/test_gate_nc.cpp` "the load-dependent model keeps
507 // every other method" and its python twin -- so porting the MATLAB delta here
508 // removes 13 working rows. C++ is the one port where the downgrade is not
509 // reached for every name: `pfqn_ncld` throws `unrecognized method` on
510 // 'comom', which is a gap in that kernel, not a reason to refuse upstream.
511 if (method == "divdiff") {
512 // The divided-difference closed form of Casale (SIGMETRICS 2017), Eqs.
513 // (15)-(16), covers load-independent queues; a think time would ask for
514 // the integral form of Cor. 3.4, which is not implemented, so pfqn_nc and
515 // pfqn_ncld both refuse one by name. An infinite server is where a think
516 // time comes from, so the envelope drops it.
517 f.unset(Feature::SchedStrategy_INF);
518 } else if (method == "rd" || method == "nrp" || method == "nrl" || method == "nre" ||
519 method == "comomld" || method == "panald" || method == "is") {
520 // The load-dependent normalizing-constant evaluators are reached by
521 // `solver_ncld` only on its CLOSED branch, where pfqn_ncld reads the
522 // method name; an open chain sends the model to the mixed route, which
523 // never reads it, so every one of these names silently became 'ncldmx'.
524 // 'is' is here for the same reason on the load-independent side: the
525 // sample-an-ordering estimator integrates over a closed population
526 // simplex and has no open-class form.
527 f.unset(Feature::OpenClass);
528 }
529 // MULTISERVER, SolverNC.m:236-243: the divided-difference closed form of
530 // 'divdiff' covers load-independent SINGLE-server queues, and
531 // `nc::nc_method_refusal` keeps wording why (a c-server station enters the
532 // constant as Seidmann's surrogate delay).
533 if (method == "divdiff") f.unset(Feature::MultiServer);
534 // FINITECAPACITY, SolverNC.m:244-254: 'mem' represents the buffer as a
535 // GE/GE/c/0;N queue (`solver_nc_mem_supports`.blocking) and the single-station
536 // M/M/1/K with tail drop is solved in closed form (qsys_mm1k_loss) under
537 // 'default' and 'exact'. The shape half of each rule stays structural, in
538 // `solver_nc_runner`'s own `check_binding_capacity` call.
539 if (method == "mem" || method == "default" || method == "exact")
540 f.set(Feature::FiniteCapacity);
541 // PERFECT SAMPLING FROM A BALANCE FUNCTION. `cftp` and `cftp.approx` draw
542 // states of the closed single-class Gordon-Newell product form and nothing
543 // else, so every construct outside it leaves the envelope. The rules the
544 // registry has no name for -- the class count, the station count and the
545 // phase count -- are structural and live in `nc::solver_nc_cftp_supports`,
546 // which the analyzer itself calls and `auto_family_refusal` asks first.
547 //
548 // The one-phase-per-station rule is deliberately NOT spelled as a list of
549 // distribution names: the sampler refuses phases(i,0) > 1, and a name is not
550 // a phase count -- a one-phase Coxian passes and a HyperExp does not, while
551 // Det only acquires its phases in sn_nonmarkov_toph.
552 if (method == "cftp" || method == "cftp.approx" || method == "cftp.exact") {
553 f.unset(Feature::OpenClass);
554 // Queue, Delay and Router are the only node kinds the sampler walks.
555 f.unset(Feature::Source);
556 f.unset(Feature::Sink);
557 f.unset(Feature::RandomSource);
558 f.unset(Feature::JobSink);
559 f.unset(Feature::ClassSwitch);
560 f.unset(Feature::StatelessClassSwitcher);
561 f.unset(Feature::Cache);
562 f.unset(Feature::CacheClassSwitcher);
563 f.unset(Feature::CacheRetrieval);
564 f.unset(Feature::CacheItemSize);
565 f.unset(Feature::ReplacementStrategy_RR);
566 f.unset(Feature::ReplacementStrategy_FIFO);
567 f.unset(Feature::ReplacementStrategy_SFIFO);
568 f.unset(Feature::ReplacementStrategy_LRU);
569 f.unset(Feature::ReplacementStrategy_HLRU);
570 f.unset(Feature::ReplacementStrategy_CLIMB);
571 f.unset(Feature::ReplacementStrategy_QLRU);
572 f.unset(Feature::Fork);
573 f.unset(Feature::Join);
574 f.unset(Feature::Forker);
575 f.unset(Feature::Joiner);
576 f.unset(Feature::JoinPartial);
577 f.unset(Feature::Place);
578 f.unset(Feature::Transition);
579 f.unset(Feature::Linkage);
580 f.unset(Feature::Enabling);
581 f.unset(Feature::Inhibiting);
582 f.unset(Feature::Timing);
583 f.unset(Feature::Firing);
584 f.unset(Feature::Storage);
585 // Disciplines outside INF/PS/FCFS/SIRO/LCFSPR have no product form the
586 // sampler encodes; NC's own DPS (Morrison), OI and PAS routes do not
587 // apply to a balance-function draw.
588 f.unset(Feature::SchedStrategy_DPS);
589 f.unset(Feature::SchedStrategy_GPS);
590 f.unset(Feature::SchedStrategy_SEPT);
591 f.unset(Feature::SchedStrategy_LEPT);
592 f.unset(Feature::SchedStrategy_HOL);
593 f.unset(Feature::SchedStrategy_LCFS);
594 f.unset(Feature::SchedStrategy_LCFSPRPRIO);
595 f.unset(Feature::SchedStrategy_FCFSPRPRIO);
596 f.unset(Feature::SchedStrategy_FCFSPR);
597 f.unset(Feature::SchedStrategy_LCFSPI);
598 f.unset(Feature::SchedStrategy_FCFSPI);
599 f.unset(Feature::SchedStrategy_LCFSPIPRIO);
600 f.unset(Feature::SchedStrategy_FCFSPIPRIO);
601 f.unset(Feature::SchedStrategy_PSPRIO);
602 f.unset(Feature::SchedStrategy_DPSPRIO);
603 f.unset(Feature::SchedStrategy_GPSPRIO);
604 f.unset(Feature::SchedStrategy_LPS);
605 f.unset(Feature::SchedStrategy_PAS);
606 f.unset(Feature::SchedStrategy_OI);
607 f.unset(Feature::SchedStrategy_POLLING);
608 f.unset(Feature::Region);
609 f.unset(Feature::LoadDependence);
610 f.unset(Feature::ClassDependence);
611 f.unset(Feature::JointDependence);
612 f.unset(Feature::GlobalDependence);
613 // A state-dependent decision is not Markovian routing, and SDR's own
614 // product form (eq. 16) is not the one the sampler encodes.
615 f.unset(Feature::RoutingStrategy_RROBIN);
616 f.unset(Feature::RoutingStrategy_WRROBIN);
617 f.unset(Feature::RoutingStrategy_JSQ);
618 f.unset(Feature::RoutingStrategy_SQ);
619 f.unset(Feature::RoutingStrategy_SDR);
620 // A discrete-time law belongs to the slotted route, which is not this one.
621 f.unset(Feature::Geometric);
622 // The Gordon-Newell balance function has no buffer; `solver_nc_cftp_supports`
623 // refuses a finite one by name. The server count is NOT withdrawn: the
624 // reference keeps it, the balance function carrying min(n,c).
625 f.unset(Feature::FiniteCapacity);
626 }
627 return f;
628}
629
630/**
631 * SolverCTMC.getFeatureSet, the reference's 104 MATLAB names in full.
632 *
633 * Balking, Reneging and Retrial are modelled: `refresh_sync` emits RENEGE and
634 * RETRY from the station's own patience and orbit, the arrival handler applies
635 * QUEUE_LENGTH balking, and `state_impatience_unsupported` refuses the laws the
636 * chain cannot carry. Breakdown is a struct field
637 * (`sn.breakdownparam`) and is modelled since 2026-09-23.
638 */
639inline FeatureSet ctmc_feature_set(const std::string& method) {
640 FeatureSet f;
641 f.set({Feature::Source, Feature::Sink});
642 // Router is INERT: used_lang_features emits a Router's routing strategy and
643 // not the node name, as getUsedLangFeatures.m:77-78 does, so nothing can
644 // emit it. Kept declared rather than deleted -- it is what SolverCTMC.m
645 // declares, and the AUTO ranking reads these sets by name.
646 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue,
647 Feature::Router});
648 // ME is INERT: ProcessType carries no ME, so it can never be emitted.
649 f.set({Feature::MAP, Feature::APH, Feature::MMPP2, Feature::MMAP, Feature::MPH, Feature::PH,
650 Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp, Feature::HyperExp,
651 Feature::ME});
652 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
653 Feature::Weibull});
654 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
655 Feature::Buffer, Feature::Dispatcher});
656 // The blocked-job overflow buffer is part of the chain, so the FCR is exact.
657 f.set(Feature::Region);
658 f.set({Feature::Cache, Feature::CacheClassSwitcher});
659 f.set(Feature::CacheRetrieval);
660 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::ServiceTunnel});
661 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS});
662 f.set({Feature::SchedStrategy_DPS, Feature::SchedStrategy_GPS});
663 f.set({Feature::SchedStrategy_SIRO, Feature::SchedStrategy_SEPT});
664 f.set({Feature::SchedStrategy_LEPT, Feature::SchedStrategy_FCFS});
665 f.set({Feature::SchedStrategy_HOL, Feature::SchedStrategy_LCFS});
666 // LCFS with priority groups, SolverCTMC.m:340: buffered like HOL and served by its own arm of
667 // after_event_station_dep, so leaving it out was an under-claim.
668 f.set(Feature::SchedStrategy_LCFSPRIO);
669 f.set({Feature::SchedStrategy_LCFSPR, Feature::SchedStrategy_LCFSPRPRIO,
670 Feature::SchedStrategy_FCFSPRPRIO});
671 // 2026-07-29: the rest of the preempt family, added to SolverCTMC.m:159 in
672 // the same change. after_event_station_arv and _dep both carry a
673 // buffer_is_tag_phase_pairs arm covering all eight, mirroring
674 // afterEventStation.m:1043-1330, so declaring only three gated off five
675 // chains the generator builds correctly.
676 f.set({Feature::SchedStrategy_LCFSPI, Feature::SchedStrategy_FCFSPR,
677 Feature::SchedStrategy_FCFSPI});
678 f.set({Feature::SchedStrategy_LCFSPIPRIO, Feature::SchedStrategy_FCFSPIPRIO});
679 f.set({Feature::SchedStrategy_PSPRIO, Feature::SchedStrategy_DPSPRIO,
680 Feature::SchedStrategy_GPSPRIO});
681 f.set({Feature::SchedStrategy_LPS, Feature::SchedStrategy_PAS, Feature::SchedStrategy_OI,
682 Feature::SchedStrategy_POLLING});
683 // SDR, JSQ and SQ(d) are evaluated for real: `rt_state` rebuilds their rows at
684 // every state (eq. (10) of Krzesinski (1987) for SDR, `sub_jsq`/`sub_sq` of
685 // refreshRoutingMatrix.m for the dispatchers), so the chain carries the
686 // routing the model declares rather than the uniform placeholder
687 // `refresh_routing` leaves in `sn.rt`. Until 2026-10-18 JSQ and SQ were
688 // declared here on the belief that the refresh refused them; it did not, and
689 // the generator answered them as RAND.
690 f.set({Feature::RoutingStrategy_SDR, Feature::RoutingStrategy_JSQ, Feature::RoutingStrategy_SQ});
691 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
692 // ROUND-ROBIN DISPATCH, now that the pointer is a coordinate of the state:
693 // `refresh_local_vars` allocates it, `append_local_vars` enumerates it,
694 // `after_event_station`/`after_event_router` advance it on every departure,
695 // and the generator reads the destination out of the ACTIVE node's
696 // post-departure row. Before that the uniform expansion was all there was,
697 // and declaring it would have answered a random-routing model.
698 f.set({Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN});
699 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO,
700 Feature::ReplacementStrategy_SFIFO, Feature::ReplacementStrategy_LRU});
701 f.set({Feature::ReplacementStrategy_HLRU, Feature::ReplacementStrategy_CLIMB,
702 Feature::ReplacementStrategy_QLRU});
703 f.set({Feature::ClosedClass, Feature::SelfLoopingClass, Feature::OpenClass, Feature::Replayer});
704 f.set({Feature::OpenSignal, Feature::ClosedSignal});
705 f.set({Feature::SignalType_NEGATIVE, Feature::SignalType_CATASTROPHE,
706 Feature::SignalType_REPLY});
707 f.set({Feature::SignalBatchRemoval, Feature::SignalRemovalPolicy});
708 f.set({Feature::Place, Feature::Transition, Feature::Linkage, Feature::Enabling,
709 Feature::Inhibiting, Feature::Timing, Feature::Firing, Feature::Storage});
710 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner});
711 f.set(Feature::Retrial);
712 // INERT: balking and reneging are call parameters of state_events.h, not
713 // struct fields, so used_lang_features cannot emit them and declaring them
714 // costs nothing.
715 //
716 // BREAKDOWN IS DIFFERENT SINCE 2026-08-15, when `sn.breakdownparam` gave it
717 // a field and `used_lang_features` started emitting it. The declaration
718 // stays, because MATLAB's SolverCTMC genuinely implements breakdowns
719 // (`State.afterEventStation`'s FAILURE and REPAIR branches, on the shared
720 // trailing marker column) and this set is that declaration transcribed. What
721 // keeps the declaration from becoming a silent wrong answer is a REFUSAL BY
722 // NAME in `solver_ctmc_analyzer.h`: this port's CTMC does not read the
723 // field, so it names the gap instead of solving a model whose server never
724 // fails. Delete that refusal in the same change that teaches solver_ctmc.h
725 // the marker column, not before.
726 f.set({Feature::Balking, Feature::Reneging, Feature::Breakdown});
727 f.set({Feature::LoadDependence, Feature::ClassDependence, Feature::JointDependence});
728 // phi(n) over the FULL population matrix; solver_ctmc.h tabulates it per state
729 f.set(Feature::GlobalDependence);
730 // c-server stations and binding buffers are both State constructs
731 // (`from_marginal` / `after_event_station`), served by the explicit
732 // generator and withdrawn from `mdd` below. SolverCTMC.m:443-446.
733 f.set({Feature::MultiServer, Feature::FiniteCapacity});
734 // Heterogeneous server pools: `ctmc::ctmc_pools` gives each compatible pool its own
735 // phase block and `after_event_station_pool` applies the LDES pool-selection rules.
736 f.set(Feature::HeteroServers);
737
738 // PER-METHOD DELTAS. Two of the three methods enumerate the generator above
739 // and share the whole envelope; `mdd` builds no generator at all, so what the
740 // rest of the envelope describes does not apply to it. The rule the registry
741 // has no name for -- the class count -- is structural and lives in
742 // `ctmc::solver_ctmc_mdd_supports`, which the analyzer itself calls. The
743 // perfect sampler `cftp` moved to SolverNC (`nc_feature_set`).
744 if (method == "mdd") {
745 // The decision diagram holds the MARKING of a closed network; an open
746 // stream makes it unbounded, so there is no finite diagram to hold. A
747 // stochastic Petri net keeps the Place/Transition names: `spn_mdd` reads
748 // the marking directly and is exempt from the closed-network rule.
749 //
750 // A FORK-JOIN MODEL IS NEITHER of the two shapes it serves: the tag
751 // augmentation a fork needs adds one auxiliary class per branch, so the
752 // struct that reaches the analyzer is never single-class however the
753 // model was written, and the level decomposition has no meaning for a
754 // firing that does not conserve the per-chain population.
755 f.unset(Feature::OpenClass);
756 f.unset(Feature::Source);
757 f.unset(Feature::Sink);
758 f.unset(Feature::RandomSource);
759 f.unset(Feature::JobSink);
760 f.unset(Feature::Fork);
761 f.unset(Feature::Join);
762 f.unset(Feature::Forker);
763 f.unset(Feature::Joiner);
764 f.unset(Feature::JoinPartial);
765 // The level decomposition reads rates, servers and phases and no
766 // sn.cap/classcap, so a buffer would be dropped. SolverCTMC.m:307-309.
767 f.unset(Feature::FiniteCapacity);
768 // the pool blocks and the pool order are State constructs, not a level decomposition
769 f.unset(Feature::HeteroServers);
770 // A cache's list state and its retrieval bitmap are State constructs too: the decision diagram holds
771 // station marginals only (SolverCTMC.m, method 'mdd').
772 for (Feature x : {Feature::Cache, Feature::CacheClassSwitcher, Feature::CacheRetrieval,
773 Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO,
774 Feature::ReplacementStrategy_SFIFO, Feature::ReplacementStrategy_LRU,
775 Feature::ReplacementStrategy_HLRU, Feature::ReplacementStrategy_CLIMB,
776 Feature::ReplacementStrategy_QLRU})
777 f.unset(x);
778 }
779 return f;
780}
781
782/**
783 * SolverSSA.getFeatureSet, 98 MATLAB names.
784 *
785 * IT IS THE SOLVER'S REACH, NOT ONE ENGINE'S, and that is what makes it usable:
786 * `solver_ssa` under `default` runs the NRM when the NRM can run the model and
787 * the event-driven serial engine when it cannot, so a construct EITHER engine
788 * covers is a construct SolverSSA covers. AUTO chooses this solver on the
789 * strength of this set (`solver_auto.h:453`), so under-declaring costs a model
790 * the only solver that can answer it and over-declaring hands AUTO a refusal.
791 *
792 * The serial engine applies the SAME event handlers `state_events.h` gives the
793 * CTMC generator, which is why the two sets are now close: a cache access, an
794 * SPN firing, a fork firing, a polling controller and the priority shares are
795 * all reached through handlers both solvers share. What is DECLARED here is
796 * what a test in `test_ssa_fj_fcr.cpp` or `test_ssa_serial.cpp` exercises end
797 * to end, never what a handler merely appears to support.
798 *
799 * Still dropped, with the C++ refusal that proves each:
800 * RoutingStrategy_RROBIN, _WRROBIN,
801 * _JSQ, _SQ, _RL ssa_dispatch.h refuses them in the NRM and
802 * `NetworkStruct::refresh_routing` refuses the
803 * state-dependent ones when the struct is built,
804 * so no engine here can receive one
805 * `ssa_check_phases` (ssa_dispatch.h) stays imperative: non-exponential service
806 * AT a given discipline is a per-(station,class) join of two features and the
807 * boolean registry cannot state it. It gates the NRM only -- the serial engine
808 * expands a phase-type process wherever the CTMC generator does -- so a model it
809 * refuses is answered by the fallback rather than refused outright.
810 */
811/**
812 * The population-network envelope of SSA method `mlmc`, transcribed from
813 * ssa_mlmc_feature_set.m. Structural checks (multiclass FCFS, a deterministic
814 * initial state) stay in `ssa::ssa_mlmc_problem`, as they do in the reference.
815 */
817 FeatureSet f;
818 f.set({Feature::Source, Feature::Sink, Feature::JobSink, Feature::Queue, Feature::Delay,
819 Feature::DelayStation, Feature::Exp, Feature::Disabled});
820 f.set({Feature::ClassSwitch, Feature::StatelessClassSwitcher, Feature::Router,
821 Feature::InfiniteServer, Feature::SharedServer, Feature::Server, Feature::Buffer,
822 Feature::Dispatcher, Feature::RandomSource});
823 f.set({Feature::ServiceTunnel, Feature::SchedStrategy_EXT, Feature::SchedStrategy_INF,
824 Feature::SchedStrategy_PS});
825 f.set({Feature::SchedStrategy_FCFS, Feature::RoutingStrategy_PROB,
826 Feature::RoutingStrategy_RAND});
827 f.set({Feature::OpenClass, Feature::ClosedClass, Feature::SelfLoopingClass,
828 Feature::MultiServer});
829 return f;
830}
831
832/** True when `method` names SSA's MLMC estimator, bare or family-qualified, in any case. */
833inline bool ssa_method_is_mlmc(const std::string& method) {
834 std::string m(method);
835 for (auto& ch : m) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
836 return m == "mlmc" || m == "ssa.mlmc";
837}
838
839inline FeatureSet ssa_feature_set(const std::string& method) {
840 // SolverSSA.getMethodFeatureSet: `mlmc` answers the restricted set, every other method the solver envelope.
841 if (ssa_method_is_mlmc(method)) return ssa_mlmc_feature_set();
842 FeatureSet f;
843 // Router is INERT here for the same reason as in ctmc_feature_set: nothing
844 // emits it. SolverSSA.m:71 declares it all the same, so it stays.
845 f.set({Feature::Sink, Feature::Source, Feature::Router});
846 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue});
847 f.set({Feature::MAP, Feature::MMPP2, Feature::MMAP, Feature::MPH, Feature::APH, Feature::PH,
848 Feature::Replayer});
849 f.set({Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp, Feature::HyperExp});
850 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
851 Feature::Weibull});
852 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer});
853 f.set({Feature::SharedServer, Feature::Buffer, Feature::Dispatcher});
854 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::ServiceTunnel});
855 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS});
856 f.set({Feature::SchedStrategy_DPS, Feature::SchedStrategy_FCFS});
857 f.set({Feature::SchedStrategy_GPS, Feature::SchedStrategy_LPS, Feature::SchedStrategy_SIRO});
858 f.set({Feature::SchedStrategy_HOL, Feature::SchedStrategy_LCFS});
859 f.set({Feature::SchedStrategy_SEPT, Feature::SchedStrategy_LEPT});
860 f.set(Feature::SchedStrategy_LCFSPR);
861 // The priority variants the reference declares, reached through the serial
862 // engine: `after_event_station_arv` and `_dep` carry the [class, phase]
863 // buffer arm, and `test_ssa_serial.cpp` runs them against the exact CTMC.
864 //
865 // THE REST OF THE PREEMPT FAMILY AND LCFSPRIO (2026-10-16). SolverSSA.m declares LCFSPI, FCFSPR, FCFSPI,
866 // LCFSPIPRIO, FCFSPIPRIO and LCFSPRIO (since 2026-09-05), and the serial engine runs every one of them
867 // through the same `after_event_station_arv`/`_dep` arms the CTMC generator uses (LCFSPRIO: the
868 // leftmost member of the best-priority group is promoted, `state_events.h`). The NRM has no reaction
869 // form for them (`ssa_check_scheduling`), so `default` falls back to the serial engine, as in MATLAB.
870 f.set({Feature::SchedStrategy_LCFSPI, Feature::SchedStrategy_FCFSPR,
871 Feature::SchedStrategy_FCFSPI});
872 f.set({Feature::SchedStrategy_LCFSPIPRIO, Feature::SchedStrategy_FCFSPIPRIO,
873 Feature::SchedStrategy_LCFSPRIO});
874 f.set({Feature::SchedStrategy_LCFSPRPRIO, Feature::SchedStrategy_FCFSPRPRIO});
875 f.set({Feature::SchedStrategy_PSPRIO, Feature::SchedStrategy_DPSPRIO,
876 Feature::SchedStrategy_GPSPRIO});
877 // PAS / OI and the polling controller, reached through the serial engine:
878 // the NRM refuses all three by name and `default` falls back. PAS and OI
879 // needed `to_marginal`'s ordered-list arm (ported 2026-07-31) before the
880 // queue length at such a station was a number at all.
881 f.set({Feature::SchedStrategy_PAS, Feature::SchedStrategy_OI,
882 Feature::SchedStrategy_POLLING});
883 // Fork-join, on the TAG-AUGMENTED copy the analyzer builds; the sibling
884 // classes are folded back before the table is returned.
885 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner});
886 // Finite capacity regions: DROP censors the path exactly as it censors the
887 // CTMC's space, and WAITQ runs the shared token-FIFO relation.
888 f.set(Feature::Region);
889 // A cache access is a state-dependent class switch plus a replacement, both
890 // in `after_event_cache`; the analyzer writes the realized hit ratio back.
891 f.set({Feature::Cache, Feature::CacheClassSwitcher, Feature::CacheRetrieval});
892 // The stochastic Petri net path: a firing is a GLOBAL synchronization, which
893 // the serial engine walks alongside the ordinary ones.
894 f.set({Feature::Place, Feature::Transition, Feature::Linkage, Feature::Enabling,
895 Feature::Inhibiting, Feature::Timing, Feature::Firing, Feature::Storage});
896 // Fluid places, flows, guards, impulses and level-dependent rates: a PDMP on the NRM Petri runner only
897 // (`ssa_fluid_refusal`, ssa_dispatch.h), which withdraws the serial engine by name.
898 f.set(Feature::FluidPlace);
899 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
900 // SDR, through the serial engine: the NRM refuses every state-dependent
901 // strategy by name and `default` falls back, and the serial engine reads the
902 // same per-state table (`rt_state`) the CTMC generator does.
903 f.set(Feature::RoutingStrategy_SDR);
904 // THE DISPATCHERS, both engines. The NRM walks the outgoing arcs itself and
905 // keeps its own cursor (`resolve_state_dependent_dest`); the SERIAL engine
906 // reads the pointer the state now carries, the same one the CTMC generator
907 // reads, and re-reads JSQ and SQ(d) rows from `rt_state` at every state, as
908 // the reference's serial engine reads `sn.rtfun`.
909 f.set({Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
910 Feature::RoutingStrategy_JSQ, Feature::RoutingStrategy_SQ});
911 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO,
912 Feature::ReplacementStrategy_SFIFO, Feature::ReplacementStrategy_LRU});
913 f.set({Feature::ReplacementStrategy_HLRU, Feature::ReplacementStrategy_CLIMB,
914 Feature::ReplacementStrategy_QLRU});
915 f.set({Feature::SchedStrategy_EXT, Feature::ClosedClass, Feature::SelfLoopingClass,
916 Feature::OpenClass});
917 f.set({Feature::OpenSignal, Feature::ClosedSignal});
918 f.set({Feature::SignalType_NEGATIVE, Feature::SignalType_CATASTROPHE});
919 // Synchronous calls, through the serial engine: `after_event` keeps the caller's server in
920 // the reply block and the analyzer adds the hold to QLen and Util (`reply_held`). The NRM
921 // refuses them (`ssa_check_reply`), so `default` falls back, as SolverSSA.m does.
922 f.set(Feature::SignalType_REPLY);
923 f.set({Feature::SignalBatchRemoval, Feature::SignalRemovalPolicy});
924 f.set(Feature::Retrial);
925 // INERT: balking and reneging are call parameters of state_events.h, not struct fields.
926 f.set({Feature::Balking, Feature::Reneging});
927 f.set(Feature::LoadDependence);
928 // Class- and joint-dependent scaling: the NRM refuses both because it builds
929 // its rates without evaluating the handle, and the serial engine evaluates
930 // it through `state_events.h` exactly as the CTMC does.
931 f.set({Feature::ClassDependence, Feature::JointDependence});
932 // A global (Whittle) dependence is likewise refused by the NRM and carried by
933 // the serial engine, which evaluates phi(n) once per sample-path state.
934 f.set(Feature::GlobalDependence);
935 // c-server stations and binding buffers: the serial engine walks the same
936 // State arms ctmc_feature_set declares and the NRM honours both.
937 // SolverSSA.m:178-181.
938 f.set({Feature::MultiServer, Feature::FiniteCapacity});
939 return f;
940}
941
942/**
943 * SolverFLD.getFeatureSet, transcribed, MINUS what the requested method cannot
944 * evaluate -- the port of `@@SolverFLD/getMethodFeatureSet`.
945 *
946 * THE PER-METHOD SUBTRACTION IS NOT COSMETIC. A station whose discipline has no
947 * branch in `ode_rates_closing_factors` keeps g = x, i.e. it is integrated as an
948 * INFINITE SERVER, and the answer is wrong with no warning at all: on
949 * Delay(Z=1) -> Queue(c=1), N=4, whose exact Q2 is 3.0154, the fall-through
950 * returns 2.0000. The closing family therefore has to reject the disciplines it
951 * lacks a branch for rather than accept them into the wrong drift. Only the
952 * closing family is affected: matrix/pnorm build a PS drift, which is the right
953 * aggregate for any work-conserving discipline.
954 *
955 * `CacheRetrieval` is DELIBERATELY absent upstream (SolverFLD.m:95-101 records
956 * the flow-conservation failure on examples/basic/cacheModel/retrieval_simple).
957 * `NHPP`, `MAPt` and `PHt` are the time-inhomogeneous families. Every method
958 * accepts them, as the reference does: the first-order methods integrate the
959 * time-averaged NOMINAL pair, `kp` integrates the schedule itself, and a
960 * time-varying rate multiplier is refused on the options rather than here.
961 * What the C++ fluid
962 * solver ignores silently (Region, Fork/Join, Router, Retrial, Place/Transition,
963 * the signals, ClassDependence) is already absent from the MATLAB set.
964 */
965/**
966 * The CLASS envelope, `SolverFLD.getFeatureSet()`: what the static `SolverFLD.supports(model)` reads,
967 * and so what SolverAUTO's candidate filter and its unqualified ranking ask. The per-method deltas of
968 * `fluid_feature_set` narrow it (HOL to mfq, GPS to minnormal, LoadDependence to the closing family) and
969 * widen it for dae alone, so 'default' is NOT this set.
970 */
972 FeatureSet f;
973 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue});
974 f.set({Feature::Cache, Feature::CacheClassSwitcher});
975 f.set({Feature::Cox2, Feature::Coxian, Feature::Erlang, Feature::Exp, Feature::HyperExp});
976 f.set({Feature::APH, Feature::Det, Feature::MAP, Feature::MMPP2, Feature::NHPP});
977 // The time-inhomogeneous families, which only `kp` integrates; the gate is
978 // per-method below, so declaring them here is not a claim that every fluid
979 // method reads a schedule.
980 f.set({Feature::MAPt, Feature::PHt});
981 // Non-Markovian renewal laws reach the ODE as acyclic PH via sn_nonmarkov_toph.
982 f.set({Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
983 Feature::Weibull});
984 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
985 Feature::Buffer, Feature::Dispatcher});
986 f.set({Feature::Server, Feature::ServiceTunnel});
987 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS});
988 f.set({Feature::SchedStrategy_DPS, Feature::SchedStrategy_FCFS, Feature::SchedStrategy_HOL});
989 f.set({Feature::SchedStrategy_GPS});
990 f.set({Feature::SchedStrategy_SIRO, Feature::SchedStrategy_LCFS,
991 Feature::SchedStrategy_LCFSPR});
992 f.set(Feature::LoadDependence);
993 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO});
994 f.set(Feature::ReplacementStrategy_SFIFO);
995 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
996 f.set({Feature::ClosedClass, Feature::SelfLoopingClass, Feature::Replayer});
997 f.set({Feature::RandomSource, Feature::Sink, Feature::Source, Feature::OpenClass,
998 Feature::JobSink});
999 // Fork-join through the MMT (or Heidelberger-Trivedi) transform, driven by
1000 // `mva::fj_fixed_point` with a fluid inner solve; see fluid_runner.h. The
1001 // transform emits only Source, Delay, Queue, Router and ClassSwitch, all of
1002 // which the drift already carries, so nothing fork-join specific enters the
1003 // ODE. The per-method gate below removes it again where the fixed point
1004 // cannot run.
1005 f.set({Feature::Fork, Feature::Forker, Feature::Join, Feature::Joiner,
1006 Feature::JoinPartial});
1007 // c-server stations: the drifts carry min(n,c); withdrawn from 'diffusion'
1008 // and 'mfq' below. SolverFLD.m:605-607.
1009 f.set(Feature::MultiServer);
1010 // A binding buffer, SolverFLD.m:608-614: 'dae' carries it as an algebraic
1011 // constraint, 'mol' IS the Mt/G/s/0 loss system and the AoI arm of 'mfq' is
1012 // a bufferless or single-buffer queue. WHICH method serves one is the
1013 // structural rule `fluid_runner`'s own `check_binding_capacity` call asks,
1014 // so no per-method delta duplicates it here; the reference withdraws it from
1015 // no method for exactly that reason.
1016 f.set(Feature::FiniteCapacity);
1017 return f;
1018}
1019
1020inline FeatureSet fluid_feature_set(const std::string& method) {
1022
1023 // THE CANONICAL NAME, which every test below is on. Stripping only the
1024 // `fld.` prefix here left two holes: `aoi` is an alias of `mfq`
1025 // (fluid_runner.h `fluid_unqualify`) and lost the HOL grant `mfq` keeps,
1026 // reachable through AUTO, which passes the method name unmapped; and the
1027 // Qsys methods now use their bare API names as canonical selectors.
1028 std::string m =
1029 (method.size() > 4 && method.compare(0, 4, "fld.") == 0) ? method.substr(4) : method;
1030 if (m == "aoi") m = "mfq";
1031 const bool closing_family =
1032 m == "closing" || m == "statedep" || m == "softmin" || m == "tbi" || m == "minnormal" ||
1033 m == "refined" || m == "dae";
1034 // Limited load dependence composes with the closure as a rate multiplier
1035 // alpha(n_i) on the scheduling share, which only the closing family evaluates.
1036 // The matrix, pnorm, softmin, statedep, tbi, diffusion, mfq, kp and rmf paths
1037 // build their drift independently and would silently ignore alpha.
1038 if (!(m == "closing" || m == "minnormal" || m == "refined" || m == "dae"))
1039 f.unset(Feature::LoadDependence);
1040 // SIRO, LCFS and LCFSPR have no branch in the closing drift. The metric reader
1041 // of `solver_fluid_closing` caught LCFS/LCFSPR/HOL, but `solver_fluid_moments`
1042 // reads the event representation directly and never reached that guard, and
1043 // SIRO is worse still: the reader accepts it AS FCFS, so the ODE integrated it
1044 // as INF while the metrics were read as if it shared the server.
1045 if (closing_family) {
1046 f.unset(Feature::SchedStrategy_SIRO);
1047 f.unset(Feature::SchedStrategy_LCFS);
1048 f.unset(Feature::SchedStrategy_LCFSPR);
1049 }
1050 // GPS splits the server by weight among the BACKLOGGED classes, so its share
1051 // is a function of the backlog INDICATOR. A first-order closure cannot express
1052 // it at all: with continuous x_k > 0 every class is always backlogged and the
1053 // share collapses to the constant w_k/sum_j w_j, the heavy-traffic limit,
1054 // regardless of load. Only `minnormal` supplies the P(X_k >= 1) it needs --
1055 // `refined` reads its rates at the MEAN-FIELD variance, which is exactly the
1056 // degenerate case.
1057 if (m != "minnormal") f.unset(Feature::SchedStrategy_GPS);
1058 // HOL allocates capacity in PRIORITY order, not in proportion to population,
1059 // and no fluid drift reads the class priorities except the single-queue `mfq`
1060 // priority branch.
1061 if (m != "mfq") f.unset(Feature::SchedStrategy_HOL);
1062 // DPS is a WEIGHTED share and only the closing family reads the weights: the
1063 // matrix drift is the PS aggregate (which the runner refuses by name for
1064 // `matrix` and `pnorm`), `rmf` steps that same drift, `kp` shares min(n,c)/n
1065 // equally and the diffusion SDE refuses DPS. MATLAB SolverFLD.getMethodFeatureSet.
1066 if (m == "matrix" || m == "pnorm" || m == "rmf" || m == "kp" || m == "diffusion")
1067 f.unset(Feature::SchedStrategy_DPS);
1068 // MULTISERVER, SolverFLD.m:387-395: the drifts carry min(n,c) except
1069 // `fluid_diffusion`, whose SDE is written for one or infinitely many
1070 // servers, and `solver_mfq`, a single-queue model on the same server counts
1071 // (off them `mfq` resolves to `matrix`, so the delta binds only where it
1072 // runs as itself). fluid_diffusion.h:89-93 keeps wording that refusal.
1073 if (m == "diffusion" || m == "mfq") f.unset(Feature::MultiServer);
1074 // `dae` is the one method that WIDENS this set, and the widening is why the
1075 // gate has to be per-method: a Finite Capacity Region is a linear inequality
1076 // on the state, which the DAE form carries as an algebraic equation beside
1077 // the drift and no ODE method can carry at all. Every other fluid method has
1078 // to go on refusing it -- an ODE integrated through a cap it cannot see
1079 // returns the UNCONSTRAINED answer with no warning -- so the widening is
1080 // stated here rather than in the static set. `fluid_dae_constraints` still
1081 // refuses by name the region forms that are not constraints on this drift
1082 // (drop, BAS/BBS/RSRD, retrial, per-class admission weights), and
1083 // `solver_fluid_dae` refuses the FCR transient, which needs event restarts.
1084 //
1085 // DPS goes the other way: it closes on the covariance BETWEEN a station's
1086 // class coordinates, where the DAE form carries one scalar variance per
1087 // station. `minnormal` carries those matrix blocks through its outer
1088 // iteration; adding an unknown block per station here would restore the
1089 // quartic cost that keeping Sigma out of the Newton vector avoids. GPS is
1090 // already unset above for everything but `minnormal`, for the same reason.
1091 if (m == "dae") {
1092 f.set(Feature::Region);
1093 f.unset(Feature::SchedStrategy_DPS);
1094 }
1095 // A STOCHASTIC PETRI NET HAS NO DRIFT OUTSIDE THE DAE FORM: its conserved
1096 // quantities are P-invariants rather than chain populations, an immediate
1097 // transition is an algebraic FLOW rather than an event with a rate, and a
1098 // bounded place is a linear inequality on the marking (fluid_petri.h). Every
1099 // other fluid method builds its drift from the station/class/phase encoding,
1100 // where a Place contributes no coordinate at all, so it would integrate the
1101 // net as an empty model and report zeros without a warning -- which is why
1102 // the names are declared for `dae` alone rather than in the static set.
1103 // `dae` ALONE, not `default`: the reference's own gate removes them for every
1104 // other method including the unresolved default (SolverFLD.m:240), and the
1105 // Petri route is reached by name or by the runner's own branch, never by a
1106 // default that has not yet resolved.
1107 // Fluid stochastic Petri nets ride the same route, one more coordinate per fluid place in the same
1108 // min-normal drift, so FluidPlace is declared for `dae` alone as well.
1109 if (m == "dae")
1110 f.set({Feature::Place, Feature::Transition, Feature::Linkage, Feature::Enabling,
1111 Feature::Inhibiting, Feature::Timing, Feature::Firing, Feature::Storage,
1112 Feature::FluidPlace});
1113 // THE MMT FIXED POINT, and which methods can actually run it. This list used
1114 // to hold seven names on the argument that the transform hands the inner
1115 // solve a MIXED network -- the parallelism rides on auxiliary OPEN classes
1116 // even when the model is closed -- so a method that takes only closed
1117 // models, only open ones, a single queue, or a decomposition could not
1118 // drive it. MEASURED on a SYMMETRIC closed fork-join (Delay -> Fork -> two
1119 // identical FCFS queues -> Join, N = 2), against the exact chain
1120 // Q1 = Q2 = 0.664, J = 0.624, D = 1.024, five of the seven answer it and
1121 // answer it symmetrically, which is the one property no approximation of a
1122 // symmetric model may lose:
1123 //
1124 // statedep, mfq, rmf 0.5714 0.5714 0.5714 1.1429 (the `matrix` drift)
1125 // refined 0.2715 0.2715 0.9713 0.4857
1126 // tbi 0.2500 0.2500 1.0000 0.5000
1127 //
1128 // So the argument does not hold for those five and the names come back;
1129 // MATLAB and native python have always run them there. It DOES hold for the
1130 // other two, which is why they stay:
1131 //
1132 // diffusion 0 0 2 0 the whole population on ONE station and zero
1133 // (and 0 1.997 0 0.003 on a rerun -- a DIFFERENT station), so
1134 // the SDE is not integrating this model at all
1135 // kp 0 0 0 0 on a symmetric OPEN fork-join fed at rate
1136 // 0.5, i.e. an empty network where jobs are arriving
1137 //
1138 // Both are silent wrong answers rather than refusals, so keeping the names
1139 // off is what makes the model refused instead of mis-answered. MATLAB and
1140 // native python are over-permissive here and now drop them too.
1141 if (m == "diffusion" || m == "kp") {
1142 f.unset(Feature::Fork);
1143 f.unset(Feature::Forker);
1144 f.unset(Feature::Join);
1145 f.unset(Feature::Joiner);
1146 f.unset(Feature::JoinPartial);
1147 }
1148 // MAPt/PHt/NHPP are NOT removed per method, and that is the reference's own
1149 // position rather than an oversight. `solver_fluid.m:20-35` and
1150 // `solver_fluid_matrix.m:18-27` install the width-weighted NOMINAL pair for a
1151 // schedule and integrate it deliberately: the nominal is the stationary
1152 // carrier of the phase structure the schedule modulates, so a steady-state
1153 // answer at the time-averaged rate is the intended approximation, not a
1154 // silent substitution. `kp` is the method that integrates the SCHEDULE, and
1155 // `getMethodFeatureSet` in the reference restricts none of the three.
1156 //
1157 // The refusal that does exist lives on the OPTIONS, not here: a time-varying
1158 // rate multiplier (`nhpp_sched` / `rate_traj` / `rate_sched`) makes the drift
1159 // non-autonomous, which the moment closures cannot take, and
1160 // `fluid_minnormal_applicable` declines `minnormal` for it while
1161 // `fluid_moment_terms` raises. Those are conditions on the OPTIONS a caller
1162 // passes and cannot be decided from the model alone, which is why a featset
1163 // is the wrong place for them.
1164 // ON THE CANONICAL NAME, not the raw one. These two branches used to compare
1165 // `method`, so a direct call with a `fld.`-qualified spelling silently got
1166 // a DIFFERENT envelope than the bare one; it was masked on the solver path
1167 // only because `fluid_resolve_method` unqualifies on entry. test_feature_set
1168 // asserts the invariant for `minnormal` alone, and the same assertion written
1169 // for `fld.tvms` would have failed.
1170 if (m == "ggisgi" || m == "ggingi" || m == "tvms") {
1171 // The only fluid methods in LINE stated for a queue customers ABANDON.
1172 // Reneging stays out of the base FLD envelope: the network drift carries
1173 // no abandonment flow, so every other method would integrate the model
1174 // as if nobody left.
1175 f.set(Feature::Reneging);
1176 }
1177 if (m == "ggisgi" || m == "ggingi" || m == "tvms" || m == "mtginf" ||
1178 m == "mol") {
1179 // Every one of them is stated for a single open station; the base
1180 // envelope's closed classes have no meaning there.
1181 f.unset(Feature::ClosedClass);
1182 f.unset(Feature::SelfLoopingClass);
1183 }
1184 // CLOSED MODELS ONLY for `refined`, which the reference's runAnalyzer has
1185 // always enforced by name and this set never stated: the 1/N correction is
1186 // solved on orth(D) over the FULL state, so on an open model it adds a
1187 // perturbation to the SOURCE POOL mass, a normalisation constant rather than
1188 // a population. Only `minnormal` was validated open. Stating it here is what
1189 // lets a report withdraw the pair instead of offering a run that stops.
1190 if (m == "refined") {
1191 f.unset(Feature::OpenClass);
1192 f.unset(Feature::Source);
1193 f.unset(Feature::Sink);
1194 f.unset(Feature::RandomSource);
1195 f.unset(Feature::JobSink);
1196 }
1197 // The diffusion SDE PROJECTS each class back onto its own fixed population
1198 // at every step, which is the closed-network constraint itself: an open
1199 // class has no population to project onto, and a Source is not a station the
1200 // SDE has a coordinate for. `fluid_diffusion` refuses both by name, and
1201 // stating them here is what lets a CALLER see the refusal before running.
1202 if (m == "diffusion") {
1203 f.unset(Feature::OpenClass);
1204 f.unset(Feature::Source);
1205 f.unset(Feature::Sink);
1206 f.unset(Feature::RandomSource);
1207 f.unset(Feature::JobSink);
1208 }
1209 // The Ko-Pender limits are proved for an OPEN network of stations fed by
1210 // external arrival processes: a closed class has no arrival process to
1211 // modulate and no source phase to carry, and the cache and class-switch
1212 // machinery has no counterpart in the paper's event set. Only the fork-join
1213 // names were withheld here, five features against the nine MATLAB, the JAR and
1214 // native python all unset, so this port alone offered `kp` on closed, cache
1215 // and class-switching models the other three refuse -- and `doc/solver-methods.md`
1216 // states the narrowing as the documented behaviour.
1217 if (m == "kp") {
1218 f.unset(Feature::ClosedClass);
1219 f.unset(Feature::SelfLoopingClass);
1220 f.unset(Feature::Cache);
1221 f.unset(Feature::CacheClassSwitcher);
1222 f.unset(Feature::ClassSwitch);
1223 f.unset(Feature::StatelessClassSwitcher);
1224 f.unset(Feature::ReplacementStrategy_RR);
1225 f.unset(Feature::ReplacementStrategy_FIFO);
1226 f.unset(Feature::ReplacementStrategy_SFIFO);
1227 }
1228 // Trajectory-based iteration decomposes the CLOSED population into cells and
1229 // relaxes the waveforms between them; there is no cell for an unbounded open
1230 // stream. A cache model is solved by decomposition rather than by one drift,
1231 // so the cell partition has nothing to partition -- use `rmf`.
1232 if (m == "tbi") {
1233 f.unset(Feature::OpenClass);
1234 f.unset(Feature::Source);
1235 f.unset(Feature::Sink);
1236 f.unset(Feature::RandomSource);
1237 f.unset(Feature::JobSink);
1238 f.unset(Feature::Cache);
1239 f.unset(Feature::CacheClassSwitcher);
1240 f.unset(Feature::ReplacementStrategy_RR);
1241 f.unset(Feature::ReplacementStrategy_FIFO);
1242 f.unset(Feature::ReplacementStrategy_SFIFO);
1243 }
1244 return f;
1245}
1246
1247/**
1248 * SolverMAM.getFeatureSet, the union of its four setTrue calls: 55 MATLAB
1249 * names, WIDENED for 'default'/'ldqbd' only. SchedStrategy_FCFSPRPRIO is served
1250 * by the MMAPPH1PRPR/NPPR branch of solve_fcfs_station (api/mam/mmapph1prio.h).
1251 *
1252 * THE METHOD PARAMETER IS READ, to avoid the over-declaration
1253 * feature_set.h:30-33 warns against:
1254 *
1255 * +LoadDependence, method in {default, ldqbd} ONLY
1256 * solver_mam_ldqbd.h:152 reads st.lldscaling, but only the
1257 * 'ldqbd' method and 'default''s single-class closed
1258 * Delay+Queue route (mam_dispatch.h's branch 2e) ever call
1259 * it. Declaring it for every method let 'dec.source',
1260 * 'dec.poisson' and 'mna' pass a load-dependent model
1261 * straight to solver_mam_basic / solver_mna, neither of
1262 * which reads the field, so those methods silently solved
1263 * at nominal rates. The featset cannot see topology, so
1264 * 'default' still needs the structural check in
1265 * check_model_method (solver_mam_runner.h) that refuses a
1266 * load-dependent model whose shape is not the one ldqbd
1267 * can solve; 'ldqbd' itself is already self-checking
1268 * (solver_mam_ldqbd.h:109-131).
1269 * DMAP, ME, RAP and SetupDelayOff stay declared and are inert: no ProcessType
1270 * or NetworkStruct field can make used_lang_features emit them.
1271 */
1272inline FeatureSet mam_feature_set(const std::string& method) {
1273 FeatureSet f;
1274 f.set({Feature::Sink, Feature::Source});
1275 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner});
1276 f.set({Feature::Delay, Feature::DelayStation, Feature::Queue});
1277 // ME and RAP are INERT: ProcessType carries no such enumerator.
1278 f.set({Feature::APH, Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp,
1279 Feature::HyperExp, Feature::MMPP2, Feature::MAP, Feature::MMAP, Feature::MPH,
1280 Feature::DMAP, Feature::ME, Feature::RAP});
1281 // The slotted path (mam_dispatch.h branch -1). A Geometric or a
1282 // DiscreteUniform reaches solver_mam_dt on the slot lattice; off it,
1283 // sn_is_discrete_time reports the model continuous and the continuous
1284 // analyzers below take the same laws through their moment fits.
1285 f.set({Feature::Geometric, Feature::DiscreteUniform});
1286 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
1287 Feature::Weibull});
1288 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer});
1289 f.set(Feature::ClassSwitch);
1290 f.set({Feature::SharedServer, Feature::Buffer, Feature::Dispatcher});
1291 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::ServiceTunnel});
1292 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_HOL});
1293 f.set({Feature::SchedStrategy_FCFS, Feature::SchedStrategy_FCFSPRPRIO});
1294 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
1295 f.set({Feature::ClosedClass, Feature::SelfLoopingClass});
1296 f.set(Feature::OpenClass);
1297 // The G-network names are NOT here. They moved to ag_feature_set with the
1298 // RCAT methods that read `issignal`: no MAM algorithm reads the field, so
1299 // any of them would solve the model with the signals turned into ordinary
1300 // customers and report that as the answer.
1301 // Call 3, the BMAP/PH/N/N retrial queue.
1302 f.set({Feature::Retrial, Feature::BMAP, Feature::PH});
1303 // Call 4, qbd_setupdelayoff, reached through NetworkStruct::setupparam.
1304 f.set(Feature::SetupDelayOff);
1305 // c-server stations (every analyzer reads sn.nservers; the slotted path's
1306 // single-server rule stays structural, solver_mam_dt.h:73) and finite
1307 // buffers as LOSS buffers: solver_mam_basic's M/M/c/K and MMAP[K]/G/1/K,
1308 // solver_mam and solver_mna_open's truncate-and-renormalize, and
1309 // solver_mam_retrial's bufferless N/N station. SolverMAM.m:363-368.
1310 f.set({Feature::MultiServer, Feature::FiniteCapacity});
1311 // solver_mam_decmmap is an OPEN-network departure-process fixed point: it
1312 // iterates on arrival streams a closed population does not have, and its
1313 // opening station loop serves EXT, FCFS, HOL, FCFSPRPRIO and PS only (INF is
1314 // NOT in that list, so a model carrying a Delay belongs to 'dec.source').
1315 // Both restrictions are things the model HAS, so both belong in the featset;
1316 // solver_mam_decmmap.h raises the matching message when the method is named
1317 // by hand. Until this delta existed the gate offered dec.mmap on every closed
1318 // model, which the reference and the JAR answered with a table of zeros.
1319 // Fork-join goes with them: the sweep uses solver_mam_traffic, the PLAIN
1320 // traffic step, so a Fork or a Join is refused by the traffic step itself
1321 // rather than synchronized (solver_mam_decmmap.h says so in its header) --
1322 // which is why mam_dispatch routes a fork-join topology from
1323 // 'default'/'dec.source' to solver_mam_fj and never here.
1324 if (method == "dec.mmap")
1325 for (Feature ff : {Feature::ClosedClass, Feature::SelfLoopingClass,
1326 Feature::SchedStrategy_INF, Feature::Fork, Feature::Join,
1327 Feature::Forker, Feature::Joiner})
1328 f.unset(ff);
1329 // Widening over MATLAB, and only for the methods that actually read it.
1330 if (method == "default" || method == "ldqbd") f.set(Feature::LoadDependence);
1331 // Reneging reaches 'default' and 'retrial' through the MAP/M/s+G analyzer
1332 // (solver_mam_reneging.h) and nowhere else, SolverMAM.m getMethodFeatureSet.
1333 // The featset cannot see the shape, so check_model_method refuses a declared
1334 // reneging outside it rather than letting 2f ignore the patience.
1335 if (method == "default" || method == "retrial") f.set(Feature::Reneging);
1336 // Round-robin dispatching enters as a deterministic traffic split
1337 // (npfqn_traffic_split_rr), which only solver_mna_open resolves; SolverMAM.m
1338 // :60 sets it true for this method alone. The closed branch has no
1339 // counterpart and is refused structurally in check_model_method.
1340 if (method == "mna") f.set(Feature::RoutingStrategy_RROBIN);
1341 // FINITECAPACITY, SolverMAM.m:143-147: the two chains that read no sn.cap
1342 // withdraw it. The LD-QBD levels run to the population or the cutoff, and
1343 // the background chain to the state cap. A buffer a CLOSED class can fill is
1344 // refused structurally by `runner_detail::check_model_method` instead (a
1345 // closed job blocks, a loss formula does not).
1346 if (method == "ldqbd" || method == "bgchain") f.unset(Feature::FiniteCapacity);
1347 // One M/G/c/K station (SolverMAM.m getMethodFeatureSet); the shape is mam_sync_refusal's.
1348 if (method == "sync.faster" || method == "sync.slower")
1349 for (Feature ff : {Feature::SetupDelayOff, Feature::SchedStrategy_HOL,
1350 Feature::SchedStrategy_FCFSPRPRIO, Feature::Fork, Feature::Join,
1351 Feature::Forker, Feature::Joiner})
1352 f.unset(ff);
1353 return f;
1354}
1355
1356/**
1357 * SolverAG.getFeatureSet: what the RCAT decomposition can represent.
1358 *
1359 * THE G-NETWORK NAMES LIVE HERE AND NOWHERE ELSE. solver_ag.h's build_rcat is
1360 * the only code in LINE that reads `issignal`, so declaring the signal features
1361 * on any other solver would promise what that solver cannot deliver -- it would
1362 * answer with every signal turned into an ordinary customer.
1363 *
1364 * Every AG method is the same decomposition, differing only in how the reversed
1365 * rate is read off an agent, so the envelope does not vary by method; the
1366 * genuine restrictions (Markovian service law, single server) are structural and
1367 * are applied in ag::runner_detail::check_model_method.
1368 */
1369inline FeatureSet ag_feature_set(const std::string& /*method*/) {
1370 FeatureSet f;
1371 f.set({Feature::Sink, Feature::Source});
1372 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner});
1373 f.set({Feature::Delay, Feature::DelayStation, Feature::Queue});
1374 f.set({Feature::APH, Feature::Coxian, Feature::Erlang, Feature::Exp, Feature::HyperExp,
1375 Feature::MAP, Feature::MMPP2});
1376 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
1377 Feature::Weibull});
1378 // Class switching is read off `rt` by build_rcat, including a switch INTO a
1379 // removal signal (SUS-9).
1380 f.set({Feature::ClassSwitch, Feature::StatelessClassSwitcher, Feature::InfiniteServer});
1381 f.set({Feature::SharedServer, Feature::Buffer, Feature::Dispatcher});
1382 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::ServiceTunnel});
1383 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_FCFS});
1384 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
1385 // A self-looping class is its own single-station component (`solver_ag`
1386 // reads the flag), so it is declared beside the two ordinary class types.
1387 // MultiServer and FiniteCapacity are deliberately absent, as in
1388 // SolverAG.m:211-216: `check_model_method` words both refusals, RCAT driving
1389 // rho = lambda/mu with no buffer.
1390 f.set({Feature::ClosedClass, Feature::SelfLoopingClass, Feature::OpenClass});
1391 f.set({Feature::OpenSignal, Feature::ClosedSignal});
1392 f.set({Feature::SignalType_NEGATIVE, Feature::SignalType_CATASTROPHE});
1393 f.set(Feature::SignalBatchRemoval);
1394 return f;
1395}
1396
1397/**
1398 * SolverENV.supports's inline set, 28 names, transcribed unchanged.
1399 *
1400 * The narrowest declared set in the codebase. `RoutingStrategy_RROBIN` is
1401 * marked "with SolverJMT" upstream and was kept here while the CTMC stage
1402 * solver declared it; it is now DROPPED, because ctmc_feature_set no longer
1403 * does. env_dispatch.h:164 routes the statevec coupling to that same CTMC, so
1404 * declaring round robin here would promise what the stage cannot deliver.
1405 */
1406inline FeatureSet env_feature_set(const std::string& /*method*/) {
1407 FeatureSet f;
1408 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue,
1409 Feature::Sink, Feature::Source});
1410 f.set({Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp, Feature::HyperExp});
1411 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
1412 Feature::Buffer, Feature::Dispatcher, Feature::Server, Feature::JobSink,
1413 Feature::RandomSource, Feature::ServiceTunnel});
1414 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_FCFS});
1415 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
1416 f.set({Feature::ClosedClass, Feature::OpenClass});
1417 return f;
1418}
1419
1420/**
1421 * SolverLDES.getFeatureSet, transcribed WHOLE.
1422 *
1423 * Not narrowed, for the reason in the file header: the client forwards the
1424 * model.json document to the engine byte for byte, so the engine is what must
1425 * support each feature.
1426 *
1427 * SOURCE: `matlab/src/solvers/wrappers/LDES/@@SolverLDES/SolverLDES.m:108-230`,
1428 * name for name; every name there has a registry enumerator, `BatchArrival`
1429 * included.
1430 *
1431 * WHAT THE GATE STILL CANNOT SEE. The set is consulted against
1432 * `used_lang_features`, which is built from a PARSED `NetworkStruct`; the LDES
1433 * arm of the CLI never parses the document, so a model outside this port's
1434 * reader reaches the engine without passing here at all. That is intended --
1435 * forwarding losslessly is the whole point -- and it means this set governs
1436 * SolverAUTO's choice rather than the client's own admission.
1437 */
1438inline FeatureSet ldes_feature_set(const std::string& /*method*/) {
1439 FeatureSet f;
1440 f.set({Feature::Sink, Feature::Source, Feature::Queue, Feature::Delay});
1441 // JoinPartial: the engine fires the join at the k-th sibling and discards
1442 // the stragglers on arrival, so the quorum is an exact sample-path event
1443 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner,
1444 Feature::JoinPartial});
1445 // The fork overrides, all three simulated rather than flattened to the
1446 // scalar tasks-per-link: a per-(link, class) count, a count DRAWN from a
1447 // jobs-per-link law, and a branch that fires only with its probability. A
1448 // fork that declares none of them consumes nothing from the fork stream, so
1449 // every seeded golden is unmoved (`fork_is_variable` in ldes_engine.h).
1450 f.set({Feature::ForkFanoutVector, Feature::ForkFanoutRandom,
1451 Feature::ForkBranchProbability});
1452 f.set({Feature::Place, Feature::Transition, Feature::QueueingPlace});
1453 f.set({Feature::Linkage, Feature::Enabling, Feature::Inhibiting, Feature::Timing,
1454 Feature::Firing, Feature::Storage});
1455 f.set({Feature::Logger, Feature::LogTunnel, Feature::Buffer, Feature::Region});
1456 f.set({Feature::Exp, Feature::Erlang, Feature::HyperExp, Feature::PH, Feature::APH,
1457 Feature::Coxian, Feature::Cox2});
1458 f.set({Feature::MAP, Feature::DMAP, Feature::MMAP, Feature::MPH, Feature::BMAP,
1459 Feature::MMPP2, Feature::ME, Feature::RAP});
1460 f.set({Feature::Immediate, Feature::Disabled, Feature::Replayer, Feature::Trace});
1461 f.set({Feature::Det, Feature::Uniform, Feature::Gamma, Feature::Pareto, Feature::Weibull,
1462 Feature::Lognormal});
1463 // Lattice-valued and counting laws: Geometric backs Geo/Geo/1 and the
1464 // slotted mode, and the zero atom of a counting law becomes an immediate
1465 // interval in continuous mode.
1466 f.set({Feature::Geometric, Feature::Bernoulli, Feature::Binomial, Feature::Poisson});
1467 // DiscreteUniform: inverted off the same SSJ quantile the reference uses
1468 // (`uniform_int_inverse` in ldes_ssj_variates.h), so it lands on the lattice
1469 // exactly and the slotted mode accepts it.
1470 f.set(Feature::DiscreteUniform);
1471 // Source.setArrivalBatch: the model document carries `arrivalBatch` and the
1472 // engine releases the whole batch at one arrival epoch (SolverLDES.m:210).
1473 f.set(Feature::BatchArrival);
1474 // Time-inhomogeneous processes: the piecewise-constant schedule is simulated
1475 // exactly by carrying the phase across a breakpoint.
1476 // The MARKED schedules ride the same walk with their arrival arm split across the
1477 // mark blocks, at no extra draw, so a K = 1 MMAPt IS the MAPt with the same matrices.
1478 // An MPHt is stored lowered to MMAPt form, so one walk serves both.
1479 f.set({Feature::NHPP, Feature::MAPt, Feature::PHt, Feature::MMAPt, Feature::MPHt});
1480 // BMMAPt: the same schedule walk again, with the arrival arm split across
1481 // (mark, batch) blocks accumulated destination-major then mark-minor then
1482 // batch-minor, so neither label costs a draw and a B = 1 BMMAPt reproduces the
1483 // MMAPt sample path. At a Source one epoch releases a batch of jobs that all
1484 // carry the mark's class; as SERVICE it is a bulk server (BMSP) completing a
1485 // batch per firing with the mark discarded.
1486 f.set(Feature::BMMAPt);
1487 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::InfiniteServer,
1488 Feature::SharedServer, Feature::ServiceTunnel, Feature::DelayStation});
1489 f.set({Feature::SchedStrategy_FCFS, Feature::SchedStrategy_INF, Feature::SchedStrategy_HOL,
1490 Feature::SchedStrategy_FCFSPRIO});
1491 f.set({Feature::SchedStrategy_PS, Feature::SchedStrategy_DPS, Feature::SchedStrategy_GPS});
1492 f.set({Feature::SchedStrategy_LCFS, Feature::SchedStrategy_LCFSPR,
1493 Feature::SchedStrategy_LCFSPI});
1494 f.set({Feature::SchedStrategy_FCFSPR, Feature::SchedStrategy_FCFSPI,
1495 Feature::SchedStrategy_LPS, Feature::SchedStrategy_SIRO});
1496 f.set({Feature::SchedStrategy_SJF, Feature::SchedStrategy_LJF, Feature::SchedStrategy_LEPT,
1497 Feature::SchedStrategy_SEPT});
1498 f.set({Feature::SchedStrategy_SRPT, Feature::SchedStrategy_SRPTPRIO,
1499 Feature::SchedStrategy_PSJF, Feature::SchedStrategy_FB, Feature::SchedStrategy_LRPT});
1500 f.set({Feature::SchedStrategy_EXT, Feature::SchedStrategy_POLLING});
1501 f.set({Feature::SchedStrategy_PSPRIO, Feature::SchedStrategy_DPSPRIO,
1502 Feature::SchedStrategy_GPSPRIO});
1503 f.set({Feature::SchedStrategy_LCFSPRIO, Feature::SchedStrategy_LCFSPRPRIO,
1504 Feature::SchedStrategy_LCFSPIPRIO});
1505 f.set({Feature::SchedStrategy_FCFSPRPRIO, Feature::SchedStrategy_FCFSPIPRIO});
1506 f.set({Feature::SchedStrategy_FSP, Feature::SchedStrategy_PAS, Feature::SchedStrategy_OI});
1507 f.set({Feature::SchedStrategy_EDD, Feature::SchedStrategy_EDF, Feature::SchedStrategy_SETF});
1508 f.set({Feature::Router, Feature::Dispatcher, Feature::ClassSwitch,
1509 Feature::StatelessClassSwitcher});
1510 f.set({Feature::Cache, Feature::CacheClassSwitcher, Feature::CacheRetrieval,
1511 Feature::CacheItemSize});
1512 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND,
1513 Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
1514 Feature::RoutingStrategy_JSQ, Feature::RoutingStrategy_SQ});
1515 // HeteroServers: each job occupies one server of one compatible pool and is
1516 // served at that pool's own law (`free_slot_for` / `start_service`). The
1517 // sharing disciplines are refused by name in the engine rather than
1518 // flattened, so the declaration does not over-promise.
1519 f.set(Feature::HeteroServers);
1520 // SDR: the engine re-evaluates Krzesinski eq. (10) at the live marking
1521 // (`draw_sdr` in ldes_engine.h), so the sample path has an exact
1522 // counterpart in solver_nc_sdr rather than the uniform split the static
1523 // routing row carries. Declared here as well as in the reference because
1524 // the native engine now honours it; before that it was the ONE name whose
1525 // absence mattered, since the engine answered the uniform model in
1526 // silence instead of refusing.
1527 f.set(Feature::RoutingStrategy_SDR);
1528 f.set({Feature::OpenClass, Feature::ClosedClass, Feature::SelfLoopingClass});
1529 f.set({Feature::OpenSignal, Feature::ClosedSignal, Feature::SignalType_NEGATIVE,
1530 Feature::SignalType_REPLY, Feature::SignalType_CATASTROPHE,
1531 Feature::SignalBatchRemoval, Feature::SignalRemovalPolicy});
1532 f.set({Feature::LoadDependence, Feature::ClassDependence, Feature::JointDependence,
1533 Feature::SetupDelayOff});
1534 // GlobalDependence: phi(n) is read at the FULL population and re-times every
1535 // completion in flight at each change, the infinite servers included
1536 // (`gd_refresh` and the fan-out wrappers in ldes_engine.h). Utilization is
1537 // then T*E[S]/peak against the declared peak, as it is for a class
1538 // dependence. A bulk (BMSP) or pass-and-swap station is refused by name
1539 // rather than answered at the unscaled rate.
1540 f.set(Feature::GlobalDependence);
1541 // ServerParallelism: a class-r job seizes n slots at once and holds them for
1542 // the whole of its service, so the station serves at most floor(c/n) of them
1543 // and one contributes n/c to the utilization. Refused by name at an infinite
1544 // server, a sharing discipline, a polling or pass-and-swap controller, a
1545 // bulk server, a pooled station and a setup station, none of which has a
1546 // bank of interchangeable slots to take n of.
1547 f.set(Feature::ServerParallelism);
1548 f.set({Feature::Balking, Feature::Reneging, Feature::Retrial});
1549 // Breakdown is enforced here by a struct field a caller can actually set:
1550 // the LDES engine reads `sn.breakdownparam` and simulates the outage.
1551 f.set(Feature::Breakdown);
1552 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO,
1553 Feature::ReplacementStrategy_SFIFO, Feature::ReplacementStrategy_LRU,
1554 Feature::ReplacementStrategy_HLRU, Feature::ReplacementStrategy_CLIMB,
1555 Feature::ReplacementStrategy_QLRU});
1556 // c-server stations and a finite buffer with its drop rule: the engine holds
1557 // both on the sample path, so the simulated station is the one the model
1558 // declares. SolverLDES.m:230.
1559 f.set({Feature::MultiServer, Feature::FiniteCapacity});
1560 return f;
1561}
1562
1563/**
1564 * True for the JMVA algorithms that solve a CLOSED product-form network only.
1565 *
1566 * RECAL, CoMoM, Chow, Bard-Schweitzer (both spellings), AQL, Linearizer and De
1567 * Souza-Muntz Linearizer. Measured against JMT 1.2.x: each answers an open or a
1568 * mixed model with `jmt.common.exception.UnsupportedModelException: The selected
1569 * solver cannot handle open classes, please choose another.` and a
1570 * load-dependent one with the same exception naming load-dependent stations,
1571 * while the exact MVA engine behind 'jmva' and 'jmva.mva' serves both. The
1572 * single-server half of the same restriction is a station COUNT and has no
1573 * feature name, so it is refused structurally by `jmt::jmt_method_refusal`.
1574 */
1575inline bool jmva_is_closed_only(const std::string& method) {
1576 return method == "jmva.amva" || method == "jmva.recal" || method == "jmva.comom" ||
1577 method == "jmva.chow" || method == "jmva.bs" || method == "jmva.aql" ||
1578 method == "jmva.lin" || method == "jmva.dmlin";
1579}
1580
1581/**
1582 * Port of `SolverJMT.getFeatureSet` (`@@SolverJMT/SolverJMT.m:180-290`).
1583 *
1584 * JMT IS THE REFERENCE FCR ENGINE -- every other solver's finite-capacity
1585 * refusal points users here -- so `Region` is declared, and so is the whole SPN
1586 * section family, which JMT simulates natively.
1587 *
1588 * WHAT IS NARROWED AGAINST THE REFERENCE, and why each is a transcription of a
1589 * refusal this port already makes rather than a feature JMT lacks:
1590 * Normal the reference lists it, and `saveServiceStrategy` has no
1591 * branch for it: a Normal service would reach the analytic
1592 * switch with no `javaClass` and error. It is not declared.
1593 * HLRU/CLIMB/QLRU JMT has cache objects for RR, FIFO, SFIFO and LRU only;
1594 * `save_cache_strategy` refuses the other three by name
1595 * instead of falling back to LRU, so declaring them would
1596 * promise an admission the writer then denies.
1597 * SchedStrategy_LPS is declared: the limit is exported as a single-node
1598 * blocking region, not dropped.
1599 */
1600inline FeatureSet jmt_feature_set(const std::string& method) {
1601 FeatureSet f;
1602 f.set({Feature::Sink, Feature::Source, Feature::Queue, Feature::Delay, Feature::DelayStation});
1603 f.set({Feature::Router, Feature::ClassSwitch, Feature::StatelessClassSwitcher,
1604 Feature::Dispatcher});
1605 // JoinPartial: written out as a jmt PartialJoin with numRequired = k
1606 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner,
1607 Feature::JoinPartial});
1608 // A variable forking level: the writer turns isSimplifiedFork off and emits the
1609 // per-branch entries, so jmt reads the counts, the probabilities and the degree
1610 // distribution rather than sending one job down every link.
1611 f.set({Feature::ForkFanoutVector, Feature::ForkFanoutRandom,
1612 Feature::ForkBranchProbability});
1613 f.set({Feature::Logger, Feature::LogTunnel, Feature::Buffer, Feature::Region,
1614 Feature::Linkage});
1615 f.set({Feature::Enabling, Feature::Inhibiting, Feature::Timing, Feature::Firing,
1616 Feature::Storage, Feature::Place, Feature::Transition});
1617 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::InfiniteServer,
1618 Feature::SharedServer, Feature::ServiceTunnel});
1619 // Heterogeneous server pools: `jmt_writer` emits serverTypesNames,
1620 // serverTypesNumOfServers and serverTypesCompatibilities (jmt_writer.h:1023
1621 // and :1138), so jsim simulates the pools rather than a station of the same
1622 // total size. Undeclared until 2026-08-22, which made this port refuse a
1623 // model its own writer serialises and the MATLAB, JAR and python SolverJMT
1624 // all accept.
1625 f.set(Feature::HeteroServers);
1626 f.set({Feature::Exp, Feature::Erlang, Feature::HyperExp, Feature::PH, Feature::APH,
1627 Feature::Coxian, Feature::Cox2});
1628 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Weibull,
1629 Feature::Uniform});
1630 f.set({Feature::MAP, Feature::MMPP2, Feature::Replayer, Feature::Trace, Feature::Immediate,
1631 Feature::Disabled});
1632 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_FCFS, Feature::SchedStrategy_PS,
1633 Feature::SchedStrategy_DPS, Feature::SchedStrategy_GPS, Feature::SchedStrategy_LPS});
1634 f.set({Feature::SchedStrategy_SIRO, Feature::SchedStrategy_HOL,
1635 Feature::SchedStrategy_FCFSPRIO});
1636 f.set({Feature::SchedStrategy_PSPRIO, Feature::SchedStrategy_DPSPRIO,
1637 Feature::SchedStrategy_GPSPRIO});
1638 f.set({Feature::SchedStrategy_LCFS, Feature::SchedStrategy_LCFSPR,
1639 Feature::SchedStrategy_LCFSPI, Feature::SchedStrategy_LCFSPRIO,
1640 Feature::SchedStrategy_LCFSPRPRIO, Feature::SchedStrategy_LCFSPIPRIO});
1641 f.set({Feature::SchedStrategy_FCFSPR, Feature::SchedStrategy_FCFSPI,
1642 Feature::SchedStrategy_FCFSPRPRIO, Feature::SchedStrategy_FCFSPIPRIO});
1643 // save_class_parallelism writes it and server_pools carries it, so the gate
1644 // was refusing a model this writer emits. The reference, the JAR and native
1645 // Python all declare it.
1646 f.set(Feature::ServerParallelism);
1647 f.set({Feature::SchedStrategy_SEPT, Feature::SchedStrategy_LEPT, Feature::SchedStrategy_SJF,
1648 Feature::SchedStrategy_LJF});
1649 f.set({Feature::SchedStrategy_SRPT, Feature::SchedStrategy_SRPTPRIO,
1650 Feature::SchedStrategy_POLLING, Feature::SchedStrategy_EXT});
1651 // JMT 1.2.x EDDStrategy/EDFStrategy are real; the due dates ride in
1652 // <classSoftDeadlines>, and a served class without a finite deadline is refused
1653 // by name (`io::jmt_deadline_refusal`), as in SolverJMT.m.
1654 f.set({Feature::SchedStrategy_EDD, Feature::SchedStrategy_EDF});
1655 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND,
1656 Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
1657 Feature::RoutingStrategy_JSQ});
1658 // RoutingStrategy_SQ not declared: JMT's PowerOfK draws d DISTINCT candidates (JSQ once d >= #outputs), LINE's SQ(d) draws with replacement.
1659 f.set({Feature::OpenClass, Feature::ClosedClass, Feature::SelfLoopingClass});
1660 f.set({Feature::Cache, Feature::CacheClassSwitcher});
1661 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO,
1662 Feature::ReplacementStrategy_SFIFO, Feature::ReplacementStrategy_LRU});
1663 f.set({Feature::SetupDelayOff, Feature::Reneging, Feature::Balking});
1664 // Queue.setRetrial: the JSIM writer picks the retrial Queue constructor and
1665 // writes the per-class orbit delay, so jsim simulates the orbit rather than
1666 // losing the refused job. Batch arrivals are NOT declared, in the reference
1667 // either: the arrival-strategy writer has no batch element, so the stream
1668 // would be written single. SolverJMT.m:474-478.
1669 f.set(Feature::Retrial);
1670 // c-server stations (`save_number_of_servers`) and finite buffers
1671 // (`save_buffer_capacity` with the drop rule): the JSIM writer exports both,
1672 // and `io::jmt_buffer_capacity_refusal` keeps refusing the buffers JMT would
1673 // answer unconstrained (closed WAITQ, BBS, RSRD, RETRIAL_WITH_LIMIT). The
1674 // JMVA block below withdraws the buffer. SolverJMT.m:479-484.
1675 f.set({Feature::MultiServer, Feature::FiniteCapacity});
1676 // Limited load dependence reaches JMT only as a SERVER COUNT:
1677 // `save_number_of_servers` exports max(nservers, max alpha) and `write_jmva`
1678 // writes the matching <ldstation>. That is exact for alpha(n) = min(n,c) and
1679 // for nothing else, so `solver_jmt_run_analyzer` refuses any other scaling by name --
1680 // a featset cannot inspect the vector.
1681 f.set(Feature::LoadDependence);
1682
1683 // SOLVERJMT DRIVES TWO ENGINES, AND THEY ACCEPT DIFFERENT MODELS. Everything
1684 // above is the JSIM SIMULATOR's envelope, which is what 'default' and 'jsim'
1685 // run, the transient ensemble of 'default' included. The 'jmva.*' names run the JMVA ANALYTICAL engine, and
1686 // `io::write_jmva` emits for it only a station type (delay / load-independent
1687 // / load-dependent), a per-chain service demand, a per-chain visit count, the
1688 // class populations or arrival rates and a reference station. NOTHING ELSE IN
1689 // THE MODEL REACHES JMVA, so declaring the JSIM envelope for jmva was a
1690 // promise the writer could not keep: on a three-class LRU cache model all
1691 // eight closed-form jmva methods returned an entirely zero table with no
1692 // error, jmva.mva labelled 'exact' among them.
1693 //
1694 // The DISTRIBUTIONS stay: JMVA consumes a mean service demand, so any renewal
1695 // law with a finite mean is admissible, exactly as it is for mva_feature_set
1696 // and nc_feature_set. What goes is every construct whose effect is not
1697 // carried by (station type, demand, visits, population).
1698 if (method.compare(0, 4, "jmva") == 0) {
1699 for (Feature ff :
1700 {Feature::Cache, Feature::CacheClassSwitcher, Feature::ReplacementStrategy_RR,
1701 Feature::ReplacementStrategy_FIFO, Feature::ReplacementStrategy_SFIFO,
1702 Feature::ReplacementStrategy_LRU,
1703 // no fork element exists, and a visit ratio cannot express the join
1704 Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner,
1705 Feature::JoinPartial, Feature::ForkFanoutVector, Feature::ForkFanoutRandom,
1706 Feature::ForkBranchProbability,
1707 // no Petri-net counterpart, and no finite capacity region
1708 Feature::Place, Feature::Transition, Feature::Enabling, Feature::Inhibiting,
1709 Feature::Timing, Feature::Firing, Feature::Storage, Feature::Region,
1710 // no impatience element: the abandonment would simply not happen,
1711 // and no retrial element either: the orbit would be lost
1712 Feature::Reneging, Feature::Balking, Feature::Retrial,
1713 // the JMVA document has no capacity element at all
1714 Feature::FiniteCapacity,
1715 // server-side attributes the JMVA document has no slot for
1716 Feature::SetupDelayOff, Feature::ServerParallelism, Feature::HeteroServers,
1717 // the writer emits NO discipline, so a priority, weighted, size-based
1718 // or limited-sharing station would be solved as an ordinary
1719 // load-independent one. Only the four BCMP station types survive the
1720 // encoding, the same line nc_feature_set draws.
1721 Feature::SchedStrategy_DPS, Feature::SchedStrategy_GPS, Feature::SchedStrategy_HOL,
1722 Feature::SchedStrategy_FCFSPRIO, Feature::SchedStrategy_PSPRIO,
1723 Feature::SchedStrategy_DPSPRIO, Feature::SchedStrategy_GPSPRIO,
1724 Feature::SchedStrategy_LCFSPI, Feature::SchedStrategy_LCFSPIPRIO,
1725 Feature::SchedStrategy_LCFSPRIO, Feature::SchedStrategy_LCFSPRPRIO,
1726 Feature::SchedStrategy_FCFSPR, Feature::SchedStrategy_FCFSPI,
1727 Feature::SchedStrategy_FCFSPRPRIO, Feature::SchedStrategy_FCFSPIPRIO,
1728 Feature::SchedStrategy_SEPT, Feature::SchedStrategy_LEPT,
1729 Feature::SchedStrategy_SJF, Feature::SchedStrategy_LJF,
1730 Feature::SchedStrategy_SRPT, Feature::SchedStrategy_SRPTPRIO,
1731 Feature::SchedStrategy_LPS, Feature::SchedStrategy_POLLING,
1732 Feature::SchedStrategy_EDD, Feature::SchedStrategy_EDF,
1733 // the document carries MEAN visit counts, which is not what makes a
1734 // join-the-shortest-queue model behave as it does
1735 Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
1736 Feature::RoutingStrategy_JSQ, Feature::RoutingStrategy_SQ})
1737 f.unset(ff);
1738 // The eight closed-form algorithms are single-server ones
1739 // (`jmt::jmt_method_refusal` words it); exact MVA carries the count.
1740 if (jmva_is_closed_only(method))
1741 for (Feature ff :
1742 {Feature::OpenClass, Feature::LoadDependence, Feature::MultiServer})
1743 f.unset(ff);
1744 }
1745 return f;
1746}
1747
1748/**
1749 * The family prefix of a bound method name: everything before the first dot.
1750 *
1751 * Local to this header so `ba_feature_set` can apply its per-method deltas
1752 * without reaching into `line/solvers/ba`, which includes this file.
1753 */
1754inline std::string ba_family_of(const std::string& method) {
1755 const std::string::size_type dot = method.find('.');
1756 return (dot == std::string::npos) ? method : method.substr(0, dot);
1757}
1758
1759/**
1760 * The 'default'/'auto'/'qr'/'lr' aliases, duplicated from `ba::resolve_method`
1761 * for the same reason `ba_family_of` sits here: this header is included BY the
1762 * solver and cannot include it back. The two are one line apiece and are
1763 * asserted equal in `cpp/tests/test_gate_ba.cpp`.
1764 */
1765inline std::string ba_resolve_method_name(const std::string& method) {
1766 if (method == "default") return "gb.upper";
1767 if (method == "auto") return "auto.upper";
1768 if (method == "lr") return "lr.upper";
1769 if (method == "qr") return "qrf.mmi";
1770 return method;
1771}
1772
1773/**
1774 * SolverBA.getFeatureSet, transcribed name for name.
1775 *
1776 * WHAT MAKES A DISTRIBUTION ADMISSIBLE HERE IS ITS MEAN, which is why the list
1777 * of laws is longer than any product-form solver's and shorter than a
1778 * simulator's. `solver_ba_analyzer` reads `sn.rates` and `sn.visits` and nothing
1779 * else: every bound in the ABA/BJB/PB/GB/SB/Harel/MW families is a function of
1780 * the demands D = V./rates and the think time, so any renewal law with a finite
1781 * mean is admissible whatever its higher moments. `solver_ba_qrf_analyzer` is
1782 * the one that needs more, and what it needs is the {D0,D1} pair, which the
1783 * phase-type laws below carry; it refuses the rest through
1784 * `sn_to_qrf_blocking`'s own message rather than through this set.
1785 *
1786 * THE MODULATED PROCESSES ARE OUT OF EVERY FAMILY THAT READS THE MEAN ALONE.
1787 * Their mean rate exists, so the utilization law still holds, but those bounds
1788 * are derived for a product-form network in which the correlation between
1789 * successive services does not exist: bounding such a model would report a
1790 * bracket for a DIFFERENT system rather than refuse it. MAP and MMPP2 are
1791 * admitted only by the three families that read the (D0,D1) law ('mapamva',
1792 * 'qrf', 'snc'; see the delta below); MMAP and BMAP stay out everywhere. The
1793 * same reasoning keeps Cache,
1794 * Fork/Join and the Petri-net constructs out, none of which the analyzer
1795 * represents at all.
1796 *
1797 * THE STRUCTURAL NARROWING IS NOT HERE. `ba::list_valid_methods(L)` already
1798 * drops the reduction bounds off a model that is not a single-class closed
1799 * network of single servers, and the three open-network families off a closed
1800 * one; a flat feature set cannot express either, and duplicating the shape test
1801 * here is how the two drift apart.
1802 *
1803 * THE ONE ADDITION TO THE MATLAB LIST IS THE SUB-NODE SECTIONS, and it is not a
1804 * widening. `MNetwork.getUsedLangFeatures` emits no Buffer, Dispatcher, Server,
1805 * InfiniteServer, SharedServer, JobSink, RandomSource or ServiceTunnel -- its
1806 * node loop marks the distribution, the discipline and the routing and stops --
1807 * while `used_lang_features` here marks all of them for every Queue, Delay,
1808 * Source and Sink. Transcribing the MATLAB list literally therefore refused
1809 * EVERY model, an ordinary closed exponential network included, on `Buffer`;
1810 * `env_feature_set` and `ag_feature_set` carry the same eight names for the same
1811 * reason. The JAR has the same defect unfixed: its `SolverAUTO.listValidMethods`
1812 * offers no 'ba.*' method on a model SolverBA can bound.
1813 */
1814inline FeatureSet ba_feature_set(const std::string& method) {
1815 FeatureSet f;
1816 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue,
1817 Feature::Sink, Feature::Source, Feature::Router});
1818 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
1819 Feature::Buffer, Feature::Dispatcher, Feature::Server, Feature::JobSink,
1820 Feature::RandomSource, Feature::ServiceTunnel});
1821 // A self-looping class is a closed chain of one station, which the
1822 // demand-parameterized bounds read as any other chain (SolverBA.m:826-829).
1823 f.set({Feature::ClosedClass, Feature::SelfLoopingClass, Feature::OpenClass});
1824 f.set({Feature::APH, Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp,
1825 Feature::HyperExp, Feature::PH});
1826 f.set({Feature::Det, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
1827 Feature::Weibull});
1828 // MAP and MMPP2 are declared HERE and withdrawn below from every family but
1829 // the three that read the (D0,D1) law: `mapamva`, `qrf` and `snc`.
1830 f.set({Feature::MAP, Feature::MMPP2});
1831 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_FCFS,
1832 Feature::SchedStrategy_LCFSPR});
1833 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
1834 // Petri-net constructs, for the spnlp family (solver_ba_spnlp). They were
1835 // out on the "no representation" ground the rest of this set rests on, and
1836 // that ground is gone: the spnlp relaxation is indexed by the MARKING,
1837 // reads the enabling, inhibiting and firing arcs out of the node
1838 // parameters, and refuses by name the modes it cannot carry (immediate,
1839 // multi-server, marking-dependent, and phase-type on its Markovian side).
1840 // QueueingPlace is deliberately absent: a place with an embedded queue has
1841 // local state the relaxation has no variable for. Same division nc draws.
1842 f.set({Feature::Place, Feature::Transition, Feature::Linkage, Feature::Enabling,
1843 Feature::Inhibiting, Feature::Timing, Feature::Firing, Feature::Storage});
1844
1845 // THE PER-METHOD DELTAS, i.e. the premises a feature name CAN state. A
1846 // feature set says "I accept this construct", so it can refuse a model for
1847 // HAVING one and never for lacking one; that is exactly the shape of the
1848 // delay-station and closed-class premises below, and exactly not the shape
1849 // of "one class" or "one server", which have no feature name and are
1850 // refused structurally by `ba::method_refusal`. Judged on the RESOLVED name
1851 // so that 'default' carries the envelope of the gb.upper it runs as.
1852 //
1853 // DELAY STATIONS. 'sb' and 'lr' reject an infinite-server station outright,
1854 // and 'harel', 'sib' and 'scb' reject a nonzero think time, which on these
1855 // models is the same station: harel extrapolates the exact normalizing
1856 // constant of a delay-free network, SIB Section 3.2 is the extension that
1857 // would carry Z and is not implemented, and SCB Theorem 3 rests on the
1858 // delay-free balanced-network throughput. The three OPEN families reject
1859 // one too, each being derived for one server per station.
1860 //
1861 // CLASS TYPES. The three OPEN families drop ClosedClass, which is the whole
1862 // of their class premise. The MIRROR delta -- dropping OpenClass from every
1863 // demand-parameterized family -- is deliberately NOT applied: "supports
1864 // single-class closed networks only" is one rule, its single-class half has
1865 // no feature name, and splitting it across the two mechanisms would report
1866 // the closed half here and the single-class half in `ba::method_refusal`
1867 // for the same model. It is stated once, structurally. 'spnlp' takes no
1868 // delta at all: it is indexed by the marking, and whether that marking is
1869 // bounded is a question about the P-invariants of the net, which
1870 // `spn_lpbnd` answers.
1871 const std::string resolved = ba_resolve_method_name(method);
1872 const std::string fam = ba_family_of(resolved);
1873 if (fam == "bpt" || fam == "bgt" || fam == "snc") {
1874 f.unset(Feature::ClosedClass);
1875 f.unset(Feature::Delay);
1876 f.unset(Feature::DelayStation);
1877 f.unset(Feature::InfiniteServer);
1878 f.unset(Feature::SchedStrategy_INF);
1879 if (fam != "snc") {
1880 // SERVICE AND ARRIVAL LAWS. 'bpt' and 'bgt' are derived for a
1881 // MARKOVIAN open network and read the mean alone, so a
1882 // non-exponential law anywhere is not something they refuse at run
1883 // time -- it is something they silently bound as if it were
1884 // Poisson. Measured on the M/M/1 shape: replacing the Exp(1) source
1885 // by an Erlang of the same mean leaves bgt.upper at QLen 32.6667
1886 // and bpt.lower at 1.0, digit for digit. That is a bound on a
1887 // DIFFERENT system, so the laws are dropped here rather than left
1888 // to a run-time check the analyzers do not make: their own procid
1889 // test covers the queueing stations only and would miss exactly the
1890 // source case.
1891 //
1892 // 'snc' is excluded: it CONSUMES the arrival law (the same
1893 // substitution moves it from 3.8244 to 3.0092) and its analyzer
1894 // branches on a non-exponential source deliberately. Its rule is
1895 // about the SERVICE only, which no feature name can say, so it
1896 // lives in `ba::method_refusal` instead.
1897 f.unset(Feature::APH);
1898 f.unset(Feature::Coxian);
1899 f.unset(Feature::Cox2);
1900 f.unset(Feature::Erlang);
1901 f.unset(Feature::HyperExp);
1902 f.unset(Feature::PH);
1903 f.unset(Feature::Det);
1904 f.unset(Feature::Lognormal);
1905 f.unset(Feature::Pareto);
1906 f.unset(Feature::Uniform);
1907 f.unset(Feature::Weibull);
1908 }
1909 } else if (fam == "sb" || fam == "harel" || fam == "sib" || fam == "scb" || fam == "lr") {
1910 f.unset(Feature::Delay);
1911 f.unset(Feature::DelayStation);
1912 f.unset(Feature::InfiniteServer);
1913 f.unset(Feature::SchedStrategy_INF);
1914 }
1915 // MODULATED SERVICE, THE ONE DELTA THAT RUNS THE OTHER WAY, as in
1916 // SolverBA.m. MAP and MMPP2 stay for the families reading the (D0,D1) law:
1917 // 'mapamva', whose LP is written over the pair of its MAP station; 'qrf'
1918 // (and its alias qr), whose q is built from D1 and D0 at every station,
1919 // the closed MAP queueing network the QRF library is derived for; and 'snc',
1920 // which CONSUMES a Markovian Source's law (snc_env_map_fn) and refuses
1921 // non-exponential service itself. Every other bound reads the MEAN demand
1922 // alone, so a MAP or MMPP2 anywhere would be bounded as if it were Poisson --
1923 // a bracket for a DIFFERENT system, and silent. Written as a delta on the
1924 // complement rather than as a grant because a feature set can refuse a model
1925 // for HAVING a construct and never for lacking one.
1926 //
1927 // 'mapamva' takes the delay delta instead: its LP is a network of queues, and
1928 // Casale-Smirni name the delay extension as open work.
1929 if (fam == "mapamva") {
1930 f.unset(Feature::Delay);
1931 f.unset(Feature::DelayStation);
1932 f.unset(Feature::SchedStrategy_INF);
1933 } else if (fam != "qrf" && fam != "snc") {
1934 f.unset(Feature::MAP);
1935 f.unset(Feature::MMPP2);
1936 }
1937 // MULTISERVER, SolverBA.m:574-580, is out of the base envelope: every
1938 // demand-parameterized family reads one server per station (`ba::method_refusal`
1939 // names 'ssd' as the alternative), the alpha-free QRF arms refuse a c-server
1940 // station through `sn_to_qrf_alpha` and the open families through their own
1941 // refusal. What carries the count is granted here: 'ssd' (the multiserver
1942 // bound), 'ldac' (its fixed-rate form runs on the c-server rate law),
1943 // 'auto' (which picks among them) and the two load-dependent QRF arms, whose
1944 // alpha(i,n) IS min(n,c).
1945 if (fam == "ssd" || fam == "ldac" || fam == "auto" || resolved == "qrf.mmi.ld" ||
1946 resolved == "qrf.mmi.linear")
1947 f.set(Feature::MultiServer);
1948 // FINITECAPACITY, SolverBA.m:581-591: only the QRF blocking bounds carry the
1949 // buffer (the MM, MM1, ZZ, ZM, BB, F tables), which is the same split
1950 // `ba::ignores_blocking` makes; the structural refusal keeps naming them.
1951 // 'spnlp' is NOT granted, though `ignores_blocking` exempts it: its polytope
1952 // reads no Place capacity, so a capped place would be relaxed away.
1953 if (resolved.compare(0, 7, "qrf.bas") == 0 || resolved.compare(0, 8, "qrf.rsrd") == 0)
1954 f.set(Feature::FiniteCapacity);
1955 return f;
1956}
1957
1958/**
1959 * SolverLQNS.getFeatureSet on a flat Network (its qns methods), transcribed WHOLE.
1960 *
1961 * Not narrowed, for the reason in the file header: SolverLQNS implements no
1962 * numerics, only the JMVA (`qnsolver`) and `qn2lqn` (`lqns`) routes the
1963 * reference also drives.
1964 *
1965 * WHERE THE REAL REFUSALS LIVE, all of them at solve time and none expressible
1966 * as a boolean over feature names:
1967 * lqns::check_qnsolver_supported a station that is not a Queue, a Delay or a
1968 * Source, and class priorities, on the JMVA
1969 * route only -- the layered route legitimately
1970 * carries Join nodes the document cannot encode
1971 * qn::check_binding_capacity a finite buffer, which neither binary models
1972 * the multiserver gate 'suri' and 'schmidt' reach LQNS and not
1973 * `qnsolver -m`
1974 *
1975 * SOURCE: `matlab/src/solvers/wrappers/LQNS/@@SolverLQNS/SolverLQNS.m`, name for
1976 * name. `Normal` is the one name dropped: it has a registry enumerator here, and
1977 * `write_jmva` reduces every law to its mean, so a Normal service is written as
1978 * its mean like any other -- but a Normal law admits negative samples and the
1979 * reference's own JMT writer refuses it (see `jmt_feature_set`), so declaring it
1980 * on this route alone would be the only place in the port that accepts it.
1981 */
1982inline FeatureSet lqns_network_feature_set(const std::string& /*method*/) {
1983 FeatureSet f;
1984 f.set({Feature::Sink, Feature::Source, Feature::Router, Feature::ClassSwitch,
1985 Feature::Delay, Feature::DelayStation, Feature::Queue});
1986 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner});
1987 f.set({Feature::Logger, Feature::LogTunnel});
1988 f.set({Feature::Coxian, Feature::Cox2, Feature::APH, Feature::Erlang, Feature::Exp,
1989 Feature::HyperExp, Feature::PH});
1990 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
1991 Feature::Weibull});
1992 f.set({Feature::MAP, Feature::MMPP2, Feature::Replayer, Feature::Trace});
1993 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
1994 Feature::Buffer, Feature::Dispatcher, Feature::Server, Feature::JobSink,
1995 Feature::RandomSource, Feature::ServiceTunnel, Feature::Linkage});
1996 f.set({Feature::Enabling, Feature::Timing, Feature::Firing, Feature::Storage,
1997 Feature::Place, Feature::Transition});
1998 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_DPS,
1999 Feature::SchedStrategy_FCFS, Feature::SchedStrategy_GPS, Feature::SchedStrategy_SIRO,
2000 Feature::SchedStrategy_HOL, Feature::SchedStrategy_LCFS,
2001 Feature::SchedStrategy_LCFSPR});
2002 f.set({Feature::SchedStrategy_SEPT, Feature::SchedStrategy_LEPT, Feature::SchedStrategy_SJF,
2003 Feature::SchedStrategy_LJF, Feature::SchedStrategy_EXT});
2004 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND,
2005 Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
2006 Feature::RoutingStrategy_SQ});
2007 f.set({Feature::ClosedClass, Feature::OpenClass});
2008 // c-server stations: the JMVA document carries the count as an <ldstation>
2009 // and the LQN as a host multiplicity; 'suri' and 'schmidt' refuse one on the
2010 // qnsolver path, which stays structural (the multiserver gate in lqns_qnsolver.h).
2011 // FiniteCapacity is NOT declared, as in SolverLQNS.m's Network set: neither document
2012 // has a buffer, and `qn::check_binding_capacity` words the refusal.
2013 f.set(Feature::MultiServer);
2014 return f;
2015}
2016
2017} // namespace qn
2018} // namespace line
2019
2020#endif // LINE_LANG_QN_SOLVER_FEATURE_SETS_H
A subset of the registry: MATLAB's SolverFeatureSet, whose list is a flag per field.
FeatureSet & set(Feature f)
FeatureSet & unset(Feature f)
A network plus its refreshed NetworkStruct.
The language-feature gate: what a MODEL uses against what a SOLVER declares.
FeatureSet ldes_feature_set(const std::string &)
SolverLDES.getFeatureSet, transcribed WHOLE.
FeatureSet fluid_feature_set(const std::string &method)
FeatureSet ssa_feature_set(const std::string &method)
FeatureSet jmt_feature_set(const std::string &method)
Port of SolverJMT.getFeatureSet (@@SolverJMT/SolverJMT.m:180-290).
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.
Feature
One language feature.
std::string ba_family_of(const std::string &method)
The family prefix of a bound method name: everything before the first dot.
FeatureSet ba_feature_set(const std::string &method)
SolverBA.getFeatureSet, transcribed name for name.
FeatureSet fluid_class_feature_set()
SolverFLD.getFeatureSet, transcribed, MINUS what the requested method cannot evaluate – the port of @...
std::string ba_resolve_method_name(const std::string &method)
The 'default'/'auto'/'qr'/'lr' aliases, duplicated from ba::resolve_method for the same reason ba_fam...
FeatureSet env_feature_set(const std::string &)
SolverENV.supports's inline set, 28 names, transcribed unchanged.
FeatureSet ctmc_feature_set(const std::string &method)
SolverCTMC.getFeatureSet, the reference's 104 MATLAB names in full.
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)
SolverMVA.getFeatureSet, plus getMethodFeatureSet's per-method deltas.
void mva_unset_non_bcmp_sched(FeatureSet &f)
Drops every scheduling name OUTSIDE the BCMP set {INF, PS, FCFS, SIRO, LCFS-PR} that the base MVA env...
FeatureSet ssa_mlmc_feature_set()
SolverSSA.getFeatureSet, 98 MATLAB names.
std::string mva_base_method(const std::string &method)
The method name with any leading "amva." alias stripped.
bool mva_qsys_serves_method(const std::string &method)
The method names solver_mva_qsys_analyzer has an arm for.
FeatureSet ag_feature_set(const std::string &)
SolverAG.getFeatureSet: what the RCAT decomposition can represent.
bool mva_is_closed_population_method(const std::string &method)
The AMVA algorithms whose recursion is over a CLOSED population vector.
bool ssa_method_is_mlmc(const std::string &method)
True when method names SSA's MLMC estimator, bare or family-qualified, in any case.
bool jmva_is_closed_only(const std::string &method)
True for the JMVA algorithms that solve a CLOSED product-form network only.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52