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 /* An FSPN fluid place or fluid arc; appended at the tail so every earlier id stays pinned. */ \
293 X(FluidPlace)
294
295/** One language feature. COUNT is the registry size and is not a feature. */
296enum class Feature : int {
297#define LINE_QN_FEATURE_ENUM(id) id,
299#undef LINE_QN_FEATURE_ENUM
301};
302
303/** The number of registered features, MATLAB `numel(SolverFeatureSet.fields)`. */
304inline constexpr std::size_t feature_count() { return static_cast<std::size_t>(Feature::COUNT); }
305
306/** The canonical registry name, byte for byte the MATLAB field name. */
307inline const char* feature_name(Feature f) {
308 switch (f) {
309#define LINE_QN_FEATURE_NAME(id) \
310 case Feature::id: \
311 return #id;
313#undef LINE_QN_FEATURE_NAME
314 default:
315 return "Unknown";
316 }
317}
318
319/**
320 * A human-readable phrase for the reason string, e.g. "a Finite Capacity
321 * Region". Structured names are derived from their suffix so that the whole
322 * SchedStrategy_ / RoutingStrategy_ / ReplacementStrategy_ / SignalType_ /
323 * ActivityPrecedence_ families need no per-entry table to stay in step.
324 */
325inline std::string feature_phrase(Feature f) {
326 switch (f) {
327 case Feature::Region: return "a Finite Capacity Region";
328 case Feature::Fork: return "a Fork node";
329 case Feature::Join: return "a Join node";
330 case Feature::Forker: return "a fork output section";
331 case Feature::ForkFanoutVector:
332 return "a fork whose tasks per link differ by destination or by class";
333 case Feature::ForkFanoutRandom:
334 return "a fork whose tasks per link are drawn from a distribution";
335 case Feature::ForkBranchProbability:
336 return "a fork whose branches fire only with a probability";
337 case Feature::Joiner: return "a join input section";
338 case Feature::Cache: return "a Cache node";
339 case Feature::CacheRetrieval: return "a delayed-hit cache retrieval system";
340 case Feature::CacheItemSize: return "per-item storage costs with per-list cost caps";
341 case Feature::ClassSwitch: return "a ClassSwitch node";
342 case Feature::Router: return "a Router node";
343 case Feature::Logger: return "a Logger node";
344 case Feature::Source: return "a Source node";
345 case Feature::Sink: return "a Sink node";
346 case Feature::Place: return "an SPN Place";
347 case Feature::QueueingPlace: return "a queueing SPN Place";
348 case Feature::Transition: return "an SPN Transition";
349 case Feature::Inhibiting: return "an SPN inhibitor arc";
350 case Feature::FluidPlace: return "an FSPN fluid place or fluid arc";
351 case Feature::Retrial: return "a retrial orbit";
352 case Feature::Balking: return "job balking";
353 case Feature::Reneging: return "job reneging";
354 case Feature::Breakdown: return "server breakdowns";
355 case Feature::SetupDelayOff: return "server setup and delay-off times";
356 case Feature::LoadDependence: return "load-dependent service rates";
357 case Feature::ClassDependence: return "class-dependent service rates";
358 case Feature::JointDependence: return "joint-dependent service rates";
359 case Feature::GlobalDependence: return "globally state-dependent (Whittle) service rates";
360 case Feature::BatchArrival: return "batch arrivals";
361 case Feature::OpenSignal: return "an open G-network signal class";
362 case Feature::ClosedSignal: return "a closed G-network signal class";
363 case Feature::SignalBatchRemoval: return "a signal batch-removal distribution";
364 case Feature::SignalRemovalPolicy: return "a non-random signal removal policy";
365 case Feature::OpenClass: return "an open job class";
366 case Feature::ClosedClass: return "a closed job class";
367 case Feature::SelfLoopingClass: return "a self-looping job class";
368 case Feature::Queue: return "a Queue station";
369 case Feature::Delay:
370 case Feature::DelayStation: return "a Delay station";
371 // "multi-server" is the vocabulary every other refusal in this tree uses
372 // (jmt_method_refusal, ba, ssa nrm), and the gate reports must agree with them.
373 case Feature::MultiServer: return "a multi-server station";
374 case Feature::FiniteCapacity: return "a finite station or per-class buffer that binds";
375 default: break;
376 }
377 const std::string nm = feature_name(f);
378 const std::size_t us = nm.find('_');
379 if (us != std::string::npos) {
380 const std::string head = nm.substr(0, us), tail = nm.substr(us + 1);
381 if (head == "SchedStrategy") return "the " + tail + " scheduling discipline";
382 if (head == "RoutingStrategy") return "the " + tail + " routing strategy";
383 if (head == "ReplacementStrategy") return "the " + tail + " cache replacement policy";
384 if (head == "SignalType") return tail + " signals";
385 if (head == "ActivityPrecedence") return "the " + tail + " activity precedence";
386 }
387 return "the " + nm + " feature";
388}
389
390// ---------------------------------------------------------------------------
391// The set
392// ---------------------------------------------------------------------------
393
394/** A subset of the registry: MATLAB's SolverFeatureSet, whose `list` is a flag per field. */
396public:
397 FeatureSet() : bits_(feature_count(), false) {}
398
400 if (f != Feature::COUNT) bits_[static_cast<std::size_t>(f)] = true;
401 return *this;
402 }
403 FeatureSet& set(std::initializer_list<Feature> fs) {
404 for (Feature f : fs) set(f);
405 return *this;
406 }
408 if (f != Feature::COUNT) bits_[static_cast<std::size_t>(f)] = false;
409 return *this;
410 }
411 bool has(Feature f) const {
412 return f != Feature::COUNT && bits_[static_cast<std::size_t>(f)];
413 }
414 /** The features held, in registry order. */
415 std::vector<Feature> list() const {
416 std::vector<Feature> out;
417 for (std::size_t i = 0; i < bits_.size(); ++i)
418 if (bits_[i]) out.push_back(static_cast<Feature>(i));
419 return out;
420 }
421 bool empty() const {
422 for (std::size_t i = 0; i < bits_.size(); ++i)
423 if (bits_[i]) return false;
424 return true;
425 }
426
427private:
428 std::vector<bool> bits_;
429};
430
431/**
432 * The registry name a specialization falls back to when it is not declared.
433 *
434 * Some registry names denote a SPECIAL CASE of another name rather than a
435 * capability of their own: a Cox2 is a Coxian restricted to two phases, and a
436 * Trace is a Replayer under another class name. Recording only the general name
437 * left the specific entry unreachable -- dead registry surface that no model
438 * could ever set (see _kb/06-solver-catalog.md, "A registered name nothing
439 * emits gates nothing"). Recording the specific name
440 * instead would silently REJECT those models at every solver that declares only
441 * the general one, which is every solver that accepts them today.
442 *
443 * So the recorder emits the most specific name it can, and the gate resolves an
444 * undeclared specific name against its generalization here. A solver that
445 * genuinely supports only the special case (two-phase Coxian, say) keeps the
446 * option of declaring the specialization alone: the fallback is consulted only
447 * when the specific name is missing, never in the other direction.
448 *
449 * COUNT means "no generalization"; the feature stands on its own.
450 */
452 switch (f) {
453 case Feature::Cox2: return Feature::Coxian;
454 // Trace is emitted by MATLAB, the JAR and python, which each carry a
455 // Trace class distinct from Replayer. This port has no such type -- the
456 // JSON wire writes both as "Replayer" -- so the entry stays inert here
457 // and the fallback is kept only so the four tables read alike.
458 case Feature::Trace: return Feature::Replayer;
459 default: return Feature::COUNT;
460 }
461}
462
463/** The outcome of the gate: the verdict, the offending features, the message. */
465 bool ok = true;
466 std::vector<Feature> missing; ///< used but not declared, in registry order
467 std::string reason; ///< empty when ok
468};
469
470/**
471 * SolverFeatureSet.supports: is every feature the model uses declared?
472 *
473 * `solver` names the solver in the reason so the message is actionable on its
474 * own; MATLAB gets that name from the mfilename of the raising runAnalyzer.
475 */
476inline SupportResult feature_set_supports(const std::string& solver, const FeatureSet& declared,
477 const FeatureSet& used) {
479 for (std::size_t i = 0; i < feature_count(); ++i) {
480 const Feature f = static_cast<Feature>(i);
481 if (!used.has(f) || declared.has(f)) continue;
482 // A specialization the solver did not name is covered by the general
483 // capability when that one IS declared; see feature_generalization.
484 const Feature g = feature_generalization(f);
485 if (g != Feature::COUNT && declared.has(g)) continue;
486 r.missing.push_back(f);
487 }
488 if (r.missing.empty()) return r;
489 r.ok = false;
490 if (r.missing.size() == 1) {
491 r.reason = solver + ": this model uses " + feature_phrase(r.missing[0]) +
492 ", which this solver does not support";
493 } else {
494 r.reason = solver + ": this model uses features which this solver does not support: ";
495 for (std::size_t i = 0; i < r.missing.size(); ++i) {
496 if (i) r.reason += ", ";
497 r.reason += feature_phrase(r.missing[i]);
498 }
499 }
500 return r;
501}
502
503/**
504 * The refusal a failed gate raises: `LINE:unsupported:<kind>`, where the kind is the FIRST
505 * missing feature in registry order (`_kb/17-error-vocabulary.md`). The message still lists
506 * every missing feature; the id names one so a caller can branch on it without parsing prose.
507 */
509 return UnsupportedError(
510 ErrorKind{r.missing.empty() ? std::string("Unspecified") : std::string(feature_name(r.missing[0]))},
511 r.reason);
512}
513
514// ---------------------------------------------------------------------------
515// Enum to feature
516// ---------------------------------------------------------------------------
517
518/**
519 * The feature name of a distribution, MATLAB `serviceProcess{r}{3}.name`.
520 *
521 * Immediate and Disabled return COUNT: they are internal placeholders, not
522 * user-facing distributions, and getUsedLangFeatures excludes them by name.
523 * Expolynomial and Normal have no ProcessType in MATLAB either, so those two
524 * registry entries can never be emitted by this port.
525 */
527 switch (p) {
528 case ProcessType::EXP: return Feature::Exp;
529 case ProcessType::ERLANG: return Feature::Erlang;
530 case ProcessType::HYPEREXP: return Feature::HyperExp;
531 case ProcessType::PH: return Feature::PH;
532 case ProcessType::APH: return Feature::APH;
533 case ProcessType::MAP: return Feature::MAP;
534 case ProcessType::UNIFORM: return Feature::Uniform;
535 case ProcessType::DET: return Feature::Det;
536 case ProcessType::COXIAN: return Feature::Coxian;
537 case ProcessType::GAMMA: return Feature::Gamma;
538 case ProcessType::PARETO: return Feature::Pareto;
539 case ProcessType::MMPP2: return Feature::MMPP2;
540 case ProcessType::REPLAYER: return Feature::Replayer;
541 case ProcessType::COX2: return Feature::Cox2;
542 case ProcessType::WEIBULL: return Feature::Weibull;
543 case ProcessType::LOGNORMAL: return Feature::Lognormal;
544 case ProcessType::DUNIFORM: return Feature::DiscreteUniform;
545 case ProcessType::BERNOULLI: return Feature::Bernoulli;
546 case ProcessType::BINOMIAL: return Feature::Binomial;
547 case ProcessType::POISSON: return Feature::Poisson;
548 case ProcessType::GEOMETRIC: return Feature::Geometric;
549 case ProcessType::BMAP: return Feature::BMAP;
550 case ProcessType::ME: return Feature::ME;
551 case ProcessType::RAP: return Feature::RAP;
552 case ProcessType::DISCRETESAMPLER: return Feature::DiscreteSampler;
553 case ProcessType::ZIPF: return Feature::Zipf;
554 case ProcessType::DMAP: return Feature::DMAP;
555 case ProcessType::MMAP: return Feature::MMAP;
556 case ProcessType::EMPIRICALCDF: return Feature::EmpiricalCdf;
557 // A Prior IS user-facing, unlike Immediate and Disabled, and emitting it
558 // is what makes every solver but SolverUQ refuse the model instead of
559 // lowering the epistemic mixture to a rate and answering with it.
560 case ProcessType::PRIOR: return Feature::Prior;
561 case ProcessType::NHPP: return Feature::NHPP;
562 case ProcessType::MAPT: return Feature::MAPt;
563 case ProcessType::PHT: return Feature::PHt;
564 case ProcessType::MPH: return Feature::MPH;
565 case ProcessType::MMAPT: return Feature::MMAPt;
566 case ProcessType::MPHT: return Feature::MPHt;
567 case ProcessType::BMMAPT: return Feature::BMMAPt;
568 default: return Feature::COUNT;
569 }
570}
571
572/**
573 * SchedStrategy.toFeature. FORK and NONE are internal markers with no registry
574 * name; MATLAB aliases FCFSPRIO to HOL, so HOL covers both.
575 */
577 switch (s) {
578 case SchedStrategy::INF: return Feature::SchedStrategy_INF;
579 case SchedStrategy::FCFS: return Feature::SchedStrategy_FCFS;
580 case SchedStrategy::FCFSPR: return Feature::SchedStrategy_FCFSPR;
581 case SchedStrategy::FCFSPI: return Feature::SchedStrategy_FCFSPI;
582 case SchedStrategy::FCFSPRPRIO: return Feature::SchedStrategy_FCFSPRPRIO;
583 case SchedStrategy::FCFSPIPRIO: return Feature::SchedStrategy_FCFSPIPRIO;
584 case SchedStrategy::LCFS: return Feature::SchedStrategy_LCFS;
585 case SchedStrategy::LCFSPR: return Feature::SchedStrategy_LCFSPR;
586 case SchedStrategy::LCFSPI: return Feature::SchedStrategy_LCFSPI;
587 case SchedStrategy::LCFSPRIO: return Feature::SchedStrategy_LCFSPRIO;
588 case SchedStrategy::LCFSPRPRIO: return Feature::SchedStrategy_LCFSPRPRIO;
589 case SchedStrategy::LCFSPIPRIO: return Feature::SchedStrategy_LCFSPIPRIO;
590 case SchedStrategy::SEPT: return Feature::SchedStrategy_SEPT;
591 case SchedStrategy::LEPT: return Feature::SchedStrategy_LEPT;
592 case SchedStrategy::SJF: return Feature::SchedStrategy_SJF;
593 case SchedStrategy::LJF: return Feature::SchedStrategy_LJF;
594 case SchedStrategy::SRPT: return Feature::SchedStrategy_SRPT;
595 case SchedStrategy::SRPTPRIO: return Feature::SchedStrategy_SRPTPRIO;
596 case SchedStrategy::PSJF: return Feature::SchedStrategy_PSJF;
597 case SchedStrategy::FB: return Feature::SchedStrategy_FB;
598 case SchedStrategy::LRPT: return Feature::SchedStrategy_LRPT;
599 case SchedStrategy::SETF: return Feature::SchedStrategy_SETF;
600 case SchedStrategy::FSP: return Feature::SchedStrategy_FSP;
601 case SchedStrategy::PAS: return Feature::SchedStrategy_PAS;
602 case SchedStrategy::OI: return Feature::SchedStrategy_OI;
603 case SchedStrategy::PS: return Feature::SchedStrategy_PS;
604 case SchedStrategy::DPS: return Feature::SchedStrategy_DPS;
605 case SchedStrategy::GPS: return Feature::SchedStrategy_GPS;
606 case SchedStrategy::PSPRIO: return Feature::SchedStrategy_PSPRIO;
607 case SchedStrategy::DPSPRIO: return Feature::SchedStrategy_DPSPRIO;
608 case SchedStrategy::GPSPRIO: return Feature::SchedStrategy_GPSPRIO;
609 case SchedStrategy::SIRO: return Feature::SchedStrategy_SIRO;
610 case SchedStrategy::HOL: return Feature::SchedStrategy_HOL;
611 case SchedStrategy::EXT: return Feature::SchedStrategy_EXT;
612 case SchedStrategy::POLLING: return Feature::SchedStrategy_POLLING;
613 case SchedStrategy::EDD: return Feature::SchedStrategy_EDD;
614 case SchedStrategy::EDF: return Feature::SchedStrategy_EDF;
615 case SchedStrategy::LPS: return Feature::SchedStrategy_LPS;
616 case SchedStrategy::REF: return Feature::SchedStrategy_REF;
617 default: return Feature::COUNT;
618 }
619}
620
621/**
622 * RoutingStrategy.toFeature. FIRING and DISABLED are internal markers, not
623 * user-selectable capabilities, and map to no registry name (RoutingStrategy.m:132-139).
624 */
626 switch (r) {
627 case RoutingStrategy::PROB: return Feature::RoutingStrategy_PROB;
628 case RoutingStrategy::RAND: return Feature::RoutingStrategy_RAND;
629 case RoutingStrategy::RROBIN: return Feature::RoutingStrategy_RROBIN;
630 case RoutingStrategy::WRROBIN: return Feature::RoutingStrategy_WRROBIN;
631 case RoutingStrategy::JSQ: return Feature::RoutingStrategy_JSQ;
632 case RoutingStrategy::SQ: return Feature::RoutingStrategy_SQ;
633 case RoutingStrategy::SDR: return Feature::RoutingStrategy_SDR;
634 default: return Feature::COUNT;
635 }
636}
637
638/** ReplacementStrategy.toFeature. */
640 switch (s) {
641 case ReplacementStrategy::RR: return Feature::ReplacementStrategy_RR;
642 case ReplacementStrategy::FIFO: return Feature::ReplacementStrategy_FIFO;
643 case ReplacementStrategy::SFIFO: return Feature::ReplacementStrategy_SFIFO;
644 case ReplacementStrategy::LRU: return Feature::ReplacementStrategy_LRU;
645 case ReplacementStrategy::HLRU: return Feature::ReplacementStrategy_HLRU;
646 case ReplacementStrategy::CLIMB: return Feature::ReplacementStrategy_CLIMB;
647 case ReplacementStrategy::QLRU: return Feature::ReplacementStrategy_QLRU;
648 default: return Feature::COUNT;
649 }
650}
651
652/**
653 * The SERVER section a Queue gets for a discipline, MATLAB Queue.m:79-108.
654 *
655 * The section class name is itself a feature: MNetwork.addNode registers
656 * class(node.server) alongside class(node), which is where SharedServer,
657 * InfiniteServer and Server enter the used set. PreemptiveServer and
658 * PollingServer are absent from the registry, so those disciplines contribute
659 * only their SchedStrategy_ name, exactly as in the reference.
660 */
662 switch (s) {
663 case SchedStrategy::PS:
664 case SchedStrategy::DPS:
665 case SchedStrategy::GPS:
666 case SchedStrategy::PSPRIO:
667 case SchedStrategy::DPSPRIO:
668 case SchedStrategy::GPSPRIO:
669 case SchedStrategy::LPS: return Feature::SharedServer;
670 case SchedStrategy::INF: return Feature::InfiniteServer;
671 case SchedStrategy::LCFSPR:
672 case SchedStrategy::LCFSPRPRIO:
673 case SchedStrategy::LCFSPI:
674 case SchedStrategy::LCFSPIPRIO:
675 case SchedStrategy::FCFSPR:
676 case SchedStrategy::FCFSPRPRIO:
677 case SchedStrategy::FCFSPI:
678 case SchedStrategy::FCFSPIPRIO:
679 case SchedStrategy::EDF: return Feature::COUNT; // PreemptiveServer, unregistered
680 case SchedStrategy::POLLING: return Feature::COUNT; // PollingServer, unregistered
681 default: return Feature::Server;
682 }
683}
684
685// ---------------------------------------------------------------------------
686// The used side
687// ---------------------------------------------------------------------------
688
689/**
690 * getUsedLangFeatures: the features the MODEL uses.
691 *
692 * MATLAB derives them from the Network OBJECT, where a node still knows its
693 * MATLAB class and its section classes; this port derives them from the
694 * refreshed struct, which is the only thing a C++ solver runner holds. The two
695 * agree because the section classes are a pure function of the node kind and
696 * the discipline (Queue.m:48-108, Source.m:66-68, Cache.m:54-76, Fork.m:106-109,
697 * Join.m:33-35, Place.m:27-31, Transition.m:29-36), which is what
698 * feature_of_server_section and the node switch below encode.
699 *
700 * FEATURES THIS PORT CANNOT EMIT, because NetworkStruct carries no field for
701 * them. Each is a real MATLAB feature, and a model using one reaches a C++
702 * solver unflagged; the fix is a struct field, not a proxy here.
703 * SetupDelayOff MATLAB Queue.setupTime / delayoffTime, carried by
704 * NetworkStruct::setupparam and solved by solver_mam_basic.
705 * Reneging MATLAB Queue.impatienceTypes. state_events.h takes the
706 * reneging classes as a CALL PARAMETER, so the struct does
707 * not record them and they cannot be derived here.
708 * Balking MATLAB Queue.balkingStrategies. No struct field.
709 * QueueingPlace was on this list until 2026-09-16, when `add_place(nm, sched)`
710 * gave a Place a discipline and `is_queueing_place` derived the flag from the
711 * service law -- which is `Place.queueing`'s own trigger, `setService` -- so it
712 * needs no flag of its own and is emitted below like any field-backed feature.
713 * Breakdown was on this list until 2026-08-15, when
714 * `NetworkStruct::breakdownparam` was added for the LDES engine; it is derived
715 * below like any other field-backed feature. `state_events.h` still takes the
716 * breakdown nodes as a call parameter, which is why the CTMC declaration of it
717 * stays inert -- the two are independent.
718 *
719 * FEATURES EMITTED HERE THAT MATLAB DOES NOT EMIT. LoadDependence is in the
720 * registry and is declared by solvers, but getUsedLangFeatures never set it, so
721 * in MATLAB it was unenforced. Deriving it from `lldscaling` is a strict
722 * tightening: the solvers that declare it are unaffected, and the ones that do
723 * not now refuse instead of silently dropping the scaling. Retrial was in this
724 * paragraph until 2026-09-05, when MATLAB's recorder gained the same arm.
725 */
726template <class T>
727bool has_binding_capacity(const NetworkStruct<T>& sn);
728
729template <class T>
731 FeatureSet u;
732 const std::size_t R = sn.classes.size();
733
734 for (std::size_t r = 0; r < R; ++r) {
735 u.set(sn.classes[r].type == JobClassType::CLOSED ? Feature::ClosedClass
736 : Feature::OpenClass);
737 // A self-looping class is a closed class whose routing returns to its own
738 // reference station. It was declared by eight solvers and never marked, so
739 // a solver that omitted the name (the bounds, RCAT, LQNS) was never asked;
740 // it is now refused unless it declares the name.
741 if (sn.classes[r].self_looping) u.set(Feature::SelfLoopingClass);
742 if (r < sn.issignal.size() && sn.issignal[r]) {
743 // Open vs closed signal follows the class type; MATLAB resolves the
744 // unresolved Signal placeholder by the presence of a Source, which
745 // is the same decision the refresh has already made here.
746 u.set(sn.classes[r].type == JobClassType::CLOSED ? Feature::ClosedSignal
747 : Feature::OpenSignal);
748 if (r < sn.signaltype.size()) {
749 switch (sn.signaltype[r]) {
750 case lang::SignalType::NEGATIVE: u.set(Feature::SignalType_NEGATIVE); break;
751 case lang::SignalType::REPLY: u.set(Feature::SignalType_REPLY); break;
753 u.set(Feature::SignalType_CATASTROPHE);
754 break;
755 }
756 }
757 if (r < sn.signalremdist.size() && !sn.signalremdist[r].empty())
758 u.set(Feature::SignalBatchRemoval);
759 if (r < sn.signalrempolicy.size() &&
760 sn.signalrempolicy[r] != lang::RemovalPolicy::RANDOM)
761 u.set(Feature::SignalRemovalPolicy);
762 }
763 }
764
765 // Globally state-dependent scaling phi(n) over the full network state, the
766 // Whittle primitive. SolverCTMC, SolverSSA and SolverLDES plumb it, so every
767 // other solver must reject the model rather than solve it unscaled.
768 if (static_cast<bool>(sn.gdscaling)) u.set(Feature::GlobalDependence);
769
770 if (!sn.regions.empty()) u.set(Feature::Region);
771 if (sn.sourceIdx != 0) u.set(Feature::Source);
772 if (sn.sinkNode != 0) u.set(Feature::Sink);
773 if (!sn.fj.empty()) u.set({Feature::Fork, Feature::Forker, Feature::Join, Feature::Joiner});
774
775 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
776 const NodeDef& nd = sn.nodes[i];
777 const std::size_t ind = i + 1;
778 const std::size_t ist = nd.station; // 1-based, 0 when the node serves no jobs
779
780 switch (nd.nodetype) {
781 case NodeType::Queue:
782 case NodeType::Delay: {
783 u.set({nd.nodetype == NodeType::Delay ? Feature::Delay : Feature::Queue,
784 Feature::Buffer, Feature::Dispatcher});
785 if (ist == 0) break;
786 const Station<T>& st = sn.stations[ist - 1];
788 bool any_class = false;
789 for (std::size_t r = 0; r < R; ++r) {
790 // A disabled pair is MATLAB's empty serviceProcess: it emits
791 // no distribution, no discipline and no routing.
792 if (ist - 1 < sn.disabled.size() && r < sn.disabled[ist - 1].size() &&
793 sn.disabled[ist - 1][r])
794 continue;
795 any_class = true;
796 if (ist - 1 < sn.service.size() && r < sn.service[ist - 1].size())
797 u.set(feature_of_process(sn.service[ist - 1][r].type));
798 if (r < nd.routing.size()) u.set(feature_of_routing(nd.routing[r]));
799 }
800 if (any_class) u.set(feature_of_sched(st.sched));
801 if (!st.lldscaling.empty()) u.set(Feature::LoadDependence);
802 if (static_cast<bool>(st.cdscaling)) u.set(Feature::ClassDependence);
803 if (static_cast<bool>(st.jdscaling)) u.set(Feature::JointDependence);
804 // Impatience, balking and heterogeneous pools. MATLAB's
805 // getUsedLangFeatures emits Balking and Reneging from the same
806 // declarations; HeteroServers has no MATLAB counterpart and is
807 // emitted here for the same reason Retrial and LoadDependence
808 // are -- a solver that ignores the pools would answer for the
809 // homogeneous station and say nothing about the difference.
810 for (std::size_t r = 0; r < R; ++r) {
811 if (r < st.impatience.size() && st.impatience[r] != lang::ImpatienceType::NONE)
812 u.set(st.impatience[r] == lang::ImpatienceType::BALKING ? Feature::Balking
813 : Feature::Reneging);
814 if (r < st.orbit_impatience.size() && !st.orbit_impatience[r].disabled)
815 u.set(Feature::Reneging);
816 if (r < st.balking.size() &&
817 st.balking[r].strategy != lang::BalkingStrategy::NONE)
818 u.set(Feature::Balking);
819 }
820 if (!st.server_types.empty()) u.set(Feature::HeteroServers);
821 for (std::size_t n : st.server_parallelism)
822 if (n > 1) { u.set(Feature::ServerParallelism); break; }
823 break;
824 }
825 case NodeType::Source: {
826 u.set({Feature::Source, Feature::RandomSource, Feature::ServiceTunnel,
827 Feature::Dispatcher});
828 // The Source branch emits the arrival distributions only: MATLAB
829 // registers no discipline and no routing for it.
830 if (ist == 0 || ist - 1 >= sn.service.size()) break;
831 for (std::size_t r = 0; r < R && r < sn.service[ist - 1].size(); ++r)
832 u.set(feature_of_process(sn.service[ist - 1][r].type));
833 // A batch arrival releases several jobs per epoch, which no
834 // single-arrival event handler produces.
835 for (const Distrib<T>& b : sn.stations[ist - 1].arrival_batch)
836 if (!b.disabled) { u.set(Feature::BatchArrival); break; }
837 break;
838 }
839 case NodeType::Sink:
840 u.set(Feature::Sink);
841 break;
842 case NodeType::Router: {
843 // A ROUTER EMITS ITS ROUTING STRATEGY AND NOTHING ELSE. All
844 // three reference codebases agree and this port was the only
845 // one that did not: getUsedLangFeatures.m:77-78 (case 'Router'),
846 // Network.java:3184-3190 and network.py:1983-1987 each record
847 // RoutingStrategy.toFeature(...) alone. This branch additionally
848 // emitted Router, Buffer, Dispatcher and ServiceTunnel, and
849 // since neither mva_feature_set nor nc_feature_set declares
850 // Router -- correctly, SolverMVA.m:231-257 does not either --
851 // every model with a dispatcher was refused by the two solvers
852 // MATLAB and python solve it with. The whole cluster family
853 // (Network.cluster/clusterClosed/clusterMixed) is exactly that
854 // shape, and the Router is eliminated by the stochastic
855 // complement long before any solver sees it.
856 for (std::size_t r = 0; r < R && r < nd.routing.size(); ++r)
858 break;
859 }
860 case NodeType::ClassSwitch:
861 u.set({Feature::ClassSwitch, Feature::StatelessClassSwitcher, Feature::Buffer,
862 Feature::Dispatcher});
863 break;
864 case NodeType::Cache: {
865 u.set({Feature::Cache, Feature::CacheClassSwitcher, Feature::Buffer,
866 Feature::Dispatcher});
867 auto it = sn.nodeparam.find(ind);
868 if (it == sn.nodeparam.end()) break;
869 u.set(feature_of_replacement(it->second.replacestrat));
870 if (it->second.retrieval_capacity > 0 || !it->second.retrieval_classes.empty())
871 u.set(Feature::CacheRetrieval);
872 if (!it->second.costcap.empty()) u.set(Feature::CacheItemSize);
873 break;
874 }
875 case NodeType::Fork: {
876 u.set({Feature::Fork, Feature::Forker, Feature::Buffer, Feature::ServiceTunnel});
877 // Variable forking levels. A name declared but never marked is
878 // a name no solver can ever refuse, so all three are marked
879 // here. A plain fork has no override block at all, so this
880 // costs one map lookup in the classic case.
881 const qn::ForkParam<T>* fp = sn.fork_param_of(i);
882 if (fp != 0) {
883 const double tpl = sn.nodes[i - 1].tasks_per_link;
884 bool vector_valued = false, random_valued = false, probabilistic = false;
885 for (std::size_t k = 0; k < fp->fan_out_link.rows(); ++k)
886 for (std::size_t r = 0; r < fp->fan_out_link.cols(); ++r) {
887 const double p = num_traits<T>::to_double(fp->fan_out_prob(k, r));
888 if (p == 0.0) continue; // link this class does not take
889 if (p != 1.0) probabilistic = true;
890 if (num_traits<T>::to_double(fp->fan_out_link(k, r)) != tpl)
891 vector_valued = true;
892 if (!fp->fan_out_dist[k][r].disabled) random_valued = true;
893 }
894 if (vector_valued) u.set(Feature::ForkFanoutVector);
895 if (random_valued) u.set(Feature::ForkFanoutRandom);
896 if (probabilistic) u.set(Feature::ForkBranchProbability);
897 }
898 break;
899 }
900 case NodeType::Join:
901 u.set({Feature::Join, Feature::Joiner, Feature::Dispatcher,
902 Feature::ServiceTunnel});
903 break;
904 case NodeType::Logger:
905 u.set({Feature::Logger, Feature::LogTunnel, Feature::Buffer, Feature::Dispatcher});
906 break;
907 case NodeType::Place:
908 u.set({Feature::Place, Feature::Storage, Feature::Linkage,
909 Feature::ServiceTunnel});
910 // A QUEUEING PLACE, i.e. one `setService` gave a law to. The
911 // three reference recorders emit this name and NOTHING else for
912 // it -- not the discipline, not the service distribution, not
913 // the server section `installQueueServer` swapped in -- and
914 // this port must not emit more: a feature set refuses a model
915 // for HAVING a name, so a name MATLAB does not raise is a model
916 // this port alone would turn away.
917 if (nd.station != 0 && sn.is_queueing_place(nd.station - 1))
918 u.set(Feature::QueueingPlace);
919 // A FLUID place (FSPN) holds a real level that only solvers declaring FluidPlace read; every
920 // other solver would answer the token net alone and drop the fluid without a word.
921 if (sn.is_fluid_node(ind)) u.set(Feature::FluidPlace);
922 break;
923 case NodeType::Transition: {
924 u.set({Feature::Transition, Feature::Enabling, Feature::Timing, Feature::Firing});
925 auto it = sn.transparam.find(ind);
926 if (it == sn.transparam.end()) break;
927 // Inhibiting only for a FINITE threshold: Inf is "never blocks",
928 // and a plain SPN must not be gated out of solvers lacking it.
929 for (const auto& row : it->second.inhibiting) {
930 bool finite = false;
931 for (std::size_t p = 0; p < row.rows() && !finite; ++p)
932 for (std::size_t r = 0; r < row.cols(); ++r)
933 if (std::isfinite(num_traits<T>::to_double(row(p, r)))) {
934 finite = true;
935 break;
936 }
937 if (finite) { u.set(Feature::Inhibiting); break; }
938 }
939 // Fluid arcs without a fluid place still need a solver that reads them.
940 if (it->second.has_fluid_arcs()) u.set(Feature::FluidPlace);
941 break;
942 }
943 case NodeType::Region:
944 u.set(Feature::Region);
945 break;
946 }
947 }
948
949 // A ClassSwitch is never materialised as a node by this port (see the
950 // NetworkStruct header), so the switch matrix is the only trace it leaves.
951 if (!sn.csmatrix.empty())
952 u.set({Feature::ClassSwitch, Feature::StatelessClassSwitcher, Feature::Buffer,
953 Feature::Dispatcher});
954
955 if (!sn.retrialparam.empty()) u.set(Feature::Retrial);
956 // Breakdown became a STRUCT FIELD on 2026-08-15, so it is derivable here
957 // where balking and reneging still are not: a solver that does not declare
958 // it is now refused on a model that carries it, instead of running as
959 // though the server never failed.
960 if (!sn.breakdownparam.empty()) u.set(Feature::Breakdown);
961 if (!sn.setupparam.empty()) u.set(Feature::SetupDelayOff);
962 // A PARTIAL join fires on a quorum rather than on every sibling, and a FIFO
963 // depository releases a served token only after the earlier ones: both
964 // change which transitions exist, so a solver that has neither must refuse.
965 for (const auto& kv : sn.joindecl)
966 if (kv.second.strategy == lang::JoinStrategy::PARTIAL) u.set(Feature::JoinPartial);
967 for (const Station<T>& st : sn.stations)
969 if (d != lang::DepartureDiscipline::NORMAL) u.set(Feature::DepartureDiscipline);
970 for (const auto& row : sn.droprule)
971 for (DropStrategy d : row)
972 if (d == DropStrategy::RETRIAL || d == DropStrategy::RETRIAL_WITH_LIMIT)
973 u.set(Feature::Retrial);
974
975 // A finite-server station serving several jobs at once. A Delay carries an
976 // infinite nservers and is NOT one, which is what `has_multi_server` already
977 // tests: the single-server recursions answered a c-server station as one
978 // server of the same rate, and only a structural predicate could say so.
979 if (sn.has_multi_server()) u.set(Feature::MultiServer);
980 // A station or per-class buffer that can BIND: the one predicate
981 // `check_binding_capacity` refuses on (station caps against the class
982 // populations, open classes always bind, Cache models exempt), asked here so
983 // the refusal has a registry name and a solver method that does not declare
984 // it is gated on exactly the models the structural gate refuses.
985 if (has_binding_capacity(sn)) u.set(Feature::FiniteCapacity);
986
987 return u;
988}
989
990// ---------------------------------------------------------------------------
991// The gate
992// ---------------------------------------------------------------------------
993
994/**
995 * runAnalyzerChecks: refuse a model the solver does not declare, by name.
996 *
997 * Throws UnsupportedError, which is what every other by-name refusal in this
998 * port throws and what the CLI reports without a stack.
999 *
1000 * THE TWO MESSAGE FORMS, NetworkSolver.m:184-190. The reference emits a
1001 * different message when `resolveMethod` changed the method under the user's
1002 * feet than when the user named it:
1003 *
1004 * method == options.method "features not supported by the solver"
1005 * otherwise "features not supported by the solver's '%s' method"
1006 *
1007 * That distinction is not cosmetic. The only resolution today is MVA's
1008 * `default` -> `rqna` upgrade, and rqna's set WITHDRAWS ClosedClass and
1009 * SelfLoopingClass, so a user who asked for nothing but `default` can be
1010 * refused for having a closed class. Without the method in the message the
1011 * refusal cannot be understood, because nothing the user typed mentions rqna.
1012 *
1013 * Both method arguments default to empty, which selects the plain form and
1014 * leaves every call site that does not resolve a method unchanged.
1015 */
1016template <class T>
1017void feature_gate(const std::string& solver, const FeatureSet& declared,
1018 const NetworkStruct<T>& sn, const std::string& requested_method = "",
1019 const std::string& resolved_method = "") {
1020 const bool upgraded = !resolved_method.empty() && resolved_method != requested_method;
1021 const std::string label = upgraded ? solver + "'s '" + resolved_method + "' method" : solver;
1022 const SupportResult r = feature_set_supports(label, declared, used_lang_features(sn));
1023 if (!r.ok) throw feature_refusal(r);
1024}
1025
1026/** `%g` for a capacity, so the message reads 2 and not 2.000000. */
1027inline std::string capacity_str(double v) {
1028 std::ostringstream os;
1029 os << v;
1030 return os.str();
1031}
1032
1033/**
1034 * Which solvers to point at when a finite capacity is refused.
1035 *
1036 * The two lists differ, and naming the wrong one sends the user to a solver that
1037 * also refuses. An OPEN refused arrival is LOST, which SolverJMT reproduces (its
1038 * queue section carries the drop rule directly). A CLOSED one BLOCKS: LINE
1039 * disables the upstream departure and holds the job where it is, and no JMT drop
1040 * strategy expresses that -- 'waiting queue' does not enforce the size at all and
1041 * 'BAS blocking' completes the service before blocking, a different queueing
1042 * model. The JMT writer refuses the closed case by name
1043 * (`assert_station_cap_exportable` in `jmt_writer.h`, BUG-81), so it must not be
1044 * advertised here for it.
1045 */
1046inline std::string capacity_fallback_advice(bool is_open_class) {
1047 return is_open_class ? "Use SolverCTMC, SolverJMT or SolverLDES"
1048 : "Use SolverCTMC, SolverSSA or SolverLDES";
1049}
1050
1051/**
1052 * The refusal `check_binding_capacity` raises, as a string, or empty when no
1053 * buffer binds. Split out so that a caller which must DECIDE on the same rule --
1054 * `fluid_resolve_method`, which sends a blocked model to the one method that
1055 * carries the constraint -- asks the gate's own question rather than a second
1056 * copy of it that could drift from it.
1057 */
1058template <class T>
1059std::string binding_capacity_reason(const std::string& solver, const NetworkStruct<T>& sn) {
1060 for (const NodeDef& nd : sn.nodes)
1061 if (nd.nodetype == NodeType::Cache) return std::string();
1062
1063 double totaljobs = 0.0; // infinite as soon as one class is open
1064 bool any_open = false;
1065 for (const JobClass& c : sn.classes) {
1066 totaljobs += c.population;
1067 if (std::isinf(c.population)) any_open = true;
1068 }
1069
1070 for (const Station<T>& st : sn.stations) {
1071 if (st.nodetype == NodeType::Source || st.nodetype == NodeType::Sink) continue;
1072 if (std::isfinite(st.cap) && st.cap >= 0.0 && st.cap < totaljobs)
1073 return solver + ": finite station capacity (setCapacity=" + capacity_str(st.cap) +
1074 ") at station '" + st.name +
1075 "' is not supported by this solver, which has no "
1076 "representation of a finite buffer and would return the "
1077 "unconstrained answer. " +
1078 capacity_fallback_advice(any_open);
1079 const std::size_t n = std::min(st.classcap.size(), sn.classes.size());
1080 for (std::size_t r = 0; r < n; ++r)
1081 if (std::isfinite(st.classcap[r]) && st.classcap[r] > 0.0 &&
1082 st.classcap[r] < sn.classes[r].population)
1083 return solver + ": finite per-class capacity (classCap=" +
1084 capacity_str(st.classcap[r]) + " for class " + std::to_string(r + 1) +
1085 ") at station '" + st.name +
1086 "' is not supported by this solver, which has no representation of a "
1087 "finite buffer and would return the unconstrained answer. " +
1088 capacity_fallback_advice(std::isinf(sn.classes[r].population));
1089 }
1090 return std::string();
1091}
1092
1093/** Does any station buffer BIND? The gate's own question, asked without raising. */
1094template <class T>
1096 return !binding_capacity_reason(std::string("solver"), sn).empty();
1097}
1098
1099/**
1100 * NetworkSolver.checkBindingCapacity (NetworkSolver.m:1172-1228): the shared
1101 * structural gate for finite station capacity (setCapacity) and finite
1102 * per-class buffers (classCap), used by the product-form solvers.
1103 *
1104 * There is no LINE_QN_FEATURE_LIST enumerator for plain capacity -- it is a
1105 * NUMBER on a station, not a construct -- so `feature_gate` above cannot see
1106 * it, and a product-form solver has no representation of a finite buffer.
1107 * Without this check the solve returns the UNCONSTRAINED answer (QLen=4 where
1108 * the M/M/1/2 value is 0.8525), which is a wrong number rather than a refusal.
1109 *
1110 * The test reads the STATION-level cap/classcap the user set, never the derived
1111 * `sn.cap`/`sn.classcap`: refresh_capacity gives every closed model a finite
1112 * derived classcap (the chain population), so an sn-level test would reject
1113 * every closed model.
1114 *
1115 * Only a capacity that can actually BIND is refused. A closed model whose
1116 * station capacity is at least the total population can never block a job, so
1117 * the declaration is a no-op and the product-form answer stays exact -- a
1118 * common idiom is setCapacity(N) on a station of an N-job closed model. The
1119 * population is infinite for an open class, so any finite capacity an open
1120 * class reaches binds.
1121 *
1122 * Cache models are exempt: a Cache sets classcap=1 on the retrieval queues it
1123 * builds, and MVA/NC solve those through their cache analyzers rather than as a
1124 * buffer constraint.
1125 */
1126template <class T>
1127void check_binding_capacity(const std::string& solver, const NetworkStruct<T>& sn) {
1128 const std::string reason = binding_capacity_reason(solver, sn);
1129 if (!reason.empty()) throw UnsupportedError(ErrorKind{"FiniteCapacity"}, reason);
1130}
1131
1132} // namespace qn
1133} // namespace line
1134
1135#endif // LINE_LANG_QN_FEATURE_SET_H
A refusal: the feature, method or analysis is not covered: LINE:unsupported:<kind>.
Definition error.h:117
UnsupportedError(const std::string &what)
Definition error.h:119
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:185
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:431
@ REPLY
completes a synchronous call, releasing a held server
Definition lang_types.h:172
@ NEGATIVE
removes a batch of jobs (Gelenbe's negative customer)
Definition lang_types.h:173
@ CATASTROPHE
removes EVERY job at the station
Definition lang_types.h:174
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
DepartureDiscipline
When a Place releases a served token, MATLAB DepartureDiscipline.
Definition lang_types.h:465
@ RANDOM
uniform over waiting AND in-service jobs
Definition lang_types.h:179
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:385
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)
NetworkSolver.checkBindingCapacity (NetworkSolver.m:1172-1228): the shared structural gate for finite...
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.
UnsupportedError feature_refusal(const SupportResult &r)
The refusal a failed gate raises: LINE:unsupported:<kind>, where the kind is the FIRST missing featur...
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)
Which solvers to point at when a finite capacity is refused.
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.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
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