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