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