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