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