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