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