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