LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
network_generator.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_GEN_NETWORK_GENERATOR_H
6#define LINE_GEN_NETWORK_GENERATOR_H
7
8/**
9 * @file
10 * @ingroup line_gen
11 * Random queueing-network generation: the C++ twin of MATLAB
12 * `@NetworkGenerator`, the JAR `jline.gen.NetworkGenerator` and the native
13 * Python `line_solver.gen.network_generator`.
14 *
15 * WHAT IT PRODUCES. A `qn::Network<T>` with `numQueues` queues, `numDelays`
16 * delay stations, `numOClass` open classes and `numCClass` closed classes, wired
17 * over a random strongly connected topology, with random service laws, random
18 * scheduling, random routing and (optionally) random ClassSwitch nodes on the
19 * links. It is the model source the test suites, the benchmark sweeps and the
20 * solver-comparison harnesses draw from, so the shapes it can emit matter more
21 * than any single draw: every arm of the reference is reproduced, including the
22 * multi-chain class-switch masks and the load bands.
23 *
24 * HOW IT RELATES TO THE REFERENCES:
25 *
26 * 1. MATLAB'S STREAM, DRAW FOR DRAW. Every draw, the spanning tree and the
27 * permutations included, comes from one `MatlabRng` (matlab_rng.h), which
28 * yields MATLAB's stream after `rng(seed)`: `rand`, `randi`, `randperm` and
29 * Stafford's `randfixedsum` as MATLAB computes them, in MATLAB's order. So
30 * `NetworkGenerator<T> g(seed, kind)` builds the model MATLAB builds after
31 * `rng(seed); NetworkGenerator('topologyFcn', ...)`, as do the JAR and
32 * native Python. The topology probe draws (see the constructor), so seed
33 * BEFORE it: through the constructor, or `set_seed` then `set_topology`. The only
34 * departure is a RESAMPLED draw (point 3), which MATLAB never makes; the
35 * first attempt is MATLAB's model. The DEFAULT flags differ too: MATLAB
36 * turns varying rates, multiserver queues, random CS nodes and multi-chain
37 * CS on, this port leaves them off, so set them to match a default MATLAB run.
38 *
39 * 2. LINKS ARE A ROUTING MATRIX, NOT `addLink`. The reference calls
40 * `model.addLink(a, b)` and then `setProbRouting`/`setRouting`; this port has
41 * no `addLink` -- `qn::Network::link(P)` derives the connection graph from
42 * the routing matrix itself (see `network_struct.h`, the RAND expansion).
43 * So a RANDOM-routed (node, class) pair is given its arcs in `P` as well as
44 * its strategy: the arcs are what make the pair connected, and `route_eff`
45 * then overwrites the probabilities with the uniform split RAND means. The
46 * two spellings describe the same model.
47 *
48 * 3. THE REJECTION TEST IS STRUCTURAL. The JAR resamples when `sn_refresh_visits`
49 * throws a message containing "no recurrent flow"; no such throw exists any
50 * more in any codebase, so that gate is dead code there. This port tests what
51 * the gate was meant to test: after the struct is materialised, EVERY CLOSED
52 * CHAIN must actually visit its own reference station. A closed chain
53 * stranded in a component that does not contain its reference station gets
54 * zero visits there, its visit vector is not normalisable, and the model is
55 * not a valid closed network -- such a draw is discarded and resampled, up to
56 * `max_generate_attempts()`.
57 *
58 * 4. `initializeStates` IS NOT A FIELD HERE. Its only effect in the JAR is to
59 * force `getStruct()` so the rejection test has something to test; this port
60 * materialises the struct unconditionally for exactly that reason. The C++
61 * model has no per-node `setState` to fill in either: an initial state is
62 * declared through `Network::set_state_prior`, over a state SPACE rather
63 * than one row, so there is nothing here to port the flag onto.
64 *
65 * ARITHMETIC. Every random quantity is drawn as a double (the reference draws
66 * are `nextDouble`/`nextInt`) and lifted into T, so the generator instantiates
67 * at exact arithmetic as well. The one transcendental step is the HyperExp
68 * moment fit, which is done in double and its three parameters lifted, the same
69 * bargain `sn_aggregate_chains` strikes for the same reason.
70 */
71
72#include <algorithm>
73#include <cctype>
74#include <cmath>
75#include <cstddef>
76#include <functional>
77#include <string>
78#include <vector>
79
84#include "line/util/error.h"
85#include "line/util/matrix.h"
86#include "line/gen/matlab_rng.h"
87
88namespace line {
89namespace gen {
90
91/** The two topology generators the reference ships, `randGraph` and `cyclicGraph`. */
92enum class TopologyKind { Rand, Cyclic };
93
94// ---------------------------------------------------------------------------
95// Topology generators (module-level in Python, static in the JAR)
96// ---------------------------------------------------------------------------
97
98/**
99 * `randSpanningTree(n)`: vertex i (i >= 1) is attached to a uniformly chosen
100 * earlier vertex, `randi(i)` in MATLAB's 1-based terms, and always a tree rooted at 0.
101 */
102inline Matrix<double> rand_spanning_tree(std::size_t num_vertices, MatlabRng& r) {
103 Matrix<double> tree(num_vertices, num_vertices, 0.0);
104 for (std::size_t i = 1; i < num_vertices; ++i)
105 tree(static_cast<std::size_t>(r.randi(1, int(i)) - 1), i) = 1.0;
106 return tree;
107}
108
109namespace detail {
110
111/** The DFS bookkeeping of `strongConnect`, Tarjan's low-link walk. */
112struct RandGraphState {
113 int global_start_time = 0;
114 std::vector<int> start_time, lowest_link, inv_start_time;
115 explicit RandGraphState(std::size_t n)
116 : start_time(n, -1), lowest_link(n, -1), inv_start_time(n, -1) {}
117};
118
119/**
120 * The repair half of `randGraph`: a DFS that, on leaving the root of a strongly
121 * connected component that is not the root of the whole graph, adds ONE back
122 * edge from a random descendant to a random ancestor. That is what turns the
123 * spanning tree into a strongly connected digraph without making it complete.
124 */
125inline void strong_connect(Matrix<double>& g, std::size_t v, RandGraphState& st,
126 MatlabRng& r) {
127 st.start_time[v] = st.global_start_time;
128 st.inv_start_time[st.start_time[v]] = static_cast<int>(v);
129 st.lowest_link[v] = st.start_time[v];
130 ++st.global_start_time;
131
132 std::vector<std::size_t> out;
133 for (std::size_t w = 0; w < g.cols(); ++w)
134 if (g(v, w) > 0.0) out.push_back(w);
135
136 for (std::size_t k = 0; k < out.size(); ++k) {
137 const std::size_t w = out[k];
138 if (st.start_time[w] == -1) {
139 strong_connect(g, w, st, r);
140 st.lowest_link[v] = std::min(st.lowest_link[v], st.lowest_link[w]);
141 } else {
142 st.lowest_link[v] = std::min(st.lowest_link[v], st.start_time[w]);
143 }
144 }
145
146 if (st.lowest_link[v] == st.start_time[v] && st.start_time[v] > 0) {
147 // MATLAB's randi([st, gst-1]) and randi([1, st-1]) over its 1-based start times.
148 const int descendant = r.randi(st.start_time[v] + 1, st.global_start_time) - 1;
149 const int ancestor = r.randi(1, st.start_time[v]) - 1;
150 g(static_cast<std::size_t>(st.inv_start_time[descendant]),
151 static_cast<std::size_t>(st.inv_start_time[ancestor])) = 1.0;
152 st.lowest_link[v] = ancestor;
153 }
154}
155
156} // namespace detail
157
158/**
159 * `randGraph(n)`: a random strongly connected digraph on n vertices, as a
160 * random spanning tree closed up by `strongConnect` and then relabelled by a
161 * random permutation (without which vertex 0 would always be the DFS root).
162 */
163inline Matrix<double> rand_graph(std::size_t num_vertices, MatlabRng& r) {
164 if (num_vertices == 0) throw InputError("randGraph: the number of vertices must be positive");
165 if (num_vertices == 1) {
166 Matrix<double> adj(1, 1, 0.0);
167 adj(0, 0) = 1.0;
168 return adj;
169 }
170 Matrix<double> g = rand_spanning_tree(num_vertices, r);
171 detail::RandGraphState st(num_vertices);
172 detail::strong_connect(g, 0, st, r);
173
174 // `reordernodes(g, randperm(n))`: new vertex k is old vertex perm[k].
175 const std::vector<std::size_t> perm = r.randperm(num_vertices);
176 std::vector<std::size_t> new_index(num_vertices);
177 for (std::size_t k = 0; k < num_vertices; ++k) new_index[perm[k]] = k;
178
179 Matrix<double> out(num_vertices, num_vertices, 0.0);
180 for (std::size_t i = 0; i < num_vertices; ++i)
181 for (std::size_t j = 0; j < num_vertices; ++j)
182 if (g(i, j) > 0.0) out(new_index[i], new_index[j]) = 1.0;
183 return out;
184}
185
186/** `cyclicGraph(n)`: the single cycle 0 -> 1 -> ... -> n-1 -> 0. */
187inline Matrix<double> cyclic_graph(std::size_t num_vertices) {
188 if (num_vertices == 0) throw InputError("cyclicGraph: the number of vertices must be positive");
189 Matrix<double> adj(num_vertices, num_vertices, 0.0);
190 for (std::size_t i = 0; i + 1 < num_vertices; ++i) adj(i, i + 1) = 1.0;
191 adj(num_vertices - 1, 0) = 1.0;
192 return adj;
193}
194
195// ---------------------------------------------------------------------------
196// The two fixed-sum samplers
197// ---------------------------------------------------------------------------
198
199/**
200 * `randfixedsumone(n)`: n probabilities summing to exactly 1.
201 *
202 * The reference's own recipe, quirks kept: a uniform point of the simplex from
203 * Stafford's `randfixedsum` (which draws even at n = 1), each entry rounded UP to
204 * a multiple of 1e-3 (so the rounded vector oversums), and the FIRST LARGEST
205 * entry absorbing the whole residual. Rounding to three digits is what keeps the
206 * emitted `model.json` readable; the absorption keeps the row stochastic after it.
207 */
208inline std::vector<double> randfixedsumone(std::size_t num_elems, MatlabRng& r) {
209 if (num_elems == 0) return std::vector<double>();
210
211 std::vector<double> values = r.randfixedsum(num_elems, 1.0, 0.0, 1.0);
212 for (std::size_t i = 0; i < num_elems; ++i)
213 values[i] = std::ceil(values[i] * 1000.0) / 1000.0;
214
215 std::size_t max_idx = 0;
216 for (std::size_t i = 1; i < num_elems; ++i)
217 if (values[i] > values[max_idx]) max_idx = i;
218 double current = 0.0;
219 for (std::size_t i = 0; i < num_elems; ++i) current += values[i];
220 values[max_idx] -= (current - 1.0);
221 return values;
222}
223
224/**
225 * `randintfixedsum(s, n)`: n STRICTLY POSITIVE integers summing to s.
226 *
227 * The reference's recursion: draw the first part uniformly from 1..s-n (which
228 * leaves at least one unit for each remaining part) and recurse on the rest.
229 * There is no shuffle here: `genCSMask` permutes the result with `randperm`.
230 */
231inline std::vector<int> randintfixedsum(int s, int n, MatlabRng& r) {
232 if (n <= 0) throw InputError("randintfixedsum: the number of parts must be positive");
233 if (s < n) throw InputError("randintfixedsum: the sum must be at least the number of parts");
234 if (n == 1) return std::vector<int>(1, s);
235 if (s == n) return std::vector<int>(static_cast<std::size_t>(n), 1);
236
237 const int first = r.randi(1, s - n);
238 std::vector<int> out = randintfixedsum(s - first, n - 1, r);
239 out.insert(out.begin(), first);
240 return out;
241}
242
243// ---------------------------------------------------------------------------
244// The generator
245// ---------------------------------------------------------------------------
246
247/**
248 * A random `qn::Network<T>` source, configured once and then drawn from.
249 *
250 * Every setter validates its argument against the same vocabulary the reference
251 * accepts, so a typo is an error at configuration time rather than a silently
252 * different model.
253 */
254template <class T>
256 public:
257 /**
258 * MATLAB's `rng(seed)` followed by `NetworkGenerator('topologyFcn', ...)`:
259 * the stream is seeded FIRST and the constructor then validates the topology
260 * on a two-vertex call, as MATLAB's `set.topologyFcn` does, so a `Random`
261 * topology costs the five draws of `randGraph(2)` and a `Cyclic` one none.
262 */
263 explicit NetworkGenerator(long long seed = 0, TopologyKind kind = TopologyKind::Rand)
264 : rng_(seed) {
265 set_topology(kind);
266 }
267
268 /** Reseed the single stream every draw comes from, as MATLAB `rng(seed)`. */
269 void set_seed(long long seed) { rng_.reseed(seed); }
270
271 /** The name given to the generated model. `nw` is the reference's. */
272 void set_model_name(const std::string& nm) { model_name_ = nm; }
273 const std::string& model_name() const { return model_name_; }
274
275 /** fcfs, ps, inf, lcfs, lcfspr, siro, sjf, ljf, sept, lept, or randomize. */
276 void set_sched_strat(const std::string& strat) {
277 if (!is_one_of(strat, sched_vocabulary()))
278 throw InputError("NetworkGenerator: scheduling strategy '" + strat +
279 "' does not exist or is not supported");
280 sched_strat_ = strat;
281 }
282 const std::string& sched_strat() const { return sched_strat_; }
283
284 /** Probabilities, Random, or randomize. */
285 void set_routing_strat(const std::string& strat) {
286 if (strat != "Probabilities" && strat != "Random" && strat != "randomize")
287 throw InputError("NetworkGenerator: routing strategy '" + strat +
288 "' does not exist or is not supported");
289 routing_strat_ = strat;
290 }
291 const std::string& routing_strat() const { return routing_strat_; }
292
293 /** Exp, Erlang, HyperExp, or randomize. */
294 void set_distribution(const std::string& d) {
295 if (!ieq(d, "Exp") && !ieq(d, "Erlang") && !ieq(d, "HyperExp") && !ieq(d, "randomize"))
296 throw InputError("NetworkGenerator: distribution '" + d +
297 "' does not exist or is not supported");
298 distribution_ = d;
299 }
300 const std::string& distribution() const { return distribution_; }
301
302 /** high, medium, low, or randomize: the population band of a closed class. */
303 void set_cclass_job_load(const std::string& load) {
304 if (!ieq(load, "high") && !ieq(load, "medium") && !ieq(load, "low") &&
305 !ieq(load, "randomize"))
306 throw InputError("NetworkGenerator: model load can only be high, medium, low or "
307 "randomize");
308 cclass_job_load_ = load;
309 }
310 const std::string& cclass_job_load() const { return cclass_job_load_; }
311
312 /** Service means spread over 2^-6 .. 2^6 instead of all being 1. */
313 void set_varying_service_rates(bool v) { varying_service_rates_ = v; }
314 bool varying_service_rates() const { return varying_service_rates_; }
315
316 /** Queues get 1..40 servers instead of exactly one. */
317 void set_multi_server_queues(bool v) { multi_server_queues_ = v; }
318 bool multi_server_queues() const { return multi_server_queues_; }
319
320 /** Each link gets a ClassSwitch node inserted on a coin flip. */
321 void set_random_cs_nodes(bool v) { random_cs_nodes_ = v; }
322 bool random_cs_nodes() const { return random_cs_nodes_; }
323
324 /**
325 * Classes are partitioned into random chains and switching is confined to a
326 * chain, instead of the default "all open classes switch among themselves
327 * and all closed classes among themselves".
328 */
329 void set_multi_chain_cs(bool v) { multi_chain_cs_ = v; }
330 bool multi_chain_cs() const { return multi_chain_cs_; }
331
332 /** Pick one of the two shipped topology generators; validated like `set_topology_fcn`. */
334 if (kind == TopologyKind::Cyclic)
335 set_topology_fcn([](std::size_t n, MatlabRng&) { return cyclic_graph(n); });
336 else
337 set_topology_fcn([](std::size_t n, MatlabRng& r) { return rand_graph(n, r); });
338 }
339
340 /**
341 * A custom topology: any function from a vertex count (and the generator's
342 * own stream, so a custom topology stays reproducible too) to a square 0/1
343 * adjacency matrix. Validated on a two-vertex call, as the reference does, and
344 * that call draws from the generator's own stream exactly as MATLAB's does.
345 */
346 void set_topology_fcn(const std::function<Matrix<double>(std::size_t, MatlabRng&)>& fcn) {
347 if (!fcn) throw InputError("NetworkGenerator: the topology function is empty");
348 const Matrix<double> adj = fcn(2, rng_);
349 if (adj.rows() != 2 || adj.cols() != 2)
350 throw InputError("NetworkGenerator: topologyFcn must take a positive integer and "
351 "return a square adjacency matrix of that order");
352 topology_fcn_ = fcn;
353 }
354
355 /** Resampling budget before an unsatisfiable configuration is reported. */
356 static std::size_t max_generate_attempts() { return 100; }
357
358 // -----------------------------------------------------------------------
359 // generate
360 // -----------------------------------------------------------------------
361
362 /**
363 * The full form. `num_delays < 0` means "decide for me", which is the
364 * reference's `[]`: one delay when there is a single queue, otherwise a
365 * coin flip between none and one. `num_cclass < 0` likewise draws 1..4,
366 * after the delays, which is MATLAB's order.
367 */
368 qn::Network<T> generate(int num_queues, int num_delays, int num_oclass, int num_cclass) {
369 if (num_delays < 0) num_delays = (num_queues > 1) ? rng_.randi(0, 1) : 1;
370 if (num_cclass < 0) num_cclass = rng_.randi(1, 4); // after numDelays, as in MATLAB
371 validate_args(num_queues, num_delays, num_oclass, num_cclass);
372
373 std::string last_reject;
374 for (std::size_t attempt = 0; attempt < max_generate_attempts(); ++attempt) {
375 try {
376 qn::Network<T> model(model_name_);
377 build(model, num_queues, num_delays, num_oclass, num_cclass);
378 const qn::NetworkStruct<T>& sn = model.get_struct();
379 const std::string why = why_invalid(sn);
380 if (!why.empty()) {
381 last_reject = why;
382 continue; // resample: the draw is not a valid closed network
383 }
384 return model;
385 } catch (const Error& e) {
386 last_reject = e.what();
387 continue;
388 }
389 }
390 throw InputError("NetworkGenerator: could not generate a model with recurrent closed-chain "
391 "routing in " + std::to_string(max_generate_attempts()) +
392 " attempts. The requested topology may not admit a strongly connected "
393 "closed chain. Last rejection: " + last_reject);
394 }
395
396 /** `generate(numQueues, numDelays, numOClass)`, closed-class count drawn 1..4. */
397 qn::Network<T> generate(int num_queues, int num_delays, int num_oclass) {
398 return generate(num_queues, num_delays, num_oclass, -1);
399 }
400 /** `generate(numQueues, numDelays)`, no open classes. */
401 qn::Network<T> generate(int num_queues, int num_delays) {
402 return generate(num_queues, num_delays, 0, -1);
403 }
404 /** `generate(numQueues)`, delays decided by the rule above. */
405 qn::Network<T> generate(int num_queues) {
406 return generate(num_queues, -1, 0, -1);
407 }
408 /** Everything drawn: 1..8 queues, 1..4 closed classes, no open classes. */
410 const int nq = rng_.randi(1, 8);
411 return generate(nq, -1, 0, -1);
412 }
413
414 private:
415 // -----------------------------------------------------------------------
416 // Configuration
417 // -----------------------------------------------------------------------
418
419 static const std::vector<std::string>& sched_vocabulary() {
420 static const std::vector<std::string> v = {"fcfs", "ps", "inf", "lcfs", "lcfspr", "siro",
421 "sjf", "ljf", "sept", "lept", "randomize"};
422 return v;
423 }
424
425 static bool ieq(const std::string& a, const std::string& b) {
426 if (a.size() != b.size()) return false;
427 for (std::size_t i = 0; i < a.size(); ++i)
428 if (std::tolower(static_cast<unsigned char>(a[i])) !=
429 std::tolower(static_cast<unsigned char>(b[i])))
430 return false;
431 return true;
432 }
433
434 static bool is_one_of(const std::string& s, const std::vector<std::string>& v) {
435 return std::find(v.begin(), v.end(), s) != v.end();
436 }
437
438 static void validate_args(int nq, int nd, int no, int nc) {
439 if (nq < 0 || nd < 0 || no < 0 || nc < 0)
440 throw InputError("NetworkGenerator: the station and class counts must be non-negative");
441 if (nq + nd <= 0 || no + nc <= 0)
442 throw InputError("NetworkGenerator: at least one station and one job class are "
443 "required");
444 }
445
446 // -----------------------------------------------------------------------
447 // The draws
448 // -----------------------------------------------------------------------
449
450 double choose_num_servers() {
451 return multi_server_queues_ ? double(rng_.randi(1, MAX_SERVERS)) : 1.0;
452 }
453
454 double choose_num_jobs() {
455 if (ieq(cclass_job_load_, "high")) return band(HIGH_LO, HIGH_HI);
456 if (ieq(cclass_job_load_, "medium")) return band(MED_LO, MED_HI);
457 if (ieq(cclass_job_load_, "low")) return band(LOW_LO, LOW_HI);
458 return double(rng_.randi(1, HIGH_HI)); // randomize: the whole 1..40 range
459 }
460
461 double band(int lo, int hi) { return double(rng_.randi(lo, hi)); }
462
463 lang::SchedStrategy choose_sched_strat() {
464 if (ieq(sched_strat_, "randomize"))
465 return rng_.randi(1, 2) == 1 ? lang::SchedStrategy::FCFS : lang::SchedStrategy::PS;
466 return sched_from_name(sched_strat_);
467 }
468
469 static lang::SchedStrategy sched_from_name(const std::string& s) {
470 if (s == "fcfs") return lang::SchedStrategy::FCFS;
471 if (s == "ps") return lang::SchedStrategy::PS;
472 if (s == "inf") return lang::SchedStrategy::INF;
473 if (s == "lcfs") return lang::SchedStrategy::LCFS;
474 if (s == "lcfspr") return lang::SchedStrategy::LCFSPR;
475 if (s == "siro") return lang::SchedStrategy::SIRO;
476 if (s == "sjf") return lang::SchedStrategy::SJF;
477 if (s == "ljf") return lang::SchedStrategy::LJF;
478 if (s == "sept") return lang::SchedStrategy::SEPT;
479 if (s == "lept") return lang::SchedStrategy::LEPT;
480 throw UnsupportedError(ErrorKind{"SchedStrategy"}, "NetworkGenerator: scheduling strategy '" + s + "' is not supported");
481 }
482
483 /** "Random" or "Probabilities"; `randomize` picks between them per class. */
484 std::string choose_routing_strat() {
485 if (ieq(routing_strat_, "randomize"))
486 return rng_.randi(1, 2) == 1 ? std::string("Random") : std::string("Probabilities");
487 return routing_strat_;
488 }
489
490 /**
491 * A service or arrival law. The mean is 1 unless varying rates are asked
492 * for, in which case it is a power of two in 2^-6 .. 2^6; the Erlang order
493 * and the HyperExp SCV are powers of two in 1 .. 64, which is the reference's
494 * way of covering both sides of the exponential in a few draws.
495 */
496 lang::Distrib<T> choose_distribution() {
497 int id;
498 if (ieq(distribution_, "Exp")) id = 1;
499 else if (ieq(distribution_, "Erlang")) id = 2;
500 else if (ieq(distribution_, "HyperExp")) id = 3;
501 else id = rng_.randi(1, 3);
502
503 const double mean =
504 varying_service_rates_ ? std::pow(2.0, double(rng_.randi(-6, 6))) : 1.0;
505
506 if (id == 1) return lang::Distrib<T>::exp_rate(num_traits<T>::from_double(1.0 / mean));
507 if (id == 2) {
508 const std::size_t k = std::size_t(std::pow(2.0, double(rng_.randi(0, 6))));
509 return lang::erlang_fit_mean_order(num_traits<T>::from_double(mean), k);
510 }
511 const double scv = std::pow(2.0, double(rng_.randi(0, 6)));
512 return fit_hyperexp(mean, scv);
513 }
514
515 /**
516 * `HyperExp.fitMeanAndSCV`, which is `map_hyperexp` at p = 0.99 read back as
517 * (p, mu1, mu2). The fit needs a square root of the moment discriminant, so
518 * at exact arithmetic it is done in double and the three parameters lifted:
519 * a two-moment fit carries no exactness claim, and refusing would leave the
520 * HyperExp arm with no exact-arithmetic path at all.
521 */
522 static lang::Distrib<T> fit_hyperexp(double mean, double scv) {
523 const lang::Distrib<double> dd = lang::hyperexp_fit_mean_scv<double>(mean, scv);
524 return lang::Distrib<T>::hyperexp(num_traits<T>::from_double(dd.params[0]),
525 num_traits<T>::from_double(dd.params[1]),
526 num_traits<T>::from_double(dd.params[2]));
527 }
528
529 // -----------------------------------------------------------------------
530 // Construction
531 // -----------------------------------------------------------------------
532
533 void build(qn::Network<T>& model, int num_queues, int num_delays, int num_oclass,
534 int num_cclass) {
535 stations_.clear();
536 source_ = 0;
537 sink_ = 0;
538
539 create_stations(model, num_queues, num_delays, num_oclass > 0);
540 create_classes(model, num_oclass, num_cclass);
541 set_service_processes(model);
542 define_topology(model, num_oclass > 0);
543 }
544
545 void create_stations(qn::Network<T>& model, int num_queues, int num_delays, bool has_oclass) {
546 for (int i = 0; i < num_queues; ++i) {
547 const lang::SchedStrategy sched = choose_sched_strat();
548 const std::size_t nd = model.add_queue("queue" + std::to_string(i + 1), sched);
549 // The draw is unconditional, as in the reference: an INF-scheduled
550 // queue ignores the count but must not change the stream.
551 const double servers = choose_num_servers();
552 model.set_number_of_servers(nd, servers);
553 stations_.push_back(nd);
554 }
555 for (int i = 0; i < num_delays; ++i)
556 stations_.push_back(model.add_delay("delay" + std::to_string(i + 1)));
557 if (has_oclass) {
558 source_ = model.add_source("source");
559 sink_ = model.add_sink("sink");
560 }
561 }
562
563 void create_classes(qn::Network<T>& model, int num_oclass, int num_cclass) {
564 classes_.clear();
565 for (int i = 0; i < num_oclass; ++i) {
566 const std::size_t r = model.add_open_class("OClass" + std::to_string(i + 1));
567 model.set_arrival(source_, r, choose_distribution());
568 classes_.push_back(r);
569 }
570 if (num_cclass > 0) {
571 // THE REFERENCE STATION IS NEVER THE SOURCE. MATLAB draws over
572 // `getNumberOfStations - (numOClass > 0)`, which excludes it; the
573 // JAR draws over `getStations()`, which includes it and can hand a
574 // closed class a Source reference station. MATLAB is ground truth.
575 const std::size_t ref = stations_[std::size_t(rng_.randi(1, int(stations_.size())) - 1)];
576 for (int i = 0; i < num_cclass; ++i)
577 classes_.push_back(model.add_closed_class("CClass" + std::to_string(i + 1),
578 choose_num_jobs(), ref));
579 }
580 }
581
582 void set_service_processes(qn::Network<T>& model) {
583 for (std::size_t c = 0; c < classes_.size(); ++c)
584 for (std::size_t s = 0; s < stations_.size(); ++s)
585 model.set_service(stations_[s], classes_[c], choose_distribution());
586 }
587
588 /**
589 * The topology, the ClassSwitch insertions and the routing, in the
590 * reference's order: the class-switch mask is drawn BEFORE the Source and
591 * Sink rows are grafted onto the adjacency matrix.
592 */
593 void define_topology(qn::Network<T>& model, bool has_oclass) {
594 Matrix<double> topo = topology_fcn_(stations_.size(), rng_);
595 if (topo.rows() != stations_.size() || topo.cols() != stations_.size())
596 throw InputError("NetworkGenerator: the topology function returned an adjacency "
597 "matrix of the wrong order");
598
599 const Matrix<T> mask = gen_cs_mask(model);
600
601 // Stations own adjacency rows 0..S-1; the Source and Sink take the two
602 // rows appended here, matching the node order the builder created.
603 std::vector<std::size_t> row_node = stations_;
604 if (has_oclass) {
605 const std::size_t S = stations_.size();
606 Matrix<double> grown(S + 2, S + 2, 0.0);
607 for (std::size_t i = 0; i < S; ++i)
608 for (std::size_t j = 0; j < S; ++j) grown(i, j) = topo(i, j);
609 grown(S, std::size_t(rng_.randi(1, int(S)) - 1)) = 1.0;
610 grown(std::size_t(rng_.randi(1, int(S)) - 1), S + 1) = 1.0;
611 topo = grown;
612 row_node.push_back(source_);
613 row_node.push_back(sink_);
614 }
615
616 // The Sink is not a station and routes nothing, so it owns no row here:
617 // `apply_sink_closure` writes the Sink -> Source arc during the refresh.
618 const std::size_t nrows = has_oclass ? stations_.size() + 1 : stations_.size();
619
620 qn::RoutingMatrix<T> P;
621 for (std::size_t i = 0; i < nrows; ++i) {
622 std::vector<std::size_t> dest; // 1-based node indices, ascending
623 for (std::size_t j = 0; j < topo.cols(); ++j)
624 if (topo(i, j) > 0.0) dest.push_back(row_node[j]);
625 std::sort(dest.begin(), dest.end());
626 const std::vector<std::size_t> outgoing =
627 add_outgoing_links(model, P, row_node[i], dest, mask);
628 set_routing_strategies(model, P, row_node[i], outgoing);
629 }
630 model.link(P);
631 }
632
633 /**
634 * `genCSMask`: which class may switch into which at an inserted ClassSwitch.
635 *
636 * The default confines switching to the open block and the closed block,
637 * which is what keeps a closed chain closed. `multi_chain_cs` refines that
638 * into randomly sized chains inside each block, so a model can carry several
639 * independent chains of the same kind.
640 */
641 Matrix<T> gen_cs_mask(qn::Network<T>& model) {
642 const qn::NetworkStruct<T>& sn = model.raw_struct();
643 std::size_t num_open = 0, num_closed = 0;
644 for (std::size_t r = 0; r < sn.classes.size(); ++r) {
645 if (sn.classes[r].type == lang::JobClassType::OPEN) ++num_open;
646 else ++num_closed;
647 }
648 const std::size_t K = num_open + num_closed;
649 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
650 Matrix<T> mask(K, K, zero);
651
652 if (!multi_chain_cs_) {
653 for (std::size_t i = 0; i < num_open; ++i)
654 for (std::size_t j = 0; j < num_open; ++j) mask(i, j) = one;
655 for (std::size_t i = num_open; i < K; ++i)
656 for (std::size_t j = num_open; j < K; ++j) mask(i, j) = one;
657 return mask;
658 }
659
660 std::vector<int> sizes;
661 if (num_open > 0) {
662 const std::vector<int> s = assign_chains(static_cast<int>(num_open));
663 sizes.insert(sizes.end(), s.begin(), s.end());
664 }
665 if (num_closed > 0) {
666 const std::vector<int> s = assign_chains(static_cast<int>(num_closed));
667 sizes.insert(sizes.end(), s.begin(), s.end());
668 }
669 std::size_t start = 0;
670 for (std::size_t c = 0; c < sizes.size(); ++c) {
671 const std::size_t end = start + std::size_t(sizes[c]);
672 for (std::size_t i = start; i < end; ++i)
673 for (std::size_t j = start; j < end; ++j) mask(i, j) = one;
674 start = end;
675 }
676 return mask;
677 }
678
679 /** `assignChains`: `randi(n)` chain sizes from `randintfixedsum`, permuted by `randperm`. */
680 std::vector<int> assign_chains(int num_classes) {
681 const int num_chains = rng_.randi(1, num_classes);
682 const std::vector<int> sizes = randintfixedsum(num_classes, num_chains, rng_);
683 const std::vector<std::size_t> perm = rng_.randperm(std::size_t(num_chains));
684 std::vector<int> out(sizes.size());
685 for (std::size_t i = 0; i < perm.size(); ++i) out[i] = sizes[perm[i]];
686 return out;
687 }
688
689 /** A ClassSwitch matrix: each row is a `randfixedsumone` over its mask. */
690 Matrix<T> rand_class_switch_matrix(const Matrix<T>& mask) {
691 const std::size_t K = mask.rows();
692 const T zero = num_traits<T>::from_int(0);
693 Matrix<T> C(K, K, zero);
694 for (std::size_t i = 0; i < K; ++i) {
695 std::vector<std::size_t> valid;
696 for (std::size_t j = 0; j < K; ++j)
697 if (num_traits<T>::to_double(mask(i, j)) > 0.0) valid.push_back(j);
698 if (valid.empty()) {
699 C(i, i) = num_traits<T>::from_int(1); // a class no mask reaches keeps itself
700 continue;
701 }
702 const std::vector<double> probs = randfixedsumone(valid.size(), rng_);
703 for (std::size_t k = 0; k < valid.size(); ++k)
704 C(i, valid[k]) = num_traits<T>::from_double(probs[k]);
705 }
706 return C;
707 }
708
709 /**
710 * Walk the row's destinations, optionally interposing a ClassSwitch node.
711 *
712 * Returns the nodes the row actually routes INTO, which is the destination
713 * itself or the interposed node; the second leg cs -> dest is deterministic
714 * in the arriving class and is written here.
715 */
716 std::vector<std::size_t> add_outgoing_links(qn::Network<T>& model, qn::RoutingMatrix<T>& P,
717 std::size_t from,
718 const std::vector<std::size_t>& dest,
719 const Matrix<T>& mask) {
720 const qn::NetworkStruct<T>& sn = model.raw_struct();
721 const std::size_t K = sn.classes.size();
722 const T one = num_traits<T>::from_int(1);
723 std::vector<std::size_t> outgoing;
724 for (std::size_t d = 0; d < dest.size(); ++d) {
725 const std::size_t to = dest[d];
726 const bool from_source = sn.nodes[from - 1].nodetype == lang::NodeType::Source;
727 const bool to_sink = sn.nodes[to - 1].nodetype == lang::NodeType::Sink;
728 if (random_cs_nodes_ && rng_.randi(0, 1) == 1 && !from_source && !to_sink) {
729 const Matrix<T> C = rand_class_switch_matrix(mask);
730 const std::size_t cs = model.add_class_switch(
731 "cs_" + sn.nodes[from - 1].name + "_" + sn.nodes[to - 1].name, C);
732 outgoing.push_back(cs);
733 for (std::size_t r = 1; r <= K; ++r) P.set(r, r, cs, to, one);
734 } else {
735 outgoing.push_back(to);
736 }
737 }
738 return outgoing;
739 }
740
741 /**
742 * The per-class routing of one row.
743 *
744 * A Source routes each OPEN class into every outgoing node with probability
745 * one (its row has a single destination by construction, so the row is
746 * stochastic). Every other row draws a strategy per class: `Probabilities`
747 * spreads a `randfixedsumone` over the destinations, `Random` declares RAND
748 * and lets `route_eff` spread uniformly over the node's connections.
749 *
750 * A CLOSED CLASS IS NEVER ROUTED INTO THE SINK, under either strategy: the
751 * Sink would absorb a job the population has to conserve. The reference
752 * zeroes the trailing Sink entry in the Probabilities arm and relies on
753 * `getRoutingMatrix` to drop it in the Random arm; here it is dropped in
754 * both, because the raw routing matrix is also what the model DOCUMENT
755 * carries and what the reducibility test reads.
756 */
757 void set_routing_strategies(qn::Network<T>& model, qn::RoutingMatrix<T>& P, std::size_t from,
758 const std::vector<std::size_t>& outgoing) {
759 const qn::NetworkStruct<T>& sn = model.raw_struct();
760 const std::size_t K = sn.classes.size();
761 const T one = num_traits<T>::from_int(1);
762
763 if (sn.nodes[from - 1].nodetype == lang::NodeType::Source) {
764 for (std::size_t r = 1; r <= K; ++r) {
765 if (sn.classes[r - 1].type != lang::JobClassType::OPEN) continue;
766 for (std::size_t k = 0; k < outgoing.size(); ++k) P.set(r, r, from, outgoing[k], one);
767 }
768 return;
769 }
770
771 for (std::size_t r = 1; r <= K; ++r) {
772 const bool closed = sn.classes[r - 1].type != lang::JobClassType::OPEN;
773 std::vector<std::size_t> allowed;
774 for (std::size_t k = 0; k < outgoing.size(); ++k) {
775 if (closed && sn.nodes[outgoing[k] - 1].nodetype == lang::NodeType::Sink) continue;
776 allowed.push_back(outgoing[k]);
777 }
778 const std::string strat = choose_routing_strat();
779 if (strat == "Random") {
780 model.set_routing(from, r, lang::RoutingStrategy::RAND);
781 if (allowed.empty()) continue;
782 const double share = 1.0 / double(allowed.size());
783 for (std::size_t k = 0; k < allowed.size(); ++k)
784 P.set(r, r, from, allowed[k], num_traits<T>::from_double(share));
785 } else {
786 model.set_routing(from, r, lang::RoutingStrategy::PROB);
787 if (allowed.empty()) continue;
788 const std::vector<double> probs = randfixedsumone(allowed.size(), rng_);
789 for (std::size_t k = 0; k < allowed.size(); ++k)
790 P.set(r, r, from, allowed[k], num_traits<T>::from_double(probs[k]));
791 }
792 }
793 }
794
795 // -----------------------------------------------------------------------
796 // The rejection test
797 // -----------------------------------------------------------------------
798
799 /**
800 * Empty when the draw is a valid network, otherwise why it is not.
801 *
802 * A closed chain whose reference station collects no visits has no
803 * normalisable visit vector: the jobs are declared at a station the chain
804 * never reaches, which is exactly the stranded-chain draw the reference
805 * resamples. Every other structural defect the builder can produce throws
806 * out of `get_struct()` and is caught by the caller.
807 */
808 static std::string why_invalid(const qn::NetworkStruct<T>& sn) {
809 for (std::size_t c = 0; c < sn.nchains; ++c) {
810 if (c >= sn.inchain.size() || sn.inchain[c].empty()) continue;
811 const std::vector<std::size_t>& ic = sn.inchain[c];
812 bool closed = true;
813 for (std::size_t x = 0; x < ic.size(); ++x)
814 if (!std::isfinite(num_traits<T>::to_double(sn.classes[ic[x] - 1].population)))
815 closed = false;
816 if (!closed) continue;
817 const std::size_t rs = sn.classes[ic[0] - 1].refstat;
818 if (rs == 0 || rs > sn.station_to_node.size()) continue;
819 const std::size_t node = sn.station_to_node[rs - 1];
820 if (c >= sn.nodevisits.size() || node == 0 || node > sn.nodevisits[c].rows()) continue;
821 double total = 0.0;
822 for (std::size_t x = 0; x < ic.size(); ++x)
823 total += num_traits<T>::to_double(sn.nodevisits[c](node - 1, ic[x] - 1));
824 if (!(total > lang::GlobalConstants::FineTol))
825 return "closed chain " + std::to_string(c + 1) +
826 " never visits its reference station '" + sn.nodes[node - 1].name + "'";
827 }
828 return std::string();
829 }
830
831 // -----------------------------------------------------------------------
832 // State
833 // -----------------------------------------------------------------------
834
835 static constexpr int MAX_SERVERS = 40;
836 static constexpr int HIGH_LO = 31, HIGH_HI = 40;
837 static constexpr int MED_LO = 11, MED_HI = 20;
838 static constexpr int LOW_LO = 1, LOW_HI = 5;
839
840 MatlabRng rng_;
841 std::string model_name_ = "nw";
842 std::string sched_strat_ = "randomize";
843 std::string routing_strat_ = "randomize";
844 std::string distribution_ = "randomize";
845 std::string cclass_job_load_ = "randomize";
846 bool varying_service_rates_ = false;
847 bool multi_server_queues_ = false;
848 bool random_cs_nodes_ = false;
849 bool multi_chain_cs_ = false;
850 std::function<Matrix<double>(std::size_t, MatlabRng&)> topology_fcn_; // set by the constructor
851
852 std::vector<std::size_t> stations_; // node indices of the queues and delays
853 std::vector<std::size_t> classes_; // class indices, open first
854 std::size_t source_ = 0, sink_ = 0;
855};
856
857} // namespace gen
858} // namespace line
859
860#endif // LINE_GEN_NETWORK_GENERATOR_H
Base error for the multiprecision C++ port.
Definition error.h:73
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:119
One MATLAB global random stream, as left by rng(seed).
Definition matlab_rng.h:39
std::vector< std::size_t > randperm(std::size_t n)
MATLAB randperm(n), returned 0-based.
Definition matlab_rng.h:53
int randi(int lo, int hi)
MATLAB randi([lo, hi]).
Definition matlab_rng.h:50
std::vector< double > randfixedsum(std::size_t n, double s, double a, double b)
One column of randfixedsum(n,1,s,a,b) (matlab/lib/thirdparty/randfixedsum.m): draws rand(n-1,...
Definition matlab_rng.h:67
qn::Network< T > generate(int num_queues, int num_delays)
generate(numQueues, numDelays), no open classes.
static std::size_t max_generate_attempts()
Resampling budget before an unsatisfiable configuration is reported.
void set_cclass_job_load(const std::string &load)
high, medium, low, or randomize: the population band of a closed class.
qn::Network< T > generate(int num_queues, int num_delays, int num_oclass)
generate(numQueues, numDelays, numOClass), closed-class count drawn 1..4.
void set_model_name(const std::string &nm)
The name given to the generated model.
const std::string & cclass_job_load() const
void set_seed(long long seed)
Reseed the single stream every draw comes from, as MATLAB rng(seed).
void set_topology(TopologyKind kind)
Pick one of the two shipped topology generators; validated like set_topology_fcn.
void set_distribution(const std::string &d)
Exp, Erlang, HyperExp, or randomize.
void set_topology_fcn(const std::function< Matrix< double >(std::size_t, MatlabRng &)> &fcn)
A custom topology: any function from a vertex count (and the generator's own stream,...
qn::Network< T > generate(int num_queues)
generate(numQueues), delays decided by the rule above.
qn::Network< T > generate()
Everything drawn: 1..8 queues, 1..4 closed classes, no open classes.
const std::string & distribution() const
void set_multi_chain_cs(bool v)
Classes are partitioned into random chains and switching is confined to a chain, instead of the defau...
void set_sched_strat(const std::string &strat)
fcfs, ps, inf, lcfs, lcfspr, siro, sjf, ljf, sept, lept, or randomize.
void set_random_cs_nodes(bool v)
Each link gets a ClassSwitch node inserted on a coin flip.
const std::string & routing_strat() const
const std::string & sched_strat() const
const std::string & model_name() const
qn::Network< T > generate(int num_queues, int num_delays, int num_oclass, int num_cclass)
The full form.
void set_routing_strat(const std::string &strat)
Probabilities, Random, or randomize.
void set_varying_service_rates(bool v)
Service means spread over 2^-6 .
void set_multi_server_queues(bool v)
Queues get 1..40 servers instead of exactly one.
NetworkGenerator(long long seed=0, TopologyKind kind=TopologyKind::Rand)
MATLAB's rng(seed) followed by NetworkGenerator('topologyFcn', ...): the stream is seeded FIRST and t...
A network plus its refreshed NetworkStruct.
A queueing network under construction.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
The moment fitters the reference distributions carry as STATIC FACTORIES: Erlang.fitMeanAndOrder,...
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
MATLAB-faithful random draws for the model generators: the C++ twin of the JAR jline....
Dense matrix and non-owning view.
Matrix< double > rand_graph(std::size_t num_vertices, MatlabRng &r)
randGraph(n): a random strongly connected digraph on n vertices, as a random spanning tree closed up ...
std::vector< int > randintfixedsum(int s, int n, MatlabRng &r)
randintfixedsum(s, n): n STRICTLY POSITIVE integers summing to s.
std::vector< double > randfixedsumone(std::size_t num_elems, MatlabRng &r)
randfixedsumone(n): n probabilities summing to exactly 1.
TopologyKind
The two topology generators the reference ships, randGraph and cyclicGraph.
Matrix< double > cyclic_graph(std::size_t num_vertices)
cyclicGraph(n): the single cycle 0 -> 1 -> ... -> n-1 -> 0.
Matrix< double > rand_spanning_tree(std::size_t num_vertices, MatlabRng &r)
randSpanningTree(n): vertex i (i >= 1) is attached to a uniformly chosen earlier vertex,...
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
Distrib< T > erlang_fit_mean_order(const T &mean, std::size_t k)
Erlang.fitMeanAndOrder(MEAN, k): k phases, each of rate k / MEAN.
Distrib< T > hyperexp_fit_mean_scv(const T &mean, const T &scv)
HyperExp.fitMeanAndSCV(MEAN, SCV), which is map_hyperexp at p = 0.99 read back as (p,...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
The Network constructor API: Queue, Delay, Source, Sink, Router, ClassSwitch, Cache,...
A queueing network and its refreshed NetworkStruct.
static Distrib exp_rate(const T &r)
Definition lang_types.h:955
static Distrib hyperexp(const T &p, const T &lambda1, const T &lambda2)
HyperExp(p, lambda1, lambda2): phase i chosen with probability p_i.
static constexpr double FineTol
Definition lang_types.h:765