LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
network_builder.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_NETWORK_BUILDER_H
6#define LINE_LANG_QN_NETWORK_BUILDER_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * The Network constructor API: Queue, Delay, Source, Sink, Router,
12 * ClassSwitch, Cache, Fork and Join, the job classes, and `link`.
13 *
14 * This is the port of what a MATLAB model script writes -- `Network`,
15 * `Queue(model, name, sched)`, `queue.setService(class, dist)`,
16 * `model.link(P)` -- feeding the SAME refresh as every other front end, so a
17 * model built here and a model read from a file produce the same
18 * `NetworkStruct`. The split is deliberate and mirrors `lqn_builder.h` /
19 * `lqn_reader.h`: stage one constructs, stage two (`NetworkStruct::refresh_*`)
20 * derives, and only stage two is allowed to compute anything.
21 *
22 * INDEX SPACES. Every method takes and returns 1-based NODE indices, which is
23 * what a model script deals in; the station index is an internal detail of the
24 * struct and is looked up here. `add_*` returns the node index of the node it
25 * created, and `add_*_class` the class index.
26 *
27 * WHAT IS REFUSED, and where. The builder accepts the feature set SolverMVA
28 * declares (`SolverMVA.getFeatureSet`); the refresh refuses a state-dependent
29 * routing strategy by name, and each solver refuses what it cannot analyse.
30 * Nothing is silently mapped onto a neighbour.
31 */
32
33#include <cmath>
34#include <cstddef>
35#include <limits>
36#include <map>
37#include <string>
38#include <utility>
39#include <vector>
40
43#include "line/lang/prior.h"
45#include "line/num/number.h"
46#include "line/util/error.h"
47#include "line/util/matrix.h"
48
49namespace line {
50namespace qn {
51
52/**
53 * The routing matrix a model script fills in, MATLAB's `P` cell array.
54 *
55 * `P{r,s}(i,j)` is the probability that a job leaving node i in class r enters
56 * node j in class s. The single-class form `set(i, j, p)` is the shorthand a
57 * one-class model uses, and is exactly `set(1, 1, i, j, p)`.
58 */
59template <class T>
61 public:
62 void set(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T& p) {
63 entries[Key(r, s, i, j)] = p;
64 }
65 void set(std::size_t i, std::size_t j, const T& p) { set(1, 1, i, j, p); }
66
67 /**
68 * `P.set(class, Network.serialRouting(...))`: install a one-class block.
69 *
70 * The source block's class coordinates are deliberately ignored. A serial
71 * routing matrix is a topology block; this call assigns that topology to
72 * the requested class, exactly as the MATLAB, JAR and Python APIs do.
73 */
74 void set(std::size_t cls, const RoutingMatrix<T>& block) {
75 for (const auto& kv : block.entries)
76 set(cls, cls, kv.first.i, kv.first.j, kv.second);
77 }
78
79 T get(std::size_t r, std::size_t s, std::size_t i, std::size_t j) const {
80 auto it = entries.find(Key(r, s, i, j));
81 return it == entries.end() ? num_traits<T>::from_int(0) : it->second;
82 }
83
84 struct Key {
85 std::size_t r, s, i, j;
86 Key(std::size_t r_, std::size_t s_, std::size_t i_, std::size_t j_)
87 : r(r_), s(s_), i(i_), j(j_) {}
88 bool operator<(const Key& o) const {
89 if (r != o.r) return r < o.r;
90 if (s != o.s) return s < o.s;
91 if (i != o.i) return i < o.i;
92 return j < o.j;
93 }
94 };
95 std::map<Key, T> entries;
96};
97
98/**
99 * A queueing network under construction.
100 *
101 * The object owns a `NetworkStruct` and hands out a refreshed reference to it.
102 * `get_struct()` runs the whole refresh chain each time it is called, because
103 * every setter can have moved a quantity the chain derives; a caller that
104 * changes nothing and asks twice pays for the second refresh, which is the same
105 * bargain MATLAB's `hasStruct` cache makes on the other side.
106 */
107template <class T>
108class Network {
109 public:
110 explicit Network(const std::string& nm) { sn_.name = nm; }
111
112 // -----------------------------------------------------------------------
113 // Nodes
114 // -----------------------------------------------------------------------
115
116 /**
117 * A queueing station. The default discipline is FCFS, as in MATLAB.
118 *
119 * INF SCHEDULING BUILDS A DELAY. MATLAB's Queue constructor sets
120 * numberOfServers = Inf on that branch and getNodeTypes then reports the
121 * station as NodeType.Delay (the JAR does both with Integer.MAX_VALUE), and
122 * the analyzers partition the stations on those two fields:
123 * sn_get_product_form_chain_params splits by NODETYPE, so a Queue node left
124 * at one server was handed to the linearizer as a finite-server queue and
125 * reported utilization 1 and a saturated response time where the reference
126 * reports a delay (2.4494/19.5506 against 0.1253/21.8747 on a two-station
127 * closed model with 22 jobs).
128 */
129 std::size_t add_queue(const std::string& nm, SchedStrategy sched = SchedStrategy::FCFS) {
130 Station<T> st;
131 st.name = nm;
132 st.nodetype = (sched == SchedStrategy::INF) ? NodeType::Delay : NodeType::Queue;
133 st.sched = sched;
134 st.nservers = (sched == SchedStrategy::INF)
135 ? std::numeric_limits<double>::infinity()
136 : 1.0;
137 const std::size_t ist = sn_.add_station(st);
138 sn_.nodes[sn_.station_to_node[ist - 1] - 1].queue_object = true;
139 init_node(sn_.station_to_node[ist - 1]);
140 return sn_.station_to_node[ist - 1];
141 }
142
143 /** An infinite-server station (a Delay, MATLAB's `Delay` / `DelayStation`). */
144 std::size_t add_delay(const std::string& nm) {
145 Station<T> st;
146 st.name = nm;
147 st.nodetype = NodeType::Delay;
148 st.sched = SchedStrategy::INF;
149 st.nservers = std::numeric_limits<double>::infinity();
150 const std::size_t ist = sn_.add_station(st);
151 init_node(sn_.station_to_node[ist - 1]);
152 return sn_.station_to_node[ist - 1];
153 }
154
155 /**
156 * The external arrival station.
157 *
158 * It IS a station -- its "service" process is the arrival process -- with
159 * the EXT discipline and one server, exactly as MATLAB's Source is built.
160 */
161 std::size_t add_source(const std::string& nm) {
162 if (sn_.sourceIdx != 0) throw InputError("Network: the model already has a Source");
163 Station<T> st;
164 st.name = nm;
165 st.nodetype = NodeType::Source;
166 st.sched = SchedStrategy::EXT;
167 st.nservers = 1.0;
168 const std::size_t ist = sn_.add_station(st);
169 sn_.sourceIdx = ist;
170 init_node(sn_.station_to_node[ist - 1]);
171 return sn_.station_to_node[ist - 1];
172 }
173
174 /** The external departure node. It is NOT a station and holds no jobs. */
175 std::size_t add_sink(const std::string& nm) {
176 if (sn_.sinkNode != 0) throw InputError("Network: the model already has a Sink");
177 const std::size_t nd = sn_.add_node(nm, NodeType::Sink, false);
178 sn_.sinkNode = nd;
179 init_node(nd);
180 return nd;
181 }
182
183 /** A stateless routing node. */
184 std::size_t add_router(const std::string& nm) {
185 const std::size_t nd = sn_.add_node(nm, NodeType::Router, false);
186 init_node(nd);
187 return nd;
188 }
189
190 /**
191 * A Logger node: a pass-through that records every job crossing it.
192 *
193 * It holds no jobs and changes no routing probability, so it is eliminated
194 * by the same stochastic complement that removes a Router; what makes it a
195 * distinct node type is that `used_lang_features` emits Logger/LogTunnel
196 * from it, so a solver with no logging refuses the model by name rather
197 * than silently dropping the trace the user asked for.
198 */
199 std::size_t add_logger(const std::string& nm, const std::string& log_file = std::string()) {
200 const std::size_t nd = sn_.add_node(nm, NodeType::Logger, false);
201 init_node(nd);
202 // `Logger.m` splits the argument and keeps only the base name; the
203 // directory is the model's log path, which every Logger shares.
204 std::string base = log_file;
205 const std::size_t slash = base.find_last_of('/');
206 if (slash != std::string::npos) base = base.substr(slash + 1);
207 sn_.nodes[nd - 1].logger.file_name = base;
208 return nd;
209 }
210
211 /** `Network.setLogPath`: the directory every Logger of this model writes into. */
212 void set_log_path(const std::string& path) { sn_.log_path = path; }
213
214 /**
215 * A ClassSwitch node carrying the (nclasses x nclasses) switching matrix.
216 *
217 * The classes must exist before the node, since the matrix is indexed by
218 * them; that is also the order a MATLAB script writes.
219 */
220 std::size_t add_class_switch(const std::string& nm, const Matrix<T>& C) {
221 if (C.rows() != sn_.classes.size() || C.cols() != sn_.classes.size())
222 throw InputError("ClassSwitch '" + nm +
223 "': the matrix must be (nclasses x nclasses); declare the classes "
224 "before the node");
225 const std::size_t nd = sn_.add_node(nm, NodeType::ClassSwitch, false);
226 sn_.csmatrix[nd] = C;
227 init_node(nd);
228 return nd;
229 }
230
231 /**
232 * A ClassSwitch node whose matrix is installed LATER, by
233 * `set_class_switch_matrix`.
234 *
235 * FOR A FILE READER, which meets the nodes before the classes. A `.jsimg`
236 * and a `.lqnx` both list their nodes first, and a closed class names its
237 * reference STATION, so neither order can be satisfied without deferring
238 * one of the two -- MATLAB's own `ClassSwitch` constructor stores the matrix
239 * without checking it against a class list that does not exist yet, which is
240 * the same deferral by another name.
241 *
242 * THE MATRIX IS LEFT EMPTY, not filled with an identity: an identity is a
243 * valid switching matrix (every class keeps its own), so a caller who forgot
244 * to install the real one would get a plausible model instead of an error.
245 */
246 std::size_t add_class_switch(const std::string& nm) {
247 const std::size_t nd = sn_.add_node(nm, NodeType::ClassSwitch, false);
248 sn_.csmatrix[nd] = Matrix<T>();
249 init_node(nd);
250 return nd;
251 }
252
253 /** Install the switching matrix of a ClassSwitch created without one. */
254 void set_class_switch_matrix(std::size_t node, const Matrix<T>& C) {
255 if (node == 0 || node > sn_.nodes.size())
256 throw InputError("set_class_switch_matrix: node index is out of range");
257 if (sn_.nodes[node - 1].nodetype != NodeType::ClassSwitch)
258 throw InputError("set_class_switch_matrix: node '" + sn_.nodes[node - 1].name +
259 "' is not a ClassSwitch");
260 if (C.rows() != sn_.classes.size() || C.cols() != sn_.classes.size())
261 throw InputError("set_class_switch_matrix: the matrix of '" +
262 sn_.nodes[node - 1].name +
263 "' must be (nclasses x nclasses)");
264 sn_.csmatrix[node] = C;
265 }
266
267 /** A Fork node. It holds no jobs and is removed by the stochastic complement.
268 * `tasks_per_link` (Fork.output.tasksPerLink) defaults to 1. */
269 std::size_t add_fork(const std::string& nm, double tasks_per_link = 1.0) {
270 const std::size_t nd = sn_.add_node(nm, NodeType::Fork, false);
271 sn_.nodes[nd - 1].tasks_per_link = tasks_per_link;
272 init_node(nd);
273 return nd;
274 }
275
276 /**
277 * Variable forking levels on an existing Fork, the twin of MATLAB
278 * `Fork.setTasksPerLink(jobclass, n)`,
279 * `Fork.setTasksPerLinkDistribution(jobclass, dist [, destNode])` and
280 * `Fork.setBranchProbability(jobclass, destNode, p)`.
281 *
282 * The block is allocated lazily and only on a fork that actually declares an
283 * override, so a plain fork has no `sn.forkparam` entry at all and every
284 * consumer can tell the classic case from the variable one by asking
285 * `sn.fork_param_of(f)` for a null.
286 *
287 * `dest_node` 0 means every outgoing link of that class. Classes and nodes
288 * are 1-based, as everywhere in this port.
289 *
290 * An override is RECORDED and replayed when the routing is installed, so
291 * these may be called in either order with respect to `link()` -- as the
292 * MATLAB, Java and Python setters may, which store the override on the
293 * Forker section and materialise it at refresh time. Recording rather than
294 * writing is what buys that: `dest_node = 0` means "every link this class
295 * takes", a set that does not exist until the routing does.
296 */
297 void set_fork_tasks_per_link(std::size_t fork_node, std::size_t jobclass,
298 double tasks, std::size_t dest_node = 0) {
299 ForkOverride ov;
300 ov.kind = ForkOverride::TASKS;
301 ov.fork = fork_node;
302 ov.cls = jobclass;
303 ov.dest = dest_node;
304 ov.value = tasks;
305 record_fork_override(ov);
306 }
307
308 /** A random jobs-per-link degree, redrawn per link and per forked job. */
309 void set_fork_tasks_per_link_dist(std::size_t fork_node, std::size_t jobclass,
310 const lang::Distrib<T>& dist,
311 std::size_t dest_node = 0) {
313 throw InputError("set_fork_tasks_per_link_dist: the jobs-per-link "
314 "distribution must be a DiscreteSampler");
315 ForkOverride ov;
316 ov.kind = ForkOverride::DIST;
317 ov.fork = fork_node;
318 ov.cls = jobclass;
319 ov.dest = dest_node;
320 ov.dist = dist;
321 record_fork_override(ov);
322 }
323
324 /** A branch that fires only with probability `prob`. */
325 void set_fork_branch_probability(std::size_t fork_node, std::size_t jobclass,
326 std::size_t dest_node, double prob) {
327 if (prob < 0.0 || prob > 1.0)
328 throw InputError("set_fork_branch_probability: a branch activation "
329 "probability must lie in [0,1]");
330 ForkOverride ov;
331 ov.kind = ForkOverride::PROB;
332 ov.fork = fork_node;
333 ov.cls = jobclass;
334 ov.dest = dest_node;
335 ov.value = prob;
336 record_fork_override(ov);
337 }
338
339 /**
340 * A Join node, which IS a station: it serves at an infinite rate, and the
341 * synchronisation delay is supplied by the fork-join transform.
342 */
343 std::size_t add_join(const std::string& nm, std::size_t fork_node) {
344 const std::size_t nd = add_join_unbound(nm);
345 bind_join(nd, fork_node);
346 return nd;
347 }
348
349 /**
350 * The Join station on its own, with the fork left to `bind_join`.
351 *
352 * A Join IS a station, so creating it late shifts every station index after
353 * it, and the result document is indexed by station row. A reader that has
354 * to see the Fork first therefore declares the Join here, in the position
355 * the model gives it, and binds the pair once the Fork exists.
356 */
357 std::size_t add_join_unbound(const std::string& nm) {
358 Station<T> st;
359 st.name = nm;
360 st.nodetype = NodeType::Join;
361 st.sched = SchedStrategy::INF;
362 st.nservers = std::numeric_limits<double>::infinity();
363 const std::size_t ist = sn_.add_station(st);
364 const std::size_t nd = sn_.station_to_node[ist - 1];
365 init_node(nd);
366 return nd;
367 }
368
369 /** Record which Fork a Join created by `add_join_unbound` closes. */
370 void bind_join(std::size_t join_node, std::size_t fork_node) {
371 if (join_node == 0 || join_node > sn_.nodes.size() ||
372 sn_.nodes[join_node - 1].nodetype != NodeType::Join)
373 throw InputError("bind_join: the node being bound is not a Join");
374 if (fork_node == 0 || fork_node > sn_.nodes.size() ||
375 sn_.nodes[fork_node - 1].nodetype != NodeType::Fork)
376 throw InputError("Join '" + sn_.nodes[join_node - 1].name +
377 "': the node it closes is not a Fork");
378 sn_.fj.emplace_back(fork_node, join_node);
379 }
380
381 /**
382 * A Place: an SPN token container. Modelled as an INF-scheduled station so
383 * the marginal machinery treats its tokens as "in service", which is the
384 * encoding `State.toMarginal` folds the buffer slot back into.
385 */
386 std::size_t add_place(const std::string& nm) { return add_place(nm, SchedStrategy::INF); }
387
388 /**
389 * A Place whose EMBEDDED QUEUE is served under `sched`: the QUEUEING PLACE
390 * of a queueing Petri net, `Place(model, name, schedStrategy)` in MATLAB.
391 *
392 * A token arriving here is NOT immediately available to the output
393 * transitions. It joins the place's own queue, is served by the place's own
394 * servers, and only on completion does it reach the DEPOSITORY that the
395 * output arcs draw from. That is the whole of the construct, and it is why
396 * the discipline has to be declared at construction: MATLAB's
397 * `installQueueServer` picks the server SECTION from it, and the section is
398 * what decides the server count.
399 *
400 * DECLARING THE DISCIPLINE DOES NOT YET MAKE IT A QUEUEING PLACE.
401 * `is_queueing_place` keys on a service process being present, which is
402 * `Place.queueing` being raised by `setService` and not by the constructor;
403 * a place built with a discipline and left without a service law is an
404 * ordinary pass-through place, exactly as in the reference.
405 *
406 * The server count follows the section: one server for the queueing
407 * disciplines and infinitely many for INF, which is also the ordinary
408 * place's own shape, so `add_place(nm)` is this call at INF.
409 */
410 std::size_t add_place(const std::string& nm, SchedStrategy sched) {
411 Station<T> st;
412 st.name = nm;
413 st.nodetype = NodeType::Place;
414 st.sched = sched;
415 st.nservers = (sched == SchedStrategy::INF)
416 ? std::numeric_limits<double>::infinity()
417 : 1.0;
418 const std::size_t ist = sn_.add_station(st);
419 init_node(sn_.station_to_node[ist - 1]);
420 return sn_.station_to_node[ist - 1];
421 }
422
423 /**
424 * A Transition: the firing rules of an SPN, as `Transition` in MATLAB.
425 *
426 * A transition has MODES, not classes: each mode has its own enabling and
427 * inhibiting conditions over the places, its own firing effect, and its own
428 * firing process. The parameters are transcribed from the reference's
429 * `refreshPetriNetNodes.m`, so `State.fromMarginal` finds the fields it
430 * reads instead of a node it cannot decode.
431 */
432 std::size_t add_transition(const std::string& nm, const TransitionParam<T>& par) {
433 if (par.nmodes == 0)
434 throw InputError("add_transition: a transition needs at least one mode");
435 if (par.enabling.size() != par.nmodes || par.firing.size() != par.nmodes)
436 throw InputError(
437 "add_transition: enabling and firing must have one entry per mode");
438 const std::size_t nd = sn_.add_node(nm, NodeType::Transition, true);
439 sn_.transparam[nd] = par;
440 init_node(nd);
441 return nd;
442 }
443
444 /**
445 * A Transition with NO modes yet: `Transition(model, name)` as MATLAB, the
446 * JAR and Python spell it, with the modes declared afterwards.
447 *
448 * THE NODE IS REGISTERED FIRST, which is the whole point of this overload.
449 * A transition is itself a node, so the arc matrices of the form above are
450 * (nnodes x nclasses) over a node count that does not exist until the LAST
451 * transition has been added -- a caller building a net through that
452 * overload has to hand-count the total and re-count it whenever the net
453 * gains a node. Here the arcs are held SPARSELY as (mode, node, class)
454 * triples and materialised against the finished model by `get_struct()`,
455 * so nothing has to be counted and nothing can be counted wrong.
456 */
457 std::size_t add_transition(const std::string& nm) {
458 const std::size_t nd = sn_.add_node(nm, NodeType::Transition, true);
459 sn_.transparam[nd] = TransitionParam<T>();
460 pending_arcs_[nd];
461 init_node(nd);
462 return nd;
463 }
464
465 /**
466 * `Transition.addMode(name)`: a new firing mode, returning its 1-based
467 * index. The defaults are the reference's own -- one server, TIMED,
468 * priority one, weight one, and no firing law until `set_mode_distribution`
469 * gives it one.
470 */
471 std::size_t add_mode(std::size_t node, const std::string& nm) {
472 TransitionParam<T>& tp = declarative_transition(node, "add_mode");
473 tp.modenames.push_back(nm);
476 tp.firingphases.push_back(0);
477 tp.nmodeservers.push_back(1.0);
478 tp.firingprio.push_back(1.0);
479 tp.fireweight.push_back(num_traits<T>::from_int(1));
480 tp.firingdep.push_back(std::function<T(const std::vector<T>&)>());
481 // Placeholders: the arcs are sparse until `materialize_transitions()`
482 // sizes them, but every per-mode vector must stay the same length.
483 tp.enabling.push_back(Matrix<T>());
484 tp.inhibiting.push_back(Matrix<T>());
485 tp.firing.push_back(Matrix<T>());
486 tp.nmodes = tp.modenames.size();
487 return tp.nmodes;
488 }
489
490 /** `Transition.setDistribution(mode, dist)`: the mode's firing law. */
491 void set_mode_distribution(std::size_t node, std::size_t mode, const Distrib<T>& d) {
492 mode_param(node, mode, "set_mode_distribution").firingproc[mode - 1] = d;
493 }
494
495 /** `Transition.setTimingStrategy(mode, strategy)`: TIMED or IMMEDIATE. */
496 void set_mode_timing(std::size_t node, std::size_t mode, lang::TimingStrategy ts) {
497 mode_param(node, mode, "set_mode_timing").timing[mode - 1] = ts;
498 }
499
500 /** `Transition.setNumberOfServers(mode, n)`; `GlobalConstants::MaxInt` is infinite. */
501 void set_mode_servers(std::size_t node, std::size_t mode, double n) {
502 mode_param(node, mode, "set_mode_servers").nmodeservers[mode - 1] = n;
503 }
504
505 /** `Transition.setFiringPriorities(mode, priority)`. */
506 void set_firing_priority(std::size_t node, std::size_t mode, double prio) {
507 mode_param(node, mode, "set_firing_priority").firingprio[mode - 1] = prio;
508 }
509
510 /** `Transition.setFiringWeights(mode, weight)`: the share among tied modes. */
511 void set_firing_weight(std::size_t node, std::size_t mode, const T& w) {
512 mode_param(node, mode, "set_firing_weight").fireweight[mode - 1] = w;
513 }
514
515 /** `Transition.setFiringRateDependence(mode, g)`: g(marking) scales the rate. */
516 void set_mode_firing_dependence(std::size_t node, std::size_t mode,
517 const std::function<T(const std::vector<T>&)>& g) {
518 mode_param(node, mode, "set_mode_firing_dependence").firingdep[mode - 1] = g;
519 }
520
521 /**
522 * `Transition.setEnablingConditions(mode, class, place, tokens)`: how many
523 * class-r tokens the mode needs at `place` before it may fire.
524 *
525 * The CLASS IS NOT DECORATION: a mode needing two Class1 tokens must not be
526 * enabled by Class2 tokens sitting at the same place, so the arc is keyed
527 * by the (place, class) pair and not by the place alone.
528 */
529 void set_enabling_conditions(std::size_t node, std::size_t mode, std::size_t cls,
530 std::size_t place, const T& tokens) {
531 arc_target(node, mode, cls, place, true, "set_enabling_conditions");
532 pending_arcs_[node].enab.push_back(TransitionArc(mode, place, cls, tokens));
533 }
534
535 /**
536 * `Transition.setInhibitingConditions(mode, class, place, tokens)`: the
537 * class-r count at `place` that BLOCKS the mode. Absent means never, which
538 * the materialised matrix spells as infinity.
539 */
540 void set_inhibiting_conditions(std::size_t node, std::size_t mode, std::size_t cls,
541 std::size_t place, const T& tokens) {
542 arc_target(node, mode, cls, place, true, "set_inhibiting_conditions");
543 pending_arcs_[node].inhib.push_back(TransitionArc(mode, place, cls, tokens));
544 }
545
546 /**
547 * `Transition.setFiringOutcome(mode, class, node, tokens)`: the class-r
548 * tokens the firing deposits. The destination is any node the firing may
549 * reach, a Sink included, which is why it is not narrowed to a Place.
550 */
551 void set_firing_outcome(std::size_t node, std::size_t mode, std::size_t cls,
552 std::size_t dest, const T& tokens) {
553 arc_target(node, mode, cls, dest, false, "set_firing_outcome");
554 pending_arcs_[node].fire.push_back(TransitionArc(mode, dest, cls, tokens));
555 }
556
557 /**
558 * `Queue.setRetrial(...)`: a station with an ORBIT instead of a waiting
559 * line. An arrival finding every server busy joins the orbit and re-attempts
560 * at `rate`; a completion does NOT promote from the orbit, so the state is
561 * an (in-service, orbit) split rather than an ordered buffer.
562 */
563 void set_retrial(std::size_t node, std::size_t cls, const Distrib<T>& proc, const T& rate,
564 int max_attempts = 0) {
565 if (node == 0 || node > sn_.nodes.size())
566 throw InputError("set_retrial: node index is out of range");
567 const std::size_t ist = sn_.nodes[node - 1].station;
568 if (ist == 0) throw InputError("set_retrial: node is not a station");
569 // Size against the LIVE class list: sn_.nclasses is a finalize-time
570 // field and is still zero while the model is being built, so sizing
571 // against it left the vectors empty and the write below went out of
572 // bounds.
573 const std::size_t K = sn_.classes.size();
574 if (cls == 0 || cls > K)
575 throw InputError("set_retrial: class index is out of range");
576 RetrialParam<T>& rp = sn_.retrialparam[ist];
577 if (rp.retrial_proc.size() < K) {
578 rp.retrial_proc.resize(K);
579 rp.retrial_rate.resize(K, num_traits<T>::from_int(0));
580 rp.max_attempts.resize(K, 0);
581 }
582 rp.retrial_proc[cls - 1] = proc;
583 rp.retrial_rate[cls - 1] = rate;
584 rp.max_attempts[cls - 1] = max_attempts;
585 }
586
587 /**
588 * `Queue.setPatience(class, dist, type)`: the abandonment timer of a job
589 * WAITING at the station, and which impatience rule the timer belongs to.
590 */
591 void set_patience(std::size_t node, std::size_t cls, const Distrib<T>& dist,
593 Station<T>& st = station_ref(node, cls, "set_patience");
594 grow_class_slot(st.patience, cls, Distrib<T>::disabled_dist());
595 grow_class_slot(st.impatience, cls, lang::ImpatienceType::NONE);
596 st.patience[cls - 1] = dist;
597 st.impatience[cls - 1] = kind;
598 }
599
600 /** `Queue.setOrbitImpatience(class, dist)`: abandonment from the retrial orbit. */
601 void set_orbit_impatience(std::size_t node, std::size_t cls, const Distrib<T>& dist) {
602 Station<T>& st = station_ref(node, cls, "set_orbit_impatience");
603 grow_class_slot(st.orbit_impatience, cls, Distrib<T>::disabled_dist());
604 st.orbit_impatience[cls - 1] = dist;
605 }
606
607 /** `Queue.setBatchRejectProbability(class, p)`. */
608 void set_batch_reject(std::size_t node, std::size_t cls, const T& p) {
609 Station<T>& st = station_ref(node, cls, "set_batch_reject");
610 grow_class_slot(st.batch_reject, cls, num_traits<T>::from_int(0));
611 st.batch_reject[cls - 1] = p;
612 }
613
614 /**
615 * `Queue.setBalking(class, strategy, thresholds)`: an arrival that refuses
616 * to JOIN, on the state it finds. Distinct from reneging, which abandons a
617 * job that has already joined.
618 */
619 void set_balking(std::size_t node, std::size_t cls, lang::BalkingStrategy strategy,
620 const std::vector<typename Station<T>::BalkingThreshold>& thresholds) {
621 Station<T>& st = station_ref(node, cls, "set_balking");
622 grow_class_slot(st.balking, cls, typename Station<T>::BalkingParam());
623 st.balking[cls - 1].strategy = strategy;
624 st.balking[cls - 1].thresholds = thresholds;
625 }
626
627 /** `Queue.addServerType(...)`: one heterogeneous server pool of the station. */
628 void add_server_type(std::size_t node, const typename Station<T>::ServerType& stype) {
629 const std::size_t ist = station_of(node, "add_server_type");
630 sn_.stations[ist - 1].server_types.push_back(stype);
631 // The pools size the station, as in MATLAB/JAR updateTotalServerCount
632 double total = 0.0;
633 for (const auto& pool : sn_.stations[ist - 1].server_types) total += pool.count;
634 sn_.stations[ist - 1].nservers = total;
635 }
636
637 /**
638 * `Queue.setServerParallelism(class, n)`: the servers a job seizes for the
639 * whole of its service. The station then serves at most floor(c/n) such jobs
640 * at a time.
641 */
642 void set_server_parallelism(std::size_t node, std::size_t cls, std::size_t n) {
643 Station<T>& st = station_ref(node, cls, "set_server_parallelism");
644 if (n < 1) {
645 throw InputError("set_server_parallelism: parallelism must be a positive integer");
646 }
647 if (std::isfinite(st.nservers) && static_cast<double>(n) > st.nservers) {
648 throw InputError(
649 "set_server_parallelism: parallelism " + std::to_string(n) + " exceeds the " +
650 std::to_string(static_cast<long long>(st.nservers)) + " servers of station '" +
651 sn_.nodes[node - 1].name + "', so a job of this class could never enter service");
652 }
653 grow_class_slot(st.server_parallelism, cls, static_cast<std::size_t>(1));
654 st.server_parallelism[cls - 1] = n;
655 }
656
657 /** `Queue.setHeteroSchedPolicy(...)`: how the server pools are picked among. */
658 void set_hetero_sched_policy(std::size_t node, lang::HeteroSchedPolicy policy) {
659 sn_.stations[station_of(node, "set_hetero_sched_policy") - 1].hetero_policy = policy;
660 }
661
662 /**
663 * `Source.setArrivalBatch(class, dist)`: the batch-size law released at each
664 * arrival epoch. The arrival process itself only spaces the epochs.
665 */
666 void set_arrival_batch(std::size_t node, std::size_t cls, const Distrib<T>& dist) {
667 Station<T>& st = station_ref(node, cls, "set_arrival_batch");
668 grow_class_slot(st.arrival_batch, cls, Distrib<T>::disabled_dist());
669 st.arrival_batch[cls - 1] = dist;
670 }
671
672 /** `Source.markedClasses`: the 1-based class carried by each mark of an MMAP. */
673 void set_marked_classes(std::size_t node, const std::vector<std::size_t>& classes) {
674 sn_.stations[station_of(node, "set_marked_classes") - 1].marked_classes = classes;
675 }
676
677 /** `Place.setDepartureDiscipline(class, rule)`. */
678 void set_departure_discipline(std::size_t node, std::size_t cls,
680 Station<T>& st = station_ref(node, cls, "set_departure_discipline");
682 st.departure_discipline[cls - 1] = rule;
683 }
684
685 /** `Place.setState(marking)`: the initial token count of the place, per class. */
686 void set_initial_marking(std::size_t node, const std::vector<T>& tokens) {
687 if (node == 0 || node > sn_.nodes.size())
688 throw InputError("set_initial_marking: node index is out of range");
689 if (sn_.nodes[node - 1].nodetype != NodeType::Place)
690 throw InputError("set_initial_marking: only a Place carries an initial marking");
691 sn_.initmarking[node] = tokens;
692 }
693
694 /**
695 * `StatefulNode.setStatePrior(space, prior)`: a distribution over the rows
696 * of a DECLARED state space. The two are set together because a prior
697 * indexes that space and means nothing without it.
698 */
699 void set_state_prior(std::size_t node, const Matrix<T>& space, const std::vector<T>& prior) {
700 if (node == 0 || node > sn_.nodes.size())
701 throw InputError("set_state_prior: node index is out of range");
702 if (space.rows() != prior.size())
703 throw InputError(
704 "set_state_prior: the prior has one entry per ROW of the declared state space");
705 sn_.statespace[node] = space;
706 sn_.stateprior[node] = prior;
707 }
708
709 /** `Join.setStrategy(...)`: STD waits for every sibling, PARTIAL for a quorum. */
710 void set_join_strategy(std::size_t node, lang::JoinStrategy strategy, double quorum = 0.0) {
711 if (node == 0 || node > sn_.nodes.size())
712 throw InputError("set_join_strategy: node index is out of range");
713 if (sn_.nodes[node - 1].nodetype != NodeType::Join)
714 throw InputError("set_join_strategy: the node is not a Join");
715 typename NetworkStruct<T>::JoinDecl jd;
716 jd.strategy = strategy;
717 jd.quorum = quorum;
718 sn_.joindecl[node] = jd;
719 }
720
721 /** The per-destination weights of a WRROBIN dispatcher, per (node, class). */
722 void set_routing_weights(std::size_t node, std::size_t cls,
723 const std::map<std::size_t, double>& weights) {
724 if (node == 0 || node > sn_.nodes.size())
725 throw InputError("set_routing_weights: node index is out of range");
726 std::vector<std::map<std::size_t, double>>& rw = sn_.nodes[node - 1].routing_weights;
727 if (rw.size() < sn_.classes.size()) rw.resize(sn_.classes.size());
728 if (cls == 0 || cls > rw.size())
729 throw InputError("set_routing_weights: class index is out of range");
730 rw[cls - 1] = weights;
731 }
732
733 /** The d of a power-of-d (SQ) dispatcher, per (node, class). */
734 void set_routing_param(std::size_t node, std::size_t cls, int d) {
735 if (node == 0 || node > sn_.nodes.size())
736 throw InputError("set_routing_param: node index is out of range");
737 std::vector<int>& rp = sn_.nodes[node - 1].routing_param;
738 if (rp.size() < sn_.classes.size()) rp.resize(sn_.classes.size(), 0);
739 if (cls == 0 || cls > rp.size())
740 throw InputError("set_routing_param: class index is out of range");
741 rp[cls - 1] = d;
742 }
743
744 /**
745 * `Queue.setDelayOff(class, setupTime, delayoffTime)`: the station powers
746 * down after sitting idle for the delay-off time, and the next arrival pays
747 * the setup time before service. The pair is set together because a setup
748 * with no delay-off never fires and a delay-off with no setup is free.
749 */
750 void set_setup_delayoff(std::size_t node, std::size_t cls, const Distrib<T>& setup,
751 const Distrib<T>& delayoff) {
752 const std::size_t ist = station_of(node, "set_setup_delayoff");
753 const std::size_t K = sn_.classes.size();
754 if (cls == 0 || cls > K)
755 throw InputError("set_setup_delayoff: class index is out of range");
756 SetupDelayOffParam<T>& sp = sn_.setupparam[ist];
757 if (sp.setup.size() < K) {
758 sp.setup.resize(K, Distrib<T>::disabled_dist());
759 sp.delayoff.resize(K, Distrib<T>::disabled_dist());
760 }
761 sp.setup[cls - 1] = setup;
762 sp.delayoff[cls - 1] = delayoff;
763 }
764
765 /**
766 * `Queue.setBreakdown(failure, repair, downService)`: the server alternates
767 * up and down on the two clocks.
768 *
769 * BOTH CLOCKS ARE REQUIRED. A server that fails and is never repaired is a
770 * different model -- an absorbing one -- and the reference declines to infer
771 * it from a missing repair time rather than treating it as infinite.
772 *
773 * `down_service` is per class and OPTIONAL; an entry left disabled means the
774 * class gets no service while the server is down, which is the ordinary
775 * reading. A declared one must be EXPONENTIAL: a phase-type degraded service
776 * would need its own phase block in the joint chain and no codebase builds
777 * one, so it is refused by name rather than approximated by its mean.
778 */
779 void set_breakdown(std::size_t node, const Distrib<T>& failure, const Distrib<T>& repair,
780 const std::vector<Distrib<T>>& down_service = std::vector<Distrib<T>>()) {
781 const std::size_t ist = station_of(node, "set_breakdown");
782 if (failure.disabled || repair.disabled)
783 throw InputError(
784 "set_breakdown: both a failure and a repair time are required; a server that "
785 "never recovers is an absorbing model, not a breakdown");
786 const double fm = num_traits<T>::to_double(failure.mean);
787 const double rm = num_traits<T>::to_double(repair.mean);
788 if (!(fm > 0.0) || !(rm > 0.0))
789 throw InputError("set_breakdown: the failure and repair times must have positive means");
790 const std::size_t K = sn_.classes.size();
791 BreakdownParam<T>& bp = sn_.breakdownparam[ist];
792 bp.failure = failure;
793 bp.repair = repair;
794 bp.failure_rate = T(num_traits<T>::from_int(1) / failure.mean);
795 bp.repair_rate = T(num_traits<T>::from_int(1) / repair.mean);
797 for (std::size_t r = 0; r < K && r < down_service.size(); ++r) {
798 const Distrib<T>& d = down_service[r];
799 if (d.disabled) continue;
801 throw UnsupportedError(
802 "set_breakdown: station '" + sn_.stations[ist - 1].name +
803 "': the down-server service distribution must be exponential; a phase-type "
804 "degraded service would need its own phase block in the joint chain");
805 if (num_traits<T>::to_double(d.mean) > 0.0)
807 }
808 }
809
810 /** A Cache node with its item population, list capacities and popularity. */
811 std::size_t add_cache(const std::string& nm, const CacheParam<T>& par) {
812 const std::size_t nd = sn_.add_node(nm, NodeType::Cache, true);
813 sn_.nodeparam[nd] = par;
814 init_node(nd);
815 return nd;
816 }
817
818 /**
819 * `Cache.setItemReadClasses(readClasses, hitClasses)`: declare that
820 * `read_classes[i]` is the request stream for item i at this cache. Use at the
821 * cache the exogenous requests enter, where the per-item classes are the
822 * caller's own; item popularity is then carried by the per-class request rates
823 * rather than by a popularity the cache draws from. This is what keeps a cache
824 * network free of arc-level class switching, so no class acquires a default
825 * route into the cache the model never intended. `hit_classes` is either one
826 * class shared by every item or one per item.
827 */
828 void set_item_read_classes(std::size_t cache_node,
829 const std::vector<std::size_t>& read_classes,
830 const std::vector<std::size_t>& hit_classes) {
831 auto it = sn_.nodeparam.find(cache_node);
832 if (it == sn_.nodeparam.end())
833 throw InputError("setItemReadClasses: node is not a Cache");
834 CacheParam<T>& cp = it->second;
835 const std::size_t nitems = cp.nitems;
836 if (read_classes.size() != nitems)
837 throw InputError("setItemReadClasses: pass exactly one read class per item");
838 const std::vector<std::size_t> hit =
839 per_item_classes(hit_classes, nitems, "setItemReadClasses hit");
840 const std::size_t K = sn_.classes.size();
841 cp.pread.resize(K);
842 cp.preadkind.resize(K);
843 cp.hitclass.resize(K, 0);
844 cp.missclass.resize(K, 0);
845 cp.classitem.resize(K, 0);
846 for (std::size_t i = 0; i < nitems; ++i) {
847 std::vector<T> onehot(nitems, num_traits<T>::from_int(0));
848 onehot[i] = num_traits<T>::from_int(1);
849 cp.pread[read_classes[i] - 1] = onehot;
850 cp.hitclass[read_classes[i] - 1] = hit[i];
851 cp.classitem[read_classes[i] - 1] = i + 1;
852 }
853 cache_item_classes_[cache_node] = read_classes;
854 }
855
856 /**
857 * `Cache.setMissCache(readClass, nextCache, hitClassAtNext)`: send this cache's
858 * misses to `next_cache` preserving item identity, by minting one class per item
859 * there and making the miss class of item i here its read class for item i.
860 * Returns the minted classes. The cache-to-cache arc itself is registered here
861 * and injected by `link()`, so the caller routes only its own topology.
862 */
863 std::vector<std::size_t> set_miss_cache(std::size_t cache_node, std::size_t next_cache,
864 const std::vector<std::size_t>& hit_classes_at_next) {
865 auto it = sn_.nodeparam.find(cache_node);
866 auto itn = sn_.nodeparam.find(next_cache);
867 if (it == sn_.nodeparam.end() || itn == sn_.nodeparam.end())
868 throw InputError("setMissCache: both nodes must be Caches");
869 if (it->second.nitems != itn->second.nitems)
870 throw InputError("setMissCache: a cache network requires one common item set");
871 auto self_it = cache_item_classes_.find(cache_node);
872 if (self_it == cache_item_classes_.end())
873 throw InputError("setMissCache: call setItemReadClasses on the source cache first");
874 const std::vector<std::size_t> self_cls = self_it->second;
875 const std::size_t nitems = it->second.nitems;
876 const std::vector<std::size_t> hit =
877 per_item_classes(hit_classes_at_next, nitems, "setMissCache hit");
878
879 std::vector<std::size_t> minted;
880 minted.reserve(nitems);
881 const bool closed = sn_.classes[self_cls[0] - 1].type == JobClassType::CLOSED;
882 const std::size_t refstat = sn_.classes[self_cls[0] - 1].refstat;
883 for (std::size_t i = 0; i < nitems; ++i) {
884 std::size_t cls;
885 if (closed)
886 cls = add_closed_class(sn_.nodes[next_cache - 1].name + "_item" + std::to_string(i + 1),
887 0.0, sn_.station_to_node[refstat - 1]);
888 else
889 cls = add_open_class(sn_.nodes[next_cache - 1].name + "_item" + std::to_string(i + 1));
890 minted.push_back(cls);
891 }
892 // the class count grew, so re-size both caches' per-class vectors once
893 const std::size_t K = sn_.classes.size();
894 for (std::size_t nd : {cache_node, next_cache}) {
895 CacheParam<T>& c = sn_.nodeparam.at(nd);
896 c.pread.resize(K);
897 c.preadkind.resize(K);
898 c.hitclass.resize(K, 0);
899 c.missclass.resize(K, 0);
900 c.classitem.resize(K, 0);
901 }
902 CacheParam<T>& src = sn_.nodeparam.at(cache_node);
903 CacheParam<T>& dst = sn_.nodeparam.at(next_cache);
904 for (std::size_t i = 0; i < nitems; ++i) {
905 std::vector<T> onehot(nitems, num_traits<T>::from_int(0));
906 onehot[i] = num_traits<T>::from_int(1);
907 dst.pread[minted[i] - 1] = onehot;
908 dst.hitclass[minted[i] - 1] = hit[i];
909 dst.classitem[minted[i] - 1] = i + 1;
910 src.missclass[self_cls[i] - 1] = minted[i];
911 // the miss hop is part of the construction, not of the user's topology:
912 // register it here and let link() inject it, as the MATLAB helper does
913 // through retrievalRoutingEntries
914 cache_miss_arcs_.push_back(CacheMissArc(minted[i], cache_node, next_cache));
915 }
916 cache_item_classes_[next_cache] = minted;
917 return minted;
918 }
919
920 /**
921 * `Cache.setItemMissClass(readClass, missClasses)`: terminate a cache network,
922 * every per-item class of this cache reporting a miss as the matching entry of
923 * `miss_classes`, which the caller routes onward.
924 */
925 void set_item_miss_class(std::size_t cache_node, const std::vector<std::size_t>& miss_classes) {
926 auto it = sn_.nodeparam.find(cache_node);
927 if (it == sn_.nodeparam.end())
928 throw InputError("setItemMissClass: node is not a Cache");
929 auto self_it = cache_item_classes_.find(cache_node);
930 if (self_it == cache_item_classes_.end())
931 throw InputError("setItemMissClass: the cache has no per-item classes");
932 const std::vector<std::size_t>& self_cls = self_it->second;
933 const std::vector<std::size_t> miss =
934 per_item_classes(miss_classes, self_cls.size(), "setItemMissClass miss");
935 CacheParam<T>& cp = it->second;
936 cp.missclass.resize(sn_.classes.size(), 0);
937 for (std::size_t i = 0; i < self_cls.size(); ++i)
938 cp.missclass[self_cls[i] - 1] = miss[i];
939 }
940
941 /**
942 * `Cache.setRetrievalSystem(readClass, missClass, queues)`: a delayed-hit
943 * cache whose misses are fetched by circulating a per-item retrieval class
944 * through `queue_nodes` and back to the cache. Creates one retrieval class
945 * per item, each inheriting the read class's service at every retrieval queue
946 * (call `set_service(queue, readClass, ...)` first); the routing among the
947 * cache and the queues is inherited from the read class in `link()`. Records
948 * the retrieval capacity (nitems - total cache capacity), the queue node
949 * list, and the item -> retrieval-class map that `cache_retrieval_inputs`
950 * reads. Must be called after the read/miss classes and the cache exist.
951 */
952 void set_retrieval_system(std::size_t cache_node, std::size_t read_class,
953 std::size_t miss_class, const std::vector<std::size_t>& queue_nodes) {
954 auto it = sn_.nodeparam.find(cache_node);
955 if (it == sn_.nodeparam.end())
956 throw InputError("setRetrievalSystem: node is not a Cache");
957 CacheParam<T>& cp = it->second;
958 if (queue_nodes.empty())
959 throw InputError("setRetrievalSystem: the retrieval system has no stations");
960 const std::size_t nitems = cp.nitems;
961 int totalcap = 0;
962 for (int c : cp.itemcap) totalcap += c;
963 cp.retrieval_capacity = static_cast<int>(nitems) - totalcap;
964 cp.retrieval_queues[read_class - 1] = queue_nodes; // 0-based key, 1-based nodes
965
966 // capture the read class's service distribution at each queue up front
967 std::vector<Distrib<T> > svc;
968 svc.reserve(queue_nodes.size());
969 for (std::size_t q : queue_nodes) {
970 const std::size_t st = station_of(q, "setRetrievalSystem");
971 svc.push_back(sn_.service[st - 1][read_class - 1]);
972 }
973
974 cp.retrieval_classes.assign(nitems, std::vector<std::size_t>(sn_.classes.size(), 0));
975 const bool closed = sn_.classes[read_class - 1].type == JobClassType::CLOSED;
976 const std::size_t refstat = sn_.classes[read_class - 1].refstat;
977 for (std::size_t i = 0; i < nitems; ++i) {
978 std::size_t rc;
979 if (closed)
980 rc = add_closed_class(sn_.classes[read_class - 1].name + "_retrievalClass_" +
981 std::to_string(i + 1),
982 0.0, sn_.station_to_node[refstat - 1]);
983 else
984 rc = add_open_class(sn_.classes[read_class - 1].name + "_retrievalClass_" +
985 std::to_string(i + 1));
986 for (std::size_t s = 0; s < queue_nodes.size(); ++s) {
987 set_service(queue_nodes[s], rc, svc[s]);
988 // at most one retrieval of a given item is ever in flight
989 set_class_capacity(queue_nodes[s], rc, 1.0);
990 }
991 // grow the retrieval_classes rows to the new class count and record
992 for (std::size_t k = 0; k < nitems; ++k)
993 cp.retrieval_classes[k].resize(sn_.classes.size(), 0);
994 cp.retrieval_classes[i][read_class - 1] = rc;
995 // The returning READ of a retrieval class reads ITS OWN item, and is
996 // logged as the miss that started the fetch. Without these two the
997 // state-based solvers see a class that reads nothing, so the fetch
998 // never completes and the retrieval sub-network is never entered.
999 const std::size_t K = sn_.classes.size();
1000 cp.pread.resize(K);
1001 cp.preadkind.resize(K);
1002 cp.hitclass.resize(K, 0);
1003 cp.missclass.resize(K, 0);
1004 std::vector<T> onehot(nitems, num_traits<T>::from_int(0));
1005 onehot[i] = num_traits<T>::from_int(1);
1006 cp.pread[rc - 1] = onehot;
1007 cp.missclass[rc - 1] = miss_class;
1008 }
1009 }
1010
1011 // -----------------------------------------------------------------------
1012 // Classes
1013 // -----------------------------------------------------------------------
1014
1015 /** A closed class of the given population, referencing a station node. */
1016 std::size_t add_closed_class(const std::string& nm, double njobs, std::size_t refstat_node,
1017 int prio = 0) {
1018 if (!(njobs >= 0.0) || std::isinf(njobs))
1019 throw InputError("ClosedClass '" + nm + "': the population must be finite");
1020 JobClass cl;
1021 cl.name = nm;
1022 cl.type = JobClassType::CLOSED;
1023 cl.population = njobs;
1024 cl.refstat = station_of(refstat_node, "ClosedClass '" + nm + "'");
1025 cl.prio = prio;
1026 const std::size_t r = sn_.add_class(cl);
1027 grow_class_vectors();
1028 return r;
1029 }
1030
1031 /**
1032 * An open class. Its reference station is the Source, which must exist:
1033 * an open class with no arrival station has no reference for its visits.
1034 */
1035 std::size_t add_open_class(const std::string& nm, int prio = 0) {
1036 if (sn_.sourceIdx == 0)
1037 throw InputError("OpenClass '" + nm +
1038 "': the model has no Source to reference; add one first");
1039 JobClass cl;
1040 cl.name = nm;
1041 cl.type = JobClassType::OPEN;
1042 cl.population = std::numeric_limits<double>::infinity();
1043 cl.refstat = sn_.sourceIdx;
1044 cl.prio = prio;
1045 const std::size_t r = sn_.add_class(cl);
1046 grow_class_vectors();
1047 return r;
1048 }
1049
1050 /**
1051 * `SelfLoopingClass(model, name, njobs, refstat, prio)`: a closed class
1052 * whose jobs perpetually cycle at their reference station.
1053 *
1054 * Built as a closed class because that is all it is -- the self-loop is in
1055 * the routing, and `SelfLoopingClass.m` adds no state -- with the subclass
1056 * recorded so the wire type survives a round trip.
1057 */
1058 std::size_t add_self_looping_class(const std::string& nm, double njobs,
1059 std::size_t refstat_node, int prio = 0) {
1060 const std::size_t r = add_closed_class(nm, njobs, refstat_node, prio);
1061 sn_.classes[r - 1].self_looping = true;
1062 return r;
1063 }
1064
1065 /** `JobClass.setReferenceClass(true)`: `sn.refclass(c)` picks this class. */
1066 void set_reference_class(std::size_t cls) {
1067 class_ref(cls, "set_reference_class").is_ref_class = true;
1068 }
1069
1070 /** `JobClass.deadline`: the soft deadline EDD, EDF and JMT's tardiness use. */
1071 void set_class_deadline(std::size_t cls, double due) {
1072 class_ref(cls, "set_class_deadline").deadline = due;
1073 }
1074
1075 /**
1076 * `JobClass.spawnClass` (`sn.classspawn`): the class injected at the same
1077 * station on every completion of `cls`.
1078 */
1079 void set_class_spawn(std::size_t cls, std::size_t spawn_cls) {
1080 const std::size_t K = sn_.classes.size();
1081 if (spawn_cls == 0 || spawn_cls > K)
1082 throw InputError("set_class_spawn: the spawned class index is out of range");
1083 class_ref(cls, "set_class_spawn").spawn = spawn_cls;
1084 }
1085
1086 /**
1087 * `JobClass.setPatience(kind, dist)`: the CLASS-WIDE abandonment law.
1088 *
1089 * The reference has no class-indexed patience in `sn`: `refreshStruct`
1090 * reads it through `Queue.getPatience`, which falls back to the class
1091 * setting wherever the station declares none, so the class-level law is
1092 * materialized onto every Queue and Delay here for exactly that reason.
1093 * A station-level `setPatience` therefore wins, as it does in MATLAB.
1094 */
1095 void set_class_patience(std::size_t cls, const Distrib<T>& dist,
1097 const std::size_t K = sn_.classes.size();
1098 if (cls == 0 || cls > K) throw InputError("set_class_patience: class index out of range");
1099 for (std::size_t ist = 0; ist < sn_.stations.size(); ++ist) {
1100 const std::size_t ind = sn_.station_to_node[ist];
1101 const lang::NodeType nt = sn_.nodes[ind - 1].nodetype;
1102 if (nt != lang::NodeType::Queue && nt != lang::NodeType::Delay) continue;
1103 Station<T>& st = sn_.stations[ist];
1104 grow_class_slot(st.patience, cls, Distrib<T>::disabled_dist());
1105 grow_class_slot(st.impatience, cls, lang::ImpatienceType::NONE);
1106 if (!st.patience[cls - 1].disabled) continue; // the station setting wins
1107 st.patience[cls - 1] = dist;
1108 st.impatience[cls - 1] = kind;
1109 }
1110 }
1111
1112 /**
1113 * `JobClass.setReplySignalClass(reply)` (`sn.syncreply`), plus the
1114 * `sn.replyblock` the state layer needs.
1115 *
1116 * The reference derives the block rather than being told it
1117 * (`refreshLocalVars.m:340-386`): a server is held at every non-Source,
1118 * non-INF station the REPLY class can be routed INTO, and every such
1119 * station must be FCFS because a held server is encoded as a per-class
1120 * counter. That derivation needs the routing, so it runs in `finalize()`;
1121 * this only records the binding.
1122 */
1123 void set_reply_signal_class(std::size_t call_cls, std::size_t reply_cls) {
1124 const std::size_t K = sn_.classes.size();
1125 if (call_cls == 0 || call_cls > K || reply_cls == 0 || reply_cls > K)
1126 throw InputError("set_reply_signal_class: class index is out of range");
1127 // resize, not assign: a class added since the last binding must not erase the earlier bindings
1128 if (sn_.syncreply.size() < K) sn_.syncreply.resize(K, 0);
1129 sn_.syncreply[call_cls - 1] = reply_cls;
1131 }
1132
1133 // -----------------------------------------------------------------------
1134 // Station parameters
1135 // -----------------------------------------------------------------------
1136
1137 /**
1138 * `station.setService(class, dist)`.
1139 *
1140 * A Prior gets its MIXTURE moments here, where a Markovian family gets the
1141 * moments of its (D0,D1): both are the "what does the struct report before
1142 * anything solves" question, and leaving a Prior at mean 0 would make a
1143 * struct dump read as an Immediate.
1144 */
1145 void set_service(std::size_t node, std::size_t cls, const Distrib<T>& d) {
1146 Distrib<T> dd = d;
1147 if (dd.is_prior())
1149 else
1151 sn_.set_service(station_of(node, "setService"), cls, dd);
1152 }
1153
1154 /** `source.setArrival(class, dist)`: the same table, at the Source. */
1155 void set_arrival(std::size_t node, std::size_t cls, const Distrib<T>& d) {
1156 const std::size_t ist = station_of(node, "setArrival");
1157 if (sn_.stations[ist - 1].nodetype != NodeType::Source)
1158 throw InputError("setArrival: node '" + sn_.nodes[node - 1].name + "' is not a Source");
1159 Distrib<T> dd = d;
1160 if (dd.is_prior())
1162 else
1164 sn_.set_service(ist, cls, dd);
1165 }
1166
1167 /**
1168 * `queue.setNumberOfServers(n)`.
1169 *
1170 * IT IS A NO-OP ON AN INF-SCHEDULED STATION, which is what MATLAB does: the
1171 * method switches on the discipline and ignores the request for
1172 * SchedStrategy.INF. Lowering the multiplicity onto the station instead
1173 * looks harmless and is not -- utilization at a finite-server station is
1174 * divided by the server count, so an inf-scheduled station would report a
1175 * utilization a factor `n` too small.
1176 */
1177 void set_number_of_servers(std::size_t node, double n) {
1178 const std::size_t ist = station_of(node, "setNumberOfServers");
1179 if (sn_.stations[ist - 1].sched == SchedStrategy::INF) return;
1180 if (!(n >= 1.0)) throw InputError("setNumberOfServers: the server count must be >= 1");
1181 sn_.stations[ist - 1].nservers = n;
1182 // getNodeTypes reads the COUNT: a queue given infinitely many servers is
1183 // reported as a Delay station, which is the field the analyzers partition on
1184 if (std::isinf(n) && sn_.stations[ist - 1].nodetype == NodeType::Queue)
1185 sn_.stations[ist - 1].nodetype = NodeType::Delay;
1186 }
1187
1188 /** `station.setCapacity(k)`, the K of Kendall's notation. */
1189 void set_capacity(std::size_t node, double k) {
1190 sn_.stations[station_of(node, "setCapacity") - 1].cap = k;
1191 }
1192
1193 /** `station.setChainCapacity(class, k)`. */
1194 void set_class_capacity(std::size_t node, std::size_t cls, double k) {
1195 Station<T>& st = sn_.stations[station_of(node, "setChainCapacity") - 1];
1196 st.classcap.resize(sn_.classes.size(), std::numeric_limits<double>::infinity());
1197 st.classcap[cls - 1] = k;
1198 }
1199
1200 /**
1201 * `queue.setImmediateFeedback(class)`: a completing job of that class is fed
1202 * straight back into service, HOLDING THE SERVER, rather than being routed
1203 * out and re-queued.
1204 *
1205 * Node-scoped. `set_class_immediate_feedback` is the class-wide spelling;
1206 * `sn.immfeed` is the OR of the two, as `refreshStruct` computes it.
1207 */
1208 void set_immediate_feedback(std::size_t node, std::size_t cls) {
1209 Station<T>& st = sn_.stations[station_of(node, "setImmediateFeedback") - 1];
1210 st.immfeed.resize(sn_.classes.size(), false);
1211 st.immfeed[cls - 1] = true;
1212 }
1213
1214 /** `jobclass.setImmediateFeedback()`: the same property, class-wide. */
1215 void set_class_immediate_feedback(std::size_t cls) {
1216 sn_.classes[cls - 1].immfeed = true;
1217 }
1218
1219 /** `station.setDropRule(class, rule)`. */
1220 void set_drop_rule(std::size_t node, std::size_t cls, DropStrategy rule) {
1221 Station<T>& st = sn_.stations[station_of(node, "setDropRule") - 1];
1222 st.droprule.resize(sn_.classes.size(), 0);
1223 st.droprule[cls - 1] = static_cast<int>(rule);
1224 }
1225
1226 /**
1227 * `Queue.setServiceRateFunction(muFun)`: the TOTAL service rate of a PAS or
1228 * OI station as a function of the ordered microstate, a 1-based list of
1229 * class indices in queue order.
1230 *
1231 * Only PAS and OI take one, and the reference errors on any other
1232 * discipline. As `Queue.setServiceRateFunction` does, this ALSO installs a
1233 * representative per-class service distribution `Exp(mu([r]))`, so that the
1234 * ordinary rate/procid machinery stays consistent; the authoritative
1235 * description of the station remains mu(c). A class whose mu([r]) is not
1236 * positive and finite is disabled there, again as the reference does.
1237 *
1238 * `swap_graph` is `sn.nodeparam{ind}.swapGraph`, empty (all zero) for a
1239 * genuinely order-independent station.
1240 */
1242 std::size_t node, const std::function<T(const std::vector<std::size_t>&)>& muFun,
1243 const Matrix<T>& swap_graph = Matrix<T>()) {
1244 const std::size_t ist = station_of(node, "setServiceRateFunction");
1245 Station<T>& st = sn_.stations[ist - 1];
1246 if (st.sched != SchedStrategy::PAS && st.sched != SchedStrategy::OI)
1247 throw InputError(
1248 "setServiceRateFunction is only applicable to PAS (pass-and-swap) and OI "
1249 "(order-independent) queues");
1250 if (!muFun) throw InputError("setServiceRateFunction: the rate function is empty");
1251 st.svc_rate_fun = muFun;
1252 st.swap_graph = swap_graph;
1253 // ONE DECLARATION, TWO READERS. `solver_nc_oi` and `solver_mva_oi` read
1254 // the rate function off the Station; `to_marginal`, the PAS event
1255 // handler and the JSON writer read it off `sn.pasparam`. Filling only
1256 // one left the other with no rate function AND no error: a model built
1257 // here fell back to "every job present is in service" in every
1258 // state-space consumer, and a model loaded from JSON (which fills
1259 // `pasparam` alone, via `set_pas`) was refused by the OI solvers.
1260 pas_mirror(ist, muFun, swap_graph);
1261 for (std::size_t r = 1; r <= sn_.classes.size(); ++r) {
1262 const T rate_r = muFun(std::vector<std::size_t>{r});
1263 const double v = num_traits<T>::to_double(rate_r);
1264 set_service(node, r, (std::isfinite(v) && v > 0.0) ? Distrib<T>::exp_rate(rate_r)
1266 }
1267 }
1268
1269 /**
1270 * `Queue.setPollingType(rule, par)`: the polling discipline of a POLLING
1271 * station, identical across all class buffers as the reference assumes. Only
1272 * K-limited carries a parameter; every other rule ignores it.
1273 */
1274 void set_polling_type(std::size_t node, lang::PollingType rule, int par = 0) {
1275 Station<T>& st = sn_.stations[station_of(node, "setPollingType") - 1];
1276 if (st.sched != SchedStrategy::POLLING)
1277 throw InputError("setPollingType is only applicable to a POLLING station");
1278 if (rule == lang::PollingType::KLIMITED && par < 1)
1279 throw InputError("K-limited polling requires a parameter K >= 1");
1280 st.polling_type.assign(sn_.classes.size(), rule);
1281 st.polling_par = (rule == lang::PollingType::KLIMITED) ? par : 0;
1282 }
1283
1284 /** `Queue.setSwitchover(jobclass, distrib)`: the switchover time of a class. */
1285 void set_switchover(std::size_t node, std::size_t cls, const Distrib<T>& so) {
1286 Station<T>& st = sn_.stations[station_of(node, "setSwitchover") - 1];
1287 if (st.sched != SchedStrategy::POLLING)
1288 throw InputError("setSwitchover is only applicable to a POLLING station");
1289 st.switchover.resize(sn_.classes.size(), Distrib<T>::immediate());
1290 st.switchover[cls - 1] = so;
1291 }
1292
1293 /**
1294 * `Queue.setSwitchover(fromClass, toClass, distrib)`: the walk between two
1295 * CLASSES at an ordinary station, the reference's (K x K) form.
1296 *
1297 * An UNDECLARED pair stays Disabled and is therefore not serialized, which
1298 * is what the JAR and python write. MATLAB fills its cell with Immediate
1299 * instead and writes all K^2 entries; the two mean the same thing, since no
1300 * solver reads a pairwise switchover, and the sparse form is the one that
1301 * round-trips a document unchanged.
1302 *
1303 * Refused at a POLLING station, where a switchover is the walk out of one
1304 * BUFFER and is declared per class by the overload above. The reference
1305 * accepts the call there and then drops the extra rows when it serializes,
1306 * which is a silently different model.
1307 */
1308 void set_switchover(std::size_t node, std::size_t from_cls, std::size_t to_cls,
1309 const Distrib<T>& so) {
1310 Station<T>& st = sn_.stations[station_of(node, "setSwitchover") - 1];
1311 if (st.sched == SchedStrategy::POLLING)
1312 throw InputError(
1313 "setSwitchover(fromClass, toClass, distrib) is not applicable to a POLLING "
1314 "station, whose switchover is the walk out of one buffer and is declared per "
1315 "class");
1316 const std::size_t K = sn_.classes.size();
1317 if (from_cls == 0 || from_cls > K || to_cls == 0 || to_cls > K)
1318 throw InputError("setSwitchover: class index is out of range");
1319 if (st.switchover_pair.empty())
1320 st.switchover_pair.assign(K, std::vector<Distrib<T>>(K, Distrib<T>::disabled_dist()));
1321 st.switchover_pair[from_cls - 1][to_cls - 1] = so;
1322 }
1323
1324 /** The DPS / GPS weight of a class at a station. */
1325 void set_sched_param(std::size_t node, std::size_t cls, const T& weight) {
1326 Station<T>& st = sn_.stations[station_of(node, "setSchedParam") - 1];
1327 st.schedparam.resize(sn_.classes.size(), num_traits<T>::from_int(1));
1328 st.schedparam[cls - 1] = weight;
1329 }
1330
1331 /**
1332 * `station.setLoadDependence(alpha)`: the rate multiplier at population
1333 * 1, 2, ... The vector is indexed from population one, as `sn.lldscaling`
1334 * is, so entry 0 is the multiplier of a station holding one job.
1335 */
1336 void set_load_dependence(std::size_t node, const std::vector<T>& alpha) {
1337 if (alpha.empty()) throw InputError("setLoadDependence: the scaling vector is empty");
1338 sn_.stations[station_of(node, "setLoadDependence") - 1].lldscaling = alpha;
1339 }
1340
1341 /**
1342 * `station.setClassDependence(beta, peakRatePerClass)`.
1343 *
1344 * The peak is a scalar broadcast across the classes, or one value per class;
1345 * it is the declared max_n beta_r(n) that utilization is normalized by, and
1346 * the reference makes it mandatory because it cannot be recovered from beta
1347 * without sweeping the whole lattice -- a sweep that needs a bound the
1348 * handle does not carry, and an open class has none.
1349 *
1350 * An empty peak is accepted HERE and refused where it is READ, matching
1351 * `getLimitedClassDependencePeak`. That contract is only worth anything if
1352 * every reader honours it, and they do: SolverCTMC, `solver_mva_run_analyzer`,
1353 * `solver_nc_conv`, both SSA engines and the LDES engine each throw by name.
1354 * SolverMVA was the exception until 2026-08-19, silently writing a column of
1355 * ZEROS into U instead. `set_joint_dependence` below takes the peak as a
1356 * REQUIRED argument and refuses at declaration; both shapes end in an error.
1357 */
1358 void set_class_dependence(std::size_t node, const CdScaling<T>& fun,
1359 const std::vector<T>& peak = std::vector<T>()) {
1360 Station<T>& st = sn_.stations[station_of(node, "setClassDependence") - 1];
1361 const std::size_t K = sn_.classes.size();
1362 st.cdscaling = fun;
1363 if (peak.empty())
1364 st.cdscalingpeak.clear();
1365 else if (peak.size() == 1)
1366 st.cdscalingpeak.assign(K, peak[0]);
1367 else if (peak.size() == K)
1368 st.cdscalingpeak = peak;
1369 else
1370 throw InputError(
1371 "setClassDependence: peakRatePerClass must be a scalar or a vector of length "
1372 "nclasses");
1373 }
1374
1375 /**
1376 * `station.setJointDependence(eta, peakRatePerClass)`: MATLAB's
1377 * `Station.ljdScaling` / `ljdScalingPeak`.
1378 *
1379 * The peak is MANDATORY, exactly as in `Station.setJointDependence`, and for
1380 * the same reason as `setClassDependence`: utilization at a dependent station
1381 * is reported as T*S/peak, and max_n eta_i(n) is not recoverable from the
1382 * handle without sweeping the whole lattice.
1383 */
1384 void set_joint_dependence(std::size_t node, const CdScaling<T>& fun,
1385 const std::vector<T>& peak) {
1386 Station<T>& st = sn_.stations[station_of(node, "setJointDependence") - 1];
1387 const std::size_t K = sn_.classes.size();
1388 if (peak.empty())
1389 throw InputError(
1390 "setJointDependence: joint dependence requires an explicit peak rate; pass a "
1391 "scalar (identical peak for every class) or a per-class vector");
1392 st.jdscaling = fun;
1393 if (peak.size() == 1)
1394 st.jdscalingpeak.assign(K, peak[0]);
1395 else if (peak.size() == K)
1396 st.jdscalingpeak = peak;
1397 else
1398 throw InputError(
1399 "setJointDependence: peakRatePerClass must be a scalar or a vector of length "
1400 "nclasses");
1401 }
1402
1403 /**
1404 * `model.setGlobalDependence(phi, peak)`: MATLAB's `Network.gdScaling`.
1405 *
1406 * Declares a globally state-dependent rate scaling phi(n) whose argument is
1407 * the FULL (nstations x nclasses) population matrix, row-major, rather than
1408 * one station's slice. This is the Whittle primitive: when phi satisfies
1409 * phi_s(n) phi_t(n-e_s) = phi_t(n) phi_s(n-e_t) the chain is reversible with
1410 * pi(n) ~ Phi(n) prod rho_s^n_s and is insensitive; it also expresses
1411 * bandwidth sharing, where a route holds several links at once.
1412 *
1413 * phi returns one scalar (broadcast), one entry per station, or one entry per
1414 * (station, class) in row-major order. The peak is MANDATORY for the same
1415 * reason as `set_class_dependence`: utilization is reported as T*S/peak, at
1416 * EVERY station once a handle is present, the delays among them.
1417 * Only SolverCTMC, SolverSSA and SolverLDES honour the handle.
1418 */
1419 void set_global_dependence(const GdScaling<T>& fun, const std::vector<T>& peak) {
1420 set_global_dependence(fun, peak, 10);
1421 }
1422
1423 /**
1424 * As above, with an explicit per-slot OPEN-class truncation used when phi is
1425 * materialized onto the JSON wire (closed classes are tabulated up to their own
1426 * population). It plays no part in solving, and exists because a handle cannot
1427 * cross a language boundary: the writer needs to know how far the lattice
1428 * extends. Set it to the cutoff the model is solved at.
1429 */
1430 void set_global_dependence(const GdScaling<T>& fun, const std::vector<T>& peak,
1431 int wire_cutoff) {
1432 if (!fun)
1433 throw InputError("setGlobalDependence: the scaling must be a callable");
1434 const std::size_t M = sn_.stations.size(), K = sn_.classes.size();
1435 if (peak.empty())
1436 throw InputError(
1437 "setGlobalDependence: a global dependence requires an explicit peak rate; pass a "
1438 "scalar, one entry per station, or one entry per (station, class)");
1439 for (std::size_t i = 0; i < peak.size(); ++i)
1440 if (num_traits<T>::to_double(peak[i]) <= 0)
1441 throw InputError("setGlobalDependence: peak must be positive");
1442 // Probe now so a wrong output shape is refused at declaration time rather
1443 // than midway through state-space generation.
1444 for (int probe = 0; probe < 2; ++probe) {
1445 const std::vector<T> n(M * K, num_traits<T>::from_int(probe));
1446 const std::vector<T> v = fun(n);
1447 if (v.size() != 1 && v.size() != M && v.size() != M * K)
1448 throw InputError(
1449 "setGlobalDependence: the handle must return a scalar, one entry per station, "
1450 "or one entry per (station, class)");
1451 for (std::size_t j = 0; j < v.size(); ++j)
1452 if (!(num_traits<T>::to_double(v[j]) >= 0))
1453 throw InputError(
1454 "setGlobalDependence: the handle must return finite nonnegative scalings");
1455 }
1456 if (wire_cutoff < 1)
1457 throw InputError("setGlobalDependence: wireCutoff must be a positive integer");
1458 sn_.gdscaling = fun;
1459 sn_.gdscalingcutoff = wire_cutoff;
1460 if (peak.size() == 1)
1461 sn_.gdscalingpeak.assign(M * K, peak[0]);
1462 else if (peak.size() == M) {
1463 sn_.gdscalingpeak.assign(M * K, num_traits<T>::from_int(1));
1464 for (std::size_t i = 0; i < M; ++i)
1465 for (std::size_t r = 0; r < K; ++r) sn_.gdscalingpeak[i * K + r] = peak[i];
1466 } else if (peak.size() == M * K)
1467 sn_.gdscalingpeak = peak;
1468 else
1469 throw InputError(
1470 "setGlobalDependence: peak must be a scalar, one entry per station, or one entry "
1471 "per (station, class)");
1472 }
1473
1474 /** `node.setRouting(class, strategy)`. */
1475 void set_routing(std::size_t node, std::size_t cls, RoutingStrategy rs) {
1476 NodeDef& nd = sn_.nodes[node - 1];
1477 nd.routing.resize(sn_.classes.size(), RoutingStrategy::PROB);
1478 nd.routing[cls - 1] = rs;
1479 }
1480
1481 /**
1482 * `node.setStateDepRouting(class, departure, branches, level, C, d)`.
1483 *
1484 * Declares `entry` the entry centre e of a subnetwork Q(V,V) served by the
1485 * product-form state-dependent routing of A. E. Krzesinski, "Multiclass
1486 * Queueing Networks with State-Dependent Routing", Performance Evaluation
1487 * 7(2):125-143, 1987.
1488 *
1489 * All node indices are 1-based, as elsewhere in the builder. `departure`
1490 * may equal `entry` in a central server model. `branches` follows the
1491 * paper's own indexing: `branches[0]` must be empty because branch index 1
1492 * denotes the complement M-V, and `branches[b]` lists the nodes of branch b
1493 * with its entry centre first and its departure centre last. `level[b]` is
1494 * the index t of the subnetwork with B_b in V_t - V_{t+1}, and `level[0]`
1495 * is ignored. `C` holds the T coefficients C_t and `d` the T by B
1496 * coefficients d_tb, read for 1 <= t <= level[b].
1497 *
1498 * Negative C_t and positive d_tb make the routing prefer the least
1499 * congested branches and impose the population bounds m_b <= d_tb/(-C_t)
1500 * and v_t <= D_tt/(-C_t). The residual probability returns the customer to
1501 * the departure centre, the busy form of waiting of Sec. 2.5, so the entry
1502 * node needs a self-loop when entry and departure coincide.
1503 */
1504 void set_state_dep_routing(std::size_t entry, std::size_t departure,
1505 const std::vector<std::vector<std::size_t>>& branches,
1506 const std::vector<std::size_t>& level,
1507 const std::vector<double>& C, const Matrix<double>& d,
1508 std::size_t cls = 0) {
1509 if (branches.size() < 2 || !branches[0].empty())
1510 throw InputError("set_state_dep_routing: branches[0] must be empty, branch index 1 "
1511 "denotes the complement M-V");
1512 const std::size_t B = branches.size();
1513 if (level.size() != B)
1514 throw InputError("set_state_dep_routing: level must have one entry per branch index, "
1515 "including the unused index 0");
1516 pfqn::SdrStruct nodesdr;
1517 nodesdr.entry = entry - 1;
1518 nodesdr.departure = departure - 1;
1519 nodesdr.branch.assign(B, std::vector<std::size_t>());
1520 nodesdr.entryOf.assign(B, 0);
1521 nodesdr.departureOf.assign(B, 0);
1522 for (std::size_t b = 1; b < B; ++b) {
1523 if (branches[b].empty())
1524 throw InputError("set_state_dep_routing: branch " + std::to_string(b + 1) +
1525 " is empty");
1526 for (std::size_t k = 0; k < branches[b].size(); ++k)
1527 nodesdr.branch[b].push_back(branches[b][k] - 1);
1528 nodesdr.entryOf[b] = branches[b].front() - 1;
1529 nodesdr.departureOf[b] = branches[b].back() - 1;
1530 }
1531 nodesdr.level = level;
1532 nodesdr.C = C;
1533 nodesdr.d = d;
1534
1535 // Station-indexed twin. Every centre of an SDR network must be a
1536 // station: the product form is over queue lengths, and a stateless node
1537 // holds none.
1538 std::vector<std::size_t> node_to_station(sn_.nodes.size(), 0);
1539 std::vector<bool> is_station(sn_.nodes.size(), false);
1540 for (std::size_t k = 0; k < sn_.station_to_node.size(); ++k) {
1541 node_to_station[sn_.station_to_node[k] - 1] = k;
1542 is_station[sn_.station_to_node[k] - 1] = true;
1543 }
1544 struct Map {
1545 const std::vector<std::size_t>& n2s;
1546 const std::vector<bool>& isst;
1547 const NetworkStruct<T>& sn;
1548 std::size_t operator()(std::size_t nd) const {
1549 if (nd >= isst.size() || !isst[nd])
1550 throw InputError("set_state_dep_routing: node '" + sn.nodes[nd].name +
1551 "' takes part in state-dependent routing but is not a "
1552 "station: the product form is over queue lengths, and a "
1553 "stateless node holds none");
1554 return n2s[nd];
1555 }
1556 } to_station{node_to_station, is_station, sn_};
1557
1558 pfqn::SdrStruct stsdr = nodesdr;
1559 stsdr.entry = to_station(nodesdr.entry);
1560 stsdr.departure = to_station(nodesdr.departure);
1561 for (std::size_t b = 1; b < B; ++b) {
1562 for (std::size_t k = 0; k < nodesdr.branch[b].size(); ++k)
1563 stsdr.branch[b][k] = to_station(nodesdr.branch[b][k]);
1564 stsdr.entryOf[b] = to_station(nodesdr.entryOf[b]);
1565 stsdr.departureOf[b] = to_station(nodesdr.departureOf[b]);
1566 }
1567 pfqn::pfqn_sdrcoeff(stsdr); // validates the declaration and its population bounds
1568
1569 sn_.sdr_nodes = nodesdr;
1570 sn_.sdr = stsdr;
1571 const std::size_t K = sn_.classes.size();
1572 if (cls == 0) {
1573 for (std::size_t r = 1; r <= K; ++r) set_routing(entry, r, RoutingStrategy::SDR);
1574 } else {
1575 set_routing(entry, cls, RoutingStrategy::SDR);
1576 }
1577 }
1578
1579 // Routing
1580
1581 /** An empty routing matrix, MATLAB's `model.initRoutingMatrix`. */
1583
1584 /**
1585 * `model.serialRouting(nodes)`: a unit-probability path through `nodes`.
1586 *
1587 * A path ending at a Sink stays open. Every other path is closed back onto
1588 * its first node, matching MATLAB's `Network.serialRouting` convention for
1589 * closed models. The returned matrix is a one-class topology block and can
1590 * be passed directly to `link`, or assigned to a class with `P.set(cls, block)`.
1591 */
1592 RoutingMatrix<T> serial_routing(const std::vector<std::size_t>& nodes) const {
1594 for (std::size_t k = 0; k < nodes.size(); ++k) {
1595 if (nodes[k] == 0 || nodes[k] > sn_.nodes.size())
1596 throw InputError("serialRouting: the path names a node that does not exist");
1597 if (k + 1 < nodes.size()) P.set(nodes[k], nodes[k + 1], num_traits<T>::from_int(1));
1598 }
1599 if (nodes.size() > 1 && sn_.nodes[nodes.back() - 1].nodetype != NodeType::Sink)
1600 P.set(nodes.back(), nodes.front(), num_traits<T>::from_int(1));
1601 return P;
1602 }
1603
1604 /**
1605 * `model.link(P)`: install the routing.
1606 *
1607 * The probabilities are stored as given. A node whose strategy is RAND
1608 * needs only the CONNECTIONS -- any positive entry marks one -- and the
1609 * refresh replaces them by the uniform split.
1610 */
1611 void link(const RoutingMatrix<T>& Pm) {
1612 for (const auto& kv : Pm.entries) {
1613 const auto& k = kv.first;
1614 if (k.r == 0 || k.r > sn_.classes.size() || k.s == 0 || k.s > sn_.classes.size())
1615 throw InputError("link: the routing matrix names a class that does not exist");
1616 if (k.i == 0 || k.i > sn_.nodes.size() || k.j == 0 || k.j > sn_.nodes.size())
1617 throw InputError("link: the routing matrix names a node that does not exist");
1618 }
1619 // A CLASS SWITCH ON A LINK BECOMES A NODE, as `@MNetwork/link.m:225-329`
1620 // makes it. This port used to keep `P{r,s}(i,j)` with r != s as an EDGE
1621 // attribute and synthesize nothing, which the analytical solvers read
1622 // correctly through route_eff but which cost two things nothing else
1623 // could supply: the node table had no CS_ row to report (a node the
1624 // reference counts, so cache_replc_fifo showed 2 nodes against 3), and
1625 // the JSIM export had nowhere to put the switch at all -- `jmt_writer.h`
1626 // can only emit a ClassSwitch for a node whose type IS ClassSwitch, so
1627 // JMT silently simulated the UNSWITCHED model.
1628 //
1629 // The rewrite makes every surviving route SAME-CLASS; the switching
1630 // lives entirely in the inserted node's matrix.
1631 const std::size_t K = sn_.classes.size();
1632 const std::size_t I = sn_.nodes.size(); // before any insertion
1633 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
1634 RoutingMatrix<T> P = Pm;
1635 // Deferred miss hops from Cache.setMissCache. They are part of the cache
1636 // network's own construction, and the per-item classes they carry do not
1637 // exist when the caller builds its routing matrix.
1638 for (std::size_t a = 0; a < cache_miss_arcs_.size(); ++a) {
1639 const CacheMissArc& mc = cache_miss_arcs_[a];
1640 P.set(mc.cls, mc.cls, mc.from_node, mc.to_node, one);
1641 }
1642 std::map<std::pair<std::size_t, std::size_t>, std::size_t> csid; // (i,j) -> 1-based node
1643 for (std::size_t i = 1; i <= I; ++i)
1644 for (std::size_t j = 1; j <= I; ++j) {
1645 Matrix<T> C(K, K, zero);
1646 bool any = false;
1647 for (std::size_t r = 1; r <= K; ++r)
1648 for (std::size_t s = 1; s <= K; ++s) {
1649 const T p = P.get(r, s, i, j);
1650 if (num_traits<T>::to_double(p) != 0.0) any = true;
1651 C(r - 1, s - 1) = p;
1652 }
1653 if (!any) continue; // no link at all: the reference's identity, no node
1654 // CONDITIONED ON THE JOB TAKING THIS LINK, so each row is
1655 // renormalised; a class that never leaves i for j keeps itself.
1656 bool offdiag = false;
1657 for (std::size_t r = 1; r <= K; ++r) {
1658 T S = zero;
1659 for (std::size_t s = 1; s <= K; ++s) S += C(r - 1, s - 1);
1660 if (num_traits<T>::to_double(S) > 0) {
1661 for (std::size_t s = 1; s <= K; ++s) C(r - 1, s - 1) = T(C(r - 1, s - 1) / S);
1662 } else {
1663 C(r - 1, r - 1) = one;
1664 }
1665 for (std::size_t s = 1; s <= K; ++s)
1666 if (s != r && num_traits<T>::to_double(C(r - 1, s - 1)) != 0.0) offdiag = true;
1667 }
1668 if (!offdiag) continue; // `~isdiag`: a pure same-class link needs no node
1669 // THE SINK -> SOURCE ARC IS THE OPEN-NETWORK CLOSURE, NOT A LINK.
1670 // A job cannot be routed out of a Sink, so an arc from one into a
1671 // Source is never something a user asked for: it is what makes an
1672 // open chain irreducible for the visit computation, and the
1673 // reference adds it in refreshStruct AFTER link.m has run, so
1674 // link.m never sees it and synthesizes nothing for it. This port
1675 // reads a model.json where linemodel_save has already written the
1676 // closure in, and where it changes class -- an open model that
1677 // enters as InitClass and leaves as HitClass closes as
1678 // `P{HitClass,InitClass}(Sink,Source)` -- the class change looked
1679 // like a switch on a link and grew a CS_Sink_to_Source node. The
1680 // extra hop HALVED every downstream visit: on cache_replc_routing
1681 // both Delays reported throughput 0.2/0.3 against MATLAB's
1682 // 0.4/0.6, in every engine at once. The route is still installed
1683 // below; only the node is not.
1684 if (sn_.nodes[i - 1].nodetype == NodeType::Sink &&
1685 sn_.nodes[j - 1].nodetype == NodeType::Source)
1686 continue;
1687 csid[std::make_pair(i, j)] =
1688 add_class_switch("CS_" + sn_.nodes[i - 1].name + "_to_" + sn_.nodes[j - 1].name, C);
1689 }
1690 // RE-ROUTE i -> cs -> j, the reference's own three assignments. The
1691 // switching probability is folded into the FIRST leg in the departing
1692 // class, and the second leg is deterministic in the arriving class.
1693 for (const auto& kv : csid) {
1694 const std::size_t i = kv.first.first, j = kv.first.second, c = kv.second;
1695 for (std::size_t r = 1; r <= K; ++r)
1696 for (std::size_t s = 1; s <= K; ++s) {
1697 const T p = P.get(r, s, i, j);
1698 if (!(num_traits<T>::to_double(p) > 0)) continue;
1699 P.set(r, r, i, c, T(P.get(r, r, i, c) + p));
1700 P.set(r, s, i, j, zero);
1701 P.set(s, s, c, j, one);
1702 }
1703 }
1704 for (const auto& kv : P.entries) {
1705 const auto& k = kv.first;
1706 sn_.set_route(k.r, k.s, k.i, k.j, kv.second);
1707 }
1708 // A retrieval class inherits the read class's routing among the cache and
1709 // the retrieval queues: it circulates them the same way, entering at the
1710 // cache and returning to it. Set once P is installed (setRetrievalSystem
1711 // runs before link, as the reference documents).
1712 for (const auto& np : sn_.nodeparam) {
1713 const std::size_t ci = np.first; // 1-based cache node
1714 const CacheParam<T>& cp = np.second;
1715 if (cp.retrieval_capacity <= 0) continue;
1716 for (const auto& rq : cp.retrieval_queues) {
1717 const std::size_t rd = rq.first + 1; // 1-based read class
1718 std::vector<std::size_t> nodeset = rq.second; // 1-based queue nodes
1719 nodeset.push_back(ci);
1720 bool minted = false;
1721 const std::size_t Inodes = sn_.nodes.size();
1722 for (std::size_t i = 0; i < cp.nitems; ++i) {
1723 const std::size_t rc = cp.retrieval_classes[i][rd - 1];
1724 if (rc == 0) continue;
1725 minted = true;
1726 // The template is a DEFAULT: a source node the caller ALREADY routed for
1727 // this retrieval class keeps its own row, since merging the two can only
1728 // yield a row summing past 1. Decided before any write, so the template's
1729 // own entries never count as caller-declared.
1730 std::vector<bool> declared(nodeset.size(), false);
1731 for (std::size_t si = 0; si < nodeset.size(); ++si)
1732 for (std::size_t b = 1; b <= Inodes && !declared[si]; ++b)
1733 if (Pm.get(rc, rc, nodeset[si], b) > num_traits<T>::from_int(0))
1734 declared[si] = true;
1735 for (std::size_t si = 0; si < nodeset.size(); ++si) {
1736 if (declared[si]) continue;
1737 for (std::size_t b : nodeset) {
1738 const T p = Pm.get(rd, rd, nodeset[si], b);
1739 if (p > num_traits<T>::from_int(0))
1740 sn_.set_route(rc, rc, nodeset[si], b, p);
1741 }
1742 }
1743 }
1744 // CONSUME the read class's template edges over the queue set,
1745 // as `link.m:187-194` does. They were only ever a TEMPLATE from
1746 // which each retrieval class's circulation is copied; leaving
1747 // them in place makes the read class circulate the fetch
1748 // stations on its own, so each retrieval class becomes a
1749 // separate communicating class and the chain decomposition
1750 // reports one singleton chain per item instead of one chain
1751 // over all of them. Measured before the fix on a closed
1752 // Delay+Cache+Fetch model: 4 chains against MATLAB's 1, and
1753 // every visit at the fetch station zero against MATLAB's 1.5
1754 // per retrieval class.
1755 if (minted)
1756 for (std::size_t a : nodeset)
1757 for (std::size_t b : nodeset)
1758 if (Pm.get(rd, rd, a, b) > num_traits<T>::from_int(0))
1759 sn_.set_route(rd, rd, a, b, num_traits<T>::from_int(0));
1760 }
1761 }
1762
1763 // A (node, class) PAIR THE CALLER NEVER ROUTED IS LEFT ON RAND, which is
1764 // where `addLink` leaves it in the reference: `setProbRouting` is what
1765 // turns a pair PROB, and it is called only for the entries the routing
1766 // matrix actually carries. The distinction is not bookkeeping.
1767 //
1768 // It decides the VISITS. `getRoutingMatrix` expands RAND uniformly over
1769 // the node's connections, so the pair gets a row; PROB with no entries
1770 // leaves the row EMPTY, and an empty row is an UNVISITED station. On
1771 // `gallery_erlerl1` -- Class1 declares Source->Queue, Class2 declares
1772 // Queue->Sink -- the Queue came out unvisited by Class1 and every metric
1773 // of the model was zero. The `served` mask in `refresh_visits` is what
1774 // keeps the fill honest: it drops the (station, class) pairs with no
1775 // service law.
1776 //
1777 // It also decides the SAMPLE PATH. `saveRoutingStrategy` writes a
1778 // RandomStrategy for a RAND pair and an EmpiricalStrategy for a PROB
1779 // one, and JMT draws from the node's stream for a random split EVEN AT
1780 // ONE DESTINATION -- so the two consume the stream differently and part
1781 // company at the same seed. On `fj_cs_prefork` that was a 14% gap on
1782 // Queue1 against a golden JMT itself produced.
1783 //
1784 // A SELF-LOOPING CLASS AND A SIGNAL ARE LEFT ALONE: the first never
1785 // leaves its station, and the second is routed explicitly by P.
1786 const std::size_t Kc = sn_.classes.size(), Ic = sn_.nodes.size();
1787 for (std::size_t i = 1; i <= Ic; ++i) {
1788 NodeDef& nd = sn_.nodes[i - 1];
1789 nd.routing.resize(Kc, RoutingStrategy::PROB);
1790 bool linked = false;
1791 for (std::size_t j = 1; j <= Ic && !linked; ++j)
1792 for (std::size_t a = 1; a <= Kc && !linked; ++a)
1793 for (std::size_t b = 1; b <= Kc && !linked; ++b)
1794 if (num_traits<T>::to_double(sn_.get_route(a, b, i, j)) > 0.0) linked = true;
1795 if (!linked) continue;
1796 for (std::size_t r = 1; r <= Kc; ++r) {
1797 if (nd.routing[r - 1] != RoutingStrategy::PROB) continue;
1798 if (sn_.classes[r - 1].self_looping) continue;
1799 if (r <= sn_.issignal.size() && sn_.issignal[r - 1]) continue;
1800 bool routed = false;
1801 for (std::size_t j = 1; j <= Ic && !routed; ++j)
1802 for (std::size_t s = 1; s <= Kc && !routed; ++s)
1803 if (num_traits<T>::to_double(sn_.get_route(r, s, i, j)) > 0.0) routed = true;
1804 if (!routed) nd.routing[r - 1] = RoutingStrategy::RAND;
1805 }
1806 }
1807
1808 // A variable forking level declared BEFORE the routing existed. Its
1809 // destination set is read off the routing, so this is the first moment
1810 // it can be resolved; a call made after `link()` is applied where it
1811 // stands, which is what raising the flag first buys, and replaying here
1812 // is idempotent.
1813 routing_linked_ = true;
1814 apply_fork_overrides();
1815 }
1816
1817 // -----------------------------------------------------------------------
1818 // Struct
1819 // -----------------------------------------------------------------------
1820
1821 /** The refreshed struct, MATLAB's `model.getStruct()`. */
1823 materialize_transitions();
1824 if (routing_installed()) apply_fork_overrides();
1825 validate();
1826 sn_.refresh_struct();
1827 // The fork block of refreshStruct.m, and it belongs HERE rather than in
1828 // refresh_struct(): it needs the MMT transformation, which is built on a
1829 // refreshed struct, exactly as the reference calls ModelAdapter.mmt after
1830 // refreshChains. fj_tag's own refresh_struct() therefore skips it, which
1831 // is what `~isFJAugmented` buys the reference.
1833 return sn_;
1834 }
1835
1836 /** The struct WITHOUT refreshing it, for a caller that is still building. */
1837 NetworkStruct<T>& raw_struct() { return sn_; }
1838
1839 /**
1840 * `Queue.setService(@(c) ...)` for a pass-and-swap / order-independent
1841 * station: the total service rate mu(c) of an ordered list of 1-based
1842 * class indices, and the swap graph saying which class may take another's
1843 * place. An OI station is the special case of an empty swap graph.
1844 */
1845 void set_pas(std::size_t node,
1846 const std::function<T(const std::vector<std::size_t>&)>& mu,
1847 const std::vector<std::vector<bool>>& swap_graph =
1848 std::vector<std::vector<bool>>()) {
1849 const std::size_t ist = station_of(node, "set_pas");
1850 typename NetworkStruct<T>::PasParam pp;
1851 pp.svc_rate_fun = mu;
1852 pp.swap_graph = swap_graph;
1853 if (pp.swap_graph.empty())
1854 pp.swap_graph.assign(sn_.classes.size(), std::vector<bool>(sn_.classes.size(), false));
1855 sn_.pasparam[ist] = pp;
1856 // The other half of the same declaration; see `set_service_rate_function`.
1857 Station<T>& stp = sn_.stations[ist - 1];
1858 stp.svc_rate_fun = mu;
1859 const std::size_t R = pp.swap_graph.size();
1861 for (std::size_t a = 0; a < R; ++a)
1862 for (std::size_t b = 0; b < pp.swap_graph[a].size(); ++b)
1863 if (pp.swap_graph[a][b]) G(a, b) = num_traits<T>::from_int(1);
1864 stp.swap_graph = G;
1865 }
1866
1867 /**
1868 * Copy a PAS/OI declaration into `sn.pasparam`, the form the state-space
1869 * layer reads. The (R x R) swap adjacency crosses as a boolean matrix
1870 * because that is the shape `after_event`'s swap walk indexes.
1871 */
1872 void pas_mirror(std::size_t ist,
1873 const std::function<T(const std::vector<std::size_t>&)>& mu,
1874 const Matrix<T>& swap_graph) {
1875 typename NetworkStruct<T>::PasParam pp;
1876 pp.svc_rate_fun = mu;
1877 const std::size_t R = sn_.classes.size();
1878 pp.swap_graph.assign(R, std::vector<bool>(R, false));
1879 for (std::size_t a = 0; a < R && a < swap_graph.rows(); ++a)
1880 for (std::size_t b = 0; b < R && b < swap_graph.cols(); ++b)
1881 pp.swap_graph[a][b] = num_traits<T>::to_double(swap_graph(a, b)) != 0.0;
1882 sn_.pasparam[ist] = pp;
1883 }
1884
1885 /**
1886 * `Queue.setPollingType(...)`: the polling discipline of a POLLING station
1887 * and the switchover walks between its buffers.
1888 *
1889 * `switchover[r-1]` is the walk the server takes when LEAVING buffer r. An
1890 * Immediate walk is folded rather than represented, so it costs no state.
1891 */
1892 void set_polling(std::size_t node, lang::PollingType ptype,
1893 const std::vector<Distrib<T>>& switchover = std::vector<Distrib<T>>(),
1894 std::size_t pk = 1) {
1895 const std::size_t ist = station_of(node, "set_polling");
1896 if (sn_.stations[ist - 1].sched != SchedStrategy::POLLING)
1897 throw InputError("set_polling: the station is not POLLING-scheduled");
1899 pp.ptype = ptype;
1900 pp.pk = pk;
1901 pp.switchover = switchover;
1902 if (pp.switchover.size() < sn_.classes.size())
1903 pp.switchover.resize(sn_.classes.size(), Distrib<T>::disabled_dist());
1904 sn_.pollingparam[ist] = pp;
1905 }
1906
1907 /**
1908 * Declare a SYNCHRONOUS call: a job of `call_cls` leaving `node` keeps its
1909 * server until a job of the REPLY class `reply_cls` arrives back here.
1910 *
1911 * The block is one nvars column per (node, calling class), so it stays
1912 * zero-width -- and every other model's state width unchanged -- unless a
1913 * model actually declares a reply. `node` is checked here and marked, but
1914 * `refresh_replyblock` re-derives the whole block from the routing on every
1915 * refresh, exactly as the reference does; naming a node is therefore an
1916 * assertion about this model, not the definition of the block.
1917 */
1918 void set_sync_reply(std::size_t node, std::size_t call_cls, std::size_t reply_cls) {
1919 const std::size_t K = sn_.classes.size();
1920 if (node == 0 || node > sn_.nodes.size())
1921 throw InputError("set_sync_reply: node index is out of range");
1922 if (call_cls == 0 || call_cls > K || reply_cls == 0 || reply_cls > K)
1923 throw InputError("set_sync_reply: class index is out of range");
1924 const std::size_t ist = sn_.nodes[node - 1].station;
1925 if (ist == 0) throw InputError("set_sync_reply: node is not a station");
1926 // No discipline check here: like MATLAB refreshLocalVars, refresh_replyblock refuses a non-FCFS, non-INF
1927 // station by name, and only where the reply actually arrives, so declaring the binding is always legal
1928 // grow, never reassign: a node or class added since the last call must not erase earlier bindings
1929 if (sn_.replyblock.size() < sn_.nodes.size()) sn_.replyblock.resize(sn_.nodes.size());
1930 for (std::vector<bool>& row : sn_.replyblock)
1931 if (row.size() < K) row.resize(K, false);
1932 if (sn_.syncreply.size() < K) sn_.syncreply.resize(K, 0);
1933 sn_.replyblock[node - 1][call_cls - 1] = true;
1934 sn_.syncreply[call_cls - 1] = reply_cls;
1936 }
1937
1938 /**
1939 * `FiniteCapacityRegion(model, nodes)`: a cap on the jobs held ACROSS a set
1940 * of stations.
1941 *
1942 * `class_max_jobs[r]` and `global_max_jobs` are -1 for unbounded, the
1943 * reference's sentinel; the per-class cap is additionally tightened by
1944 * `floor(class_max_memory[r] / class_size[r])` when a memory budget is set,
1945 * because a class whose footprint exceeds the budget cannot have as many
1946 * jobs resident as its job cap alone would allow.
1947 *
1948 * @param nodes 1-based node indices; each must be a station
1949 * @param class_max_jobs (K) per-class job cap inside the region, -1 for unbounded
1950 * @param global_max_jobs cap on the total jobs inside the region, -1 for unbounded
1951 * @param rule (K) per-class drop strategy applied when the cap is reached
1952 * @param class_max_memory (K) per-class memory budget, -1 for unbounded
1953 * @param class_size (K) per-job memory footprint of each class
1954 * @param global_max_memory cap on the total memory inside the region, -1 for unbounded
1955 */
1956 std::size_t add_region(const std::vector<std::size_t>& nodes,
1957 const std::vector<double>& class_max_jobs,
1958 double global_max_jobs = -1.0,
1959 const std::vector<DropStrategy>& rule = std::vector<DropStrategy>(),
1960 const std::vector<double>& class_max_memory = std::vector<double>(),
1961 const std::vector<T>& class_size = std::vector<T>(),
1962 double global_max_memory = -1.0,
1963 const std::string& name = std::string()) {
1964 const std::size_t M = sn_.stations.size(), K = sn_.classes.size();
1965 typename NetworkStruct<T>::Region rg;
1966 rg.name = name;
1967 rg.cap.assign(M, std::vector<double>(K + 1, -1.0));
1968 rg.maxmem.assign(M, -1.0);
1969 rg.members.assign(M, false);
1970 // A class beyond the region's own vectors defaults to WAITQ, weight 1
1971 // and size 1, exactly as refreshRegions does -- NOT to DROP, which
1972 // would silently start losing jobs of a class added after the region.
1973 rg.rule.assign(K, DropStrategy::WAITQ);
1974 rg.weight.assign(K, num_traits<T>::from_int(1));
1975 rg.size.assign(K, num_traits<T>::from_int(1));
1976 for (std::size_t r = 0; r < K && r < rule.size(); ++r) rg.rule[r] = rule[r];
1977 for (std::size_t r = 0; r < K && r < class_size.size(); ++r) rg.size[r] = class_size[r];
1978
1979 for (std::size_t j = 0; j < nodes.size(); ++j) {
1980 const std::size_t ist = station_of(nodes[j], "addRegion");
1981 rg.members[ist - 1] = true;
1982 for (std::size_t r = 0; r < K; ++r) {
1983 // A class beyond the region's own job-cap vector is UNBOUNDED and
1984 // is not clamped by the memory budget either: refreshRegions.m
1985 // takes the `continue` before it reaches the memory block. Applying
1986 // the clamp here would cap a class MATLAB leaves free, whenever
1987 // class_max_memory is the longer of the two vectors.
1988 if (r >= class_max_jobs.size()) {
1989 rg.cap[ist - 1][r] = -1.0;
1990 continue;
1991 }
1992 double c = class_max_jobs[r];
1993 if (r < class_max_memory.size() && class_max_memory[r] != -1.0) {
1994 const double sz = num_traits<T>::to_double(rg.size[r]);
1995 if (sz > 0) {
1996 const double memjobs = std::floor(class_max_memory[r] / sz);
1997 c = c == -1.0 ? memjobs : std::min(c, memjobs);
1998 }
1999 }
2000 rg.cap[ist - 1][r] = c;
2001 }
2002 rg.cap[ist - 1][K] = global_max_jobs;
2003 rg.maxmem[ist - 1] = global_max_memory;
2004 }
2005 sn_.regions.push_back(rg);
2006 return sn_.regions.size();
2007 }
2008
2009 /**
2010 * `FiniteCapacityRegion.setClassWeight`: the per-class weight the region's
2011 * global cap counts a job against, defaulting to 1. A weight of 2 makes one
2012 * job of the class consume two of the region's slots.
2013 */
2014 void set_region_weights(std::size_t region, const std::vector<T>& weight) {
2015 if (region == 0 || region > sn_.regions.size())
2016 throw InputError("set_region_weights: region index is out of range");
2017 typename NetworkStruct<T>::Region& rg = sn_.regions[region - 1];
2018 for (std::size_t r = 0; r < rg.weight.size() && r < weight.size(); ++r)
2019 rg.weight[r] = weight[r];
2020 }
2021
2022 /** The optional linear constraint A n <= b a region may carry beyond its caps. */
2023 void set_region_constraint(std::size_t region, const Matrix<T>& A, const std::vector<T>& b) {
2024 if (region == 0 || region > sn_.regions.size())
2025 throw InputError("set_region_constraint: region index is out of range");
2026 if (A.rows() != b.size())
2027 throw InputError("set_region_constraint: A and b disagree on the number of rows");
2028 sn_.regions[region - 1].lincon_A = A;
2029 sn_.regions[region - 1].lincon_b = b;
2030 }
2031
2032 /**
2033 * `model.setReward(name, fn)`: a named reward evaluated on the AGGREGATE
2034 * state row, the per-(station, class) job counts in `(ist-1)*K + k` order.
2035 *
2036 * Redeclaring a name REPLACES it rather than adding a second reward under
2037 * the same name, so a caller refining a definition does not end up with two
2038 * answers labelled identically.
2039 */
2040 void set_reward(const std::string& nm,
2041 const std::function<T(const std::vector<T>&)>& fn,
2042 const std::string& kind = std::string(), std::size_t node = 0,
2043 std::size_t cls = 0) {
2044 for (std::size_t i = 0; i < sn_.reward.size(); ++i)
2045 if (sn_.reward[i].name == nm) {
2046 sn_.reward[i].fn = fn;
2047 sn_.reward[i].kind = kind;
2048 sn_.reward[i].node = node;
2049 sn_.reward[i].cls = cls;
2050 return;
2051 }
2052 typename NetworkStruct<T>::Reward rw;
2053 rw.name = nm;
2054 rw.fn = fn;
2055 rw.kind = kind;
2056 rw.node = node;
2057 rw.cls = cls;
2058 sn_.reward.push_back(rw);
2059 }
2060
2061 /**
2062 * Declare a class to be a G-network SIGNAL rather than a job.
2063 *
2064 * A signal never joins a station: it removes jobs already there and is
2065 * annihilated. `target` is the 1-based class it may remove, or 0 for the
2066 * classic untargeted Gelenbe customer. `remdist` is the batch-size pmf
2067 * indexed by batch size 0,1,2,...; empty means "remove exactly one".
2068 */
2069 void set_signal(std::size_t cls, lang::SignalType type,
2071 std::size_t target = 0,
2072 const std::vector<T>& remdist = std::vector<T>()) {
2073 const std::size_t K = sn_.classes.size();
2074 if (cls == 0 || cls > K) throw InputError("set_signal: class index is out of range");
2075 if (target > K) throw InputError("set_signal: target class index is out of range");
2076 // GROW, never reset: a class added after an earlier set_signal must not erase that declaration
2077 if (sn_.issignal.size() < K) {
2078 sn_.issignal.resize(K, false);
2079 sn_.signaltype.resize(K, lang::SignalType::NEGATIVE);
2080 sn_.signaltarget.resize(K, 0);
2081 sn_.signalrempolicy.resize(K, lang::RemovalPolicy::RANDOM);
2082 sn_.signalremdist.resize(K, std::vector<T>());
2083 }
2084 sn_.issignal[cls - 1] = true;
2085 sn_.signaltype[cls - 1] = type;
2086 sn_.signaltarget[cls - 1] = target;
2087 sn_.signalrempolicy[cls - 1] = policy;
2088 sn_.signalremdist[cls - 1] = remdist;
2089 }
2090
2091 std::size_t station_index(std::size_t node) const { return station_of(node, "station_index"); }
2092
2093 private:
2094 NetworkStruct<T> sn_;
2095
2096 /** One arc declared through the setter API, before it has a matrix to live in. */
2097 struct TransitionArc {
2098 std::size_t mode, node, cls;
2099 T count;
2100 TransitionArc(std::size_t m, std::size_t n, std::size_t c, const T& v)
2101 : mode(m), node(n), cls(c), count(v) {}
2102 };
2103
2104 /** The pre, inhibitor and post arcs of one transition, still sparse. */
2105 struct TransitionArcs {
2106 std::vector<TransitionArc> enab, inhib, fire;
2107 };
2108
2109 /**
2110 * The sparse arcs of every transition built through `add_transition(name)`,
2111 * keyed by node.
2112 *
2113 * Membership is also what tells a DECLARATIVE transition from one handed a
2114 * finished `TransitionParam`, whose matrices belong to the caller and are
2115 * normalised rather than rebuilt.
2116 */
2117 std::map<std::size_t, TransitionArcs> pending_arcs_;
2118
2119 /** The param block of a transition whose modes are still being declared. */
2120 TransitionParam<T>& declarative_transition(std::size_t node, const char* what) {
2121 if (node == 0 || node > sn_.nodes.size())
2122 throw InputError(std::string(what) + ": node index is out of range");
2123 if (sn_.nodes[node - 1].nodetype != NodeType::Transition)
2124 throw InputError(std::string(what) + ": node '" + sn_.nodes[node - 1].name +
2125 "' is not a Transition");
2126 if (pending_arcs_.find(node) == pending_arcs_.end())
2127 throw InputError(std::string(what) + ": the Transition '" + sn_.nodes[node - 1].name +
2128 "' was created from a finished TransitionParam, so its modes are "
2129 "already fixed; create it as add_transition(name) to declare them "
2130 "one at a time");
2131 return sn_.transparam[node];
2132 }
2133
2134 /** The same, with the mode index checked against the modes declared so far. */
2135 TransitionParam<T>& mode_param(std::size_t node, std::size_t mode, const char* what) {
2136 TransitionParam<T>& tp = declarative_transition(node, what);
2137 if (mode == 0 || mode > tp.nmodes)
2138 throw InputError(std::string(what) + ": the Transition '" + sn_.nodes[node - 1].name +
2139 "' has no mode " + std::to_string(mode));
2140 return tp;
2141 }
2142
2143 /** Check the (mode, class, node) an arc names; a pre-arc must name a Place. */
2144 void arc_target(std::size_t node, std::size_t mode, std::size_t cls, std::size_t target,
2145 bool must_be_place, const char* what) {
2146 mode_param(node, mode, what);
2147 if (cls == 0 || cls > sn_.classes.size())
2148 throw InputError(std::string(what) +
2149 ": class index is out of range; declare the job class before the "
2150 "arcs that move its tokens");
2151 if (target == 0 || target > sn_.nodes.size())
2152 throw InputError(std::string(what) + ": the node the arc names is out of range");
2153 if (must_be_place && sn_.nodes[target - 1].nodetype != NodeType::Place)
2154 throw InputError(std::string(what) + ": '" + sn_.nodes[target - 1].name +
2155 "' is not a Place, and only a Place holds the tokens a mode tests");
2156 }
2157
2158 /** Write the sparse arcs of one kind into the matrices just sized for them. */
2159 static void fill_arcs(std::vector<Matrix<T> >& dst, const std::vector<TransitionArc>& arcs) {
2160 for (std::size_t a = 0; a < arcs.size(); ++a)
2161 dst[arcs[a].mode - 1](arcs[a].node - 1, arcs[a].cls - 1) = arcs[a].count;
2162 }
2163
2164 /**
2165 * Bring a caller's own arc matrices to (nnodes x nclasses).
2166 *
2167 * A SHORT one is padded, because the entries it lacks are arcs the caller
2168 * never declared. A WIDER one is refused: every consumer walks an arc
2169 * matrix row by row against the marking, so extra rows are read past the
2170 * end of it, and `Matrix::operator()` does not bounds-check.
2171 */
2172 static void normalize_arcs(std::vector<Matrix<T> >& a, std::size_t N, std::size_t K,
2173 bool inhibitor, const std::string& nm, const char* what) {
2174 for (std::size_t m = 0; m < a.size(); ++m) {
2175 if (a[m].rows() == N && a[m].cols() == K) continue;
2176 if (a[m].rows() > N || a[m].cols() > K)
2177 throw InputError(
2178 "the Transition '" + nm + "' has an " + what + " matrix of " +
2179 std::to_string(a[m].rows()) + "x" + std::to_string(a[m].cols()) +
2180 " against a model of " + std::to_string(N) + " nodes and " +
2181 std::to_string(K) + " classes; an arc matrix is read row by row against "
2182 "the marking, so a wider one is read past its end");
2183 Matrix<T> grown(N, K, inhibitor ? no_inhibitor() : num_traits<T>::from_int(0));
2184 for (std::size_t q = 0; q < a[m].rows(); ++q)
2185 for (std::size_t r = 0; r < a[m].cols(); ++r) grown(q, r) = a[m](q, r);
2186 a[m] = grown;
2187 }
2188 }
2189
2190 /**
2191 * The "no inhibitor arc here" entry, ON DEMAND.
2192 *
2193 * It is infinity, which an EXACT-ARITHMETIC T cannot hold: building one
2194 * eagerly threw "Cannot convert a non-finite number to an integer" on every
2195 * `Network<Rational>`, Petri net or not. Only a net that actually has an
2196 * inhibitor matrix to fill pays for it.
2197 */
2198 static T no_inhibitor() {
2199 return num_traits<T>::from_double(std::numeric_limits<double>::infinity());
2200 }
2201
2202 /**
2203 * Give every transition its (nnodes x nclasses) arc matrices.
2204 *
2205 * A DECLARATIVE transition's matrices are REBUILT from its sparse arcs, so
2206 * the call is idempotent and a net that gained a node or a class after its
2207 * arcs were declared still gets matrices of the right shape. One built from
2208 * a finished `TransitionParam` keeps the caller's matrices and is only
2209 * normalised.
2210 */
2211 void materialize_transitions() {
2212 if (sn_.transparam.empty()) return;
2213 const std::size_t N = sn_.nodes.size(), K = sn_.classes.size();
2214 const T zero = num_traits<T>::from_int(0);
2215 for (typename std::map<std::size_t, TransitionParam<T> >::iterator it =
2216 sn_.transparam.begin();
2217 it != sn_.transparam.end(); ++it) {
2218 TransitionParam<T>& tp = it->second;
2219 const std::string& nm = sn_.nodes[it->first - 1].name;
2220 if (pending_arcs_.find(it->first) == pending_arcs_.end()) {
2221 normalize_arcs(tp.enabling, N, K, false, nm, "enabling");
2222 normalize_arcs(tp.inhibiting, N, K, true, nm, "inhibiting");
2223 normalize_arcs(tp.firing, N, K, false, nm, "firing");
2224 continue;
2225 }
2226 if (tp.nmodes == 0)
2227 throw InputError("Network '" + sn_.name + "': the Transition '" + nm +
2228 "' has no mode, and every transition needs at least one");
2229 const TransitionArcs& pa = pending_arcs_[it->first];
2230 tp.enabling.assign(tp.nmodes, Matrix<T>(N, K, zero));
2231 tp.inhibiting.assign(tp.nmodes, Matrix<T>(N, K, no_inhibitor()));
2232 tp.firing.assign(tp.nmodes, Matrix<T>(N, K, zero));
2233 fill_arcs(tp.enabling, pa.enab);
2234 fill_arcs(tp.inhibiting, pa.inhib);
2235 fill_arcs(tp.firing, pa.fire);
2236 // The phase count is DERIVED from the law AND the timing, so it is
2237 // recomputed here rather than left to whichever of the two setters
2238 // the caller happened to call last.
2239 for (std::size_t m = 0; m < tp.nmodes; ++m)
2240 tp.firingphases[m] =
2241 tp.timing[m] == lang::TimingStrategy::IMMEDIATE || tp.firingproc[m].disabled
2242 ? 0
2243 : lang::dist_to_map(tp.firingproc[m]).order();
2244 }
2245 }
2246
2247 /** Per-item read classes of each cache in a cache network, keyed by cache node. */
2248 std::map<std::size_t, std::vector<std::size_t> > cache_item_classes_;
2249
2250 /** A miss hop registered by `set_miss_cache`, injected into P by `link()`. */
2251 struct CacheMissArc {
2252 std::size_t cls, from_node, to_node;
2253 CacheMissArc(std::size_t c, std::size_t f, std::size_t t)
2254 : cls(c), from_node(f), to_node(t) {}
2255 };
2256 std::vector<CacheMissArc> cache_miss_arcs_;
2257
2258 /** One class shared by every item, or one per item. */
2259 static std::vector<std::size_t> per_item_classes(const std::vector<std::size_t>& spec,
2260 std::size_t nitems, const char* what) {
2261 if (spec.size() == nitems) return spec;
2262 if (spec.size() == 1) return std::vector<std::size_t>(nitems, spec[0]);
2263 throw InputError(std::string(what) +
2264 ": pass one class per item or a single class shared by all");
2265 }
2266
2267 /** The class record, with the index checked against the live class list. */
2268 JobClass& class_ref(std::size_t cls, const char* what) {
2269 if (cls == 0 || cls > sn_.classes.size())
2270 throw InputError(std::string(what) + ": class index is out of range");
2271 return sn_.classes[cls - 1];
2272 }
2273
2274 void init_node(std::size_t nd) {
2275 sn_.nodes[nd - 1].routing.assign(sn_.classes.size(), RoutingStrategy::PROB);
2276 }
2277
2278 /** Grow the per-class vectors of every node and station after a new class. */
2279 void grow_class_vectors() {
2280 const std::size_t K = sn_.classes.size();
2281 for (NodeDef& nd : sn_.nodes) nd.routing.resize(K, RoutingStrategy::PROB);
2282 for (Station<T>& st : sn_.stations) {
2283 if (!st.classcap.empty())
2284 st.classcap.resize(K, std::numeric_limits<double>::infinity());
2285 if (!st.droprule.empty()) st.droprule.resize(K, 0);
2286 if (!st.schedparam.empty()) st.schedparam.resize(K, num_traits<T>::from_int(1));
2287 }
2288 }
2289
2290 /** The station of a node, with the class index checked against the live class list. */
2291 Station<T>& station_ref(std::size_t node, std::size_t cls, const char* what) {
2292 const std::size_t ist = station_of(node, what);
2293 if (cls == 0 || cls > sn_.classes.size())
2294 throw InputError(std::string(what) + ": class index is out of range");
2295 return sn_.stations[ist - 1];
2296 }
2297
2298 /**
2299 * Widen an OPTIONAL per-class vector to hold `cls`, filling with the
2300 * "not declared" value. The vectors start empty on purpose -- an empty one
2301 * means the station declares none of this property at all -- so they cannot
2302 * be sized in `grow_class_vectors` without turning every station into one
2303 * that declares it.
2304 */
2305 template <class V>
2306 void grow_class_slot(std::vector<V>& v, std::size_t cls, const V& fill) {
2307 const std::size_t K = sn_.classes.size();
2308 if (v.size() < K) v.resize(K < cls ? cls : K, fill);
2309 }
2310
2311 std::size_t station_of(std::size_t node, const std::string& what) const {
2312 if (node == 0 || node > sn_.nodes.size())
2313 throw InputError(what + ": node index " + std::to_string(node) + " does not exist");
2314 const std::size_t ist = sn_.nodes[node - 1].station;
2315 if (ist == 0)
2316 throw InputError(what + ": node '" + sn_.nodes[node - 1].name +
2317 "' is not a station (it serves no jobs)");
2318 return ist;
2319 }
2320
2321 /**
2322 * The fork's node record, with its variable-forking-level matrices sized on
2323 * first use.
2324 *
2325 * They start EMPTY on every fork, so a consumer can tell a plain fork from
2326 * one with overrides without inspecting entries; the first override is what
2327 * allocates them, seeded from `tasks_per_link` and probability 1 on the
2328 * links the model actually declares.
2329 */
2330 /** One recorded `Fork.set*` call, replayed once the routing is installed. */
2331 struct ForkOverride {
2332 enum Kind { TASKS, DIST, PROB };
2333 Kind kind = TASKS;
2334 std::size_t fork = 0, cls = 0, dest = 0;
2335 double value = 0.0;
2336 lang::Distrib<T> dist;
2337 };
2338 std::vector<ForkOverride> fork_overrides_;
2339 /** Set by `link()`. See `routing_installed()`. */
2340 bool routing_linked_ = false;
2341
2342 /**
2343 * Record an override, and apply it at once when the routing already exists.
2344 *
2345 * Applying eagerly is not an optimisation: it is what makes an override
2346 * naming a node that the fork does not reach fail AT THE CALL, where the
2347 * caller can see which line is wrong, rather than at `get_struct()`.
2348 */
2349 void record_fork_override(const ForkOverride& ov) {
2350 if (ov.fork == 0 || ov.fork > sn_.nodes.size() ||
2351 sn_.nodes[ov.fork - 1].nodetype != NodeType::Fork)
2352 throw InputError("the node given is not a Fork of this model");
2353 if (ov.cls == 0 || ov.cls > sn_.classes.size())
2354 throw InputError("a fork override names a class that does not exist");
2355 fork_overrides_.push_back(ov);
2356 if (routing_installed()) apply_fork_override(ov);
2357 }
2358
2359 /**
2360 * True once `link()` has written a routing block a fork override can read.
2361 *
2362 * A FLAG, not a size test on `rtnodes`. The size test could only become
2363 * true after a `get_struct()` had already refreshed the struct, so an
2364 * override recorded AFTER `link()` was applied by neither the eager path
2365 * here nor the replay in `get_struct()`, and was silently dropped.
2366 */
2367 bool routing_installed() const { return routing_linked_; }
2368
2369 /** Replay every recorded override, in the order the model declared them. */
2370 void apply_fork_overrides() {
2371 for (std::size_t i = 0; i < fork_overrides_.size(); ++i)
2372 apply_fork_override(fork_overrides_[i]);
2373 }
2374
2375 void apply_fork_override(const ForkOverride& ov) {
2376 qn::ForkParam<T>& f = fork_param(ov.fork);
2377 const std::vector<std::size_t> dests = fork_dests(ov.fork, ov.dest);
2378 for (std::size_t x = 0; x < dests.size(); ++x) {
2379 const std::size_t k = dests[x];
2380 switch (ov.kind) {
2381 case ForkOverride::TASKS:
2382 f.fan_out_link(k - 1, ov.cls - 1) = num_traits<T>::from_double(ov.value);
2383 break;
2384 case ForkOverride::DIST:
2385 f.fan_out_dist[k - 1][ov.cls - 1] = ov.dist;
2386 // the scalar slot carries the mean, so a consumer that only
2387 // reads fan_out_link still sees E[tasks per link]
2388 f.fan_out_link(k - 1, ov.cls - 1) = ov.dist.mean;
2389 break;
2390 case ForkOverride::PROB:
2391 f.fan_out_prob(k - 1, ov.cls - 1) = num_traits<T>::from_double(ov.value);
2392 break;
2393 }
2394 }
2395 refresh_fork_scalar(sn_.nodes[ov.fork - 1], f);
2396 }
2397
2398 qn::ForkParam<T>& fork_param(std::size_t fork_node) {
2399 if (fork_node == 0 || fork_node > sn_.nodes.size() ||
2400 sn_.nodes[fork_node - 1].nodetype != NodeType::Fork)
2401 throw InputError("the node given is not a Fork of this model");
2402 qn::ForkParam<T>& f = sn_.forkparam[fork_node];
2403 const std::size_t I = sn_.nodes.size(), K = sn_.classes.size();
2404 if (f.fan_out_link.rows() == I && f.fan_out_link.cols() == K) return f;
2405 const T zero = num_traits<T>::from_int(0);
2406 f.fan_out_link = Matrix<T>(I, K, zero);
2407 f.fan_out_prob = Matrix<T>(I, K, zero);
2408 f.fan_out_dist.assign(I, std::vector<lang::Distrib<T> >(K));
2409 const T tpl = num_traits<T>::from_double(sn_.nodes[fork_node - 1].tasks_per_link);
2410 const T one = num_traits<T>::from_int(1);
2411 for (std::size_t k = 1; k <= I; ++k)
2412 for (std::size_t r = 1; r <= K; ++r)
2413 if (fork_links_to(fork_node, k, r)) {
2414 f.fan_out_link(k - 1, r - 1) = tpl;
2415 f.fan_out_prob(k - 1, r - 1) = one;
2416 }
2417 return f;
2418 }
2419
2420 /**
2421 * Keep the scalar `tasks_per_link` consistent with the per-link mean, so a
2422 * solver that has not been taught the matrices degrades to E[tasks per
2423 * link] and not to a value the fork never emits.
2424 *
2425 * The branch probability is folded in HERE and not into `fan_out_link`,
2426 * because JMT and LDES read the two separately: `fan_out_link` is the count
2427 * GIVEN the branch fires, `fan_out_prob` is whether it fires at all.
2428 */
2429 void refresh_fork_scalar(qn::NodeDef& nd, const qn::ForkParam<T>& f) {
2430 double acc = 0.0;
2431 std::size_t cnt = 0;
2432 for (std::size_t k = 0; k < f.fan_out_link.rows(); ++k)
2433 for (std::size_t r = 0; r < f.fan_out_link.cols(); ++r) {
2434 if (num_traits<T>::to_double(f.fan_out_prob(k, r)) == 0.0) continue;
2435 acc += num_traits<T>::to_double(f.fan_out_link(k, r)) *
2436 num_traits<T>::to_double(f.fan_out_prob(k, r));
2437 ++cnt;
2438 }
2439 if (cnt > 0) nd.tasks_per_link = acc / static_cast<double>(cnt);
2440 }
2441
2442 /**
2443 * True when class r of `fork_node` routes to node k.
2444 *
2445 * READ OFF THE ROUTING `link()` WRITES, not off `rtnodes`. `rtnodes` is
2446 * DERIVED, built by `refresh_routing()` inside `refresh_struct()`, so it is
2447 * still empty at the end of `link()` -- which is precisely where the
2448 * recorded overrides are replayed. Asking it there answered "this fork
2449 * links nowhere" for every destination: `dest_node = 0` raised "call link()
2450 * before the override" from inside `link()` itself, and a named destination
2451 * fared worse, seeding the whole fan-out with zeros so every branch got
2452 * probability 0 and the model solved on a fork that never fires. `route_eff`
2453 * is the expansion when there is one and the declared `P` otherwise, which
2454 * is the same source the linked-node scan in `link()` already uses.
2455 */
2456 bool fork_links_to(std::size_t fork_node, std::size_t k, std::size_t r) const {
2457 const std::size_t K = sn_.classes.size();
2458 for (std::size_t s = 1; s <= K; ++s)
2459 if (num_traits<T>::to_double(sn_.route_eff(r, s, fork_node, k)) != 0.0) return true;
2460 return false;
2461 }
2462
2463 /**
2464 * Node indexes a fork override applies to. `dest_node` 0 means every
2465 * destination the fork actually links to, so the routing must be in place
2466 * by the time this runs -- which is what the recorded-override replay in
2467 * `link()` guarantees whichever order the caller used.
2468 */
2469 std::vector<std::size_t> fork_dests(std::size_t fork_node, std::size_t dest_node) const {
2470 std::vector<std::size_t> out;
2471 const std::size_t I = sn_.nodes.size(), K = sn_.classes.size();
2472 if (dest_node != 0) {
2473 if (dest_node > I)
2474 throw InputError("a fork override names a node that does not exist");
2475 out.push_back(dest_node);
2476 return out;
2477 }
2478 for (std::size_t k = 1; k <= I; ++k)
2479 for (std::size_t r = 1; r <= K; ++r)
2480 if (fork_links_to(fork_node, k, r)) { out.push_back(k); break; }
2481 if (out.empty())
2482 throw InputError("a fork override was set on '" + sn_.nodes[fork_node - 1].name +
2483 "', which links nowhere yet: call link() before the override");
2484 return out;
2485 }
2486
2487 /**
2488 * The checks a model must pass before its struct means anything.
2489 *
2490 * They are the ones whose absence produces a struct that solves to a
2491 * plausible wrong answer rather than to an error: a class with no service
2492 * anywhere, an open class with no arrival, a Fork with no Join.
2493 */
2494 void validate() const {
2495 if (sn_.classes.empty()) throw InputError("Network '" + sn_.name + "': it has no classes");
2496 if (sn_.stations.empty())
2497 throw InputError("Network '" + sn_.name + "': it has no stations");
2498 // WHO SWITCHES INTO WHOM. A job can enter a class by arriving in it, or
2499 // by CLASS SWITCHING into it from another class -- through a routing
2500 // block with r != s, through a ClassSwitch node's matrix, or through a
2501 // cache's hit/miss/retrieval switch. `switches` is that edge relation,
2502 // and the only test below that reads it is the "no service process
2503 // anywhere" one: a class reached ONLY by switching legitimately has no
2504 // service of its own at some stations.
2505 //
2506 // There is deliberately NO reachability closure over these edges any
2507 // more. It existed to refuse an open class no job can enter, which is
2508 // not an error -- see the note on `gallery_erlerl1` below. MATLAB and
2509 // python have no check of this kind at all, so there was never a
2510 // reference predicate to copy. `validate()` runs BEFORE
2511 // `refresh_routing`, so the edges are read from the raw `P` and from
2512 // `csmatrix`, not from `Peff`, which does not exist yet.
2513 const std::size_t K = sn_.classes.size();
2514 const T zero = num_traits<T>::from_int(0);
2515 std::vector<std::vector<bool>> switches(K, std::vector<bool>(K, false));
2516 for (std::size_t r = 0; r < K; ++r)
2517 for (std::size_t sc = 0; sc < K; ++sc) {
2518 if (r == sc) continue;
2519 for (std::size_t i = 1; i <= sn_.nodes.size() && !switches[r][sc]; ++i)
2520 for (std::size_t j = 1; j <= sn_.nodes.size(); ++j)
2521 if (sn_.get_route(r + 1, sc + 1, i, j) > zero) {
2522 switches[r][sc] = true;
2523 break;
2524 }
2525 }
2526 for (const auto& kv : sn_.csmatrix) {
2527 const Matrix<T>& C = kv.second;
2528 for (std::size_t r = 0; r < K && r < C.rows(); ++r)
2529 for (std::size_t sc = 0; sc < K && sc < C.cols(); ++sc)
2530 if (r != sc && C(r, sc) > zero) switches[r][sc] = true;
2531 }
2532 for (const auto& kv : sn_.nodeparam) {
2533 const CacheParam<T>& cp = kv.second;
2534 for (std::size_t r = 0; r < K; ++r) {
2535 if (r < cp.hitclass.size() && cp.hitclass[r] >= 1 && cp.hitclass[r] <= K)
2536 switches[r][cp.hitclass[r] - 1] = true;
2537 if (r < cp.missclass.size() && cp.missclass[r] >= 1 && cp.missclass[r] <= K)
2538 switches[r][cp.missclass[r] - 1] = true;
2539 }
2540 for (const auto& row : cp.retrieval_classes)
2541 for (std::size_t r = 0; r < K && r < row.size(); ++r)
2542 if (row[r] >= 1 && row[r] <= K) switches[r][row[r] - 1] = true;
2543 }
2544 // SPAWN ON COMPLETION REACHES A CLASS TOO. A phase-2 continuation is
2545 // injected by the completion of its trigger and never arrives at a
2546 // Source or crosses a switch, so without this edge it reads as a class
2547 // no job can enter and a well-formed LQN phase-2 model is refused.
2548 for (std::size_t r = 0; r < K; ++r)
2549 if (sn_.classes[r].spawn >= 1 && sn_.classes[r].spawn <= K)
2550 switches[r][sn_.classes[r].spawn - 1] = true;
2551 for (std::size_t r = 0; r < sn_.classes.size(); ++r) {
2552 // A class reached only by switching legitimately has no service of
2553 // its own at some stations; the served test still applies to the
2554 // rest, so it is kept for every class that is not switched into.
2555 bool switched_into = false;
2556 for (std::size_t q = 0; q < K; ++q)
2557 if (switches[q][r]) switched_into = true;
2558 bool served = false;
2559 for (std::size_t i = 0; i < sn_.stations.size(); ++i)
2560 if (!sn_.service[i][r].disabled) served = true;
2561 // In an SPN the timing lives in the transition modes, not in station
2562 // service: a Place is a token container and only a QUEUEING place
2563 // carries a service process, so a token class served nowhere is a
2564 // well-formed net rather than an incomplete one.
2565 bool petri = false;
2566 for (std::size_t nd = 0; nd < sn_.nodes.size(); ++nd)
2567 if (sn_.nodes[nd].nodetype == NodeType::Transition) petri = true;
2568 if (!served && !switched_into && !petri)
2569 throw InputError("Network '" + sn_.name + "': class '" + sn_.classes[r].name +
2570 "' has no service process at any station");
2571 // AN UNREACHABLE OPEN CLASS IS WELL FORMED, and this used to refuse
2572 // it. `gallery_erlerl1` ships in all three reference suites with a
2573 // second open class whose arrival is Disabled and which nothing
2574 // routes into; MATLAB and native Python both SOLVE it and simply
2575 // report no row for that class, because a class no job can enter
2576 // carries zero of every metric and the table drops an all-zero row.
2577 // The refusal made this port the only one that could not read its
2578 // own gallery. Same argument as the fork-with-no-join case below:
2579 // a construct the reference suites ship and the reference solvers
2580 // answer is not an input error, whatever it looks like in isolation.
2581 }
2582 // A FORK WITH NO JOIN IS WELL FORMED ONLY IF ITS SIBLINGS CAN LEAVE.
2583 // `fj_nojoin` ships in all three reference suites as an OPEN model
2584 // whose fork branches each end at the Sink, and MATLAB and native
2585 // Python both solve it, so a blanket refusal is wrong: the
2586 // synchronisation point is what a Join provides, and a model that
2587 // never synchronises simply has none (see fj_driver.h, which drives
2588 // forkLambda from the fork's own firing rate in that case).
2589 //
2590 // A join-less fork whose branches RETURN INTO THE MODEL is a different
2591 // object. Every firing turns one job into k siblings, none of them ever
2592 // merges and none ever departs, so the population is not conserved and
2593 // grows without bound. The reference has no check for it and cannot
2594 // solve it either: `sortForks` calls `nestedForks(f, [])`, whose
2595 // `startNode == endNode` test can never hold against an empty join, and
2596 // on a closed model it recurses until MATLAB reports "Out of memory.
2597 // The likely cause is an infinite recursion" (measured 2026-07-30).
2598 // Refusing here names the node the caller has to close.
2599 for (std::size_t i = 0; i < sn_.nodes.size(); ++i) {
2600 if (sn_.nodes[i].nodetype != NodeType::Fork) continue;
2601 bool closed = false;
2602 for (const auto& fjp : sn_.fj)
2603 if (fjp.first == i + 1) closed = true;
2604 if (closed) continue;
2605 // Can a sibling ever leave? Reachability of a Sink from the Fork
2606 // over the raw routing graph, any class pair -- `validate()` runs
2607 // before refresh_routing, so `Peff` does not exist yet.
2608 std::vector<bool> seen(sn_.nodes.size(), false);
2609 std::vector<std::size_t> stack(1, i + 1);
2610 seen[i] = true;
2611 bool departs = false;
2612 while (!stack.empty() && !departs) {
2613 const std::size_t u = stack.back();
2614 stack.pop_back();
2615 for (std::size_t v = 1; v <= sn_.nodes.size() && !departs; ++v) {
2616 if (seen[v - 1]) continue;
2617 bool edge = false;
2618 for (std::size_t r = 1; r <= K && !edge; ++r)
2619 for (std::size_t s = 1; s <= K; ++s)
2620 if (sn_.get_route(r, s, u, v) > zero) {
2621 edge = true;
2622 break;
2623 }
2624 if (!edge) continue;
2625 if (sn_.nodes[v - 1].nodetype == NodeType::Sink) {
2626 departs = true;
2627 break;
2628 }
2629 seen[v - 1] = true;
2630 stack.push_back(v);
2631 }
2632 }
2633 if (!departs)
2634 throw InputError("Network '" + sn_.name + "': the Fork '" + sn_.nodes[i].name +
2635 "' is not closed by a Join and no Sink is reachable from it, "
2636 "so its siblings can neither merge nor depart");
2637 }
2638 }
2639};
2640
2641} // namespace qn
2642} // namespace line
2643
2644#endif // LINE_LANG_QN_NETWORK_BUILDER_H
Malformed or inconsistent input (dimensions, negative populations, ...).
Definition error.h:37
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
void set_service_rate_function(std::size_t node, const std::function< T(const std::vector< std::size_t > &)> &muFun, const Matrix< T > &swap_graph=Matrix< T >())
Queue.setServiceRateFunction(muFun): the TOTAL service rate of a PAS or OI station as a function of t...
void set_drop_rule(std::size_t node, std::size_t cls, DropStrategy rule)
station.setDropRule(class, rule).
void set_state_prior(std::size_t node, const Matrix< T > &space, const std::vector< T > &prior)
StatefulNode.setStatePrior(space, prior): a distribution over the rows of a DECLARED state space.
void set_departure_discipline(std::size_t node, std::size_t cls, lang::DepartureDiscipline rule)
Place.setDepartureDiscipline(class, rule).
std::size_t add_logger(const std::string &nm, const std::string &log_file=std::string())
A Logger node: a pass-through that records every job crossing it.
void set_global_dependence(const GdScaling< T > &fun, const std::vector< T > &peak, int wire_cutoff)
As above, with an explicit per-slot OPEN-class truncation used when phi is materialized onto the JSON...
void set_load_dependence(std::size_t node, const std::vector< T > &alpha)
station.setLoadDependence(alpha): the rate multiplier at population 1, 2, ... The vector is indexed f...
void set_class_capacity(std::size_t node, std::size_t cls, double k)
std::size_t add_source(const std::string &nm)
The external arrival station.
std::size_t add_fork(const std::string &nm, double tasks_per_link=1.0)
A Fork node.
void bind_join(std::size_t join_node, std::size_t fork_node)
void set_enabling_conditions(std::size_t node, std::size_t mode, std::size_t cls, std::size_t place, const T &tokens)
Transition.setEnablingConditions(mode, class, place, tokens): how many class-r tokens the mode needs ...
void set_arrival_batch(std::size_t node, std::size_t cls, const Distrib< T > &dist)
Source.setArrivalBatch(class, dist): the batch-size law released at each arrival epoch.
std::size_t add_open_class(const std::string &nm, int prio=0)
void set_mode_timing(std::size_t node, std::size_t mode, lang::TimingStrategy ts)
Transition.setTimingStrategy(mode, strategy): TIMED or IMMEDIATE.
std::size_t add_place(const std::string &nm, SchedStrategy sched)
A Place whose EMBEDDED QUEUE is served under sched: the QUEUEING PLACE of a queueing Petri net,...
Network(const std::string &nm)
void set_firing_weight(std::size_t node, std::size_t mode, const T &w)
Transition.setFiringWeights(mode, weight): the share among tied modes.
void set_class_patience(std::size_t cls, const Distrib< T > &dist, lang::ImpatienceType kind=lang::ImpatienceType::RENEGING)
JobClass.setPatience(kind, dist): the CLASS-WIDE abandonment law.
std::size_t add_delay(const std::string &nm)
An infinite-server station (a Delay, MATLAB's Delay / DelayStation).
void set_class_spawn(std::size_t cls, std::size_t spawn_cls)
JobClass.spawnClass (sn.classspawn): the class injected at the same station on every completion of cl...
void set_region_constraint(std::size_t region, const Matrix< T > &A, const std::vector< T > &b)
The optional linear constraint A n <= b a region may carry beyond its caps.
RoutingMatrix< T > init_routing_matrix() const
An empty routing matrix, MATLAB's model.initRoutingMatrix.
std::vector< std::size_t > set_miss_cache(std::size_t cache_node, std::size_t next_cache, const std::vector< std::size_t > &hit_classes_at_next)
Cache.setMissCache(readClass, nextCache, hitClassAtNext): send this cache's misses to next_cache pres...
void set_class_immediate_feedback(std::size_t cls)
jobclass.setImmediateFeedback(): the same property, class-wide.
void set_mode_distribution(std::size_t node, std::size_t mode, const Distrib< T > &d)
Transition.setDistribution(mode, dist): the mode's firing law.
void set_polling(std::size_t node, lang::PollingType ptype, const std::vector< Distrib< T > > &switchover=std::vector< Distrib< T > >(), std::size_t pk=1)
Queue.setPollingType(...): the polling discipline of a POLLING station and the switchover walks betwe...
std::size_t add_router(const std::string &nm)
A stateless routing node.
void set_initial_marking(std::size_t node, const std::vector< T > &tokens)
Place.setState(marking): the initial token count of the place, per class.
void set_inhibiting_conditions(std::size_t node, std::size_t mode, std::size_t cls, std::size_t place, const T &tokens)
Transition.setInhibitingConditions(mode, class, place, tokens): the class-r count at place that BLOCK...
void set_number_of_servers(std::size_t node, double n)
queue.setNumberOfServers(n).
RoutingMatrix< T > serial_routing(const std::vector< std::size_t > &nodes) const
model.serialRouting(nodes): a unit-probability path through nodes.
void set_joint_dependence(std::size_t node, const CdScaling< T > &fun, const std::vector< T > &peak)
station.setJointDependence(eta, peakRatePerClass): MATLAB's Station.ljdScaling / ljdScalingPeak.
void set_fork_tasks_per_link(std::size_t fork_node, std::size_t jobclass, double tasks, std::size_t dest_node=0)
Variable forking levels on an existing Fork, the twin of MATLAB Fork.setTasksPerLink(jobclass,...
void set_patience(std::size_t node, std::size_t cls, const Distrib< T > &dist, lang::ImpatienceType kind=lang::ImpatienceType::RENEGING)
Queue.setPatience(class, dist, type): the abandonment timer of a job WAITING at the station,...
void set_mode_firing_dependence(std::size_t node, std::size_t mode, const std::function< T(const std::vector< T > &)> &g)
Transition.setFiringRateDependence(mode, g): g(marking) scales the rate.
void set_reply_signal_class(std::size_t call_cls, std::size_t reply_cls)
JobClass.setReplySignalClass(reply) (sn.syncreply), plus the sn.replyblock the state layer needs.
std::size_t add_queue(const std::string &nm, SchedStrategy sched=SchedStrategy::FCFS)
A queueing station.
void set_class_switch_matrix(std::size_t node, const Matrix< T > &C)
Install the switching matrix of a ClassSwitch created without one.
void set_routing(std::size_t node, std::size_t cls, RoutingStrategy rs)
node.setRouting(class, strategy).
std::size_t add_transition(const std::string &nm)
A Transition with NO modes yet: Transition(model, name) as MATLAB, the JAR and Python spell it,...
void set_join_strategy(std::size_t node, lang::JoinStrategy strategy, double quorum=0.0)
Join.setStrategy(...): STD waits for every sibling, PARTIAL for a quorum.
void set_fork_branch_probability(std::size_t fork_node, std::size_t jobclass, std::size_t dest_node, double prob)
A branch that fires only with probability prob.
std::size_t add_cache(const std::string &nm, const CacheParam< T > &par)
A Cache node with its item population, list capacities and popularity.
void set_reward(const std::string &nm, const std::function< T(const std::vector< T > &)> &fn, const std::string &kind=std::string(), std::size_t node=0, std::size_t cls=0)
model.setReward(name, fn): a named reward evaluated on the AGGREGATE state row, the per-(station,...
std::size_t add_mode(std::size_t node, const std::string &nm)
Transition.addMode(name): a new firing mode, returning its 1-based index.
std::size_t add_self_looping_class(const std::string &nm, double njobs, std::size_t refstat_node, int prio=0)
SelfLoopingClass(model, name, njobs, refstat, prio): a closed class whose jobs perpetually cycle at t...
void set_marked_classes(std::size_t node, const std::vector< std::size_t > &classes)
Source.markedClasses: the 1-based class carried by each mark of an MMAP.
std::size_t add_closed_class(const std::string &nm, double njobs, std::size_t refstat_node, int prio=0)
void set_sched_param(std::size_t node, std::size_t cls, const T &weight)
The DPS / GPS weight of a class at a station.
std::size_t add_class_switch(const std::string &nm, const Matrix< T > &C)
A ClassSwitch node carrying the (nclasses x nclasses) switching matrix.
void set_fork_tasks_per_link_dist(std::size_t fork_node, std::size_t jobclass, const lang::Distrib< T > &dist, std::size_t dest_node=0)
A random jobs-per-link degree, redrawn per link and per forked job.
void set_firing_priority(std::size_t node, std::size_t mode, double prio)
Transition.setFiringPriorities(mode, priority).
std::size_t add_sink(const std::string &nm)
The external departure node.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
std::size_t add_class_switch(const std::string &nm)
A ClassSwitch node whose matrix is installed LATER, by set_class_switch_matrix.
void set_batch_reject(std::size_t node, std::size_t cls, const T &p)
Queue.setBatchRejectProbability(class, p).
void set_class_dependence(std::size_t node, const CdScaling< T > &fun, const std::vector< T > &peak=std::vector< T >())
station.setClassDependence(beta, peakRatePerClass).
void set_state_dep_routing(std::size_t entry, std::size_t departure, const std::vector< std::vector< std::size_t > > &branches, const std::vector< std::size_t > &level, const std::vector< double > &C, const Matrix< double > &d, std::size_t cls=0)
node.setStateDepRouting(class, departure, branches, level, C, d).
std::size_t add_place(const std::string &nm)
A Place: an SPN token container.
void set_routing_weights(std::size_t node, std::size_t cls, const std::map< std::size_t, double > &weights)
The per-destination weights of a WRROBIN dispatcher, per (node, class).
void set_item_miss_class(std::size_t cache_node, const std::vector< std::size_t > &miss_classes)
Cache.setItemMissClass(readClass, missClasses): terminate a cache network, every per-item class of th...
void set_log_path(const std::string &path)
Network.setLogPath: the directory every Logger of this model writes into.
void set_capacity(std::size_t node, double k)
station.setCapacity(k), the K of Kendall's notation.
NetworkStruct< T > & raw_struct()
The struct WITHOUT refreshing it, for a caller that is still building.
void set_switchover(std::size_t node, std::size_t cls, const Distrib< T > &so)
Queue.setSwitchover(jobclass, distrib): the switchover time of a class.
void set_immediate_feedback(std::size_t node, std::size_t cls)
queue.setImmediateFeedback(class): a completing job of that class is fed straight back into service,...
void set_balking(std::size_t node, std::size_t cls, lang::BalkingStrategy strategy, const std::vector< typename Station< T >::BalkingThreshold > &thresholds)
Queue.setBalking(class, strategy, thresholds): an arrival that refuses to JOIN, on the state it finds...
void add_server_type(std::size_t node, const typename Station< T >::ServerType &stype)
Queue.addServerType(...): one heterogeneous server pool of the station.
void set_setup_delayoff(std::size_t node, std::size_t cls, const Distrib< T > &setup, const Distrib< T > &delayoff)
Queue.setDelayOff(class, setupTime, delayoffTime): the station powers down after sitting idle for the...
void set_retrieval_system(std::size_t cache_node, std::size_t read_class, std::size_t miss_class, const std::vector< std::size_t > &queue_nodes)
Cache.setRetrievalSystem(readClass, missClass, queues): a delayed-hit cache whose misses are fetched ...
void set_class_deadline(std::size_t cls, double due)
JobClass.deadline: the soft deadline EDD, EDF and JMT's tardiness use.
void link(const RoutingMatrix< T > &Pm)
model.link(P): install the routing.
void set_routing_param(std::size_t node, std::size_t cls, int d)
The d of a power-of-d (SQ) dispatcher, per (node, class).
void set_retrial(std::size_t node, std::size_t cls, const Distrib< T > &proc, const T &rate, int max_attempts=0)
Queue.setRetrial(...): a station with an ORBIT instead of a waiting line.
void set_service(std::size_t node, std::size_t cls, const Distrib< double > &d)
void set_reference_class(std::size_t cls)
JobClass.setReferenceClass(true): sn.refclass(c) picks this class.
void pas_mirror(std::size_t ist, const std::function< double(const std::vector< std::size_t > &)> &mu, const Matrix< double > &swap_graph)
std::size_t add_join_unbound(const std::string &nm)
void set_hetero_sched_policy(std::size_t node, lang::HeteroSchedPolicy policy)
Queue.setHeteroSchedPolicy(...): how the server pools are picked among.
std::size_t add_join(const std::string &nm, std::size_t fork_node)
A Join node, which IS a station: it serves at an infinite rate, and the synchronisation delay is supp...
void set_breakdown(std::size_t node, const Distrib< T > &failure, const Distrib< T > &repair, const std::vector< Distrib< T > > &down_service=std::vector< Distrib< T > >())
Queue.setBreakdown(failure, repair, downService): the server alternates up and down on the two clocks...
void set_pas(std::size_t node, const std::function< T(const std::vector< std::size_t > &)> &mu, const std::vector< std::vector< bool > > &swap_graph=std::vector< std::vector< bool > >())
Queue.setService(@(c) ...) for a pass-and-swap / order-independent station: the total service rate mu...
void set_item_read_classes(std::size_t cache_node, const std::vector< std::size_t > &read_classes, const std::vector< std::size_t > &hit_classes)
Cache.setItemReadClasses(readClasses, hitClasses): declare that read_classes[i] is the request stream...
void set_mode_servers(std::size_t node, std::size_t mode, double n)
Transition.setNumberOfServers(mode, n); GlobalConstants::MaxInt is infinite.
void set_switchover(std::size_t node, std::size_t from_cls, std::size_t to_cls, const Distrib< T > &so)
Queue.setSwitchover(fromClass, toClass, distrib): the walk between two CLASSES at an ordinary station...
void set_firing_outcome(std::size_t node, std::size_t mode, std::size_t cls, std::size_t dest, const T &tokens)
Transition.setFiringOutcome(mode, class, node, tokens): the class-r tokens the firing deposits.
std::size_t add_transition(const std::string &nm, const TransitionParam< T > &par)
A Transition: the firing rules of an SPN, as Transition in MATLAB.
void set_orbit_impatience(std::size_t node, std::size_t cls, const Distrib< T > &dist)
Queue.setOrbitImpatience(class, dist): abandonment from the retrial orbit.
void set_sync_reply(std::size_t node, std::size_t call_cls, std::size_t reply_cls)
Declare a SYNCHRONOUS call: a job of call_cls leaving node keeps its server until a job of the REPLY ...
void set_signal(std::size_t cls, lang::SignalType type, lang::RemovalPolicy policy=lang::RemovalPolicy::RANDOM, std::size_t target=0, const std::vector< double > &remdist=std::vector< double >())
void set_global_dependence(const GdScaling< T > &fun, const std::vector< T > &peak)
model.setGlobalDependence(phi, peak): MATLAB's Network.gdScaling.
void set_server_parallelism(std::size_t node, std::size_t cls, std::size_t n)
Queue.setServerParallelism(class, n): the servers a job seizes for the whole of its service.
std::size_t station_index(std::size_t node) const
void set_region_weights(std::size_t region, const std::vector< T > &weight)
FiniteCapacityRegion.setClassWeight: the per-class weight the region's global cap counts a job agains...
void set_arrival(std::size_t node, std::size_t cls, const Distrib< T > &d)
source.setArrival(class, dist): the same table, at the Source.
std::size_t add_region(const std::vector< std::size_t > &nodes, const std::vector< double > &class_max_jobs, double global_max_jobs=-1.0, const std::vector< DropStrategy > &rule=std::vector< DropStrategy >(), const std::vector< double > &class_max_memory=std::vector< double >(), const std::vector< T > &class_size=std::vector< T >(), double global_max_memory=-1.0, const std::string &name=std::string())
FiniteCapacityRegion(model, nodes): a cap on the jobs held ACROSS a set of stations.
void set_polling_type(std::size_t node, lang::PollingType rule, int par=0)
Queue.setPollingType(rule, par): the polling discipline of a POLLING station, identical across all cl...
The routing matrix a model script fills in, MATLAB's P cell array.
void set(std::size_t cls, const RoutingMatrix< T > &block)
P.set(class, Network.serialRouting(...)): install a one-class block.
T get(std::size_t r, std::size_t s, std::size_t i, std::size_t j) const
void set(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
void set(std::size_t i, std::size_t j, const T &p)
std::map< Key, double > entries
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Dense matrix and non-owning view.
void sn_fj_nodevisits_mmt(qn::NetworkStruct< T > &sn)
Rewrite sn.nodevisits with the MMT correction.
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:426
TimingStrategy
SPN transition timing, with the values of MATLAB TimingStrategy.
Definition lang_types.h:363
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:365
@ TIMED
fires after its firing distribution elapses
Definition lang_types.h:364
BalkingStrategy
Balking rules, with the values of MATLAB BalkingStrategy.
Definition lang_types.h:447
SignalType
G-network signal classes, with the values of MATLAB SignalType.
Definition lang_types.h:167
@ 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
void prior_refresh_moments(Distrib< T > &d)
Write the mixture moments onto a Prior, the counterpart of dist_refresh_moments for the Markovian fam...
Definition prior.h:346
JoinStrategy
Join rules, with the values of MATLAB JoinStrategy.
Definition lang_types.h:463
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
DepartureDiscipline
When a Place releases a served token, MATLAB DepartureDiscipline.
Definition lang_types.h:460
RemovalPolicy
Which job a negative signal removes, with the values of MATLAB RemovalPolicy.
Definition lang_types.h:174
@ RANDOM
uniform over waiting AND in-service jobs
Definition lang_types.h:175
HeteroSchedPolicy
How a heterogeneous station picks among its server types, MATLAB HeteroSchedPolicy.
Definition lang_types.h:453
PollingType
Polling service disciplines, with the values of MATLAB PollingType.
Definition lang_types.h:372
@ KLIMITED
serve at most K per visit (K in pollingPar)
Definition lang_types.h:375
std::function< std::vector< T >(const std::vector< T > &)> GdScaling
A globally state-dependent scaling, sn.gdscaling.
Definition lang_types.h:744
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:731
void dist_refresh_moments(Distrib< T > &d)
Fill in the first two moments of a distribution given by its matrices.
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
ImpatienceType
Impatience kinds, with the values of MATLAB ImpatienceType.
Definition lang_types.h:444
SdrCoeff pfqn_sdrcoeff(const SdrStruct &sdr)
Validates an SDR structure and returns its derived coefficients.
Definition pfqn_sdr.h:105
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
Prior: parameter uncertainty as a weighted set of alternative models.
Post-MMT node visits of a fork-join model.
bool is_prior() const
Definition lang_types.h:878
Topology and coefficients of a state-dependent routing subnetwork.
Definition pfqn_sdr.h:59
std::vector< std::size_t > departureOf
departureOf[b] is the departure centre d(b) of branch b.
Definition pfqn_sdr.h:69
std::vector< std::size_t > entryOf
entryOf[b] is the entry centre e(b) of branch b.
Definition pfqn_sdr.h:67
std::vector< std::size_t > level
level[b] is the unique t with B_b in V_t - V_{t+1}; level[0] is unused.
Definition pfqn_sdr.h:71
Matrix< double > d
Coefficients d_tb of eq.
Definition pfqn_sdr.h:75
std::vector< double > C
Coefficients C_t of eq.
Definition pfqn_sdr.h:73
std::size_t departure
Departure centre d of Q(V,V); may equal entry.
Definition pfqn_sdr.h:63
std::vector< std::vector< std::size_t > > branch
branch[b] holds the centres of branch b, b >= 1; branch[0] is unused.
Definition pfqn_sdr.h:65
std::size_t entry
Entry centre e of Q(V,V).
Definition pfqn_sdr.h:61
Server breakdown and repair of a station whose server fails and is repaired.
T failure_rate
sn.breakdownMu: 1 / mean failure time
lang::Distrib< T > repair
time to repair of a down server
lang::Distrib< T > failure
time to failure of an up server
T repair_rate
sn.repairMu: 1 / mean repair time
std::vector< T > down_service_rates
sn.downServiceRates(ist, :): per class, 0 = no service while down.
std::vector< Popularity > preadkind
per class, parallel to pread
std::map< std::size_t, std::vector< std::size_t > > retrieval_queues
read class(0-based)->nodes
std::vector< std::vector< std::size_t > > retrieval_classes
(nitems x nclasses), 1-based
std::vector< int > itemcap
std::vector< std::size_t > missclass
std::vector< std::size_t > hitclass
std::vector< std::size_t > classitem
Item read by each per-item class of a cache network (MATLAB Cache.setItemReadClasses,...
std::vector< std::vector< T > > pread
(u) x (n), empty row = NaN
static Distrib exp_rate(const T &r)
Definition lang_types.h:945
static Distrib disabled_dist()
Definition lang_types.h:988
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:977
One job class of the network.
std::size_t refstat
1-based reference station
double population
infinite for an open class
The DECLARED join rule of a Join node, by 1-based node index.
The G-network signal declaration, per CLASS.
std::function< T(const std::vector< std::size_t > &)> svc_rate_fun
std::vector< std::vector< bool > > swap_graph
The polling controller of a POLLING station, keyed by station index.
std::size_t pk
the K of K-LIMITED
std::vector< lang::Distrib< T > > switchover
FINITE CAPACITY REGIONS, MATLAB's refreshRegions output.
std::vector< std::vector< double > > cap
(nstations x nclasses+1), -1 = unbounded
std::string name
The region's declared name, as the wire carries it; a generated one otherwise.
std::vector< double > maxmem
per member station, -1 = unbounded
std::vector< DropStrategy > rule
per class
std::vector< T > size
per class; size is the memory footprint
std::vector< bool > members
membership, independent of the caps
sn.reward: the user-declared reward functions, MATLAB's model.setReward(name, fn).
std::function< T(const std::vector< T > &)> fn
std::string kind
The DECLARATIVE form the reward was built from, when it was: the template name (QLen,...
A node of the network.
std::vector< RoutingStrategy > routing
sn.routing, per class.
The parameters of a retrial station: MATLAB sn.retrialProc and friends.
std::vector< int > max_attempts
0 = unbounded
std::vector< T > retrial_rate
mu_r, the per-class orbit retry rate
std::vector< lang::Distrib< T > > retrial_proc
retrial_proc[r] is the class-r retrial process; empty = not a retrial class.
Key(std::size_t r_, std::size_t s_, std::size_t i_, std::size_t j_)
bool operator<(const Key &o) const
Setup and delay-off of a station that powers down when it falls idle.
std::vector< lang::Distrib< T > > setup
per class, disabled = not declared
std::vector< lang::Distrib< T > > delayoff
per class
Per class; strategy == NONE is a class that declares no balking.
One balking threshold: with min_jobs <= n <= max_jobs at the station, an arriving job of the class re...
A heterogeneous server pool: count servers that serve only compatible classes, each with its own serv...
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...
std::vector< T > jdscalingpeak
sn.jdscalingpeak for this station: the declared peak joint-dependent scaling per class.
std::vector< BalkingParam > balking
std::function< T(const std::vector< std::size_t > &)> svc_rate_fun
sn.nodeparam{ind}.svcRateFun for a PAS / OI station: the TOTAL service rate as a function of the orde...
std::vector< T > batch_reject
Queue.setBatchRejectProbability: per-class rejection of a whole batch.
std::vector< Distrib< T > > patience
Queue.setPatience(class, dist): the abandonment timer of a WAITING job, with impatience[r] naming whi...
std::vector< std::size_t > server_parallelism
Queue.setServerParallelism(class, n): the servers a job seizes for the whole of its service,...
std::vector< int > droprule
Per-class blocking rule as an INT, with 0 meaning "not set".
SchedStrategy sched
std::vector< std::vector< Distrib< T > > > switchover_pair
Queue.setSwitchover(fromClass, toClass, distrib): the walk the server takes when it turns from servin...
double nservers
may be infinite (a Delay, or an inf-scheduled task)
std::vector< lang::PollingType > polling_type
Polling parameters for a POLLING station, MATLAB's pollingType, switchoverTime and pollingPar on the ...
Matrix< T > swap_graph
sn.nodeparam{ind}.swapGraph: which class a departing job promotes the jobs behind it into.
CdScaling< T > jdscaling
sn.jdscaling for this station: MATLAB's Station.ljdScaling, the JOINT dependence map eta_i(n),...
std::vector< T > cdscalingpeak
sn.cdscalingpeak for this station: the DECLARED peak rate scaling per class, empty when the station i...
std::vector< lang::ImpatienceType > impatience
std::vector< T > schedparam
sn.schedparam, per class: the DPS / GPS weight, or the SEPT / LEPT rank.
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< Distrib< T > > arrival_batch
Source.setArrivalBatch(class, dist): the batch-size law released at each arrival epoch.
std::vector< Distrib< T > > switchover
std::vector< bool > immfeed
Node-level immediate feedback, per class; empty when the station sets none.
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
std::vector< double > firingprio
firing priority per mode
std::vector< lang::TimingStrategy > timing
immediate or timed
std::vector< std::string > modenames
std::vector< lang::Distrib< T > > firingproc
firing distribution per mode
std::vector< double > nmodeservers
servers per mode, may be infinite
std::vector< T > fireweight
weight among simultaneously enabled modes
std::vector< Matrix< T > > firing
firing[m](p,r): class-r tokens mode m moves to/from place p when it fires.
std::vector< Matrix< T > > enabling
enabling[m](p,r): class-r tokens of place p (0-based node) mode m needs.
std::vector< std::function< T(const std::vector< T > &)> > firingdep
Marking-dependent firing-rate multiplier g_m(marking); an empty entry is the unit multiplier.
std::vector< Matrix< T > > inhibiting
inhibiting[m](p,r): class-r tokens of p that BLOCK mode m (Inf = never).
std::vector< std::size_t > firingphases
phase count per mode, 0 when non-Markovian