LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
layered_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_LAYERED_NETWORK_GENERATOR_H
6#define LINE_GEN_LAYERED_NETWORK_GENERATOR_H
7
8/**
9 * @file
10 * @ingroup line_gen
11 * Random layered-queueing-network generation: the C++ twin of MATLAB
12 * `@LayeredNetworkGenerator`, the JAR `jline.gen.LayeredNetworkGenerator` and
13 * the native Python `line_solver.gen.layered_network_generator`.
14 *
15 * WHAT IT PRODUCES. A layered model of `numClients` reference tasks calling
16 * down through `numLevels` layers of `numTasks` server tasks, hosted on
17 * `numProcessors` processors. Every client is a REF task on its own
18 * infinite-server processor with a think-time activity; every server task has
19 * one entry and one activity that replies to it; the layers are wired top-down
20 * so a task of level l is called by a task of level l-1, and the tasks are
21 * spread over the processors in contiguous blocks.
22 *
23 * THIS ONE IS SAMPLE-PATH IDENTICAL TO THE JAR, unlike its flat twin
24 * `NetworkGenerator`. Every draw the reference makes goes through the ONE
25 * `java.util.Random` that `setSeed` replaces -- there is no `randGraph` and no
26 * `Collections.shuffle` on a separate source here -- and `rng::JavaRandom` is a
27 * bit-exact reproduction of that generator. The draw ORDER is preserved too:
28 * the builder needs a processor before the task that sits on it, and a task
29 * before its entry, where the reference creates the objects first and wires
30 * them afterwards, so the PARAMETERS are drawn in the reference's order into
31 * vectors and the builder calls are issued from those. Nothing reorders a draw.
32 *
33 * RANGES. Each `*_range` is a closed interval, sampled as the reference samples
34 * it: an INTEGER range rounds its bounds inwards (`ceil` of the lower, `floor`
35 * of the upper) and draws uniformly over the integers between, a REAL range
36 * draws `lo + (hi - lo) * U`. The defaults are all `{1, 1}` with both infinity
37 * probabilities zero, which generates the deterministic skeleton -- one job per
38 * client, unit think times, unit demands, single-server tasks and processors.
39 *
40 * MEANS BECOME DISTRIBUTIONS the way `setHostDemand` does: a mean at or below
41 * `FineTol` is an `Immediate`, anything else an `Exp` of that mean.
42 */
43
44#include <algorithm>
45#include <cmath>
46#include <cstddef>
47#include <limits>
48#include <string>
49#include <vector>
50
53#include "line/util/error.h"
54#include "line/util/rng_ssj.h"
55
56namespace line {
57namespace gen {
58
59/** A closed interval the generator samples from. */
60struct Range {
61 double lo = 1.0;
62 double hi = 1.0;
63 Range() {}
64 Range(double a, double b) : lo(a), hi(b) {}
65};
66
67/**
68 * A random `lqn::LqnBuilder<T>` source, configured once and then drawn from.
69 *
70 * `generate` returns the BUILDER rather than the finalised struct: a caller
71 * that wants the model `SolverLN` consumes calls `build()` on it, and one that
72 wants the `.lqnx` document calls `lqn::write_lqnx(g.model(), ...)` or
73 * `lqn::lqnx_to_string(g.model(), ...)`. Handing back the struct alone would
74 * lose the raw model the writer needs.
75 */
76template <class T>
78 public:
80 explicit LayeredNetworkGenerator(long long seed) : rng_(seed) {}
81
82 /** Reseed the single stream every draw comes from. */
83 void set_seed(long long seed) { rng_.set_seed(seed); }
84
85 /** The `name` of the generated model. `lnw` is the reference's. */
86 void set_model_name(const std::string& nm) { model_name_ = nm; }
87 const std::string& model_name() const { return model_name_; }
88
89 /** Reference-task populations. Strictly positive: a client with no job runs nothing. */
90 void set_population_range(const Range& r) {
91 population_range_ = check_range(r, "Population range", true);
92 }
93 /** Client think times. Zero is allowed and means an Immediate think. */
94 void set_think_time_range(const Range& r) {
95 think_time_range_ = check_range(r, "Think time range", false);
96 }
97 /** Probability that a server task is infinite-server rather than FCFS. */
98 void set_task_inf_probability(double p) {
99 task_inf_probability_ = check_probability(p, "Task infinite probability");
100 }
101 /** Probability that a processor is infinite-server rather than PS. */
103 proc_inf_probability_ = check_probability(p, "Processor infinite probability");
104 }
105 /** Task multiplicities, for the tasks that are not infinite-server. */
107 task_multi_range_ = check_range(r, "Task multiplicity range", true);
108 }
109 /** Processor multiplicities, for the processors that are not infinite-server. */
111 proc_multi_range_ = check_range(r, "Processor multiplicity range", true);
112 }
113 /** Activity host demands. Zero is allowed and means an Immediate activity. */
115 host_demand_range_ = check_range(r, "Host demand range", false);
116 }
117 /** Mean number of synchronous calls on a call arc. Strictly positive. */
119 synch_call_range_ = check_range(r, "Synchronous call range", true);
120 }
121
122 const Range& population_range() const { return population_range_; }
123 const Range& think_time_range() const { return think_time_range_; }
124 double task_inf_probability() const { return task_inf_probability_; }
125 double proc_inf_probability() const { return proc_inf_probability_; }
126 const Range& task_multi_range() const { return task_multi_range_; }
127 const Range& proc_multi_range() const { return proc_multi_range_; }
128 const Range& host_demand_range() const { return host_demand_range_; }
129 const Range& synch_call_range() const { return synch_call_range_; }
130
131 /** How many tasks landed on each level, after the last `generate`. */
132 const std::vector<int>& tasks_per_level() const { return tasks_per_level_; }
133 /** How many tasks landed on each processor, after the last `generate`. */
134 const std::vector<int>& tasks_per_processor() const { return tasks_per_processor_; }
135
136 // -----------------------------------------------------------------------
137 // generate
138 // -----------------------------------------------------------------------
139
140 lqn::LqnBuilder<T> generate(int num_clients, int num_levels, int num_tasks,
141 int num_processors) {
142 validate_args(num_clients, num_levels, num_tasks, num_processors);
143
145 create_clients(b, num_clients);
146
147 // THE PARAMETERS ARE DRAWN HERE, in the reference's order, and the
148 // builder calls are issued below: `LqnBuilder::task` names its processor
149 // at creation, while the reference assigns processors only after every
150 // task and processor exists. Drawing first is what keeps the stream
151 // identical to the reference's despite the different construction order.
152 std::vector<TaskSpec> tspec;
153 tspec.reserve(static_cast<std::size_t>(num_tasks));
154 for (int t = 0; t < num_tasks; ++t) {
155 TaskSpec s;
156 if (choose_boolean(task_inf_probability_)) {
157 s.mult = std::numeric_limits<double>::infinity();
158 s.sched = lang::SchedStrategy::INF;
159 } else {
160 s.mult = double(sample_integer(task_multi_range_));
162 }
163 s.hostdem = as_distribution(sample_real(host_demand_range_));
164 tspec.push_back(s);
165 }
166
167 std::vector<TaskSpec> pspec;
168 pspec.reserve(static_cast<std::size_t>(num_processors));
169 for (int p = 0; p < num_processors; ++p) {
170 TaskSpec s;
171 if (choose_boolean(proc_inf_probability_)) {
172 s.mult = std::numeric_limits<double>::infinity();
173 s.sched = lang::SchedStrategy::INF;
174 } else {
175 s.mult = double(sample_integer(proc_multi_range_));
176 s.sched = lang::SchedStrategy::PS;
177 }
178 pspec.push_back(s);
179 }
180
181 tasks_per_level_ = make_integer_vector(num_levels, num_tasks);
182 tasks_per_processor_ = make_integer_vector(num_processors, num_tasks);
183
184 for (int p = 0; p < num_processors; ++p)
185 b.processor(proc_name(p), pspec[std::size_t(p)].mult, pspec[std::size_t(p)].sched);
186
187 // The tasks take the processors in contiguous blocks, which is exactly
188 // what `connectTasksToProcessors` does after the fact; it makes no draw,
189 // so folding it into the creation loop changes nothing but the order of
190 // two assignments.
191 std::vector<std::string> host(static_cast<std::size_t>(num_tasks));
192 {
193 int filled = 0;
194 for (int p = 0; p < num_processors; ++p)
195 for (int k = 0; k < tasks_per_processor_[std::size_t(p)]; ++k)
196 host[std::size_t(filled++)] = proc_name(p);
197 }
198 for (int t = 0; t < num_tasks; ++t) {
199 const TaskSpec& s = tspec[std::size_t(t)];
200 b.task(task_name(t), s.mult, s.sched, host[std::size_t(t)]);
201 b.entry(entry_name(t), task_name(t));
202 b.activity(act_name(t), s.hostdem, task_name(t));
203 b.bound_to(act_name(t), entry_name(t));
204 b.replies_to(act_name(t), entry_name(t));
205 }
206
207 connect_clients_to_tasks(b, num_clients);
208 connect_tasks_to_tasks(b, num_levels);
209 return b;
210 }
211
212 private:
213 struct TaskSpec {
214 double mult = 1.0;
216 lang::Distrib<T> hostdem;
217 };
218
219 // -----------------------------------------------------------------------
220 // Names
221 // -----------------------------------------------------------------------
222
223 static std::string cproc_name(int c) { return "c_processor_" + std::to_string(c + 1); }
224 static std::string ctask_name(int c) { return "c_task_" + std::to_string(c + 1); }
225 static std::string centry_name(int c) { return "c_entry_" + std::to_string(c + 1); }
226 static std::string cact_name(int c) { return "c_activity_" + std::to_string(c + 1); }
227 static std::string proc_name(int p) { return "processor_" + std::to_string(p + 1); }
228 static std::string task_name(int t) { return "task_" + std::to_string(t + 1); }
229 static std::string entry_name(int t) { return "entry_" + std::to_string(t + 1); }
230 static std::string act_name(int t) { return "activity_" + std::to_string(t + 1); }
231
232 // -----------------------------------------------------------------------
233 // Validation
234 // -----------------------------------------------------------------------
235
236 static Range check_range(const Range& r, const std::string& name, bool strictly_positive) {
237 const bool lower_ok = strictly_positive ? r.lo > 0.0 : r.lo >= 0.0;
238 if (!(lower_ok && r.lo <= r.hi))
239 throw InputError("LayeredNetworkGenerator: " + name + " is not valid");
240 return r;
241 }
242
243 static double check_probability(double v, const std::string& name) {
244 if (!(v >= 0.0 && v <= 1.0))
245 throw InputError("LayeredNetworkGenerator: " + name + " is not valid");
246 return v;
247 }
248
249 static void validate_args(int clients, int levels, int tasks, int procs) {
250 if (clients < 1) throw InputError("LayeredNetworkGenerator: the number of clients is less "
251 "than one");
252 if (levels < 1) throw InputError("LayeredNetworkGenerator: the number of levels is less "
253 "than one");
254 if (tasks < 1) throw InputError("LayeredNetworkGenerator: the number of tasks is less than "
255 "one");
256 if (procs < 1) throw InputError("LayeredNetworkGenerator: the number of processors is less "
257 "than one");
258 if (levels > tasks)
259 throw InputError("LayeredNetworkGenerator: the number of levels is greater than that "
260 "of tasks");
261 if (procs > tasks)
262 throw InputError("LayeredNetworkGenerator: the number of processors is greater than "
263 "that of tasks");
264 }
265
266 // -----------------------------------------------------------------------
267 // Construction
268 // -----------------------------------------------------------------------
269
270 void create_clients(lqn::LqnBuilder<T>& b, int num_clients) {
271 for (int c = 0; c < num_clients; ++c) {
272 const int population = sample_integer(population_range_);
273 const double think = sample_real(think_time_range_);
274 b.processor(cproc_name(c), std::numeric_limits<double>::infinity(),
276 b.task(ctask_name(c), double(population), lang::SchedStrategy::REF, cproc_name(c));
277 b.entry(centry_name(c), ctask_name(c));
278 // THE THINK TIME IS THE CLIENT ACTIVITY'S HOST DEMAND, not
279 // `Task.setThinkTime`. That is what the reference builds, and the
280 // two are not the same model: a think time is served by nothing,
281 // while this demand is served by the client's own INF processor.
282 b.activity(cact_name(c), as_distribution(think), ctask_name(c));
283 b.bound_to(cact_name(c), centry_name(c));
284 }
285 }
286
287 /**
288 * Every first-level task is called by some client, and every client calls
289 * some first-level task. The first loop guarantees the former, the second
290 * sweeps up the clients the draws happened to miss.
291 */
292 void connect_clients_to_tasks(lqn::LqnBuilder<T>& b, int num_clients) {
293 std::vector<bool> connected(static_cast<std::size_t>(num_clients), false);
294 for (int t = 0; t < tasks_per_level_[0]; ++t) {
295 const double calls = sample_real(synch_call_range_);
296 const int c = sample_integer(Range(1.0, double(num_clients))) - 1;
297 b.sync_call(cact_name(c), entry_name(t), num_traits<T>::from_double(calls));
298 connected[std::size_t(c)] = true;
299 }
300 for (int c = 0; c < num_clients; ++c) {
301 if (connected[std::size_t(c)]) continue;
302 const double calls = sample_real(synch_call_range_);
303 const int t = sample_integer(Range(1.0, double(tasks_per_level_[0]))) - 1;
304 b.sync_call(cact_name(c), entry_name(t), num_traits<T>::from_double(calls));
305 connected[std::size_t(c)] = true;
306 }
307 }
308
309 /** Each task of level l is called by one task drawn from level l-1. */
310 void connect_tasks_to_tasks(lqn::LqnBuilder<T>& b, int num_levels) {
311 int seen = tasks_per_level_[0];
312 for (int l = 1; l < num_levels; ++l) {
313 for (int t2 = seen; t2 < seen + tasks_per_level_[std::size_t(l)]; ++t2) {
314 const double calls = sample_real(synch_call_range_);
315 const int t1 =
316 sample_integer(Range(double(seen - tasks_per_level_[std::size_t(l - 1)] + 1),
317 double(seen))) - 1;
318 b.sync_call(act_name(t1), entry_name(t2), num_traits<T>::from_double(calls));
319 }
320 seen += tasks_per_level_[std::size_t(l)];
321 }
322 }
323
324 // -----------------------------------------------------------------------
325 // The draws
326 // -----------------------------------------------------------------------
327
328 /** `randi([ceil(lo), floor(hi)])`: the bounds are rounded INWARDS. */
329 int sample_integer(const Range& r) {
330 const int lower = static_cast<int>(std::ceil(r.lo));
331 const int upper = static_cast<int>(std::floor(r.hi));
332 if (upper < lower)
333 throw InputError("LayeredNetworkGenerator: an integer range rounds inwards to an "
334 "empty interval");
335 return lower + rng_.next_int(upper - lower + 1);
336 }
337
338 double sample_real(const Range& r) { return r.lo + (r.hi - r.lo) * rng_.next_double(); }
339
340 bool choose_boolean(double probability) { return rng_.next_double() < probability; }
341
342 /**
343 * `length` strictly positive integers summing to `sum`: one each, then the
344 * surplus handed out one unit at a time to a uniformly drawn element. Not
345 * the same law as `randintfixedsum` (this one is multinomial about the mean,
346 * that one is uniform over the compositions), and the reference uses each
347 * where it uses it.
348 */
349 std::vector<int> make_integer_vector(int length, int sum) {
350 if (length < 1) throw InputError("LayeredNetworkGenerator: makeIntegerVector needs a "
351 "positive length");
352 if (sum < length)
353 throw InputError("LayeredNetworkGenerator: makeIntegerVector cannot reach a sum below "
354 "its length");
355 std::vector<int> v(static_cast<std::size_t>(length), 1);
356 for (int s = 0; s < sum - length; ++s) ++v[std::size_t(rng_.next_int(length))];
357 return v;
358 }
359
360 /** `setHostDemand(mean)`: an Immediate below FineTol, an Exp of that mean above. */
361 static lang::Distrib<T> as_distribution(double mean) {
363 return lang::Distrib<T>::exp_rate(num_traits<T>::from_double(1.0 / mean));
364 }
365
366 // -----------------------------------------------------------------------
367 // State
368 // -----------------------------------------------------------------------
369
370 rng::JavaRandom rng_;
371 std::string model_name_ = "lnw";
372 Range population_range_ = Range(1.0, 1.0);
373 Range think_time_range_ = Range(1.0, 1.0);
374 Range task_multi_range_ = Range(1.0, 1.0);
375 Range proc_multi_range_ = Range(1.0, 1.0);
376 Range host_demand_range_ = Range(1.0, 1.0);
377 Range synch_call_range_ = Range(1.0, 1.0);
378 double task_inf_probability_ = 0.0;
379 double proc_inf_probability_ = 0.0;
380
381 std::vector<int> tasks_per_level_;
382 std::vector<int> tasks_per_processor_;
383};
384
385} // namespace gen
386} // namespace line
387
388#endif // LINE_GEN_LAYERED_NETWORK_GENERATOR_H
InputError(const std::string &what)
Definition error.h:39
void set_host_demand_range(const Range &r)
Activity host demands.
lqn::LqnBuilder< T > generate(int num_clients, int num_levels, int num_tasks, int num_processors)
void set_proc_inf_probability(double p)
Probability that a processor is infinite-server rather than PS.
void set_synch_call_range(const Range &r)
Mean number of synchronous calls on a call arc.
void set_model_name(const std::string &nm)
The name of the generated model.
void set_population_range(const Range &r)
Reference-task populations.
const std::vector< int > & tasks_per_level() const
How many tasks landed on each level, after the last generate.
void set_think_time_range(const Range &r)
Client think times.
void set_proc_multi_range(const Range &r)
Processor multiplicities, for the processors that are not infinite-server.
const std::vector< int > & tasks_per_processor() const
How many tasks landed on each processor, after the last generate.
void set_task_multi_range(const Range &r)
Task multiplicities, for the tasks that are not infinite-server.
void set_task_inf_probability(double p)
Probability that a server task is infinite-server rather than FCFS.
void set_seed(long long seed)
Reseed the single stream every draw comes from.
void bound_to(const std::string &act, const std::string &entry_name)
Bind an activity to an entry: it is the entry's first activity.
std::size_t activity(const std::string &name, const Distrib< T > &hostdem, const std::string &on_task)
Add an activity on a task, with its host demand.
std::size_t processor(const std::string &name, double mult, SchedStrategy sched, double repl=1.0)
Add a processor.
Definition lqn_builder.h:49
void replies_to(const std::string &act, const std::string &entry_name)
Mark an activity as the one that replies to an entry.
std::size_t entry(const std::string &name, const std::string &on_task)
Add an entry on a task.
std::size_t task(const std::string &name, double mult, SchedStrategy sched, const std::string &on_processor, double repl=1.0)
Add a task on a processor.
Definition lqn_builder.h:61
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Build a layered queueing network in code, as the MATLAB constructors do.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
The two random number generators the Java LDES engine draws from, reproduced exactly: SSJ's MRG32k3a ...
A closed interval the generator samples from.
Range(double a, double b)
static Distrib exp_rate(const T &r)
Definition lang_types.h:945
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:977
static constexpr double FineTol
Definition lang_types.h:760