LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
feature_set.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_FEATURE_SET_H
6#define LINE_LANG_QN_FEATURE_SET_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * The language-feature gate: what a MODEL uses against what a SOLVER declares.
12 *
13 * SCOPE. This is the port of three MATLAB pieces that together form the single
14 * universal check every `runAnalyzer` performs before it computes anything:
15 *
16 * matlab/src/solvers/SolverFeatureSet.m the canonical name registry
17 * and the SUPPORTS comparison
18 * matlab/src/lang/@@MNetwork/getUsedLangFeatures.m the USED side
19 * matlab/src/solvers/@@NetworkSolver/NetworkSolver.m:169-216
20 * runAnalyzerChecks, which
21 * raises line_error naming the
22 * offending feature
23 *
24 * WHY IT EXISTS. Without it, every refusal is ad hoc and per solver, and the
25 * constructs nobody wrote a refusal for are neither handled nor rejected: the
26 * solver returns numbers for a model the user did not describe. A finite
27 * capacity region silently ignored is not a small error, it is a different
28 * model. A generic "unsupported model" is nearly as bad, because the user
29 * cannot tell which construct to remove, so `supports` returns the offending
30 * FEATURES and a reason naming them.
31 *
32 * THE ASYMMETRY THAT MATTERS. A feature DECLARED but not implemented is worse
33 * than one left undeclared: the undeclared one produces a clean refusal, the
34 * over-declared one produces a wrong number. So a C++ solver's declared set is
35 * what its C++ code actually handles, never a transcription of the MATLAB set.
36 *
37 * WHAT THE REGISTRY IS FOR. `Feature` lists every name in
38 * SolverFeatureSet.fields, including names this port can never emit. That is
39 * deliberate and is the reference's own convention: SUPPORTS iterates the
40 * registry, so a capability name MISSING from the registry is invisible to the
41 * gate and passes as if the capability were absent. Keeping the registry
42 * complete keeps that failure mode out.
43 *
44 * WHAT NetworkStruct CANNOT EXPRESS. `used_lang_features` derives each feature
45 * from a field that exists. Where MATLAB has a feature and NetworkStruct has no
46 * field for it, the feature is NEVER emitted, and that is recorded by name at
47 * the point where it would have been derived rather than faked from a proxy.
48 * The list is SetupDelayOff, Reneging, Balking, QueueingPlace and
49 * CacheRetrieval's MATLAB-only cousins; see the comments in
50 * used_lang_features. Breakdown LEFT that list on 2026-08-15, when
51 * `NetworkStruct::breakdownparam` gave it a field to be derived from.
52 */
53
54#include <cmath>
55#include <cstddef>
56#include <algorithm>
57#include <initializer_list>
58#include <sstream>
59#include <string>
60#include <vector>
61
64#include "line/util/error.h"
65
66namespace line {
67namespace qn {
68
69// ---------------------------------------------------------------------------
70// The canonical feature registry
71// ---------------------------------------------------------------------------
72
73/**
74 * SolverFeatureSet.fields, in its order, as an X-list.
75 *
76 * The order is the reference's and is kept so that the two lists can be
77 * diffed entry by entry; `supports` reports in this order too, which makes the
78 * reason string reproducible.
79 */
80#define LINE_QN_FEATURE_LIST(X) \
81 X(ClassSwitch) \
82 X(Cache) \
83 X(Delay) \
84 X(DelayStation) \
85 X(Fork) \
86 X(Join) \
87 X(Logger) \
88 X(Place) \
89 X(QueueingPlace) \
90 X(Queue) \
91 X(JobSink) \
92 X(Sink) \
93 X(Source) \
94 X(Router) \
95 X(Transition) \
96 X(Coxian) \
97 X(Cox2) \
98 X(APH) \
99 X(Det) \
100 X(Disabled) \
101 X(Erlang) \
102 X(Exp) \
103 X(Gamma) \
104 X(HyperExp) \
105 X(Immediate) \
106 X(Lognormal) \
107 X(MAP) \
108 X(DMAP) \
109 X(MMAP) \
110 X(BMAP) \
111 X(MMPP2) \
112 X(NHPP) \
113 X(MAPt) \
114 X(PHt) \
115 X(EmpiricalCdf) \
116 X(Expolynomial) \
117 X(Normal) \
118 X(Pareto) \
119 X(PH) \
120 X(ME) \
121 X(RAP) \
122 X(Replayer) \
123 X(Trace) \
124 X(Uniform) \
125 X(Weibull) \
126 X(Bernoulli) \
127 X(Binomial) \
128 X(Geometric) \
129 X(Poisson) \
130 X(DiscreteSampler) \
131 X(DiscreteUniform) \
132 X(Empirical) \
133 X(GMM) \
134 X(MMDP) \
135 X(MMDP2) \
136 X(MMPP) \
137 X(MultivariateNormal) \
138 X(NegBinomial) \
139 X(Prior) \
140 X(Zipf) \
141 X(StatelessClassSwitcher) \
142 X(CacheClassSwitcher) \
143 X(CacheRetrieval) \
144 X(CacheItemSize) \
145 X(InfiniteServer) \
146 X(Forker) \
147 X(Joiner) \
148 X(LogTunnel) \
149 X(SharedServer) \
150 X(Buffer) \
151 X(Region) \
152 X(Linkage) \
153 X(Enabling) \
154 X(Inhibiting) \
155 X(Timing) \
156 X(Firing) \
157 X(Storage) \
158 X(RandomSource) \
159 X(Dispatcher) \
160 X(Server) \
161 X(ServiceTunnel) \
162 X(RoutingStrategy_PROB) \
163 X(RoutingStrategy_RAND) \
164 X(RoutingStrategy_RROBIN) \
165 X(RoutingStrategy_WRROBIN) \
166 X(RoutingStrategy_JSQ) \
167 X(RoutingStrategy_SQ) \
168 X(RoutingStrategy_SDR) \
169 X(SchedStrategy_INF) \
170 X(SchedStrategy_FCFS) \
171 X(SchedStrategy_FCFSPR) \
172 X(SchedStrategy_FCFSPI) \
173 X(SchedStrategy_FCFSPRIO) \
174 X(SchedStrategy_FCFSPRPRIO) \
175 X(SchedStrategy_FCFSPIPRIO) \
176 X(SchedStrategy_LCFS) \
177 X(SchedStrategy_LCFSPR) \
178 X(SchedStrategy_LCFSPI) \
179 X(SchedStrategy_LCFSPRIO) \
180 X(SchedStrategy_LCFSPRPRIO) \
181 X(SchedStrategy_LCFSPIPRIO) \
182 X(SchedStrategy_SEPT) \
183 X(SchedStrategy_LEPT) \
184 X(SchedStrategy_SJF) \
185 X(SchedStrategy_LJF) \
186 X(SchedStrategy_SRPT) \
187 X(SchedStrategy_SRPTPRIO) \
188 X(SchedStrategy_PSJF) \
189 X(SchedStrategy_FB) \
190 X(SchedStrategy_LRPT) \
191 X(SchedStrategy_SETF) \
192 X(SchedStrategy_FSP) \
193 X(SchedStrategy_PAS) \
194 X(SchedStrategy_OI) \
195 X(SchedStrategy_PS) \
196 X(SchedStrategy_DPS) \
197 X(SchedStrategy_GPS) \
198 X(SchedStrategy_PSPRIO) \
199 X(SchedStrategy_DPSPRIO) \
200 X(SchedStrategy_GPSPRIO) \
201 X(SchedStrategy_SIRO) \
202 X(SchedStrategy_HOL) \
203 X(SchedStrategy_EXT) \
204 X(SchedStrategy_POLLING) \
205 X(SchedStrategy_EDD) \
206 X(SchedStrategy_EDF) \
207 X(SchedStrategy_LPS) \
208 X(ReplacementStrategy_RR) \
209 X(ReplacementStrategy_FIFO) \
210 X(ReplacementStrategy_SFIFO) \
211 X(ReplacementStrategy_LRU) \
212 X(ReplacementStrategy_HLRU) \
213 X(ReplacementStrategy_CLIMB) \
214 X(ReplacementStrategy_QLRU) \
215 X(ClosedClass) \
216 X(OpenClass) \
217 X(SelfLoopingClass) \
218 X(OpenSignal) \
219 X(ClosedSignal) \
220 X(SignalType_NEGATIVE) \
221 X(SignalType_REPLY) \
222 X(SignalType_CATASTROPHE) \
223 X(SignalBatchRemoval) \
224 X(SignalRemovalPolicy) \
225 X(BatchArrival) \
226 X(LoadDependence) \
227 X(ClassDependence) \
228 X(JointDependence) \
229 X(GlobalDependence) \
230 X(SetupDelayOff) \
231 X(ServerParallelism) \
232 X(Retrial) \
233 X(Balking) \
234 X(Reneging) \
235 X(Breakdown) \
236 X(HeteroServers) \
237 X(DepartureDiscipline) \
238 X(JoinPartial) \
239 X(Host) \
240 X(Processor) \
241 X(Task) \
242 X(Entry) \
243 X(Activity) \
244 X(SyncCall) \
245 X(AsyncCall) \
246 X(ActivityPrecedence_PRE_SEQ) \
247 X(ActivityPrecedence_POST_SEQ) \
248 X(ActivityPrecedence_PRE_AND) \
249 X(ActivityPrecedence_POST_AND) \
250 X(ActivityPrecedence_PRE_OR) \
251 X(ActivityPrecedence_POST_OR) \
252 X(SchedStrategy_REF) \
253 /* Layered cache-queueing models: a CacheTask holds a segmented cache whose
254 items are ItemEntry entries, and a read is a call to one of them whose
255 bound activity branches on a POST_CACHE precedence into hit and miss.
256 Registered in all four codebases because SolverLDES.getLNFeatureSet
257 DECLARES all three, and an unregistered name makes setTrue line_error --
258 so that whole feature set threw before it could compare anything. */ \
259 X(CacheTask) \
260 X(ItemEntry) \
261 X(ActivityPrecedence_POST_CACHE) \
262 /* Variable forking levels. A Fork emits tasksPerLink jobs on every
263 outgoing link; these three name the ways that degree stops being one
264 number. ForkFanoutVector: the count differs by destination or by class
265 (sn.nodeparam{f}.fanOutLink). ForkFanoutRandom: the count is a draw from
266 a DiscreteSampler, redrawn per link and per forked job
267 (sn.nodeparam{f}.fanOutDist). ForkBranchProbability: a branch fires only
268 with probability p, so the SIBLING COUNT is random even when each link
269 carries a fixed number (sn.nodeparam{f}.fanOutProb). Appended at the tail
270 so every earlier index is unchanged. */ \
271 X(ForkFanoutVector) \
272 X(ForkFanoutRandom) \
273 X(ForkBranchProbability) \
274 /* Ported from MATLAB SolverFeatureSet.m (2026-09-05), in this order, so
275 that every earlier enumerator is unchanged. Both are "having something"
276 properties the registry could not name, so every rule about them lived
277 only in structural predicates and was invisible to the gate.
278 MultiServer: a finite-server station serving more than one job at once,
279 i.e. a station whose nservers is finite and > 1 (a Delay / INF station is
280 not one), which `NetworkStruct::has_multi_server` already tests.
281 FiniteCapacity: a station or per-class buffer that can BIND, exactly the
282 condition `has_binding_capacity` tests (station cap / classcap below the
283 population that can reach it; an open class always binds; a Cache model
284 is exempt), which is also what `check_binding_capacity` refuses on. */ \
285 X(MultiServer) \
286 X(FiniteCapacity) \
287 X(MPH) \
288 X(MMAPt) \
289 X(MPHt) \
290 /* Marked + scheduled + BATCHED; see process_is_batch. */ \
291 X(BMMAPt)
292
293/** One language feature. COUNT is the registry size and is not a feature. */
294enum class Feature : int {
295#define LINE_QN_FEATURE_ENUM(id) id,
297#undef LINE_QN_FEATURE_ENUM
299};
300
301/** The number of registered features, MATLAB `numel(SolverFeatureSet.fields)`. */
302inline constexpr std::size_t feature_count() { return static_cast<std::size_t>(Feature::COUNT); }
303
304/** The canonical registry name, byte for byte the MATLAB field name. */
305inline const char* feature_name(Feature f) {
306 switch (f) {
307#define LINE_QN_FEATURE_NAME(id) \
308 case Feature::id: \
309 return #id;
311#undef LINE_QN_FEATURE_NAME
312 default:
313 return "Unknown";
314 }
315}
316
317/**
318 * A human-readable phrase for the reason string, e.g. "a Finite Capacity
319 * Region". Structured names are derived from their suffix so that the whole
320 * SchedStrategy_ / RoutingStrategy_ / ReplacementStrategy_ / SignalType_ /
321 * ActivityPrecedence_ families need no per-entry table to stay in step.
322 */
323inline std::string feature_phrase(Feature f) {
324 switch (f) {
325 case Feature::Region: return "a Finite Capacity Region";
326 case Feature::Fork: return "a Fork node";
327 case Feature::Join: return "a Join node";
328 case Feature::Forker: return "a fork output section";
329 case Feature::ForkFanoutVector:
330 return "a fork whose tasks per link differ by destination or by class";
331 case Feature::ForkFanoutRandom:
332 return "a fork whose tasks per link are drawn from a distribution";
333 case Feature::ForkBranchProbability:
334 return "a fork whose branches fire only with a probability";
335 case Feature::Joiner: return "a join input section";
336 case Feature::Cache: return "a Cache node";
337 case Feature::CacheRetrieval: return "a delayed-hit cache retrieval system";
338 case Feature::CacheItemSize: return "per-item storage costs with per-list cost caps";
339 case Feature::ClassSwitch: return "a ClassSwitch node";
340 case Feature::Router: return "a Router node";
341 case Feature::Logger: return "a Logger node";
342 case Feature::Source: return "a Source node";
343 case Feature::Sink: return "a Sink node";
344 case Feature::Place: return "an SPN Place";
345 case Feature::QueueingPlace: return "a queueing SPN Place";
346 case Feature::Transition: return "an SPN Transition";
347 case Feature::Inhibiting: return "an SPN inhibitor arc";
348 case Feature::Retrial: return "a retrial orbit";
349 case Feature::Balking: return "job balking";
350 case Feature::Reneging: return "job reneging";
351 case Feature::Breakdown: return "server breakdowns";
352 case Feature::SetupDelayOff: return "server setup and delay-off times";
353 case Feature::LoadDependence: return "load-dependent service rates";
354 case Feature::ClassDependence: return "class-dependent service rates";
355 case Feature::JointDependence: return "joint-dependent service rates";
356 case Feature::GlobalDependence: return "globally state-dependent (Whittle) service rates";
357 case Feature::BatchArrival: return "batch arrivals";
358 case Feature::OpenSignal: return "an open G-network signal class";
359 case Feature::ClosedSignal: return "a closed G-network signal class";
360 case Feature::SignalBatchRemoval: return "a signal batch-removal distribution";
361 case Feature::SignalRemovalPolicy: return "a non-random signal removal policy";
362 case Feature::OpenClass: return "an open job class";
363 case Feature::ClosedClass: return "a closed job class";
364 case Feature::SelfLoopingClass: return "a self-looping job class";
365 case Feature::Queue: return "a Queue station";
366 case Feature::Delay:
367 case Feature::DelayStation: return "a Delay station";
368 // "multi-server" is the vocabulary every other refusal in this tree uses
369 // (jmt_method_refusal, ba, ssa nrm), and the gate reports must agree with them.
370 case Feature::MultiServer: return "a multi-server station";
371 case Feature::FiniteCapacity: return "a finite station or per-class buffer that binds";
372 default: break;
373 }
374 const std::string nm = feature_name(f);
375 const std::size_t us = nm.find('_');
376 if (us != std::string::npos) {
377 const std::string head = nm.substr(0, us), tail = nm.substr(us + 1);
378 if (head == "SchedStrategy") return "the " + tail + " scheduling discipline";
379 if (head == "RoutingStrategy") return "the " + tail + " routing strategy";
380 if (head == "ReplacementStrategy") return "the " + tail + " cache replacement policy";
381 if (head == "SignalType") return tail + " signals";
382 if (head == "ActivityPrecedence") return "the " + tail + " activity precedence";
383 }
384 return "the " + nm + " feature";
385}
386
387// ---------------------------------------------------------------------------
388// The set
389// ---------------------------------------------------------------------------
390
391/** A subset of the registry: MATLAB's SolverFeatureSet, whose `list` is a flag per field. */
393public:
394 FeatureSet() : bits_(feature_count(), false) {}
395
397 if (f != Feature::COUNT) bits_[static_cast<std::size_t>(f)] = true;
398 return *this;
399 }
400 FeatureSet& set(std::initializer_list<Feature> fs) {
401 for (Feature f : fs) set(f);
402 return *this;
403 }
405 if (f != Feature::COUNT) bits_[static_cast<std::size_t>(f)] = false;
406 return *this;
407 }
408 bool has(Feature f) const {
409 return f != Feature::COUNT && bits_[static_cast<std::size_t>(f)];
410 }
411 /** The features held, in registry order. */
412 std::vector<Feature> list() const {
413 std::vector<Feature> out;
414 for (std::size_t i = 0; i < bits_.size(); ++i)
415 if (bits_[i]) out.push_back(static_cast<Feature>(i));
416 return out;
417 }
418 bool empty() const {
419 for (std::size_t i = 0; i < bits_.size(); ++i)
420 if (bits_[i]) return false;
421 return true;
422 }
423
424private:
425 std::vector<bool> bits_;
426};
427
428/**
429 * The registry name a specialization falls back to when it is not declared.
430 *
431 * Some registry names denote a SPECIAL CASE of another name rather than a
432 * capability of their own: a Cox2 is a Coxian restricted to two phases, and a
433 * Trace is a Replayer under another class name. Recording only the general name
434 * left the specific entry unreachable -- dead registry surface that no model
435 * could ever set (see _kb/06-solver-catalog.md, "A registered name nothing
436 * emits gates nothing"). Recording the specific name
437 * instead would silently REJECT those models at every solver that declares only
438 * the general one, which is every solver that accepts them today.
439 *
440 * So the recorder emits the most specific name it can, and the gate resolves an
441 * undeclared specific name against its generalization here. A solver that
442 * genuinely supports only the special case (two-phase Coxian, say) keeps the
443 * option of declaring the specialization alone: the fallback is consulted only
444 * when the specific name is missing, never in the other direction.
445 *
446 * COUNT means "no generalization"; the feature stands on its own.
447 */
449 switch (f) {
450 case Feature::Cox2: return Feature::Coxian;
451 // Trace is emitted by MATLAB, the JAR and python, which each carry a
452 // Trace class distinct from Replayer. This port has no such type -- the
453 // JSON wire writes both as "Replayer" -- so the entry stays inert here
454 // and the fallback is kept only so the four tables read alike.
455 case Feature::Trace: return Feature::Replayer;
456 default: return Feature::COUNT;
457 }
458}
459
460/** The outcome of the gate: the verdict, the offending features, the message. */
462 bool ok = true;
463 std::vector<Feature> missing; ///< used but not declared, in registry order
464 std::string reason; ///< empty when ok
465};
466
467/**
468 * SolverFeatureSet.supports: is every feature the model uses declared?
469 *
470 * `solver` names the solver in the reason so the message is actionable on its
471 * own; MATLAB gets that name from the mfilename of the raising runAnalyzer.
472 */
473inline SupportResult feature_set_supports(const std::string& solver, const FeatureSet& declared,
474 const FeatureSet& used) {
476 for (std::size_t i = 0; i < feature_count(); ++i) {
477 const Feature f = static_cast<Feature>(i);
478 if (!used.has(f) || declared.has(f)) continue;
479 // A specialization the solver did not name is covered by the general
480 // capability when that one IS declared; see feature_generalization.
481 const Feature g = feature_generalization(f);
482 if (g != Feature::COUNT && declared.has(g)) continue;
483 r.missing.push_back(f);
484 }
485 if (r.missing.empty()) return r;
486 r.ok = false;
487 if (r.missing.size() == 1) {
488 r.reason = solver + ": this model uses " + feature_phrase(r.missing[0]) +
489 ", which this solver does not support";
490 } else {
491 r.reason = solver + ": this model uses features which this solver does not support: ";
492 for (std::size_t i = 0; i < r.missing.size(); ++i) {
493 if (i) r.reason += ", ";
494 r.reason += feature_phrase(r.missing[i]);
495 }
496 }
497 return r;
498}
499
500// ---------------------------------------------------------------------------
501// Enum to feature
502// ---------------------------------------------------------------------------
503
504/**
505 * The feature name of a distribution, MATLAB `serviceProcess{r}{3}.name`.
506 *
507 * Immediate and Disabled return COUNT: they are internal placeholders, not
508 * user-facing distributions, and getUsedLangFeatures excludes them by name.
509 * Expolynomial and Normal have no ProcessType in MATLAB either, so those two
510 * registry entries can never be emitted by this port.
511 */
513 switch (p) {
514 case ProcessType::EXP: return Feature::Exp;
515 case ProcessType::ERLANG: return Feature::Erlang;
516 case ProcessType::HYPEREXP: return Feature::HyperExp;
517 case ProcessType::PH: return Feature::PH;
518 case ProcessType::APH: return Feature::APH;
519 case ProcessType::MAP: return Feature::MAP;
520 case ProcessType::UNIFORM: return Feature::Uniform;
521 case ProcessType::DET: return Feature::Det;
522 case ProcessType::COXIAN: return Feature::Coxian;
523 case ProcessType::GAMMA: return Feature::Gamma;
524 case ProcessType::PARETO: return Feature::Pareto;
525 case ProcessType::MMPP2: return Feature::MMPP2;
526 case ProcessType::REPLAYER: return Feature::Replayer;
527 case ProcessType::COX2: return Feature::Cox2;
528 case ProcessType::WEIBULL: return Feature::Weibull;
529 case ProcessType::LOGNORMAL: return Feature::Lognormal;
530 case ProcessType::DUNIFORM: return Feature::DiscreteUniform;
531 case ProcessType::BERNOULLI: return Feature::Bernoulli;
532 case ProcessType::BINOMIAL: return Feature::Binomial;
533 case ProcessType::POISSON: return Feature::Poisson;
534 case ProcessType::GEOMETRIC: return Feature::Geometric;
535 case ProcessType::BMAP: return Feature::BMAP;
536 case ProcessType::ME: return Feature::ME;
537 case ProcessType::RAP: return Feature::RAP;
538 case ProcessType::DISCRETESAMPLER: return Feature::DiscreteSampler;
539 case ProcessType::ZIPF: return Feature::Zipf;
540 case ProcessType::DMAP: return Feature::DMAP;
541 case ProcessType::MMAP: return Feature::MMAP;
542 case ProcessType::EMPIRICALCDF: return Feature::EmpiricalCdf;
543 // A Prior IS user-facing, unlike Immediate and Disabled, and emitting it
544 // is what makes every solver but SolverUQ refuse the model instead of
545 // lowering the epistemic mixture to a rate and answering with it.
546 case ProcessType::PRIOR: return Feature::Prior;
547 case ProcessType::NHPP: return Feature::NHPP;
548 case ProcessType::MAPT: return Feature::MAPt;
549 case ProcessType::PHT: return Feature::PHt;
550 case ProcessType::MPH: return Feature::MPH;
551 case ProcessType::MMAPT: return Feature::MMAPt;
552 case ProcessType::MPHT: return Feature::MPHt;
553 case ProcessType::BMMAPT: return Feature::BMMAPt;
554 default: return Feature::COUNT;
555 }
556}
557
558/**
559 * SchedStrategy.toFeature. FORK and NONE are internal markers with no registry
560 * name; MATLAB aliases FCFSPRIO to HOL, so HOL covers both.
561 */
563 switch (s) {
564 case SchedStrategy::INF: return Feature::SchedStrategy_INF;
565 case SchedStrategy::FCFS: return Feature::SchedStrategy_FCFS;
566 case SchedStrategy::FCFSPR: return Feature::SchedStrategy_FCFSPR;
567 case SchedStrategy::FCFSPI: return Feature::SchedStrategy_FCFSPI;
568 case SchedStrategy::FCFSPRPRIO: return Feature::SchedStrategy_FCFSPRPRIO;
569 case SchedStrategy::FCFSPIPRIO: return Feature::SchedStrategy_FCFSPIPRIO;
570 case SchedStrategy::LCFS: return Feature::SchedStrategy_LCFS;
571 case SchedStrategy::LCFSPR: return Feature::SchedStrategy_LCFSPR;
572 case SchedStrategy::LCFSPI: return Feature::SchedStrategy_LCFSPI;
573 case SchedStrategy::LCFSPRIO: return Feature::SchedStrategy_LCFSPRIO;
574 case SchedStrategy::LCFSPRPRIO: return Feature::SchedStrategy_LCFSPRPRIO;
575 case SchedStrategy::LCFSPIPRIO: return Feature::SchedStrategy_LCFSPIPRIO;
576 case SchedStrategy::SEPT: return Feature::SchedStrategy_SEPT;
577 case SchedStrategy::LEPT: return Feature::SchedStrategy_LEPT;
578 case SchedStrategy::SJF: return Feature::SchedStrategy_SJF;
579 case SchedStrategy::LJF: return Feature::SchedStrategy_LJF;
580 case SchedStrategy::SRPT: return Feature::SchedStrategy_SRPT;
581 case SchedStrategy::SRPTPRIO: return Feature::SchedStrategy_SRPTPRIO;
582 case SchedStrategy::PSJF: return Feature::SchedStrategy_PSJF;
583 case SchedStrategy::FB: return Feature::SchedStrategy_FB;
584 case SchedStrategy::LRPT: return Feature::SchedStrategy_LRPT;
585 case SchedStrategy::SETF: return Feature::SchedStrategy_SETF;
586 case SchedStrategy::FSP: return Feature::SchedStrategy_FSP;
587 case SchedStrategy::PAS: return Feature::SchedStrategy_PAS;
588 case SchedStrategy::OI: return Feature::SchedStrategy_OI;
589 case SchedStrategy::PS: return Feature::SchedStrategy_PS;
590 case SchedStrategy::DPS: return Feature::SchedStrategy_DPS;
591 case SchedStrategy::GPS: return Feature::SchedStrategy_GPS;
592 case SchedStrategy::PSPRIO: return Feature::SchedStrategy_PSPRIO;
593 case SchedStrategy::DPSPRIO: return Feature::SchedStrategy_DPSPRIO;
594 case SchedStrategy::GPSPRIO: return Feature::SchedStrategy_GPSPRIO;
595 case SchedStrategy::SIRO: return Feature::SchedStrategy_SIRO;
596 case SchedStrategy::HOL: return Feature::SchedStrategy_HOL;
597 case SchedStrategy::EXT: return Feature::SchedStrategy_EXT;
598 case SchedStrategy::POLLING: return Feature::SchedStrategy_POLLING;
599 case SchedStrategy::EDD: return Feature::SchedStrategy_EDD;
600 case SchedStrategy::EDF: return Feature::SchedStrategy_EDF;
601 case SchedStrategy::LPS: return Feature::SchedStrategy_LPS;
602 case SchedStrategy::REF: return Feature::SchedStrategy_REF;
603 default: return Feature::COUNT;
604 }
605}
606
607/**
608 * RoutingStrategy.toFeature. FIRING and DISABLED are internal markers, not
609 * user-selectable capabilities, and map to no registry name (RoutingStrategy.m:132-139).
610 */
612 switch (r) {
613 case RoutingStrategy::PROB: return Feature::RoutingStrategy_PROB;
614 case RoutingStrategy::RAND: return Feature::RoutingStrategy_RAND;
615 case RoutingStrategy::RROBIN: return Feature::RoutingStrategy_RROBIN;
616 case RoutingStrategy::WRROBIN: return Feature::RoutingStrategy_WRROBIN;
617 case RoutingStrategy::JSQ: return Feature::RoutingStrategy_JSQ;
618 case RoutingStrategy::SQ: return Feature::RoutingStrategy_SQ;
619 case RoutingStrategy::SDR: return Feature::RoutingStrategy_SDR;
620 default: return Feature::COUNT;
621 }
622}
623
624/** ReplacementStrategy.toFeature. */
626 switch (s) {
627 case ReplacementStrategy::RR: return Feature::ReplacementStrategy_RR;
628 case ReplacementStrategy::FIFO: return Feature::ReplacementStrategy_FIFO;
629 case ReplacementStrategy::SFIFO: return Feature::ReplacementStrategy_SFIFO;
630 case ReplacementStrategy::LRU: return Feature::ReplacementStrategy_LRU;
631 case ReplacementStrategy::HLRU: return Feature::ReplacementStrategy_HLRU;
632 case ReplacementStrategy::CLIMB: return Feature::ReplacementStrategy_CLIMB;
633 case ReplacementStrategy::QLRU: return Feature::ReplacementStrategy_QLRU;
634 default: return Feature::COUNT;
635 }
636}
637
638/**
639 * The SERVER section a Queue gets for a discipline, MATLAB Queue.m:79-108.
640 *
641 * The section class name is itself a feature: MNetwork.addNode registers
642 * class(node.server) alongside class(node), which is where SharedServer,
643 * InfiniteServer and Server enter the used set. PreemptiveServer and
644 * PollingServer are absent from the registry, so those disciplines contribute
645 * only their SchedStrategy_ name, exactly as in the reference.
646 */
648 switch (s) {
649 case SchedStrategy::PS:
650 case SchedStrategy::DPS:
651 case SchedStrategy::GPS:
652 case SchedStrategy::PSPRIO:
653 case SchedStrategy::DPSPRIO:
654 case SchedStrategy::GPSPRIO:
655 case SchedStrategy::LPS: return Feature::SharedServer;
656 case SchedStrategy::INF: return Feature::InfiniteServer;
657 case SchedStrategy::LCFSPR:
658 case SchedStrategy::LCFSPRPRIO:
659 case SchedStrategy::LCFSPI:
660 case SchedStrategy::LCFSPIPRIO:
661 case SchedStrategy::FCFSPR:
662 case SchedStrategy::FCFSPRPRIO:
663 case SchedStrategy::FCFSPI:
664 case SchedStrategy::FCFSPIPRIO:
665 case SchedStrategy::EDF: return Feature::COUNT; // PreemptiveServer, unregistered
666 case SchedStrategy::POLLING: return Feature::COUNT; // PollingServer, unregistered
667 default: return Feature::Server;
668 }
669}
670
671// ---------------------------------------------------------------------------
672// The used side
673// ---------------------------------------------------------------------------
674
675/**
676 * getUsedLangFeatures: the features the MODEL uses.
677 *
678 * MATLAB derives them from the Network OBJECT, where a node still knows its
679 * MATLAB class and its section classes; this port derives them from the
680 * refreshed struct, which is the only thing a C++ solver runner holds. The two
681 * agree because the section classes are a pure function of the node kind and
682 * the discipline (Queue.m:48-108, Source.m:66-68, Cache.m:54-76, Fork.m:106-109,
683 * Join.m:33-35, Place.m:27-31, Transition.m:29-36), which is what
684 * feature_of_server_section and the node switch below encode.
685 *
686 * FEATURES THIS PORT CANNOT EMIT, because NetworkStruct carries no field for
687 * them. Each is a real MATLAB feature, and a model using one reaches a C++
688 * solver unflagged; the fix is a struct field, not a proxy here.
689 * SetupDelayOff MATLAB Queue.setupTime / delayoffTime, carried by
690 * NetworkStruct::setupparam and solved by solver_mam_basic.
691 * Reneging MATLAB Queue.impatienceTypes. state_events.h takes the
692 * reneging classes as a CALL PARAMETER, so the struct does
693 * not record them and they cannot be derived here.
694 * Balking MATLAB Queue.balkingStrategies. No struct field.
695 * QueueingPlace was on this list until 2026-09-16, when `add_place(nm, sched)`
696 * gave a Place a discipline and `is_queueing_place` derived the flag from the
697 * service law -- which is `Place.queueing`'s own trigger, `setService` -- so it
698 * needs no flag of its own and is emitted below like any field-backed feature.
699 * Breakdown was on this list until 2026-08-15, when
700 * `NetworkStruct::breakdownparam` was added for the LDES engine; it is derived
701 * below like any other field-backed feature. `state_events.h` still takes the
702 * breakdown nodes as a call parameter, which is why the CTMC declaration of it
703 * stays inert -- the two are independent.
704 *
705 * FEATURES EMITTED HERE THAT MATLAB DOES NOT EMIT. LoadDependence is in the
706 * registry and is declared by solvers, but getUsedLangFeatures never set it, so
707 * in MATLAB it was unenforced. Deriving it from `lldscaling` is a strict
708 * tightening: the solvers that declare it are unaffected, and the ones that do
709 * not now refuse instead of silently dropping the scaling. Retrial was in this
710 * paragraph until 2026-09-05, when MATLAB's recorder gained the same arm.
711 */
712template <class T>
713bool has_binding_capacity(const NetworkStruct<T>& sn);
714
715template <class T>
717 FeatureSet u;
718 const std::size_t R = sn.classes.size();
719
720 for (std::size_t r = 0; r < R; ++r) {
721 u.set(sn.classes[r].type == JobClassType::CLOSED ? Feature::ClosedClass
722 : Feature::OpenClass);
723 // A self-looping class is a closed class whose routing returns to its own
724 // reference station. It was declared by eight solvers and never marked, so
725 // a solver that omitted the name (the bounds, RCAT, LQNS) was never asked;
726 // it is now refused unless it declares the name.
727 if (sn.classes[r].self_looping) u.set(Feature::SelfLoopingClass);
728 if (r < sn.issignal.size() && sn.issignal[r]) {
729 // Open vs closed signal follows the class type; MATLAB resolves the
730 // unresolved Signal placeholder by the presence of a Source, which
731 // is the same decision the refresh has already made here.
732 u.set(sn.classes[r].type == JobClassType::CLOSED ? Feature::ClosedSignal
733 : Feature::OpenSignal);
734 if (r < sn.signaltype.size()) {
735 switch (sn.signaltype[r]) {
736 case lang::SignalType::NEGATIVE: u.set(Feature::SignalType_NEGATIVE); break;
737 case lang::SignalType::REPLY: u.set(Feature::SignalType_REPLY); break;
739 u.set(Feature::SignalType_CATASTROPHE);
740 break;
741 }
742 }
743 if (r < sn.signalremdist.size() && !sn.signalremdist[r].empty())
744 u.set(Feature::SignalBatchRemoval);
745 if (r < sn.signalrempolicy.size() &&
746 sn.signalrempolicy[r] != lang::RemovalPolicy::RANDOM)
747 u.set(Feature::SignalRemovalPolicy);
748 }
749 }
750
751 // Globally state-dependent scaling phi(n) over the full network state, the
752 // Whittle primitive. SolverCTMC, SolverSSA and SolverLDES plumb it, so every
753 // other solver must reject the model rather than solve it unscaled.
754 if (static_cast<bool>(sn.gdscaling)) u.set(Feature::GlobalDependence);
755
756 if (!sn.regions.empty()) u.set(Feature::Region);
757 if (sn.sourceIdx != 0) u.set(Feature::Source);
758 if (sn.sinkNode != 0) u.set(Feature::Sink);
759 if (!sn.fj.empty()) u.set({Feature::Fork, Feature::Forker, Feature::Join, Feature::Joiner});
760
761 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
762 const NodeDef& nd = sn.nodes[i];
763 const std::size_t ind = i + 1;
764 const std::size_t ist = nd.station; // 1-based, 0 when the node serves no jobs
765
766 switch (nd.nodetype) {
767 case NodeType::Queue:
768 case NodeType::Delay: {
769 u.set({nd.nodetype == NodeType::Delay ? Feature::Delay : Feature::Queue,
770 Feature::Buffer, Feature::Dispatcher});
771 if (ist == 0) break;
772 const Station<T>& st = sn.stations[ist - 1];
774 bool any_class = false;
775 for (std::size_t r = 0; r < R; ++r) {
776 // A disabled pair is MATLAB's empty serviceProcess: it emits
777 // no distribution, no discipline and no routing.
778 if (ist - 1 < sn.disabled.size() && r < sn.disabled[ist - 1].size() &&
779 sn.disabled[ist - 1][r])
780 continue;
781 any_class = true;
782 if (ist - 1 < sn.service.size() && r < sn.service[ist - 1].size())
783 u.set(feature_of_process(sn.service[ist - 1][r].type));
784 if (r < nd.routing.size()) u.set(feature_of_routing(nd.routing[r]));
785 }
786 if (any_class) u.set(feature_of_sched(st.sched));
787 if (!st.lldscaling.empty()) u.set(Feature::LoadDependence);
788 if (static_cast<bool>(st.cdscaling)) u.set(Feature::ClassDependence);
789 if (static_cast<bool>(st.jdscaling)) u.set(Feature::JointDependence);
790 // Impatience, balking and heterogeneous pools. MATLAB's
791 // getUsedLangFeatures emits Balking and Reneging from the same
792 // declarations; HeteroServers has no MATLAB counterpart and is
793 // emitted here for the same reason Retrial and LoadDependence
794 // are -- a solver that ignores the pools would answer for the
795 // homogeneous station and say nothing about the difference.
796 for (std::size_t r = 0; r < R; ++r) {
797 if (r < st.impatience.size() && st.impatience[r] != lang::ImpatienceType::NONE)
798 u.set(st.impatience[r] == lang::ImpatienceType::BALKING ? Feature::Balking
799 : Feature::Reneging);
800 if (r < st.orbit_impatience.size() && !st.orbit_impatience[r].disabled)
801 u.set(Feature::Reneging);
802 if (r < st.balking.size() &&
803 st.balking[r].strategy != lang::BalkingStrategy::NONE)
804 u.set(Feature::Balking);
805 }
806 if (!st.server_types.empty()) u.set(Feature::HeteroServers);
807 for (std::size_t n : st.server_parallelism)
808 if (n > 1) { u.set(Feature::ServerParallelism); break; }
809 break;
810 }
811 case NodeType::Source: {
812 u.set({Feature::Source, Feature::RandomSource, Feature::ServiceTunnel,
813 Feature::Dispatcher});
814 // The Source branch emits the arrival distributions only: MATLAB
815 // registers no discipline and no routing for it.
816 if (ist == 0 || ist - 1 >= sn.service.size()) break;
817 for (std::size_t r = 0; r < R && r < sn.service[ist - 1].size(); ++r)
818 u.set(feature_of_process(sn.service[ist - 1][r].type));
819 // A batch arrival releases several jobs per epoch, which no
820 // single-arrival event handler produces.
821 for (const Distrib<T>& b : sn.stations[ist - 1].arrival_batch)
822 if (!b.disabled) { u.set(Feature::BatchArrival); break; }
823 break;
824 }
825 case NodeType::Sink:
826 u.set(Feature::Sink);
827 break;
828 case NodeType::Router: {
829 // A ROUTER EMITS ITS ROUTING STRATEGY AND NOTHING ELSE. All
830 // three reference codebases agree and this port was the only
831 // one that did not: getUsedLangFeatures.m:77-78 (case 'Router'),
832 // Network.java:3184-3190 and network.py:1983-1987 each record
833 // RoutingStrategy.toFeature(...) alone. This branch additionally
834 // emitted Router, Buffer, Dispatcher and ServiceTunnel, and
835 // since neither mva_feature_set nor nc_feature_set declares
836 // Router -- correctly, SolverMVA.m:231-257 does not either --
837 // every model with a dispatcher was refused by the two solvers
838 // MATLAB and python solve it with. The whole cluster family
839 // (Network.cluster/clusterClosed/clusterMixed) is exactly that
840 // shape, and the Router is eliminated by the stochastic
841 // complement long before any solver sees it.
842 for (std::size_t r = 0; r < R && r < nd.routing.size(); ++r)
844 break;
845 }
846 case NodeType::ClassSwitch:
847 u.set({Feature::ClassSwitch, Feature::StatelessClassSwitcher, Feature::Buffer,
848 Feature::Dispatcher});
849 break;
850 case NodeType::Cache: {
851 u.set({Feature::Cache, Feature::CacheClassSwitcher, Feature::Buffer,
852 Feature::Dispatcher});
853 auto it = sn.nodeparam.find(ind);
854 if (it == sn.nodeparam.end()) break;
855 u.set(feature_of_replacement(it->second.replacestrat));
856 if (it->second.retrieval_capacity > 0 || !it->second.retrieval_classes.empty())
857 u.set(Feature::CacheRetrieval);
858 if (!it->second.costcap.empty()) u.set(Feature::CacheItemSize);
859 break;
860 }
861 case NodeType::Fork: {
862 u.set({Feature::Fork, Feature::Forker, Feature::Buffer, Feature::ServiceTunnel});
863 // Variable forking levels. A name declared but never marked is
864 // a name no solver can ever refuse, so all three are marked
865 // here. A plain fork has no override block at all, so this
866 // costs one map lookup in the classic case.
867 const qn::ForkParam<T>* fp = sn.fork_param_of(i);
868 if (fp != 0) {
869 const double tpl = sn.nodes[i - 1].tasks_per_link;
870 bool vector_valued = false, random_valued = false, probabilistic = false;
871 for (std::size_t k = 0; k < fp->fan_out_link.rows(); ++k)
872 for (std::size_t r = 0; r < fp->fan_out_link.cols(); ++r) {
873 const double p = num_traits<T>::to_double(fp->fan_out_prob(k, r));
874 if (p == 0.0) continue; // link this class does not take
875 if (p != 1.0) probabilistic = true;
876 if (num_traits<T>::to_double(fp->fan_out_link(k, r)) != tpl)
877 vector_valued = true;
878 if (!fp->fan_out_dist[k][r].disabled) random_valued = true;
879 }
880 if (vector_valued) u.set(Feature::ForkFanoutVector);
881 if (random_valued) u.set(Feature::ForkFanoutRandom);
882 if (probabilistic) u.set(Feature::ForkBranchProbability);
883 }
884 break;
885 }
886 case NodeType::Join:
887 u.set({Feature::Join, Feature::Joiner, Feature::Dispatcher,
888 Feature::ServiceTunnel});
889 break;
890 case NodeType::Logger:
891 u.set({Feature::Logger, Feature::LogTunnel, Feature::Buffer, Feature::Dispatcher});
892 break;
893 case NodeType::Place:
894 u.set({Feature::Place, Feature::Storage, Feature::Linkage,
895 Feature::ServiceTunnel});
896 // A QUEUEING PLACE, i.e. one `setService` gave a law to. The
897 // three reference recorders emit this name and NOTHING else for
898 // it -- not the discipline, not the service distribution, not
899 // the server section `installQueueServer` swapped in -- and
900 // this port must not emit more: a feature set refuses a model
901 // for HAVING a name, so a name MATLAB does not raise is a model
902 // this port alone would turn away.
903 if (nd.station != 0 && sn.is_queueing_place(nd.station - 1))
904 u.set(Feature::QueueingPlace);
905 break;
906 case NodeType::Transition: {
907 u.set({Feature::Transition, Feature::Enabling, Feature::Timing, Feature::Firing});
908 auto it = sn.transparam.find(ind);
909 if (it == sn.transparam.end()) break;
910 // Inhibiting only for a FINITE threshold: Inf is "never blocks",
911 // and a plain SPN must not be gated out of solvers lacking it.
912 for (const auto& row : it->second.inhibiting) {
913 bool finite = false;
914 for (std::size_t p = 0; p < row.rows() && !finite; ++p)
915 for (std::size_t r = 0; r < row.cols(); ++r)
916 if (std::isfinite(num_traits<T>::to_double(row(p, r)))) {
917 finite = true;
918 break;
919 }
920 if (finite) { u.set(Feature::Inhibiting); break; }
921 }
922 break;
923 }
924 case NodeType::Region:
925 u.set(Feature::Region);
926 break;
927 }
928 }
929
930 // A ClassSwitch is never materialised as a node by this port (see the
931 // NetworkStruct header), so the switch matrix is the only trace it leaves.
932 if (!sn.csmatrix.empty())
933 u.set({Feature::ClassSwitch, Feature::StatelessClassSwitcher, Feature::Buffer,
934 Feature::Dispatcher});
935
936 if (!sn.retrialparam.empty()) u.set(Feature::Retrial);
937 // Breakdown became a STRUCT FIELD on 2026-08-15, so it is derivable here
938 // where balking and reneging still are not: a solver that does not declare
939 // it is now refused on a model that carries it, instead of running as
940 // though the server never failed.
941 if (!sn.breakdownparam.empty()) u.set(Feature::Breakdown);
942 if (!sn.setupparam.empty()) u.set(Feature::SetupDelayOff);
943 // A PARTIAL join fires on a quorum rather than on every sibling, and a FIFO
944 // depository releases a served token only after the earlier ones: both
945 // change which transitions exist, so a solver that has neither must refuse.
946 for (const auto& kv : sn.joindecl)
947 if (kv.second.strategy == lang::JoinStrategy::PARTIAL) u.set(Feature::JoinPartial);
948 for (const Station<T>& st : sn.stations)
950 if (d != lang::DepartureDiscipline::NORMAL) u.set(Feature::DepartureDiscipline);
951 for (const auto& row : sn.droprule)
952 for (DropStrategy d : row)
953 if (d == DropStrategy::RETRIAL || d == DropStrategy::RETRIAL_WITH_LIMIT)
954 u.set(Feature::Retrial);
955
956 // A finite-server station serving several jobs at once. A Delay carries an
957 // infinite nservers and is NOT one, which is what `has_multi_server` already
958 // tests: the single-server recursions answered a c-server station as one
959 // server of the same rate, and only a structural predicate could say so.
960 if (sn.has_multi_server()) u.set(Feature::MultiServer);
961 // A station or per-class buffer that can BIND: the one predicate
962 // `check_binding_capacity` refuses on (station caps against the class
963 // populations, open classes always bind, Cache models exempt), asked here so
964 // the refusal has a registry name and a solver method that does not declare
965 // it is gated on exactly the models the structural gate refuses.
966 if (has_binding_capacity(sn)) u.set(Feature::FiniteCapacity);
967
968 return u;
969}
970
971// ---------------------------------------------------------------------------
972// The gate
973// ---------------------------------------------------------------------------
974
975/**
976 * runAnalyzerChecks: refuse a model the solver does not declare, by name.
977 *
978 * Throws UnsupportedError, which is what every other by-name refusal in this
979 * port throws and what the CLI reports without a stack.
980 *
981 * THE TWO MESSAGE FORMS, NetworkSolver.m:184-190. The reference emits a
982 * different message when `resolveMethod` changed the method under the user's
983 * feet than when the user named it:
984 *
985 * method == options.method "features not supported by the solver"
986 * otherwise "features not supported by the solver's '%s' method"
987 *
988 * That distinction is not cosmetic. The only resolution today is MVA's
989 * `default` -> `rqna` upgrade, and rqna's set WITHDRAWS ClosedClass and
990 * SelfLoopingClass, so a user who asked for nothing but `default` can be
991 * refused for having a closed class. Without the method in the message the
992 * refusal cannot be understood, because nothing the user typed mentions rqna.
993 *
994 * Both method arguments default to empty, which selects the plain form and
995 * leaves every call site that does not resolve a method unchanged.
996 */
997template <class T>
998void feature_gate(const std::string& solver, const FeatureSet& declared,
999 const NetworkStruct<T>& sn, const std::string& requested_method = "",
1000 const std::string& resolved_method = "") {
1001 const bool upgraded = !resolved_method.empty() && resolved_method != requested_method;
1002 const std::string label = upgraded ? solver + "'s '" + resolved_method + "' method" : solver;
1003 const SupportResult r = feature_set_supports(label, declared, used_lang_features(sn));
1004 if (!r.ok) throw UnsupportedError(r.reason);
1005}
1006
1007/** `%g` for a capacity, so the message reads 2 and not 2.000000. */
1008inline std::string capacity_str(double v) {
1009 std::ostringstream os;
1010 os << v;
1011 return os.str();
1012}
1013
1014/**
1015 * NetworkSolver.checkBindingCapacity (NetworkSolver.m:1172-1228): the shared
1016 * structural gate for finite station capacity (setCapacity) and finite
1017 * per-class buffers (classCap), used by the product-form solvers.
1018 *
1019 * There is no LINE_QN_FEATURE_LIST enumerator for plain capacity -- it is a
1020 * NUMBER on a station, not a construct -- so `feature_gate` above cannot see
1021 * it, and a product-form solver has no representation of a finite buffer.
1022 * Without this check the solve returns the UNCONSTRAINED answer (QLen=4 where
1023 * the M/M/1/2 value is 0.8525), which is a wrong number rather than a refusal.
1024 *
1025 * The test reads the STATION-level cap/classcap the user set, never the derived
1026 * `sn.cap`/`sn.classcap`: refresh_capacity gives every closed model a finite
1027 * derived classcap (the chain population), so an sn-level test would reject
1028 * every closed model.
1029 *
1030 * Only a capacity that can actually BIND is refused. A closed model whose
1031 * station capacity is at least the total population can never block a job, so
1032 * the declaration is a no-op and the product-form answer stays exact -- a
1033 * common idiom is setCapacity(N) on a station of an N-job closed model. The
1034 * population is infinite for an open class, so any finite capacity an open
1035 * class reaches binds.
1036 *
1037 * Cache models are exempt: a Cache sets classcap=1 on the retrieval queues it
1038 * builds, and MVA/NC solve those through their cache analyzers rather than as a
1039 * buffer constraint.
1040 */
1041/**
1042 * Which solvers to point at when a finite capacity is refused.
1043 *
1044 * The two lists differ, and naming the wrong one sends the user to a solver that
1045 * also refuses. An OPEN refused arrival is LOST, which SolverJMT reproduces (its
1046 * queue section carries the drop rule directly). A CLOSED one BLOCKS: LINE
1047 * disables the upstream departure and holds the job where it is, and no JMT drop
1048 * strategy expresses that -- 'waiting queue' does not enforce the size at all and
1049 * 'BAS blocking' completes the service before blocking, a different queueing
1050 * model. The JMT writer refuses the closed case by name
1051 * (`assert_station_cap_exportable` in `jmt_writer.h`, BUG-81), so it must not be
1052 * advertised here for it.
1053 */
1054inline std::string capacity_fallback_advice(bool is_open_class) {
1055 return is_open_class ? "Use SolverCTMC, SolverJMT or SolverLDES"
1056 : "Use SolverCTMC, SolverSSA or SolverLDES";
1057}
1058
1059/**
1060 * The refusal `check_binding_capacity` raises, as a string, or empty when no
1061 * buffer binds. Split out so that a caller which must DECIDE on the same rule --
1062 * `fluid_resolve_method`, which sends a blocked model to the one method that
1063 * carries the constraint -- asks the gate's own question rather than a second
1064 * copy of it that could drift from it.
1065 */
1066template <class T>
1067std::string binding_capacity_reason(const std::string& solver, const NetworkStruct<T>& sn) {
1068 for (const NodeDef& nd : sn.nodes)
1069 if (nd.nodetype == NodeType::Cache) return std::string();
1070
1071 double totaljobs = 0.0; // infinite as soon as one class is open
1072 bool any_open = false;
1073 for (const JobClass& c : sn.classes) {
1074 totaljobs += c.population;
1075 if (std::isinf(c.population)) any_open = true;
1076 }
1077
1078 for (const Station<T>& st : sn.stations) {
1079 if (st.nodetype == NodeType::Source || st.nodetype == NodeType::Sink) continue;
1080 if (std::isfinite(st.cap) && st.cap >= 0.0 && st.cap < totaljobs)
1081 return solver + ": finite station capacity (setCapacity=" + capacity_str(st.cap) +
1082 ") at station '" + st.name +
1083 "' is not supported by this solver, which has no "
1084 "representation of a finite buffer and would return the "
1085 "unconstrained answer. " +
1086 capacity_fallback_advice(any_open);
1087 const std::size_t n = std::min(st.classcap.size(), sn.classes.size());
1088 for (std::size_t r = 0; r < n; ++r)
1089 if (std::isfinite(st.classcap[r]) && st.classcap[r] > 0.0 &&
1090 st.classcap[r] < sn.classes[r].population)
1091 return solver + ": finite per-class capacity (classCap=" +
1092 capacity_str(st.classcap[r]) + " for class " + std::to_string(r + 1) +
1093 ") at station '" + st.name +
1094 "' is not supported by this solver, which has no representation of a "
1095 "finite buffer and would return the unconstrained answer. " +
1096 capacity_fallback_advice(std::isinf(sn.classes[r].population));
1097 }
1098 return std::string();
1099}
1100
1101/** Does any station buffer BIND? The gate's own question, asked without raising. */
1102template <class T>
1104 return !binding_capacity_reason(std::string("solver"), sn).empty();
1105}
1106
1107template <class T>
1108void check_binding_capacity(const std::string& solver, const NetworkStruct<T>& sn) {
1109 const std::string reason = binding_capacity_reason(solver, sn);
1110 if (!reason.empty()) throw UnsupportedError(reason);
1111}
1112
1113} // namespace qn
1114} // namespace line
1115
1116#endif // LINE_LANG_QN_FEATURE_SET_H
UnsupportedError(const std::string &what)
Definition error.h:51
A subset of the registry: MATLAB's SolverFeatureSet, whose list is a flag per field.
FeatureSet & set(Feature f)
FeatureSet & set(std::initializer_list< Feature > fs)
std::vector< Feature > list() const
The features held, in registry order.
FeatureSet & unset(Feature f)
bool has(Feature f) const
A network plus its refreshed NetworkStruct.
The exception types the port throws.
#define LINE_QN_FEATURE_LIST(X)
SolverFeatureSet.fields, in its order, as an X-list.
Definition feature_set.h:80
#define LINE_QN_FEATURE_ENUM(id)
#define LINE_QN_FEATURE_NAME(id)
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:426
@ REPLY
completes a synchronous call, releasing a held server
Definition lang_types.h:168
@ NEGATIVE
removes a batch of jobs (Gelenbe's negative customer)
Definition lang_types.h:169
@ CATASTROPHE
removes EVERY job at the station
Definition lang_types.h:170
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
DepartureDiscipline
When a Place releases a served token, MATLAB DepartureDiscipline.
Definition lang_types.h:460
@ RANDOM
uniform over waiting AND in-service jobs
Definition lang_types.h:175
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:485
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:380
Feature feature_of_server_section(SchedStrategy s)
The SERVER section a Queue gets for a discipline, MATLAB Queue.m:79-108.
FeatureSet used_lang_features(const NetworkStruct< T > &sn)
Feature feature_generalization(Feature f)
The registry name a specialization falls back to when it is not declared.
Feature
One language feature.
void check_binding_capacity(const std::string &solver, const NetworkStruct< T > &sn)
std::string feature_phrase(Feature f)
A human-readable phrase for the reason string, e.g.
std::string capacity_str(double v)
g for a capacity, so the message reads 2 and not 2.000000.
bool has_binding_capacity(const NetworkStruct< T > &sn)
getUsedLangFeatures: the features the MODEL uses.
Feature feature_of_process(ProcessType p)
The feature name of a distribution, MATLAB serviceProcess{r}{3}.name.
Feature feature_of_routing(RoutingStrategy r)
RoutingStrategy.toFeature.
Feature feature_of_replacement(ReplacementStrategy s)
ReplacementStrategy.toFeature.
SupportResult feature_set_supports(const std::string &solver, const FeatureSet &declared, const FeatureSet &used)
SolverFeatureSet.supports: is every feature the model uses declared?
std::string capacity_fallback_advice(bool is_open_class)
NetworkSolver.checkBindingCapacity (NetworkSolver.m:1172-1228): the shared structural gate for finite...
void feature_gate(const std::string &solver, const FeatureSet &declared, const NetworkStruct< T > &sn, const std::string &requested_method="", const std::string &resolved_method="")
runAnalyzerChecks: refuse a model the solver does not declare, by name.
constexpr std::size_t feature_count()
The number of registered features, MATLAB numel(SolverFeatureSet.fields).
Feature feature_of_sched(SchedStrategy s)
SchedStrategy.toFeature.
const char * feature_name(Feature f)
The canonical registry name, byte for byte the MATLAB field name.
std::string binding_capacity_reason(const std::string &solver, const NetworkStruct< T > &sn)
The refusal check_binding_capacity raises, as a string, or empty when no buffer binds.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Variable forking levels, the twin of MATLAB sn.nodeparam{f}.fanOutLink / .fanOutProb / ....
std::vector< std::vector< lang::Distrib< T > > > fan_out_dist
One job class of the network.
double population
infinite for an open class
A node of the network.
std::vector< RoutingStrategy > routing
sn.routing, per class.
One station of the network.
std::vector< Distrib< T > > orbit_impatience
Queue.setOrbitImpatience(class, dist): abandonment from the RETRIAL ORBIT, which is a different popul...
double cap
Station capacity in Kendall's K, as setCapacity sets it.
std::vector< BalkingParam > balking
std::vector< std::size_t > server_parallelism
Queue.setServerParallelism(class, n): the servers a job seizes for the whole of its service,...
SchedStrategy sched
CdScaling< T > jdscaling
sn.jdscaling for this station: MATLAB's Station.ljdScaling, the JOINT dependence map eta_i(n),...
std::vector< T > lldscaling
sn.lldscaling for this station: the multiplier at population 1, 2, ... Empty when the station is not ...
std::vector< lang::ImpatienceType > impatience
CdScaling< T > cdscaling
sn.cdscaling for this station: the class-dependence map, empty when unset.
std::vector< double > classcap
Per-class buffer from setChainCapacity; infinite where unset.
std::vector< lang::DepartureDiscipline > departure_discipline
Place.departureDiscipline, per class.
std::vector< ServerType > server_types
The outcome of the gate: the verdict, the offending features, the message.
std::vector< Feature > missing
used but not declared, in registry order
std::string reason
empty when ok