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 // Heterogeneous server pools: `ctmc::ctmc_pools` gives each compatible pool its own
726 // phase block and `after_event_station_pool` applies the LDES pool-selection rules.
727 f.set(Feature::HeteroServers);
728
729 // PER-METHOD DELTAS. Two of the three methods enumerate the generator above
730 // and share the whole envelope; `mdd` builds no generator at all, so what the
731 // rest of the envelope describes does not apply to it. The rule the registry
732 // has no name for -- the class count -- is structural and lives in
733 // `ctmc::solver_ctmc_mdd_supports`, which the analyzer itself calls. The
734 // perfect sampler `cftp` moved to SolverNC (`nc_feature_set`).
735 if (method == "mdd") {
736 // The decision diagram holds the MARKING of a closed network; an open
737 // stream makes it unbounded, so there is no finite diagram to hold. A
738 // stochastic Petri net keeps the Place/Transition names: `spn_mdd` reads
739 // the marking directly and is exempt from the closed-network rule.
740 //
741 // A FORK-JOIN MODEL IS NEITHER of the two shapes it serves: the tag
742 // augmentation a fork needs adds one auxiliary class per branch, so the
743 // struct that reaches the analyzer is never single-class however the
744 // model was written, and the level decomposition has no meaning for a
745 // firing that does not conserve the per-chain population.
746 f.unset(Feature::OpenClass);
747 f.unset(Feature::Source);
748 f.unset(Feature::Sink);
749 f.unset(Feature::RandomSource);
750 f.unset(Feature::JobSink);
751 f.unset(Feature::Fork);
752 f.unset(Feature::Join);
753 f.unset(Feature::Forker);
754 f.unset(Feature::Joiner);
755 f.unset(Feature::JoinPartial);
756 // The level decomposition reads rates, servers and phases and no
757 // sn.cap/classcap, so a buffer would be dropped. SolverCTMC.m:307-309.
758 f.unset(Feature::FiniteCapacity);
759 // the pool blocks and the pool order are State constructs, not a level decomposition
760 f.unset(Feature::HeteroServers);
761 }
762 return f;
763}
764
765/**
766 * SolverSSA.getFeatureSet, 98 MATLAB names.
767 *
768 * IT IS THE SOLVER'S REACH, NOT ONE ENGINE'S, and that is what makes it usable:
769 * `solver_ssa` under `default` runs the NRM when the NRM can run the model and
770 * the event-driven serial engine when it cannot, so a construct EITHER engine
771 * covers is a construct SolverSSA covers. AUTO chooses this solver on the
772 * strength of this set (`solver_auto.h:453`), so under-declaring costs a model
773 * the only solver that can answer it and over-declaring hands AUTO a refusal.
774 *
775 * The serial engine applies the SAME event handlers `state_events.h` gives the
776 * CTMC generator, which is why the two sets are now close: a cache access, an
777 * SPN firing, a fork firing, a polling controller and the priority shares are
778 * all reached through handlers both solvers share. What is DECLARED here is
779 * what a test in `test_ssa_fj_fcr.cpp` or `test_ssa_serial.cpp` exercises end
780 * to end, never what a handler merely appears to support.
781 *
782 * Still dropped, with the C++ refusal that proves each:
783 * RoutingStrategy_RROBIN, _WRROBIN,
784 * _JSQ, _SQ, _RL ssa_dispatch.h refuses them in the NRM and
785 * `NetworkStruct::refresh_routing` refuses the
786 * state-dependent ones when the struct is built,
787 * so no engine here can receive one
788 * `ssa_check_phases` (ssa_dispatch.h) stays imperative: non-exponential service
789 * AT a given discipline is a per-(station,class) join of two features and the
790 * boolean registry cannot state it. It gates the NRM only -- the serial engine
791 * expands a phase-type process wherever the CTMC generator does -- so a model it
792 * refuses is answered by the fallback rather than refused outright.
793 */
794inline FeatureSet ssa_feature_set(const std::string& /*method*/) {
795 FeatureSet f;
796 // Router is INERT here for the same reason as in ctmc_feature_set: nothing
797 // emits it. SolverSSA.m:71 declares it all the same, so it stays.
798 f.set({Feature::Sink, Feature::Source, Feature::Router});
799 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue});
800 f.set({Feature::MAP, Feature::MMPP2, Feature::MMAP, Feature::MPH, Feature::APH, Feature::PH,
801 Feature::Replayer});
802 f.set({Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp, Feature::HyperExp});
803 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
804 Feature::Weibull});
805 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer});
806 f.set({Feature::SharedServer, Feature::Buffer, Feature::Dispatcher});
807 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::ServiceTunnel});
808 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS});
809 f.set({Feature::SchedStrategy_DPS, Feature::SchedStrategy_FCFS});
810 f.set({Feature::SchedStrategy_GPS, Feature::SchedStrategy_LPS, Feature::SchedStrategy_SIRO});
811 f.set({Feature::SchedStrategy_HOL, Feature::SchedStrategy_LCFS});
812 f.set({Feature::SchedStrategy_SEPT, Feature::SchedStrategy_LEPT});
813 f.set(Feature::SchedStrategy_LCFSPR);
814 // The priority variants the reference declares, reached through the serial
815 // engine: `after_event_station_arv` and `_dep` carry the [class, phase]
816 // buffer arm, and `test_ssa_serial.cpp` runs them against the exact CTMC.
817 //
818 // DELIBERATELY NOT THE REST OF THE PREEMPT FAMILY (LCFSPI, FCFSPR, FCFSPI,
819 // LCFSPRIO, LCFSPIPRIO, FCFSPIPRIO). The C++ serial engine runs all of them
820 // -- the same test file proves it -- but `SolverSSA.getFeatureSet` does not
821 // declare them in MATLAB, and this set is what AUTO selects on. Declaring
822 // more here would make the C++ AUTO pick SolverSSA for models the MATLAB
823 // AUTO sends elsewhere, which is a cross-codebase divergence introduced by a
824 // DECLARATION rather than by an algorithm. The reference's own list is
825 // arguably short -- its serial engine goes through the same
826 // `State.afterEvent` handlers -- and that question belongs on the MATLAB
827 // side, recorded in `_kb/06-solver-catalog.md`, not settled by diverging.
828 f.set({Feature::SchedStrategy_LCFSPRPRIO, Feature::SchedStrategy_FCFSPRPRIO});
829 f.set({Feature::SchedStrategy_PSPRIO, Feature::SchedStrategy_DPSPRIO,
830 Feature::SchedStrategy_GPSPRIO});
831 // PAS / OI and the polling controller, reached through the serial engine:
832 // the NRM refuses all three by name and `default` falls back. PAS and OI
833 // needed `to_marginal`'s ordered-list arm (ported 2026-07-31) before the
834 // queue length at such a station was a number at all.
835 f.set({Feature::SchedStrategy_PAS, Feature::SchedStrategy_OI,
836 Feature::SchedStrategy_POLLING});
837 // Fork-join, on the TAG-AUGMENTED copy the analyzer builds; the sibling
838 // classes are folded back before the table is returned.
839 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner});
840 // Finite capacity regions: DROP censors the path exactly as it censors the
841 // CTMC's space, and WAITQ runs the shared token-FIFO relation.
842 f.set(Feature::Region);
843 // A cache access is a state-dependent class switch plus a replacement, both
844 // in `after_event_cache`; the analyzer writes the realized hit ratio back.
845 f.set({Feature::Cache, Feature::CacheClassSwitcher, Feature::CacheRetrieval});
846 // The stochastic Petri net path: a firing is a GLOBAL synchronization, which
847 // the serial engine walks alongside the ordinary ones.
848 f.set({Feature::Place, Feature::Transition, Feature::Linkage, Feature::Enabling,
849 Feature::Inhibiting, Feature::Timing, Feature::Firing, Feature::Storage});
850 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
851 // SDR, through the serial engine: the NRM refuses every state-dependent
852 // strategy by name and `default` falls back, and the serial engine reads the
853 // same per-state table (`rt_state`) the CTMC generator does.
854 f.set(Feature::RoutingStrategy_SDR);
855 // THE DISPATCHERS, both engines. The NRM walks the outgoing arcs itself and
856 // keeps its own cursor (`resolve_state_dependent_dest`); the SERIAL engine
857 // reads the pointer the state now carries, the same one the CTMC generator
858 // reads. JSQ and SQ are NRM-only in their exact form -- the serial engine
859 // consumes the uniform expansion for them -- which is the reference's own
860 // position: `getRoutingMatrix.m:117` spreads both uniformly.
861 f.set({Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
862 Feature::RoutingStrategy_JSQ, Feature::RoutingStrategy_SQ});
863 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO,
864 Feature::ReplacementStrategy_SFIFO, Feature::ReplacementStrategy_LRU});
865 f.set({Feature::ReplacementStrategy_HLRU, Feature::ReplacementStrategy_CLIMB,
866 Feature::ReplacementStrategy_QLRU});
867 f.set({Feature::SchedStrategy_EXT, Feature::ClosedClass, Feature::SelfLoopingClass,
868 Feature::OpenClass});
869 f.set({Feature::OpenSignal, Feature::ClosedSignal});
870 f.set({Feature::SignalType_NEGATIVE, Feature::SignalType_CATASTROPHE});
871 // Synchronous calls, through the serial engine: `after_event` keeps the caller's server in
872 // the reply block and the analyzer adds the hold to QLen and Util (`reply_held`). The NRM
873 // refuses them (`ssa_check_reply`), so `default` falls back, as SolverSSA.m does.
874 f.set(Feature::SignalType_REPLY);
875 f.set({Feature::SignalBatchRemoval, Feature::SignalRemovalPolicy});
876 f.set(Feature::Retrial);
877 // INERT: balking and reneging are call parameters of state_events.h, not struct fields.
878 f.set({Feature::Balking, Feature::Reneging});
879 f.set(Feature::LoadDependence);
880 // Class- and joint-dependent scaling: the NRM refuses both because it builds
881 // its rates without evaluating the handle, and the serial engine evaluates
882 // it through `state_events.h` exactly as the CTMC does.
883 f.set({Feature::ClassDependence, Feature::JointDependence});
884 // A global (Whittle) dependence is likewise refused by the NRM and carried by
885 // the serial engine, which evaluates phi(n) once per sample-path state.
886 f.set(Feature::GlobalDependence);
887 // c-server stations and binding buffers: the serial engine walks the same
888 // State arms ctmc_feature_set declares and the NRM honours both.
889 // SolverSSA.m:178-181.
890 f.set({Feature::MultiServer, Feature::FiniteCapacity});
891 return f;
892}
893
894/**
895 * SolverFLD.getFeatureSet, transcribed, MINUS what the requested method cannot
896 * evaluate -- the port of `@@SolverFLD/getMethodFeatureSet`.
897 *
898 * THE PER-METHOD SUBTRACTION IS NOT COSMETIC. A station whose discipline has no
899 * branch in `ode_rates_closing_factors` keeps g = x, i.e. it is integrated as an
900 * INFINITE SERVER, and the answer is wrong with no warning at all: on
901 * Delay(Z=1) -> Queue(c=1), N=4, whose exact Q2 is 3.0154, the fall-through
902 * returns 2.0000. The closing family therefore has to reject the disciplines it
903 * lacks a branch for rather than accept them into the wrong drift. Only the
904 * closing family is affected: matrix/pnorm build a PS drift, which is the right
905 * aggregate for any work-conserving discipline.
906 *
907 * `CacheRetrieval` is DELIBERATELY absent upstream (SolverFLD.m:95-101 records
908 * the flow-conservation failure on examples/basic/cacheModel/retrieval_simple).
909 * `NHPP`, `MAPt` and `PHt` are the time-inhomogeneous families. Every method
910 * accepts them, as the reference does: the first-order methods integrate the
911 * time-averaged NOMINAL pair, `kp` integrates the schedule itself, and a
912 * time-varying rate multiplier is refused on the options rather than here.
913 * What the C++ fluid
914 * solver ignores silently (Region, Fork/Join, Router, Retrial, Place/Transition,
915 * the signals, ClassDependence) is already absent from the MATLAB set.
916 */
917inline FeatureSet fluid_feature_set(const std::string& method) {
918 FeatureSet f;
919 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue});
920 f.set({Feature::Cache, Feature::CacheClassSwitcher});
921 f.set({Feature::Cox2, Feature::Coxian, Feature::Erlang, Feature::Exp, Feature::HyperExp});
922 f.set({Feature::APH, Feature::Det, Feature::MAP, Feature::MMPP2, Feature::NHPP});
923 // The time-inhomogeneous families, which only `kp` integrates; the gate is
924 // per-method below, so declaring them here is not a claim that every fluid
925 // method reads a schedule.
926 f.set({Feature::MAPt, Feature::PHt});
927 // Non-Markovian renewal laws reach the ODE as acyclic PH via sn_nonmarkov_toph.
928 f.set({Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
929 Feature::Weibull});
930 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
931 Feature::Buffer, Feature::Dispatcher});
932 f.set({Feature::Server, Feature::ServiceTunnel});
933 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS});
934 f.set({Feature::SchedStrategy_DPS, Feature::SchedStrategy_FCFS, Feature::SchedStrategy_HOL});
935 f.set({Feature::SchedStrategy_GPS});
936 f.set({Feature::SchedStrategy_SIRO, Feature::SchedStrategy_LCFS,
937 Feature::SchedStrategy_LCFSPR});
938 f.set(Feature::LoadDependence);
939 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO});
940 f.set(Feature::ReplacementStrategy_SFIFO);
941 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
942 f.set({Feature::ClosedClass, Feature::SelfLoopingClass, Feature::Replayer});
943 f.set({Feature::RandomSource, Feature::Sink, Feature::Source, Feature::OpenClass,
944 Feature::JobSink});
945 // Fork-join through the MMT (or Heidelberger-Trivedi) transform, driven by
946 // `mva::fj_fixed_point` with a fluid inner solve; see fluid_runner.h. The
947 // transform emits only Source, Delay, Queue, Router and ClassSwitch, all of
948 // which the drift already carries, so nothing fork-join specific enters the
949 // ODE. The per-method gate below removes it again where the fixed point
950 // cannot run.
951 f.set({Feature::Fork, Feature::Forker, Feature::Join, Feature::Joiner,
952 Feature::JoinPartial});
953 // c-server stations: the drifts carry min(n,c); withdrawn from 'diffusion'
954 // and 'mfq' below. SolverFLD.m:605-607.
955 f.set(Feature::MultiServer);
956 // A binding buffer, SolverFLD.m:608-614: 'dae' carries it as an algebraic
957 // constraint, 'mol' IS the Mt/G/s/0 loss system and the AoI arm of 'mfq' is
958 // a bufferless or single-buffer queue. WHICH method serves one is the
959 // structural rule `fluid_runner`'s own `check_binding_capacity` call asks,
960 // so no per-method delta duplicates it here; the reference withdraws it from
961 // no method for exactly that reason.
962 f.set(Feature::FiniteCapacity);
963
964 // THE CANONICAL NAME, which every test below is on. Stripping only the
965 // `fld.` prefix here left two holes: `aoi` is an alias of `mfq`
966 // (fluid_runner.h `fluid_unqualify`) and lost the HOL grant `mfq` keeps,
967 // reachable through AUTO, which passes the method name unmapped; and the
968 // Qsys methods now use their bare API names as canonical selectors.
969 std::string m =
970 (method.size() > 4 && method.compare(0, 4, "fld.") == 0) ? method.substr(4) : method;
971 if (m == "aoi") m = "mfq";
972 const bool closing_family =
973 m == "closing" || m == "statedep" || m == "softmin" || m == "tbi" || m == "minnormal" ||
974 m == "refined" || m == "dae";
975 // Limited load dependence composes with the closure as a rate multiplier
976 // alpha(n_i) on the scheduling share, which only the closing family evaluates.
977 // The matrix, pnorm, softmin, statedep, tbi, diffusion, mfq, kp and rmf paths
978 // build their drift independently and would silently ignore alpha.
979 if (!(m == "closing" || m == "minnormal" || m == "refined" || m == "dae"))
980 f.unset(Feature::LoadDependence);
981 // SIRO, LCFS and LCFSPR have no branch in the closing drift. The metric reader
982 // of `solver_fluid_closing` caught LCFS/LCFSPR/HOL, but `solver_fluid_moments`
983 // reads the event representation directly and never reached that guard, and
984 // SIRO is worse still: the reader accepts it AS FCFS, so the ODE integrated it
985 // as INF while the metrics were read as if it shared the server.
986 if (closing_family) {
987 f.unset(Feature::SchedStrategy_SIRO);
988 f.unset(Feature::SchedStrategy_LCFS);
989 f.unset(Feature::SchedStrategy_LCFSPR);
990 }
991 // GPS splits the server by weight among the BACKLOGGED classes, so its share
992 // is a function of the backlog INDICATOR. A first-order closure cannot express
993 // it at all: with continuous x_k > 0 every class is always backlogged and the
994 // share collapses to the constant w_k/sum_j w_j, the heavy-traffic limit,
995 // regardless of load. Only `minnormal` supplies the P(X_k >= 1) it needs --
996 // `refined` reads its rates at the MEAN-FIELD variance, which is exactly the
997 // degenerate case.
998 if (m != "minnormal") f.unset(Feature::SchedStrategy_GPS);
999 // HOL allocates capacity in PRIORITY order, not in proportion to population,
1000 // and no fluid drift reads the class priorities except the single-queue `mfq`
1001 // priority branch.
1002 if (m != "mfq") f.unset(Feature::SchedStrategy_HOL);
1003 // MULTISERVER, SolverFLD.m:387-395: the drifts carry min(n,c) except
1004 // `fluid_diffusion`, whose SDE is written for one or infinitely many
1005 // servers, and `solver_mfq`, a single-queue model on the same server counts
1006 // (off them `mfq` resolves to `matrix`, so the delta binds only where it
1007 // runs as itself). fluid_diffusion.h:89-93 keeps wording that refusal.
1008 if (m == "diffusion" || m == "mfq") f.unset(Feature::MultiServer);
1009 // `dae` is the one method that WIDENS this set, and the widening is why the
1010 // gate has to be per-method: a Finite Capacity Region is a linear inequality
1011 // on the state, which the DAE form carries as an algebraic equation beside
1012 // the drift and no ODE method can carry at all. Every other fluid method has
1013 // to go on refusing it -- an ODE integrated through a cap it cannot see
1014 // returns the UNCONSTRAINED answer with no warning -- so the widening is
1015 // stated here rather than in the static set. `fluid_dae_constraints` still
1016 // refuses by name the region forms that are not constraints on this drift
1017 // (drop, BAS/BBS/RSRD, retrial, per-class admission weights), and
1018 // `solver_fluid_dae` refuses the FCR transient, which needs event restarts.
1019 //
1020 // DPS goes the other way: it closes on the covariance BETWEEN a station's
1021 // class coordinates, where the DAE form carries one scalar variance per
1022 // station. `minnormal` carries those matrix blocks through its outer
1023 // iteration; adding an unknown block per station here would restore the
1024 // quartic cost that keeping Sigma out of the Newton vector avoids. GPS is
1025 // already unset above for everything but `minnormal`, for the same reason.
1026 if (m == "dae") {
1027 f.set(Feature::Region);
1028 f.unset(Feature::SchedStrategy_DPS);
1029 }
1030 // A STOCHASTIC PETRI NET HAS NO DRIFT OUTSIDE THE DAE FORM: its conserved
1031 // quantities are P-invariants rather than chain populations, an immediate
1032 // transition is an algebraic FLOW rather than an event with a rate, and a
1033 // bounded place is a linear inequality on the marking (fluid_petri.h). Every
1034 // other fluid method builds its drift from the station/class/phase encoding,
1035 // where a Place contributes no coordinate at all, so it would integrate the
1036 // net as an empty model and report zeros without a warning -- which is why
1037 // the names are declared for `dae` alone rather than in the static set.
1038 // `dae` ALONE, not `default`: the reference's own gate removes them for every
1039 // other method including the unresolved default (SolverFLD.m:240), and the
1040 // Petri route is reached by name or by the runner's own branch, never by a
1041 // default that has not yet resolved.
1042 if (m == "dae")
1043 f.set({Feature::Place, Feature::Transition, Feature::Linkage, Feature::Enabling,
1044 Feature::Inhibiting, Feature::Timing, Feature::Firing, Feature::Storage});
1045 // THE MMT FIXED POINT, and which methods can actually run it. This list used
1046 // to hold seven names on the argument that the transform hands the inner
1047 // solve a MIXED network -- the parallelism rides on auxiliary OPEN classes
1048 // even when the model is closed -- so a method that takes only closed
1049 // models, only open ones, a single queue, or a decomposition could not
1050 // drive it. MEASURED on a SYMMETRIC closed fork-join (Delay -> Fork -> two
1051 // identical FCFS queues -> Join, N = 2), against the exact chain
1052 // Q1 = Q2 = 0.664, J = 0.624, D = 1.024, five of the seven answer it and
1053 // answer it symmetrically, which is the one property no approximation of a
1054 // symmetric model may lose:
1055 //
1056 // statedep, mfq, rmf 0.5714 0.5714 0.5714 1.1429 (the `matrix` drift)
1057 // refined 0.2715 0.2715 0.9713 0.4857
1058 // tbi 0.2500 0.2500 1.0000 0.5000
1059 //
1060 // So the argument does not hold for those five and the names come back;
1061 // MATLAB and native python have always run them there. It DOES hold for the
1062 // other two, which is why they stay:
1063 //
1064 // diffusion 0 0 2 0 the whole population on ONE station and zero
1065 // (and 0 1.997 0 0.003 on a rerun -- a DIFFERENT station), so
1066 // the SDE is not integrating this model at all
1067 // kp 0 0 0 0 on a symmetric OPEN fork-join fed at rate
1068 // 0.5, i.e. an empty network where jobs are arriving
1069 //
1070 // Both are silent wrong answers rather than refusals, so keeping the names
1071 // off is what makes the model refused instead of mis-answered. MATLAB and
1072 // native python are over-permissive here and now drop them too.
1073 if (m == "diffusion" || m == "kp") {
1074 f.unset(Feature::Fork);
1075 f.unset(Feature::Forker);
1076 f.unset(Feature::Join);
1077 f.unset(Feature::Joiner);
1078 f.unset(Feature::JoinPartial);
1079 }
1080 // MAPt/PHt/NHPP are NOT removed per method, and that is the reference's own
1081 // position rather than an oversight. `solver_fluid.m:20-35` and
1082 // `solver_fluid_matrix.m:18-27` install the width-weighted NOMINAL pair for a
1083 // schedule and integrate it deliberately: the nominal is the stationary
1084 // carrier of the phase structure the schedule modulates, so a steady-state
1085 // answer at the time-averaged rate is the intended approximation, not a
1086 // silent substitution. `kp` is the method that integrates the SCHEDULE, and
1087 // `getMethodFeatureSet` in the reference restricts none of the three.
1088 //
1089 // The refusal that does exist lives on the OPTIONS, not here: a time-varying
1090 // rate multiplier (`nhpp_sched` / `rate_traj` / `rate_sched`) makes the drift
1091 // non-autonomous, which the moment closures cannot take, and
1092 // `fluid_minnormal_applicable` declines `minnormal` for it while
1093 // `fluid_moment_terms` raises. Those are conditions on the OPTIONS a caller
1094 // passes and cannot be decided from the model alone, which is why a featset
1095 // is the wrong place for them.
1096 // ON THE CANONICAL NAME, not the raw one. These two branches used to compare
1097 // `method`, so a direct call with a `fld.`-qualified spelling silently got
1098 // a DIFFERENT envelope than the bare one; it was masked on the solver path
1099 // only because `fluid_resolve_method` unqualifies on entry. test_feature_set
1100 // asserts the invariant for `minnormal` alone, and the same assertion written
1101 // for `fld.tvms` would have failed.
1102 if (m == "ggisgi" || m == "ggingi" || m == "tvms") {
1103 // The only fluid methods in LINE stated for a queue customers ABANDON.
1104 // Reneging stays out of the base FLD envelope: the network drift carries
1105 // no abandonment flow, so every other method would integrate the model
1106 // as if nobody left.
1107 f.set(Feature::Reneging);
1108 }
1109 if (m == "ggisgi" || m == "ggingi" || m == "tvms" || m == "mtginf" ||
1110 m == "mol") {
1111 // Every one of them is stated for a single open station; the base
1112 // envelope's closed classes have no meaning there.
1113 f.unset(Feature::ClosedClass);
1114 f.unset(Feature::SelfLoopingClass);
1115 }
1116 // CLOSED MODELS ONLY for `refined`, which the reference's runAnalyzer has
1117 // always enforced by name and this set never stated: the 1/N correction is
1118 // solved on orth(D) over the FULL state, so on an open model it adds a
1119 // perturbation to the SOURCE POOL mass, a normalisation constant rather than
1120 // a population. Only `minnormal` was validated open. Stating it here is what
1121 // lets a report withdraw the pair instead of offering a run that stops.
1122 if (m == "refined") {
1123 f.unset(Feature::OpenClass);
1124 f.unset(Feature::Source);
1125 f.unset(Feature::Sink);
1126 f.unset(Feature::RandomSource);
1127 f.unset(Feature::JobSink);
1128 }
1129 // The diffusion SDE PROJECTS each class back onto its own fixed population
1130 // at every step, which is the closed-network constraint itself: an open
1131 // class has no population to project onto, and a Source is not a station the
1132 // SDE has a coordinate for. `fluid_diffusion` refuses both by name, and
1133 // stating them here is what lets a CALLER see the refusal before running.
1134 if (m == "diffusion") {
1135 f.unset(Feature::OpenClass);
1136 f.unset(Feature::Source);
1137 f.unset(Feature::Sink);
1138 f.unset(Feature::RandomSource);
1139 f.unset(Feature::JobSink);
1140 }
1141 // The Ko-Pender limits are proved for an OPEN network of stations fed by
1142 // external arrival processes: a closed class has no arrival process to
1143 // modulate and no source phase to carry, and the cache and class-switch
1144 // machinery has no counterpart in the paper's event set. Only the fork-join
1145 // names were withheld here, five features against the nine MATLAB, the JAR and
1146 // native python all unset, so this port alone offered `kp` on closed, cache
1147 // and class-switching models the other three refuse -- and `doc/solver-methods.md`
1148 // states the narrowing as the documented behaviour.
1149 if (m == "kp") {
1150 f.unset(Feature::ClosedClass);
1151 f.unset(Feature::SelfLoopingClass);
1152 f.unset(Feature::Cache);
1153 f.unset(Feature::CacheClassSwitcher);
1154 f.unset(Feature::ClassSwitch);
1155 f.unset(Feature::StatelessClassSwitcher);
1156 f.unset(Feature::ReplacementStrategy_RR);
1157 f.unset(Feature::ReplacementStrategy_FIFO);
1158 f.unset(Feature::ReplacementStrategy_SFIFO);
1159 }
1160 // Trajectory-based iteration decomposes the CLOSED population into cells and
1161 // relaxes the waveforms between them; there is no cell for an unbounded open
1162 // stream. A cache model is solved by decomposition rather than by one drift,
1163 // so the cell partition has nothing to partition -- use `rmf`.
1164 if (m == "tbi") {
1165 f.unset(Feature::OpenClass);
1166 f.unset(Feature::Source);
1167 f.unset(Feature::Sink);
1168 f.unset(Feature::RandomSource);
1169 f.unset(Feature::JobSink);
1170 f.unset(Feature::Cache);
1171 f.unset(Feature::CacheClassSwitcher);
1172 f.unset(Feature::ReplacementStrategy_RR);
1173 f.unset(Feature::ReplacementStrategy_FIFO);
1174 f.unset(Feature::ReplacementStrategy_SFIFO);
1175 }
1176 return f;
1177}
1178
1179/**
1180 * SolverMAM.getFeatureSet, the union of its four setTrue calls: 55 MATLAB
1181 * names, WIDENED for 'default'/'ldqbd' only. SchedStrategy_FCFSPRPRIO was
1182 * undeclared until 2026-09-24, when solve_fcfs_station gained the ported
1183 * MMAPPH1PRPR/NPPR priority branch (api/mam/mmapph1prio.h).
1184 *
1185 * THE METHOD PARAMETER IS NOW READ, where it used to be discarded. Two
1186 * consequences of discarding it were both the over-declaration
1187 * feature_set.h:30-33 warns against:
1188 *
1189 * +LoadDependence, method in {default, ldqbd} ONLY
1190 * solver_mam_ldqbd.h:152 reads st.lldscaling, but only the
1191 * 'ldqbd' method and 'default''s single-class closed
1192 * Delay+Queue route (mam_dispatch.h's branch 2e) ever call
1193 * it. Declaring it for every method let 'dec.source',
1194 * 'dec.poisson' and 'mna' pass a load-dependent model
1195 * straight to solver_mam_basic / solver_mna, neither of
1196 * which reads the field, so those methods silently solved
1197 * at nominal rates. The featset cannot see topology, so
1198 * 'default' still needs the structural check in
1199 * check_model_method (solver_mam_runner.h) that refuses a
1200 * load-dependent model whose shape is not the one ldqbd
1201 * can solve; 'ldqbd' itself is already self-checking
1202 * (solver_mam_ldqbd.h:109-131).
1203 * DMAP, ME, RAP and SetupDelayOff stay declared and are inert: no ProcessType
1204 * or NetworkStruct field can make used_lang_features emit them.
1205 */
1206inline FeatureSet mam_feature_set(const std::string& method) {
1207 FeatureSet f;
1208 f.set({Feature::Sink, Feature::Source});
1209 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner});
1210 f.set({Feature::Delay, Feature::DelayStation, Feature::Queue});
1211 // ME and RAP are INERT: ProcessType carries no such enumerator.
1212 f.set({Feature::APH, Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp,
1213 Feature::HyperExp, Feature::MMPP2, Feature::MAP, Feature::MMAP, Feature::MPH,
1214 Feature::DMAP, Feature::ME, Feature::RAP});
1215 // The slotted path (mam_dispatch.h branch -1). A Geometric or a
1216 // DiscreteUniform reaches solver_mam_dt on the slot lattice; off it,
1217 // sn_is_discrete_time reports the model continuous and the continuous
1218 // analyzers below take the same laws through their moment fits.
1219 f.set({Feature::Geometric, Feature::DiscreteUniform});
1220 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
1221 Feature::Weibull});
1222 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer});
1223 f.set(Feature::ClassSwitch);
1224 f.set({Feature::SharedServer, Feature::Buffer, Feature::Dispatcher});
1225 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::ServiceTunnel});
1226 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_HOL});
1227 f.set({Feature::SchedStrategy_FCFS, Feature::SchedStrategy_FCFSPRPRIO});
1228 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
1229 f.set({Feature::ClosedClass, Feature::SelfLoopingClass});
1230 f.set(Feature::OpenClass);
1231 // The G-network names are NOT here. They moved to ag_feature_set with the
1232 // RCAT methods that read `issignal`: no MAM algorithm reads the field, so
1233 // any of them would solve the model with the signals turned into ordinary
1234 // customers and report that as the answer.
1235 // Call 3, the BMAP/PH/N/N retrial queue.
1236 f.set({Feature::Retrial, Feature::BMAP, Feature::PH});
1237 // Call 4, qbd_setupdelayoff, reached through NetworkStruct::setupparam.
1238 f.set(Feature::SetupDelayOff);
1239 // c-server stations (every analyzer reads sn.nservers; the slotted path's
1240 // single-server rule stays structural, solver_mam_dt.h:73) and finite
1241 // buffers as LOSS buffers: solver_mam_basic's M/M/c/K and MMAP[K]/G/1/K,
1242 // solver_mam and solver_mna_open's truncate-and-renormalize, and
1243 // solver_mam_retrial's bufferless N/N station. SolverMAM.m:363-368.
1244 f.set({Feature::MultiServer, Feature::FiniteCapacity});
1245 // solver_mam_decmmap is an OPEN-network departure-process fixed point: it
1246 // iterates on arrival streams a closed population does not have, and its
1247 // opening station loop serves EXT, FCFS, HOL, FCFSPRPRIO and PS only (INF is
1248 // NOT in that list, so a model carrying a Delay belongs to 'dec.source').
1249 // Both restrictions are things the model HAS, so both belong in the featset;
1250 // solver_mam_decmmap.h raises the matching message when the method is named
1251 // by hand. Until this delta existed the gate offered dec.mmap on every closed
1252 // model, which the reference and the JAR answered with a table of zeros.
1253 // Fork-join goes with them: the sweep uses solver_mam_traffic, the PLAIN
1254 // traffic step, so a Fork or a Join is refused by the traffic step itself
1255 // rather than synchronized (solver_mam_decmmap.h says so in its header) --
1256 // which is why mam_dispatch routes a fork-join topology from
1257 // 'default'/'dec.source' to solver_mam_fj and never here.
1258 if (method == "dec.mmap")
1259 for (Feature ff : {Feature::ClosedClass, Feature::SelfLoopingClass,
1260 Feature::SchedStrategy_INF, Feature::Fork, Feature::Join,
1261 Feature::Forker, Feature::Joiner})
1262 f.unset(ff);
1263 // Widening over MATLAB, and only for the methods that actually read it.
1264 if (method == "default" || method == "ldqbd") f.set(Feature::LoadDependence);
1265 // Round-robin dispatching enters as a deterministic traffic split
1266 // (npfqn_traffic_split_rr), which only solver_mna_open resolves; SolverMAM.m
1267 // :60 sets it true for this method alone. The closed branch has no
1268 // counterpart and is refused structurally in check_model_method.
1269 if (method == "mna") f.set(Feature::RoutingStrategy_RROBIN);
1270 // FINITECAPACITY, SolverMAM.m:143-147: the two chains that read no sn.cap
1271 // withdraw it. The LD-QBD levels run to the population or the cutoff, and
1272 // the background chain to the state cap. A buffer a CLOSED class can fill is
1273 // refused structurally by `runner_detail::check_model_method` instead (a
1274 // closed job blocks, a loss formula does not).
1275 if (method == "ldqbd" || method == "bgchain") f.unset(Feature::FiniteCapacity);
1276 return f;
1277}
1278
1279/**
1280 * SolverAG.getFeatureSet: what the RCAT decomposition can represent.
1281 *
1282 * THE G-NETWORK NAMES LIVE HERE AND NOWHERE ELSE. solver_ag.h's build_rcat is
1283 * the only code in LINE that reads `issignal`, so declaring the signal features
1284 * on any other solver would promise what that solver cannot deliver -- it would
1285 * answer with every signal turned into an ordinary customer.
1286 *
1287 * Every AG method is the same decomposition, differing only in how the reversed
1288 * rate is read off an agent, so the envelope does not vary by method; the
1289 * genuine restrictions (Markovian service law, single server) are structural and
1290 * are applied in ag::runner_detail::check_model_method.
1291 */
1292inline FeatureSet ag_feature_set(const std::string& /*method*/) {
1293 FeatureSet f;
1294 f.set({Feature::Sink, Feature::Source});
1295 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner});
1296 f.set({Feature::Delay, Feature::DelayStation, Feature::Queue});
1297 f.set({Feature::APH, Feature::Coxian, Feature::Erlang, Feature::Exp, Feature::HyperExp,
1298 Feature::MAP, Feature::MMPP2});
1299 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
1300 Feature::Weibull});
1301 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer});
1302 f.set({Feature::SharedServer, Feature::Buffer, Feature::Dispatcher});
1303 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::ServiceTunnel});
1304 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_FCFS});
1305 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
1306 // A self-looping class is its own single-station component (`solver_ag`
1307 // reads the flag), so it is declared beside the two ordinary class types.
1308 // MultiServer and FiniteCapacity are deliberately absent, as in
1309 // SolverAG.m:211-216: `check_model_method` words both refusals, RCAT driving
1310 // rho = lambda/mu with no buffer.
1311 f.set({Feature::ClosedClass, Feature::SelfLoopingClass, Feature::OpenClass});
1312 f.set({Feature::OpenSignal, Feature::ClosedSignal});
1313 f.set({Feature::SignalType_NEGATIVE, Feature::SignalType_CATASTROPHE});
1314 f.set(Feature::SignalBatchRemoval);
1315 return f;
1316}
1317
1318/**
1319 * SolverENV.supports's inline set, 28 names, transcribed unchanged.
1320 *
1321 * The narrowest declared set in the codebase. `RoutingStrategy_RROBIN` is
1322 * marked "with SolverJMT" upstream and was kept here while the CTMC stage
1323 * solver declared it; it is now DROPPED, because ctmc_feature_set no longer
1324 * does. env_dispatch.h:164 routes the statevec coupling to that same CTMC, so
1325 * declaring round robin here would promise what the stage cannot deliver.
1326 */
1327inline FeatureSet env_feature_set(const std::string& /*method*/) {
1328 FeatureSet f;
1329 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue,
1330 Feature::Sink, Feature::Source});
1331 f.set({Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp, Feature::HyperExp});
1332 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
1333 Feature::Buffer, Feature::Dispatcher, Feature::Server, Feature::JobSink,
1334 Feature::RandomSource, Feature::ServiceTunnel});
1335 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_FCFS});
1336 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
1337 f.set({Feature::ClosedClass, Feature::OpenClass});
1338 return f;
1339}
1340
1341/**
1342 * SolverLDES.getFeatureSet, transcribed WHOLE.
1343 *
1344 * THE ONE SET IN THIS FILE THAT IS NOT NARROWED, and for a reason that does not
1345 * generalise: every other function here declares what the C++ code implements,
1346 * because a feature declared and not implemented yields a wrong number. The
1347 * LDES client implements NONE of these -- it forwards the model.json document to
1348 * the SSJ engine byte for byte and reads the result back -- so the code that
1349 * has to support a feature is the engine, the same engine MATLAB and native
1350 * Python drive. Narrowing here would refuse models that the reference simulates
1351 * with the very binary this port is about to run, which is the opposite of the
1352 * usual danger.
1353 *
1354 * SOURCE: `matlab/src/solvers/wrappers/LDES/@@SolverLDES/SolverLDES.m:108-230`,
1355 * name for name, and every name there has a registry enumerator. `BatchArrival`
1356 * IS one of them: the note that used to sit here called it a name no solver
1357 * declares and no recorder sets, and both halves were false -- SolverLDES.m:210
1358 * declares it and MNetwork.getUsedLangFeatures marks it, as `used_lang_features`
1359 * has here since the Source arm learned `arrival_batch`. Left undeclared, this
1360 * set refused every model built with `Source.setArrivalBatch`, which is the one
1361 * engine that simulates the batch.
1362 *
1363 * WHAT THE GATE STILL CANNOT SEE. The set is consulted against
1364 * `used_lang_features`, which is built from a PARSED `NetworkStruct`; the LDES
1365 * arm of the CLI never parses the document, so a model outside this port's
1366 * reader reaches the engine without passing here at all. That is intended --
1367 * forwarding losslessly is the whole point -- and it means this set governs
1368 * SolverAUTO's choice rather than the client's own admission.
1369 */
1370inline FeatureSet ldes_feature_set(const std::string& /*method*/) {
1371 FeatureSet f;
1372 f.set({Feature::Sink, Feature::Source, Feature::Queue, Feature::Delay});
1373 // JoinPartial: the engine fires the join at the k-th sibling and discards
1374 // the stragglers on arrival, so the quorum is an exact sample-path event
1375 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner,
1376 Feature::JoinPartial});
1377 // The fork overrides, all three simulated rather than flattened to the
1378 // scalar tasks-per-link: a per-(link, class) count, a count DRAWN from a
1379 // jobs-per-link law, and a branch that fires only with its probability. A
1380 // fork that declares none of them consumes nothing from the fork stream, so
1381 // every seeded golden is unmoved (`fork_is_variable` in ldes_engine.h).
1382 f.set({Feature::ForkFanoutVector, Feature::ForkFanoutRandom,
1383 Feature::ForkBranchProbability});
1384 f.set({Feature::Place, Feature::Transition, Feature::QueueingPlace});
1385 f.set({Feature::Linkage, Feature::Enabling, Feature::Inhibiting, Feature::Timing,
1386 Feature::Firing, Feature::Storage});
1387 f.set({Feature::Logger, Feature::LogTunnel, Feature::Buffer, Feature::Region});
1388 f.set({Feature::Exp, Feature::Erlang, Feature::HyperExp, Feature::PH, Feature::APH,
1389 Feature::Coxian, Feature::Cox2});
1390 f.set({Feature::MAP, Feature::DMAP, Feature::MMAP, Feature::MPH, Feature::BMAP,
1391 Feature::MMPP2, Feature::ME, Feature::RAP});
1392 f.set({Feature::Immediate, Feature::Disabled, Feature::Replayer, Feature::Trace});
1393 f.set({Feature::Det, Feature::Uniform, Feature::Gamma, Feature::Pareto, Feature::Weibull,
1394 Feature::Lognormal});
1395 // Lattice-valued and counting laws: Geometric backs Geo/Geo/1 and the
1396 // slotted mode, and the zero atom of a counting law becomes an immediate
1397 // interval in continuous mode.
1398 f.set({Feature::Geometric, Feature::Bernoulli, Feature::Binomial, Feature::Poisson});
1399 // DiscreteUniform: inverted off the same SSJ quantile the reference uses
1400 // (`uniform_int_inverse` in ldes_ssj_variates.h), so it lands on the lattice
1401 // exactly and the slotted mode accepts it.
1402 f.set(Feature::DiscreteUniform);
1403 // Source.setArrivalBatch: the model document carries `arrivalBatch` and the
1404 // engine releases the whole batch at one arrival epoch (SolverLDES.m:210).
1405 f.set(Feature::BatchArrival);
1406 // Time-inhomogeneous processes: the piecewise-constant schedule is simulated
1407 // exactly by carrying the phase across a breakpoint.
1408 // The MARKED schedules ride the same walk with their arrival arm split across the
1409 // mark blocks, at no extra draw, so a K = 1 MMAPt IS the MAPt with the same matrices.
1410 // An MPHt is stored lowered to MMAPt form, so one walk serves both.
1411 f.set({Feature::NHPP, Feature::MAPt, Feature::PHt, Feature::MMAPt, Feature::MPHt});
1412 // BMMAPt: the same schedule walk again, with the arrival arm split across
1413 // (mark, batch) blocks accumulated destination-major then mark-minor then
1414 // batch-minor, so neither label costs a draw and a B = 1 BMMAPt reproduces the
1415 // MMAPt sample path. At a Source one epoch releases a batch of jobs that all
1416 // carry the mark's class; as SERVICE it is a bulk server (BMSP) completing a
1417 // batch per firing with the mark discarded.
1418 f.set(Feature::BMMAPt);
1419 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::InfiniteServer,
1420 Feature::SharedServer, Feature::ServiceTunnel, Feature::DelayStation});
1421 f.set({Feature::SchedStrategy_FCFS, Feature::SchedStrategy_INF, Feature::SchedStrategy_HOL,
1422 Feature::SchedStrategy_FCFSPRIO});
1423 f.set({Feature::SchedStrategy_PS, Feature::SchedStrategy_DPS, Feature::SchedStrategy_GPS});
1424 f.set({Feature::SchedStrategy_LCFS, Feature::SchedStrategy_LCFSPR,
1425 Feature::SchedStrategy_LCFSPI});
1426 f.set({Feature::SchedStrategy_FCFSPR, Feature::SchedStrategy_FCFSPI,
1427 Feature::SchedStrategy_LPS, Feature::SchedStrategy_SIRO});
1428 f.set({Feature::SchedStrategy_SJF, Feature::SchedStrategy_LJF, Feature::SchedStrategy_LEPT,
1429 Feature::SchedStrategy_SEPT});
1430 f.set({Feature::SchedStrategy_SRPT, Feature::SchedStrategy_SRPTPRIO,
1431 Feature::SchedStrategy_PSJF, Feature::SchedStrategy_FB, Feature::SchedStrategy_LRPT});
1432 f.set({Feature::SchedStrategy_EXT, Feature::SchedStrategy_POLLING});
1433 f.set({Feature::SchedStrategy_PSPRIO, Feature::SchedStrategy_DPSPRIO,
1434 Feature::SchedStrategy_GPSPRIO});
1435 f.set({Feature::SchedStrategy_LCFSPRIO, Feature::SchedStrategy_LCFSPRPRIO,
1436 Feature::SchedStrategy_LCFSPIPRIO});
1437 f.set({Feature::SchedStrategy_FCFSPRPRIO, Feature::SchedStrategy_FCFSPIPRIO});
1438 f.set({Feature::SchedStrategy_FSP, Feature::SchedStrategy_PAS, Feature::SchedStrategy_OI});
1439 f.set({Feature::SchedStrategy_EDD, Feature::SchedStrategy_EDF, Feature::SchedStrategy_SETF});
1440 f.set({Feature::Router, Feature::Dispatcher, Feature::ClassSwitch,
1441 Feature::StatelessClassSwitcher});
1442 f.set({Feature::Cache, Feature::CacheClassSwitcher, Feature::CacheRetrieval,
1443 Feature::CacheItemSize});
1444 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND,
1445 Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
1446 Feature::RoutingStrategy_JSQ, Feature::RoutingStrategy_SQ});
1447 // HeteroServers: each job occupies one server of one compatible pool and is
1448 // served at that pool's own law (`free_slot_for` / `start_service`). The
1449 // sharing disciplines are refused by name in the engine rather than
1450 // flattened, so the declaration does not over-promise.
1451 f.set(Feature::HeteroServers);
1452 // SDR: the engine re-evaluates Krzesinski eq. (10) at the live marking
1453 // (`draw_sdr` in ldes_engine.h), so the sample path has an exact
1454 // counterpart in solver_nc_sdr rather than the uniform split the static
1455 // routing row carries. Declared here as well as in the reference because
1456 // the native engine now honours it; before that it was the ONE name whose
1457 // absence mattered, since the engine answered the uniform model in
1458 // silence instead of refusing.
1459 f.set(Feature::RoutingStrategy_SDR);
1460 f.set({Feature::OpenClass, Feature::ClosedClass, Feature::SelfLoopingClass});
1461 f.set({Feature::OpenSignal, Feature::ClosedSignal, Feature::SignalType_NEGATIVE,
1462 Feature::SignalType_REPLY, Feature::SignalType_CATASTROPHE,
1463 Feature::SignalBatchRemoval, Feature::SignalRemovalPolicy});
1464 f.set({Feature::LoadDependence, Feature::ClassDependence, Feature::JointDependence,
1465 Feature::SetupDelayOff});
1466 // GlobalDependence: phi(n) is read at the FULL population and re-times every
1467 // completion in flight at each change, the infinite servers included
1468 // (`gd_refresh` and the fan-out wrappers in ldes_engine.h). Utilization is
1469 // then T*E[S]/peak against the declared peak, as it is for a class
1470 // dependence. A bulk (BMSP) or pass-and-swap station is refused by name
1471 // rather than answered at the unscaled rate.
1472 f.set(Feature::GlobalDependence);
1473 // ServerParallelism: a class-r job seizes n slots at once and holds them for
1474 // the whole of its service, so the station serves at most floor(c/n) of them
1475 // and one contributes n/c to the utilization. Refused by name at an infinite
1476 // server, a sharing discipline, a polling or pass-and-swap controller, a
1477 // bulk server, a pooled station and a setup station, none of which has a
1478 // bank of interchangeable slots to take n of.
1479 f.set(Feature::ServerParallelism);
1480 f.set({Feature::Balking, Feature::Reneging, Feature::Retrial});
1481 // Breakdown is enforced here by a struct field a caller can actually set:
1482 // the LDES engine reads `sn.breakdownparam` and simulates the outage.
1483 f.set(Feature::Breakdown);
1484 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO,
1485 Feature::ReplacementStrategy_SFIFO, Feature::ReplacementStrategy_LRU,
1486 Feature::ReplacementStrategy_HLRU, Feature::ReplacementStrategy_CLIMB,
1487 Feature::ReplacementStrategy_QLRU});
1488 // c-server stations and a finite buffer with its drop rule: the engine holds
1489 // both on the sample path, so the simulated station is the one the model
1490 // declares. SolverLDES.m:230.
1491 f.set({Feature::MultiServer, Feature::FiniteCapacity});
1492 return f;
1493}
1494
1495/**
1496 * Port of `SolverJMT.getFeatureSet` (`@@SolverJMT/SolverJMT.m:180-290`).
1497 *
1498 * JMT IS THE REFERENCE FCR ENGINE -- every other solver's finite-capacity
1499 * refusal points users here -- so `Region` is declared, and so is the whole SPN
1500 * section family, which JMT simulates natively.
1501 *
1502 * WHAT IS NARROWED AGAINST THE REFERENCE, and why each is a transcription of a
1503 * refusal this port already makes rather than a feature JMT lacks:
1504 * Normal the reference lists it, and `saveServiceStrategy` has no
1505 * branch for it: a Normal service would reach the analytic
1506 * switch with no `javaClass` and error. It is not declared.
1507 * HLRU/CLIMB/QLRU JMT has cache objects for RR, FIFO, SFIFO and LRU only;
1508 * `save_cache_strategy` refuses the other three by name
1509 * instead of falling back to LRU, so declaring them would
1510 * promise an admission the writer then denies.
1511 * SchedStrategy_LPS is declared: the limit is exported as a single-node
1512 * blocking region, not dropped.
1513 */
1514/**
1515 * True for the JMVA algorithms that solve a CLOSED product-form network only.
1516 *
1517 * RECAL, CoMoM, Chow, Bard-Schweitzer (both spellings), AQL, Linearizer and De
1518 * Souza-Muntz Linearizer. Measured against JMT 1.2.x: each answers an open or a
1519 * mixed model with `jmt.common.exception.UnsupportedModelException: The selected
1520 * solver cannot handle open classes, please choose another.` and a
1521 * load-dependent one with the same exception naming load-dependent stations,
1522 * while the exact MVA engine behind 'jmva' and 'jmva.mva' serves both. The
1523 * single-server half of the same restriction is a station COUNT and has no
1524 * feature name, so it is refused structurally by `jmt::jmt_method_refusal`.
1525 */
1526inline bool jmva_is_closed_only(const std::string& method) {
1527 return method == "jmva.amva" || method == "jmva.recal" || method == "jmva.comom" ||
1528 method == "jmva.chow" || method == "jmva.bs" || method == "jmva.aql" ||
1529 method == "jmva.lin" || method == "jmva.dmlin";
1530}
1531
1532inline FeatureSet jmt_feature_set(const std::string& method) {
1533 FeatureSet f;
1534 f.set({Feature::Sink, Feature::Source, Feature::Queue, Feature::Delay, Feature::DelayStation});
1535 f.set({Feature::Router, Feature::ClassSwitch, Feature::StatelessClassSwitcher,
1536 Feature::Dispatcher});
1537 // JoinPartial: written out as a jmt PartialJoin with numRequired = k
1538 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner,
1539 Feature::JoinPartial});
1540 // A variable forking level: the writer turns isSimplifiedFork off and emits the
1541 // per-branch entries, so jmt reads the counts, the probabilities and the degree
1542 // distribution rather than sending one job down every link.
1543 f.set({Feature::ForkFanoutVector, Feature::ForkFanoutRandom,
1544 Feature::ForkBranchProbability});
1545 f.set({Feature::Logger, Feature::LogTunnel, Feature::Buffer, Feature::Region,
1546 Feature::Linkage});
1547 f.set({Feature::Enabling, Feature::Inhibiting, Feature::Timing, Feature::Firing,
1548 Feature::Storage, Feature::Place, Feature::Transition});
1549 f.set({Feature::Server, Feature::JobSink, Feature::RandomSource, Feature::InfiniteServer,
1550 Feature::SharedServer, Feature::ServiceTunnel});
1551 // Heterogeneous server pools: `jmt_writer` emits serverTypesNames,
1552 // serverTypesNumOfServers and serverTypesCompatibilities (jmt_writer.h:1023
1553 // and :1138), so jsim simulates the pools rather than a station of the same
1554 // total size. Undeclared until 2026-08-22, which made this port refuse a
1555 // model its own writer serialises and the MATLAB, JAR and python SolverJMT
1556 // all accept.
1557 f.set(Feature::HeteroServers);
1558 f.set({Feature::Exp, Feature::Erlang, Feature::HyperExp, Feature::PH, Feature::APH,
1559 Feature::Coxian, Feature::Cox2});
1560 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Weibull,
1561 Feature::Uniform});
1562 f.set({Feature::MAP, Feature::MMPP2, Feature::Replayer, Feature::Trace, Feature::Immediate,
1563 Feature::Disabled});
1564 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_FCFS, Feature::SchedStrategy_PS,
1565 Feature::SchedStrategy_DPS, Feature::SchedStrategy_GPS, Feature::SchedStrategy_LPS});
1566 f.set({Feature::SchedStrategy_SIRO, Feature::SchedStrategy_HOL,
1567 Feature::SchedStrategy_FCFSPRIO});
1568 f.set({Feature::SchedStrategy_PSPRIO, Feature::SchedStrategy_DPSPRIO,
1569 Feature::SchedStrategy_GPSPRIO});
1570 f.set({Feature::SchedStrategy_LCFS, Feature::SchedStrategy_LCFSPR,
1571 Feature::SchedStrategy_LCFSPI, Feature::SchedStrategy_LCFSPRIO,
1572 Feature::SchedStrategy_LCFSPRPRIO, Feature::SchedStrategy_LCFSPIPRIO});
1573 f.set({Feature::SchedStrategy_FCFSPR, Feature::SchedStrategy_FCFSPI,
1574 Feature::SchedStrategy_FCFSPRPRIO, Feature::SchedStrategy_FCFSPIPRIO});
1575 // save_class_parallelism writes it and server_pools carries it, so the gate
1576 // was refusing a model this writer emits. The reference, the JAR and native
1577 // Python all declare it.
1578 f.set(Feature::ServerParallelism);
1579 f.set({Feature::SchedStrategy_SEPT, Feature::SchedStrategy_LEPT, Feature::SchedStrategy_SJF,
1580 Feature::SchedStrategy_LJF});
1581 f.set({Feature::SchedStrategy_SRPT, Feature::SchedStrategy_SRPTPRIO,
1582 Feature::SchedStrategy_POLLING, Feature::SchedStrategy_EXT});
1583 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND,
1584 Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
1585 Feature::RoutingStrategy_JSQ, Feature::RoutingStrategy_SQ});
1586 f.set({Feature::OpenClass, Feature::ClosedClass, Feature::SelfLoopingClass});
1587 f.set({Feature::Cache, Feature::CacheClassSwitcher});
1588 f.set({Feature::ReplacementStrategy_RR, Feature::ReplacementStrategy_FIFO,
1589 Feature::ReplacementStrategy_SFIFO, Feature::ReplacementStrategy_LRU});
1590 f.set({Feature::SetupDelayOff, Feature::Reneging, Feature::Balking});
1591 // Queue.setRetrial: the JSIM writer picks the retrial Queue constructor and
1592 // writes the per-class orbit delay, so jsim simulates the orbit rather than
1593 // losing the refused job. Batch arrivals are NOT declared, in the reference
1594 // either: the arrival-strategy writer has no batch element, so the stream
1595 // would be written single. SolverJMT.m:474-478.
1596 f.set(Feature::Retrial);
1597 // c-server stations (`save_number_of_servers`) and finite buffers
1598 // (`save_buffer_capacity` with the drop rule): the JSIM writer exports both,
1599 // and `io::jmt_buffer_capacity_refusal` keeps refusing the buffers JMT would
1600 // answer unconstrained (closed WAITQ, BBS, RSRD, RETRIAL_WITH_LIMIT). The
1601 // JMVA block below withdraws the buffer. SolverJMT.m:479-484.
1602 f.set({Feature::MultiServer, Feature::FiniteCapacity});
1603 // Limited load dependence reaches JMT only as a SERVER COUNT:
1604 // `save_number_of_servers` exports max(nservers, max alpha) and `write_jmva`
1605 // writes the matching <ldstation>. That is exact for alpha(n) = min(n,c) and
1606 // for nothing else, so `solver_jmt_run_analyzer` refuses any other scaling by name --
1607 // a featset cannot inspect the vector.
1608 f.set(Feature::LoadDependence);
1609
1610 // SOLVERJMT DRIVES TWO ENGINES, AND THEY ACCEPT DIFFERENT MODELS. Everything
1611 // above is the JSIM SIMULATOR's envelope, which is what 'default' and 'jsim'
1612 // run, the transient ensemble of 'default' included. The 'jmva.*' names run the JMVA ANALYTICAL engine, and
1613 // `io::write_jmva` emits for it only a station type (delay / load-independent
1614 // / load-dependent), a per-chain service demand, a per-chain visit count, the
1615 // class populations or arrival rates and a reference station. NOTHING ELSE IN
1616 // THE MODEL REACHES JMVA, so declaring the JSIM envelope for jmva was a
1617 // promise the writer could not keep: on a three-class LRU cache model all
1618 // eight closed-form jmva methods returned an entirely zero table with no
1619 // error, jmva.mva labelled 'exact' among them.
1620 //
1621 // The DISTRIBUTIONS stay: JMVA consumes a mean service demand, so any renewal
1622 // law with a finite mean is admissible, exactly as it is for mva_feature_set
1623 // and nc_feature_set. What goes is every construct whose effect is not
1624 // carried by (station type, demand, visits, population).
1625 if (method.compare(0, 4, "jmva") == 0) {
1626 for (Feature ff :
1627 {Feature::Cache, Feature::CacheClassSwitcher, Feature::ReplacementStrategy_RR,
1628 Feature::ReplacementStrategy_FIFO, Feature::ReplacementStrategy_SFIFO,
1629 Feature::ReplacementStrategy_LRU,
1630 // no fork element exists, and a visit ratio cannot express the join
1631 Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner,
1632 Feature::JoinPartial, Feature::ForkFanoutVector, Feature::ForkFanoutRandom,
1633 Feature::ForkBranchProbability,
1634 // no Petri-net counterpart, and no finite capacity region
1635 Feature::Place, Feature::Transition, Feature::Enabling, Feature::Inhibiting,
1636 Feature::Timing, Feature::Firing, Feature::Storage, Feature::Region,
1637 // no impatience element: the abandonment would simply not happen,
1638 // and no retrial element either: the orbit would be lost
1639 Feature::Reneging, Feature::Balking, Feature::Retrial,
1640 // the JMVA document has no capacity element at all
1641 Feature::FiniteCapacity,
1642 // server-side attributes the JMVA document has no slot for
1643 Feature::SetupDelayOff, Feature::ServerParallelism, Feature::HeteroServers,
1644 // the writer emits NO discipline, so a priority, weighted, size-based
1645 // or limited-sharing station would be solved as an ordinary
1646 // load-independent one. Only the four BCMP station types survive the
1647 // encoding, the same line nc_feature_set draws.
1648 Feature::SchedStrategy_DPS, Feature::SchedStrategy_GPS, Feature::SchedStrategy_HOL,
1649 Feature::SchedStrategy_FCFSPRIO, Feature::SchedStrategy_PSPRIO,
1650 Feature::SchedStrategy_DPSPRIO, Feature::SchedStrategy_GPSPRIO,
1651 Feature::SchedStrategy_LCFSPI, Feature::SchedStrategy_LCFSPIPRIO,
1652 Feature::SchedStrategy_LCFSPRIO, Feature::SchedStrategy_LCFSPRPRIO,
1653 Feature::SchedStrategy_FCFSPR, Feature::SchedStrategy_FCFSPI,
1654 Feature::SchedStrategy_FCFSPRPRIO, Feature::SchedStrategy_FCFSPIPRIO,
1655 Feature::SchedStrategy_SEPT, Feature::SchedStrategy_LEPT,
1656 Feature::SchedStrategy_SJF, Feature::SchedStrategy_LJF,
1657 Feature::SchedStrategy_SRPT, Feature::SchedStrategy_SRPTPRIO,
1658 Feature::SchedStrategy_LPS, Feature::SchedStrategy_POLLING,
1659 // the document carries MEAN visit counts, which is not what makes a
1660 // join-the-shortest-queue model behave as it does
1661 Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
1662 Feature::RoutingStrategy_JSQ, Feature::RoutingStrategy_SQ})
1663 f.unset(ff);
1664 // The eight closed-form algorithms are single-server ones
1665 // (`jmt::jmt_method_refusal` words it); exact MVA carries the count.
1666 if (jmva_is_closed_only(method))
1667 for (Feature ff :
1668 {Feature::OpenClass, Feature::LoadDependence, Feature::MultiServer})
1669 f.unset(ff);
1670 }
1671 return f;
1672}
1673
1674/**
1675 * SolverBA.getFeatureSet, transcribed name for name.
1676 *
1677 * WHAT MAKES A DISTRIBUTION ADMISSIBLE HERE IS ITS MEAN, which is why the list
1678 * of laws is longer than any product-form solver's and shorter than a
1679 * simulator's. `solver_ba_analyzer` reads `sn.rates` and `sn.visits` and nothing
1680 * else: every bound in the ABA/BJB/PB/GB/SB/Harel/MW families is a function of
1681 * the demands D = V./rates and the think time, so any renewal law with a finite
1682 * mean is admissible whatever its higher moments. `solver_ba_qrf_analyzer` is
1683 * the one that needs more, and what it needs is the {D0,D1} pair, which the
1684 * phase-type laws below carry; it refuses the rest through
1685 * `sn_to_qrf_blocking`'s own message rather than through this set.
1686 *
1687 * THE MODULATED PROCESSES (MAP, MMPP2, MMAP, BMAP) ARE DELIBERATELY OUT. Their
1688 * mean rate exists, so the utilization law still holds, but the bounds are
1689 * derived for a product-form network in which the correlation between successive
1690 * services does not exist: bounding such a model would report a bracket for a
1691 * DIFFERENT system rather than refuse it. The same reasoning keeps Cache,
1692 * Fork/Join and the Petri-net constructs out, none of which the analyzer
1693 * represents at all.
1694 *
1695 * THE STRUCTURAL NARROWING IS NOT HERE. `ba::list_valid_methods(L)` already
1696 * drops the reduction bounds off a model that is not a single-class closed
1697 * network of single servers, and the three open-network families off a closed
1698 * one; a flat feature set cannot express either, and duplicating the shape test
1699 * here is how the two drift apart.
1700 *
1701 * THE ONE ADDITION TO THE MATLAB LIST IS THE SUB-NODE SECTIONS, and it is not a
1702 * widening. `MNetwork.getUsedLangFeatures` emits no Buffer, Dispatcher, Server,
1703 * InfiniteServer, SharedServer, JobSink, RandomSource or ServiceTunnel -- its
1704 * node loop marks the distribution, the discipline and the routing and stops --
1705 * while `used_lang_features` here marks all of them for every Queue, Delay,
1706 * Source and Sink. Transcribing the MATLAB list literally therefore refused
1707 * EVERY model, an ordinary closed exponential network included, on `Buffer`;
1708 * `env_feature_set` and `ag_feature_set` carry the same eight names for the same
1709 * reason. The JAR has MATLAB's list and a JAR-side emitter that behaves like
1710 * this one, which is why `SolverAUTO.listValidMethods` there offers no 'ba.*'
1711 * method name on a model whose bounds SolverBA computes: the same defect, unfixed.
1712 */
1713/**
1714 * The family prefix of a bound method name: everything before the first dot.
1715 *
1716 * Local to this header so `ba_feature_set` can apply its per-method deltas
1717 * without reaching into `line/solvers/ba`, which includes this file.
1718 */
1719inline std::string ba_family_of(const std::string& method) {
1720 const std::string::size_type dot = method.find('.');
1721 return (dot == std::string::npos) ? method : method.substr(0, dot);
1722}
1723
1724/**
1725 * The 'default'/'auto'/'qr'/'lr' aliases, duplicated from `ba::resolve_method`
1726 * for the same reason `ba_family_of` sits here: this header is included BY the
1727 * solver and cannot include it back. The two are one line apiece and are
1728 * asserted equal in `cpp/tests/test_gate_ba.cpp`.
1729 */
1730inline std::string ba_resolve_method_name(const std::string& method) {
1731 if (method == "default") return "gb.upper";
1732 if (method == "auto") return "auto.upper";
1733 if (method == "lr") return "lr.upper";
1734 if (method == "qr") return "qrf.mmi";
1735 return method;
1736}
1737
1738inline FeatureSet ba_feature_set(const std::string& method) {
1739 FeatureSet f;
1740 f.set({Feature::ClassSwitch, Feature::Delay, Feature::DelayStation, Feature::Queue,
1741 Feature::Sink, Feature::Source, Feature::Router});
1742 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
1743 Feature::Buffer, Feature::Dispatcher, Feature::Server, Feature::JobSink,
1744 Feature::RandomSource, Feature::ServiceTunnel});
1745 // A self-looping class is a closed chain of one station, which the
1746 // demand-parameterized bounds read as any other chain (SolverBA.m:826-829).
1747 f.set({Feature::ClosedClass, Feature::SelfLoopingClass, Feature::OpenClass});
1748 f.set({Feature::APH, Feature::Coxian, Feature::Cox2, Feature::Erlang, Feature::Exp,
1749 Feature::HyperExp, Feature::PH});
1750 f.set({Feature::Det, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
1751 Feature::Weibull});
1752 // MAP and MMPP2 are declared HERE and withdrawn below from every family but
1753 // `mapamva`, which is the one bound derived for a CORRELATED arrival stream.
1754 // Neither name was in this set at all, so `mapamva.upper`/`.lower` -- both
1755 // advertised by `solver_ba_runner` and sieved by `ba::method_refusal` -- were
1756 // refused on the only models they exist for, and this port offered no BA
1757 // method whatever on a MAP model where the other three offer `mapamva` alone.
1758 f.set({Feature::MAP, Feature::MMPP2});
1759 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_FCFS,
1760 Feature::SchedStrategy_LCFSPR});
1761 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND});
1762 // Petri-net constructs, for the spnlp family (solver_ba_spnlp). They were
1763 // out on the "no representation" ground the rest of this set rests on, and
1764 // that ground is gone: the spnlp relaxation is indexed by the MARKING,
1765 // reads the enabling, inhibiting and firing arcs out of the node
1766 // parameters, and refuses by name the modes it cannot carry (immediate,
1767 // multi-server, marking-dependent, and phase-type on its Markovian side).
1768 // QueueingPlace is deliberately absent: a place with an embedded queue has
1769 // local state the relaxation has no variable for. Same division nc draws.
1770 f.set({Feature::Place, Feature::Transition, Feature::Linkage, Feature::Enabling,
1771 Feature::Inhibiting, Feature::Timing, Feature::Firing, Feature::Storage});
1772
1773 // THE PER-METHOD DELTAS, i.e. the premises a feature name CAN state. A
1774 // feature set says "I accept this construct", so it can refuse a model for
1775 // HAVING one and never for lacking one; that is exactly the shape of the
1776 // delay-station and closed-class premises below, and exactly not the shape
1777 // of "one class" or "one server", which have no feature name and are
1778 // refused structurally by `ba::method_refusal`. Judged on the RESOLVED name
1779 // so that 'default' carries the envelope of the gb.upper it runs as.
1780 //
1781 // DELAY STATIONS. 'sb' and 'lr' reject an infinite-server station outright,
1782 // and 'harel', 'sib' and 'scb' reject a nonzero think time, which on these
1783 // models is the same station: harel extrapolates the exact normalizing
1784 // constant of a delay-free network, SIB Section 3.2 is the extension that
1785 // would carry Z and is not implemented, and SCB Theorem 3 rests on the
1786 // delay-free balanced-network throughput. The three OPEN families reject
1787 // one too, each being derived for one server per station.
1788 //
1789 // CLASS TYPES. The three OPEN families drop ClosedClass, which is the whole
1790 // of their class premise. The MIRROR delta -- dropping OpenClass from every
1791 // demand-parameterized family -- is deliberately NOT applied: "supports
1792 // single-class closed networks only" is one rule, its single-class half has
1793 // no feature name, and splitting it across the two mechanisms would report
1794 // the closed half here and the single-class half in `ba::method_refusal`
1795 // for the same model. It is stated once, structurally. 'spnlp' takes no
1796 // delta at all: it is indexed by the marking, and whether that marking is
1797 // bounded is a question about the P-invariants of the net, which
1798 // `spn_lpbnd` answers.
1799 const std::string resolved = ba_resolve_method_name(method);
1800 const std::string fam = ba_family_of(resolved);
1801 if (fam == "bpt" || fam == "bgt" || fam == "snc") {
1802 f.unset(Feature::ClosedClass);
1803 f.unset(Feature::Delay);
1804 f.unset(Feature::DelayStation);
1805 f.unset(Feature::InfiniteServer);
1806 f.unset(Feature::SchedStrategy_INF);
1807 if (fam != "snc") {
1808 // SERVICE AND ARRIVAL LAWS. 'bpt' and 'bgt' are derived for a
1809 // MARKOVIAN open network and read the mean alone, so a
1810 // non-exponential law anywhere is not something they refuse at run
1811 // time -- it is something they silently bound as if it were
1812 // Poisson. Measured on the M/M/1 shape: replacing the Exp(1) source
1813 // by an Erlang of the same mean leaves bgt.upper at QLen 32.6667
1814 // and bpt.lower at 1.0, digit for digit. That is a bound on a
1815 // DIFFERENT system, so the laws are dropped here rather than left
1816 // to a run-time check the analyzers do not make: their own procid
1817 // test covers the queueing stations only and would miss exactly the
1818 // source case.
1819 //
1820 // 'snc' is excluded: it CONSUMES the arrival law (the same
1821 // substitution moves it from 3.8244 to 3.0092) and its analyzer
1822 // branches on a non-exponential source deliberately. Its rule is
1823 // about the SERVICE only, which no feature name can say, so it
1824 // lives in `ba::method_refusal` instead.
1825 f.unset(Feature::APH);
1826 f.unset(Feature::Coxian);
1827 f.unset(Feature::Cox2);
1828 f.unset(Feature::Erlang);
1829 f.unset(Feature::HyperExp);
1830 f.unset(Feature::PH);
1831 f.unset(Feature::Det);
1832 f.unset(Feature::Lognormal);
1833 f.unset(Feature::Pareto);
1834 f.unset(Feature::Uniform);
1835 f.unset(Feature::Weibull);
1836 }
1837 } else if (fam == "sb" || fam == "harel" || fam == "sib" || fam == "scb" || fam == "lr") {
1838 f.unset(Feature::Delay);
1839 f.unset(Feature::DelayStation);
1840 f.unset(Feature::InfiniteServer);
1841 f.unset(Feature::SchedStrategy_INF);
1842 }
1843 // THE CORRELATED-ARRIVAL BOUND, which is the one family whose premise a
1844 // feature name states positively. Every other bound reads the MEAN demand
1845 // alone, so a MAP or MMPP2 anywhere would be bounded as if it were Poisson --
1846 // a bracket for a DIFFERENT system, and silent. Written as a delta on the
1847 // complement rather than as a grant because a feature set can refuse a model
1848 // for HAVING a construct and never for lacking one.
1849 //
1850 // 'mapamva' takes the delay delta instead: its LP is a network of queues, and
1851 // Casale-Smirni name the delay extension as open work.
1852 if (fam == "mapamva") {
1853 f.unset(Feature::Delay);
1854 f.unset(Feature::DelayStation);
1855 f.unset(Feature::SchedStrategy_INF);
1856 } else {
1857 f.unset(Feature::MAP);
1858 f.unset(Feature::MMPP2);
1859 }
1860 // MULTISERVER, SolverBA.m:574-580, is out of the base envelope: every
1861 // demand-parameterized family reads one server per station (`ba::method_refusal`
1862 // names 'ssd' as the alternative), the alpha-free QRF arms refuse a c-server
1863 // station through `sn_to_qrf_alpha` and the open families through their own
1864 // refusal. What carries the count is granted here: 'ssd' (the multiserver
1865 // bound), 'ldac' (its fixed-rate form runs on the c-server rate law),
1866 // 'auto' (which picks among them) and the two load-dependent QRF arms, whose
1867 // alpha(i,n) IS min(n,c).
1868 if (fam == "ssd" || fam == "ldac" || fam == "auto" || resolved == "qrf.mmi.ld" ||
1869 resolved == "qrf.mmi.linear")
1870 f.set(Feature::MultiServer);
1871 // FINITECAPACITY, SolverBA.m:581-591: only the QRF blocking bounds carry the
1872 // buffer (the MM, MM1, ZZ, ZM, BB, F tables), which is the same split
1873 // `ba::ignores_blocking` makes; the structural refusal keeps naming them.
1874 // 'spnlp' is NOT granted, though `ignores_blocking` exempts it: its polytope
1875 // reads no Place capacity, so a capped place would be relaxed away.
1876 if (resolved.compare(0, 7, "qrf.bas") == 0 || resolved.compare(0, 8, "qrf.rsrd") == 0)
1877 f.set(Feature::FiniteCapacity);
1878 return f;
1879}
1880
1881/**
1882 * SolverLQNS.getFeatureSet on a flat Network (its qns methods), transcribed WHOLE.
1883 *
1884 * THE SECOND SET IN THIS FILE THAT IS NOT NARROWED, for `ldes_feature_set`'s
1885 * reason and no other: SolverLQNS implements no numerics. It writes the JMVA
1886 * document `qnsolver` reads, or converts the model through `qn2lqn` and hands it
1887 * to `lqns` -- the same two routes, through the same two external binaries, that
1888 * `solver_qns.m` drives. The code that has to support a declared feature is
1889 * therefore the binary the reference also runs, so narrowing here would refuse
1890 * models the reference answers with the very process this port is about to
1891 * start.
1892 *
1893 * WHERE THE REAL REFUSALS LIVE, all of them at solve time and none expressible
1894 * as a boolean over feature names:
1895 * lqns::check_qnsolver_supported a station that is not a Queue, a Delay or a
1896 * Source, and class priorities, on the JMVA
1897 * route only -- the layered route legitimately
1898 * carries Join nodes the document cannot encode
1899 * qn::check_binding_capacity a finite buffer, which neither binary models
1900 * the multiserver gate 'suri' and 'schmidt' reach LQNS and not
1901 * `qnsolver -m`
1902 *
1903 * SOURCE: `matlab/src/solvers/wrappers/LQNS/@@SolverLQNS/SolverLQNS.m`, name for
1904 * name. `Normal` is the one name dropped: it has a registry enumerator here, and
1905 * `write_jmva` reduces every law to its mean, so a Normal service is written as
1906 * its mean like any other -- but a Normal law admits negative samples and the
1907 * reference's own JMT writer refuses it (see `jmt_feature_set`), so declaring it
1908 * on this route alone would be the only place in the port that accepts it.
1909 */
1910inline FeatureSet lqns_network_feature_set(const std::string& /*method*/) {
1911 FeatureSet f;
1912 f.set({Feature::Sink, Feature::Source, Feature::Router, Feature::ClassSwitch,
1913 Feature::Delay, Feature::DelayStation, Feature::Queue});
1914 f.set({Feature::Fork, Feature::Join, Feature::Forker, Feature::Joiner});
1915 f.set({Feature::Logger, Feature::LogTunnel});
1916 f.set({Feature::Coxian, Feature::Cox2, Feature::APH, Feature::Erlang, Feature::Exp,
1917 Feature::HyperExp, Feature::PH});
1918 f.set({Feature::Det, Feature::Gamma, Feature::Lognormal, Feature::Pareto, Feature::Uniform,
1919 Feature::Weibull});
1920 f.set({Feature::MAP, Feature::MMPP2, Feature::Replayer, Feature::Trace});
1921 f.set({Feature::StatelessClassSwitcher, Feature::InfiniteServer, Feature::SharedServer,
1922 Feature::Buffer, Feature::Dispatcher, Feature::Server, Feature::JobSink,
1923 Feature::RandomSource, Feature::ServiceTunnel, Feature::Linkage});
1924 f.set({Feature::Enabling, Feature::Timing, Feature::Firing, Feature::Storage,
1925 Feature::Place, Feature::Transition});
1926 f.set({Feature::SchedStrategy_INF, Feature::SchedStrategy_PS, Feature::SchedStrategy_DPS,
1927 Feature::SchedStrategy_FCFS, Feature::SchedStrategy_GPS, Feature::SchedStrategy_SIRO,
1928 Feature::SchedStrategy_HOL, Feature::SchedStrategy_LCFS,
1929 Feature::SchedStrategy_LCFSPR});
1930 f.set({Feature::SchedStrategy_SEPT, Feature::SchedStrategy_LEPT, Feature::SchedStrategy_SJF,
1931 Feature::SchedStrategy_LJF, Feature::SchedStrategy_EXT});
1932 f.set({Feature::RoutingStrategy_PROB, Feature::RoutingStrategy_RAND,
1933 Feature::RoutingStrategy_RROBIN, Feature::RoutingStrategy_WRROBIN,
1934 Feature::RoutingStrategy_SQ});
1935 f.set({Feature::ClosedClass, Feature::OpenClass});
1936 // c-server stations: the JMVA document carries the count as an <ldstation>
1937 // and the LQN as a host multiplicity; 'suri' and 'schmidt' refuse one on the
1938 // qnsolver path, which stays structural (the multiserver gate in lqns_qnsolver.h).
1939 // FiniteCapacity is NOT declared, as in SolverLQNS.m's Network set: neither document
1940 // has a buffer, and `qn::check_binding_capacity` words the refusal.
1941 f.set(Feature::MultiServer);
1942 return f;
1943}
1944
1945} // namespace qn
1946} // namespace line
1947
1948#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