LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_builder.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_LANG_LQN_LQN_BUILDER_H
6#define LINE_LANG_LQN_LQN_BUILDER_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * Build a layered queueing network in code, as the MATLAB constructors do.
12 *
13 * Port of the Processor / Task / Entry / Activity / ActivityPrecedence API in
14 * matlab/src/lang/layered/, feeding the same `lqn_finalize` (getStruct) that
15 * the .lqnx reader feeds.
16 *
17 * WHY THIS EXISTS rather than always going through a file. The .lqnx
18 * interchange is LOSSY for models a script can express. The clearest case is a
19 * think time on a non-reference task: `writeXML` omits it because lqns rejects
20 * the attribute there (see _kb, "lqnx cannot carry non-ref think time"), so
21 * exporting such a model and reading it back silently drops the think time and
22 * changes the answer. matlab/examples/basic/layeredModel/lqn_workflows.m is
23 * exactly that shape -- its T3 has both an infinite-server discipline and a
24 * think time of 10 -- so it cannot be reached through the file format at all.
25 *
26 * ORDER MATTERS, as in the reader: element indices are assigned in declaration
27 * order (all processors, then all tasks, then all entries, then all
28 * activities), and within a task the activities are ordered by declaration.
29 * Declare in the same order as the reference script and the indices agree.
30 *
31 * The builder validates references by name and throws on an unknown one; it
32 * does not silently create elements, because a typo that creates a second
33 * disconnected task is a model that still solves.
34 */
35
36#include <string>
37#include <vector>
38
40#include "line/util/error.h"
41
42namespace line {
43namespace lqn {
44
45template <class T>
47public:
48 /** Add a processor. `mult` may be infinite; INF scheduling forces it so. */
49 std::size_t processor(const std::string& name, double mult, SchedStrategy sched,
50 double repl = 1.0) {
51 detail::RawProc p;
52 p.name = name;
53 p.sched = sched;
54 p.repl = repl;
55 p.mult = sched == SchedStrategy::INF ? std::numeric_limits<double>::infinity() : mult;
56 m_.procs.push_back(p);
57 return m_.procs.size() - 1;
58 }
59
60 /** `Host`, the other name MATLAB, the JAR and python give a processor: the same element. */
61 std::size_t host(const std::string& name, double mult, SchedStrategy sched, double repl = 1.0) {
62 return processor(name, mult, sched, repl);
63 }
64
65 /** Add a task on a processor. */
66 std::size_t task(const std::string& name, double mult, SchedStrategy sched,
67 const std::string& on_processor, double repl = 1.0) {
68 detail::RawTask<T> t;
69 t.name = name;
70 t.sched = sched;
71 t.repl = repl;
72 t.mult = sched == SchedStrategy::INF ? std::numeric_limits<double>::infinity() : mult;
73 t.thinktime = Distrib<T>::immediate();
74 t.proc_slot = find_proc(on_processor);
75 m_.tasks.push_back(t);
76 return m_.tasks.size() - 1;
77 }
78
79 /**
80 * Set a task's think time.
81 *
82 * Accepted on ANY task, not only a reference one. That is what the MATLAB
83 * API allows and what the .lqnx writer cannot express; refusing it here to
84 * match the file format would make the builder strictly weaker than the
85 * reference for no gain.
86 */
87 void think_time(const std::string& task_name, const Distrib<T>& d) {
88 m_.tasks[find_task(task_name)].thinktime = d;
89 }
90
91 /**
92 * A CacheTask: a task whose entries are looked up in a cache of `nitems`.
93 *
94 * `itemcap` is the capacity of each cache list, so a plain single-level
95 * cache passes one value. The task itself is an ordinary server in its own
96 * layer; the Cache NODE appears in its HOST's layer, which is where
97 * buildLayersRecursive puts it (`iscachelayer` is a host-layer test).
98 */
99 std::size_t cache_task(const std::string& name, double mult, SchedStrategy sched,
100 const std::string& on_processor, std::size_t nitems,
101 const std::vector<int>& itemcap, ReplacementStrategy replacestrat,
102 double repl = 1.0) {
103 if (nitems == 0)
104 throw InputError("LqnBuilder::cache_task: '" + name + "' caches no item");
105 if (itemcap.empty())
106 throw InputError("LqnBuilder::cache_task: '" + name + "' has no cache list");
107 int total = 0;
108 for (std::size_t i = 0; i < itemcap.size(); ++i) total += itemcap[i];
109 if (total <= 0 || static_cast<std::size_t>(total) >= nitems)
110 throw InputError("LqnBuilder::cache_task: '" + name +
111 "' has a total capacity that is not between 1 and nitems-1; "
112 "a cache that holds every item never misses");
113 const std::size_t t = task(name, mult, sched, on_processor, repl);
114 m_.tasks[t].nitems = nitems;
115 m_.tasks[t].itemcap = itemcap;
116 m_.tasks[t].replacestrat = replacestrat;
117 return t;
118 }
119
120 /** `CacheTask.setRetrieval(true)`: concurrent misses of one item coalesce on a single fetch. */
121 void cache_retrieval(const std::string& task_name, bool on = true) {
122 detail::RawTask<T>& t = m_.tasks[find_task(task_name)];
123 if (t.nitems == 0)
124 throw InputError("LqnBuilder::cache_retrieval: '" + task_name + "' is not a cache task");
125 t.retrieval = on;
126 }
127
128 /**
129 * A SetupTask: a server that powers down when idle and pays to restart.
130 *
131 * `setup` is charged to the first arrival that finds the server off;
132 * `delayoff` is the idle timer that has to expire before it goes off, so a
133 * long delay-off makes the setup rare. Both are ordinary Task properties in
134 * the reference (`Task.setSetupTime`/`setDelayOffTime`), so this is a plain
135 * task with the two times attached rather than a distinct kind.
136 */
137 void setup_time(const std::string& task_name, const Distrib<T>& setup,
138 const Distrib<T>& delayoff) {
139 if (setup.disabled)
140 throw InputError("LqnBuilder::setup_time: '" + task_name + "' has no setup time");
141 if (delayoff.disabled)
142 throw InputError(
143 "LqnBuilder::setup_time: '" + task_name +
144 "' has a setup time but no delay-off time; a server that never shuts down pays "
145 "the setup at most once and the reference declines to model it");
146 detail::RawTask<T>& t = m_.tasks[find_task(task_name)];
147 t.setuptime = setup;
148 t.delayofftime = delayoff;
149 }
150
151 /**
152 * An ItemEntry: the entry a cache read enters, over `cardinality` items.
153 *
154 * `popularity` is the pmf of the read over those items. The reference takes
155 * a discrete Distribution (a Zipf, typically) and reads only its pmf; this
156 * port has no discrete-distribution type, so the pmf is given directly.
157 */
158 std::size_t item_entry(const std::string& name, const std::string& on_task,
159 std::size_t cardinality, const std::vector<T>& popularity) {
160 if (cardinality == 0)
161 throw InputError("LqnBuilder::item_entry: '" + name + "' indexes no item");
162 if (popularity.size() != cardinality)
163 throw InputError("LqnBuilder::item_entry: '" + name +
164 "' needs one popularity per item");
165 const std::size_t e = entry(name, on_task);
166 m_.entries[e].cardinality = cardinality;
167 m_.entries[e].popularity = popularity;
168 return e;
169 }
170
171 /** Add an entry on a task. */
172 std::size_t entry(const std::string& name, const std::string& on_task) {
173 detail::RawEntry<T> e;
174 e.name = name;
175 e.task_slot = find_task(on_task);
176 m_.entries.push_back(e);
177 return m_.entries.size() - 1;
178 }
179
180 /** An open arrival stream at an entry. */
181 void open_arrival(const std::string& entry_name, const Distrib<T>& d) {
182 detail::RawEntry<T>& e = m_.entries[find_entry(entry_name)];
183 e.has_arrival = true;
184 e.arrival = d;
185 }
186
187 /** Add an activity on a task, with its host demand. */
188 std::size_t activity(const std::string& name, const Distrib<T>& hostdem,
189 const std::string& on_task) {
190 detail::RawActivity<T> a;
191 a.name = name;
192 a.hostdem = hostdem;
193 a.thinktime = Distrib<T>::immediate();
194 a.task_slot = find_task(on_task);
195 a.phase = 1;
196 m_.acts.push_back(a);
197 return m_.acts.size() - 1;
198 }
199
200 /** Bind an activity to an entry: it is the entry's first activity. */
201 void bound_to(const std::string& act, const std::string& entry_name) {
202 m_.acts[find_act(act)].bound_to_entry = entry_name;
203 }
204
205 /** A synchronous call from an activity to an entry of another task. */
206 void sync_call(const std::string& act, const std::string& dest_entry, const T& mean) {
207 m_.acts[find_act(act)].sync_calls.push_back({dest_entry, mean});
208 }
209
210 /** An asynchronous call from an activity to an entry of another task. */
211 void async_call(const std::string& act, const std::string& dest_entry, const T& mean) {
212 m_.acts[find_act(act)].async_calls.push_back({dest_entry, mean});
213 }
214
215 /**
216 * ONE synchronous call per invocation, its destination CYCLING over the
217 * targets in the order given (`synchCallRoundRobin`).
218 *
219 * The members are ordinary sync calls of mean `mean/n`, so the aggregate
220 * call rate is `mean` either way; what round robin removes is the variance
221 * of the branching, which is what smooths the target queues. Needs at least
222 * two targets -- a group of one is not a dispatch decision.
223 */
224 void sync_call_round_robin(const std::string& act,
225 const std::vector<std::string>& dest_entries, const T& mean) {
226 add_call_group(act, dest_entries, mean, lang::RoutingStrategy::RROBIN,
227 "sync_call_round_robin");
228 }
229
230 /** As above, with the least loaded target taking the call (`synchCallJSQ`). */
231 void sync_call_jsq(const std::string& act, const std::vector<std::string>& dest_entries,
232 const T& mean) {
233 add_call_group(act, dest_entries, mean, lang::RoutingStrategy::JSQ, "sync_call_jsq");
234 }
235
236 /**
237 * Records the grouping of sync calls the activity ALREADY declares
238 * (`recordCallGroup`). The model.json reader has read the members back as
239 * ordinary sync calls, so it records the grouping alone; issuing them again
240 * would double the call rate.
241 */
242 void record_call_group(const std::string& act, const std::vector<std::string>& dest_entries,
244 if (dest_entries.size() < 2)
245 throw InputError("LqnBuilder::record_call_group needs at least two target entries: a "
246 "group of one is not a dispatch decision");
247 m_.acts[find_act(act)].call_groups.push_back(std::make_pair(rs, dest_entries));
248 }
249
250 /**
251 * Forwarding: whenever `src_entry` is invoked, with probability `prob` the
252 * request is handed onward to `dest_entry` instead of `src_entry` replying
253 * -- lqn_finalize (shared with the .lqnx reader, see lqn_reader.h) turns
254 * this into a CallType::FWD call and SolverLN's lqn_fwd_rendezvous rewrite
255 * (lqn_helpers.h) flattens it into a caller-side pseudo rendezvous.
256 */
257 void forward(const std::string& src_entry, const std::string& dest_entry, const T& prob) {
258 detail::RawEntry<T>& e = m_.entries[find_entry(src_entry)];
259 e.fwd_dest.push_back(dest_entry);
260 e.fwd_prob.push_back(prob);
261 }
262
263 /** Mark an activity as the one that replies to an entry. */
264 void replies_to(const std::string& act, const std::string& entry_name) {
265 // a reference task is a closed population, nobody waits on its reply; MATLAB, the JAR and python refuse it too
266 if (m_.tasks[m_.acts[find_act(act)].task_slot].sched == SchedStrategy::REF)
267 throw InputError(ErrorKind{"Model"}, "Activities in reference tasks cannot reply.");
268 m_.entries[find_entry(entry_name)].reply_activities.push_back(act);
269 }
270
271 /** `Activity` phase: 1 before the entry replies, 2 after it (the second phase). */
272 void activity_phase(const std::string& act, int phase) {
273 m_.acts[find_act(act)].phase = phase;
274 }
275
276 /** An activity think time, in series with the host demand. */
277 void act_think_time(const std::string& act, const Distrib<T>& d) {
278 m_.acts[find_act(act)].thinktime = d;
279 }
280
281 /**
282 * `Task.setPriority`, on the lqns scale the .lqnx `priority` attribute carries:
283 * a LARGER value is served first and the default 0 is the lowest.
284 *
285 * Read only under the priority disciplines (HOL, 'pri' and the other *PRIO
286 * ones), where a host serves the demand of the task with the largest value
287 * first and a task serves the request of the CALLER with the largest value
288 * first. SolverLN maps it to a class priority max - prio (ln_host_task_priorities).
289 */
290 void task_priority(const std::string& task_name, int priority) {
291 m_.tasks[find_task(task_name)].priority = priority;
292 }
293
294 /** `Entry.setType`: PH1PH2 (the default) or NONE; carried for the code generators only. */
295 void entry_type(const std::string& entry_name, const std::string& type) {
296 m_.entries[find_entry(entry_name)].type = type;
297 }
298
299 /** `Activity.setCallOrder`: STOCHASTIC or DETERMINISTIC, any other value reading as STOCHASTIC. */
300 void call_order(const std::string& act, const std::string& order) {
301 m_.acts[find_act(act)].call_order = detail::call_order_from_text(order);
302 }
303
304 /** Declare a processor as a `Host` rather than a `Processor`: the same element to every solver. */
305 void host_class(const std::string& proc_name, bool is_host = true) {
306 m_.procs[find_proc(proc_name)].is_host_class = is_host;
307 }
308
309 /** Processor::setSpeedFactor: the processor serves its activities' demands `speed` times faster. */
310 void speed_factor(const std::string& proc_name, double speed) {
311 if (!(speed > 0.0) || !std::isfinite(speed))
312 throw InputError("speed_factor: the speed factor of '" + proc_name + "' must be positive and finite");
313 m_.procs[find_proc(proc_name)].speed_factor = speed;
314 }
315
316 // ---- precedences ------------------------------------------------------
317
318 /** pre -> post, a plain sequence. */
319 void serial(const std::string& pre, const std::string& post) {
320 detail::RawPrecedence<T> p;
321 p.pretype = PrecedenceType::PRE_SEQ;
322 p.posttype = PrecedenceType::POST_SEQ;
323 p.preacts.push_back(pre);
324 p.postacts.push_back(post);
325 add_prec(pre, p);
326 }
327
328 /** pre -> every post, concurrently. */
329 void and_fork(const std::string& pre, const std::vector<std::string>& posts) {
330 detail::RawPrecedence<T> p;
331 p.pretype = PrecedenceType::PRE_SEQ;
332 p.posttype = PrecedenceType::POST_AND;
333 p.preacts.push_back(pre);
334 p.postacts = posts;
335 add_prec(pre, p);
336 }
337
338 /** all pres (or `quorum` of them) -> post. */
339 void and_join(const std::vector<std::string>& pres, const std::string& post,
340 std::size_t quorum = 0) {
341 detail::RawPrecedence<T> p;
342 p.pretype = PrecedenceType::PRE_AND;
343 p.posttype = PrecedenceType::POST_SEQ;
344 p.preacts = pres;
345 p.postacts.push_back(post);
346 if (quorum > 0) {
347 p.has_quorum = true;
348 p.quorum = quorum;
349 }
350 add_prec(pres.at(0), p);
351 }
352
353 /** pre -> one of the posts, with the given branch probabilities. */
354 void or_fork(const std::string& pre, const std::vector<std::string>& posts,
355 const std::vector<T>& probs) {
356 if (posts.size() != probs.size())
357 throw InputError("LqnBuilder::or_fork: one probability per branch is required");
358 detail::RawPrecedence<T> p;
359 p.pretype = PrecedenceType::PRE_SEQ;
360 p.posttype = PrecedenceType::POST_OR;
361 p.preacts.push_back(pre);
362 p.postacts = posts;
363 p.postparams = probs;
364 add_prec(pre, p);
365 }
366
367 /**
368 * `ActivityPrecedence.CacheAccess(pre, {hit, miss})`.
369 *
370 * `pre` reads the cache; the job leaves it on the HIT branch or the MISS
371 * branch. POST_CACHE lands on the two successors, not on `pre` -- that is
372 * how getStruct.m records a post type, and what SolverLN keys on.
373 */
374 void cache_access(const std::string& pre, const std::string& hit, const std::string& miss) {
375 detail::RawPrecedence<T> p;
376 p.pretype = PrecedenceType::PRE_SEQ;
377 p.posttype = PrecedenceType::POST_CACHE;
378 p.preacts.push_back(pre);
379 p.postacts.push_back(hit);
380 p.postacts.push_back(miss);
381 add_prec(pre, p);
382 }
383
384 /** any of the pres -> post. */
385 void or_join(const std::vector<std::string>& pres, const std::string& post) {
386 detail::RawPrecedence<T> p;
387 p.pretype = PrecedenceType::PRE_OR;
388 p.posttype = PrecedenceType::POST_SEQ;
389 p.preacts = pres;
390 p.postacts.push_back(post);
391 // PRE_OR carries a probability per branch in MATLAB; a plain or-join
392 // leaves them unset, which getStruct reads as absent
393 add_prec(pres.at(0), p);
394 }
395
396 /**
397 * pre -> body, repeated `count` times in expectation, then -> end.
398 *
399 * The body list is the loop body in order; `end` is the activity the loop
400 * exits to. This is MATLAB's ActivityPrecedence.Loop(pre, body, count),
401 * whose postacts vector is body followed by end.
402 */
403 void loop(const std::string& pre, const std::vector<std::string>& body,
404 const std::string& end, const T& count) {
405 detail::RawPrecedence<T> p;
406 p.pretype = PrecedenceType::PRE_SEQ;
407 p.posttype = PrecedenceType::POST_LOOP;
408 p.preacts.push_back(pre);
409 p.postacts = body;
410 p.postacts.push_back(end);
411 p.postparams.assign(body.size(), count);
412 add_prec(pre, p);
413 }
414
415 // ---- admission constraints --------------------------------------------
416
417 /**
418 * `elem.addConstraint(operands, coeffs, cap)`: sum(coeffs .* n(operands)) <= cap.
419 *
420 * `elem` is a task (whose operands are its entries) or a host (whose
421 * operands are its tasks). Port of LayeredNetworkElement.addConstraint;
422 * the operands stay NAMED until build(), because their column order is the
423 * task's entriesof / the host's tasksof and neither exists yet.
424 */
425 void add_constraint(const std::string& elem, const std::vector<std::string>& operands,
426 const std::vector<T>& coeffs, const T& cap) {
427 if (operands.size() != coeffs.size())
428 throw InputError("LqnBuilder::add_constraint: one coefficient per operand is required");
429 if (operands.empty())
430 throw InputError("LqnBuilder::add_constraint: the constraint names no operand");
431 for (std::size_t i = 0; i < operands.size(); ++i)
432 for (std::size_t j = i + 1; j < operands.size(); ++j)
433 if (operands[i] == operands[j])
434 throw InputError("LqnBuilder::add_constraint: operand '" + operands[i] +
435 "' appears twice");
436 detail::RawLinConRow<T> row;
437 row.names = operands;
438 row.coeffs = coeffs;
439 row.cap = cap;
440 linconrows_of(elem).push_back(row);
441 }
442
443 /**
444 * The positional form, `elem.setConstraint(A, b)`.
445 *
446 * Only the column COUNT is checked, at build() time, as the reference does:
447 * the columns are the element's entries or tasks in declaration order.
448 */
449 void set_constraint(const std::string& elem, const Matrix<T>& A, const std::vector<T>& b) {
450 if (A.rows() != b.size())
451 throw InputError("LqnBuilder::set_constraint: A and b disagree on the number of rows");
452 for (std::size_t i = 0; i < m_.tasks.size(); ++i)
453 if (m_.tasks[i].name == elem) {
454 m_.tasks[i].lincon_A = A;
455 m_.tasks[i].lincon_b = b;
456 return;
457 }
458 for (std::size_t i = 0; i < m_.procs.size(); ++i)
459 if (m_.procs[i].name == elem) {
460 m_.proc_lincon[i] = std::make_pair(A, b);
461 return;
462 }
463 throw InputError("LqnBuilder::set_constraint: unknown task or processor '" + elem + "'");
464 }
465
466 /**
467 * `elem.setLoadDependence(alpha)`: alpha(n) scales the rate of the layer
468 * station of ELEM when it holds n jobs in total, on top of the multiplicity.
469 */
470 void set_load_dependence(const std::string& elem, const std::vector<T>& alpha) {
471 assert_rate_dependent(elem, "Load");
472 if (alpha.empty())
473 throw InputError("LqnBuilder::set_load_dependence: alpha is empty");
474 if (is_task(elem)) {
475 m_.tasks[find_task(elem)].lldscaling = alpha;
476 } else {
477 m_.proc_lldscaling[find_proc(elem)] = alpha;
478 }
479 }
480
481 /**
482 * `elem.setClassDependence(beta, peak)`: the product-form handle, whose
483 * argument is the per-OPERAND population of ELEM -- task j of a processor,
484 * entry j of a task, in declaration order.
485 */
486 void set_class_dependence(const std::string& elem, const CdScaling<T>& beta,
487 const std::vector<T>& peak) {
488 assert_rate_dependent(elem, "Class");
489 assert_dependence_handle(beta, peak, "Class");
490 if (is_task(elem)) {
491 const std::size_t t = find_task(elem);
492 m_.tasks[t].cdscaling = beta;
493 m_.tasks[t].cdscalingpeak = peak;
494 } else {
495 const std::size_t p = find_proc(elem);
496 m_.proc_cdscaling[p] = beta;
497 m_.proc_cdscalingpeak[p] = peak;
498 }
499 }
500
501 /**
502 * `elem.setJointDependence(eta, peak)`: the non-product-form handle, read at
503 * the whole per-operand vector, so solvers treat it as an approximation.
504 */
505 void set_joint_dependence(const std::string& elem, const CdScaling<T>& eta,
506 const std::vector<T>& peak) {
507 assert_rate_dependent(elem, "Joint");
508 assert_dependence_handle(eta, peak, "Joint");
509 assert_no_pools(elem, "a joint dependence");
510 if (is_task(elem)) {
511 const std::size_t t = find_task(elem);
512 m_.tasks[t].jdscaling = eta;
513 m_.tasks[t].jdscalingpeak = peak;
514 } else {
515 const std::size_t p = find_proc(elem);
516 m_.proc_jdscaling[p] = eta;
517 m_.proc_jdscalingpeak[p] = peak;
518 }
519 }
520
521 /**
522 * `elem.addServerType(ServerType(pool, count, compatible))`: one pool of
523 * COUNT identical servers, each running at RATE, eligible for the operands
524 * named in COMPATIBLE.
525 *
526 * The operands are resolved by name against the element's own operand list
527 * at build() time, so a pool may name a task or an entry that is declared
528 * later. Pools accumulate; SolverLN lowers the whole declaration to the
529 * activated-server rate.
530 */
531 void add_server_type(const std::string& elem, const std::string& pool, double count,
532 const std::vector<std::string>& compatible, const T& rate) {
533 assert_rate_dependent(elem, "Compatibility");
534 assert_no_jd(elem);
535 if (count < 1)
536 throw InputError("LqnBuilder::add_server_type: pool '" + pool +
537 "' must hold at least one server");
538 if (compatible.empty())
539 throw InputError("LqnBuilder::add_server_type: pool '" + pool +
540 "' is compatible with no operand, so it can never serve");
541 detail::RawServerPool<T> sp;
542 sp.name = pool;
543 sp.count = count;
544 sp.rate = rate;
545 sp.compatible = compatible;
546 raw_pools_of(elem).push_back(sp);
547 }
548
549 /** Flatten into the struct SolverLN consumes. */
550 LqnStruct<T> build() const { return lqn_finalize(m_); }
551
552 const LqnModel<T>& model() const { return m_; }
553
554private:
555 LqnModel<T> m_;
556
557 std::size_t find_proc(const std::string& n) const {
558 for (std::size_t i = 0; i < m_.procs.size(); ++i)
559 if (m_.procs[i].name == n) return i;
560 throw InputError("LqnBuilder: unknown processor '" + n + "'");
561 }
562 std::size_t find_task(const std::string& n) const {
563 for (std::size_t i = 0; i < m_.tasks.size(); ++i)
564 if (m_.tasks[i].name == n) return i;
565 throw InputError("LqnBuilder: unknown task '" + n + "'");
566 }
567 std::size_t find_entry(const std::string& n) const {
568 for (std::size_t i = 0; i < m_.entries.size(); ++i)
569 if (m_.entries[i].name == n) return i;
570 throw InputError("LqnBuilder: unknown entry '" + n + "'");
571 }
572 std::size_t find_act(const std::string& n) const {
573 for (std::size_t i = 0; i < m_.acts.size(); ++i)
574 if (m_.acts[i].name == n) return i;
575 throw InputError("LqnBuilder: unknown activity '" + n + "'");
576 }
577 /** The constraint-row list of a task or a host, by name. */
578 std::vector<detail::RawLinConRow<T>>& linconrows_of(const std::string& n) {
579 for (std::size_t i = 0; i < m_.tasks.size(); ++i)
580 if (m_.tasks[i].name == n) return m_.tasks[i].linconrows;
581 for (std::size_t i = 0; i < m_.procs.size(); ++i)
582 if (m_.procs[i].name == n) return m_.proc_linconrows[i];
583 throw InputError("LqnBuilder: unknown task or processor '" + n + "'");
584 }
585
586 bool is_task(const std::string& n) const {
587 for (std::size_t i = 0; i < m_.tasks.size(); ++i)
588 if (m_.tasks[i].name == n) return true;
589 return false;
590 }
591
592 /** The declared pool list of a task or a host, by name. */
593 std::vector<detail::RawServerPool<T>>& raw_pools_of(const std::string& n) {
594 for (std::size_t i = 0; i < m_.tasks.size(); ++i)
595 if (m_.tasks[i].name == n) return m_.tasks[i].pools;
596 for (std::size_t i = 0; i < m_.procs.size(); ++i)
597 if (m_.procs[i].name == n) return m_.proc_pools[i];
598 throw InputError("LqnBuilder: unknown task or processor '" + n + "'");
599 }
600
601 /**
602 * Only a Task or a Host becomes a layer STATION, and only a PS or FCFS one
603 * admits a rate scaling. Twin of LayeredNetworkElement.assertRateDependent.
604 */
605 void assert_rate_dependent(const std::string& elem, const char* what) const {
606 SchedStrategy sched = SchedStrategy::NONE;
607 bool found = false;
608 for (std::size_t i = 0; i < m_.tasks.size() && !found; ++i)
609 if (m_.tasks[i].name == elem) {
610 sched = m_.tasks[i].sched;
611 found = true;
612 }
613 for (std::size_t i = 0; i < m_.procs.size() && !found; ++i)
614 if (m_.procs[i].name == elem) {
615 sched = m_.procs[i].sched;
616 found = true;
617 }
618 if (!found)
619 throw InputError(std::string(what) +
620 "-dependence can only be set on a Task or a Host, which are the only "
621 "elements that become server stations in a layer; '" +
622 elem + "' is neither");
623 if (sched != SchedStrategy::PS && sched != SchedStrategy::FCFS)
624 throw InputError(std::string(what) +
625 "-dependence supported only for processor sharing (PS) and "
626 "first-come first-serve (FCFS) servers, but '" +
627 elem + "' is scheduled otherwise");
628 }
629
630 /** The peak rate is a model input; without it utilization has no normalizer. */
631 void assert_dependence_handle(const CdScaling<T>& f, const std::vector<T>& peak,
632 const char* what) const {
633 if (!f)
634 throw InputError(std::string(what) + "-dependence needs a handle");
635 if (peak.empty())
636 throw InputError(std::string(what) +
637 "-dependence needs a peak rate per operand, which normalizes "
638 "utilization as U = T*S/peak");
639 }
640
641 /** One rate law per server: pools and an explicit handle would both claim it. */
642 void assert_no_pools(const std::string& elem, const char* what) {
643 if (!raw_pools_of(elem).empty())
644 throw InputError("LqnBuilder: '" + elem + "' already declares server pools, which are "
645 "themselves a rate law, so it cannot also take " + what);
646 }
647
648 void assert_no_jd(const std::string& elem) {
649 bool has = false;
650 for (std::size_t i = 0; i < m_.tasks.size(); ++i)
651 if (m_.tasks[i].name == elem && m_.tasks[i].jdscaling) has = true;
652 for (std::size_t i = 0; i < m_.procs.size(); ++i)
653 if (m_.procs[i].name == elem && m_.proc_jdscaling.count(i)) has = true;
654 if (has)
655 throw InputError("LqnBuilder: '" + elem + "' already declares a joint dependence, so "
656 "it cannot also declare server pools, which are a rate law of their "
657 "own");
658 }
659
660 /** A precedence belongs to the task owning its activities. */
661 void add_prec(const std::string& anchor_act, const detail::RawPrecedence<T>& p) {
662 m_.tasks[m_.acts[find_act(anchor_act)].task_slot].precedences.push_back(p);
663 }
664
665 /**
666 * A routed group: n ordinary sync calls of mean/n, plus the record that
667 * they are one dispatch. Splitting the mean is the reference's own
668 * `addCallGroup` (Activity.m), and it is what keeps the aggregate call rate
669 * equal to the probabilistic twin's.
670 */
671 void add_call_group(const std::string& act, const std::vector<std::string>& dest_entries,
672 const T& mean, lang::RoutingStrategy rs, const char* who) {
673 if (dest_entries.size() < 2)
674 throw InputError(std::string("LqnBuilder::") + who +
675 " needs at least two target entries: a group of one is not a "
676 "dispatch decision");
677 const T share = T(mean / num_traits<T>::from_int(int(dest_entries.size())));
678 for (const std::string& d : dest_entries) sync_call(act, d, share);
679 m_.acts[find_act(act)].call_groups.push_back(std::make_pair(rs, dest_entries));
680 }
681};
682
683} // namespace lqn
684} // namespace line
685
686#endif // LINE_LANG_LQN_LQN_BUILDER_H
InputError(const std::string &what)
Definition error.h:105
std::size_t rows() const
Definition matrix.h:89
void record_call_group(const std::string &act, const std::vector< std::string > &dest_entries, lang::RoutingStrategy rs)
Records the grouping of sync calls the activity ALREADY declares (recordCallGroup).
void sync_call_jsq(const std::string &act, const std::vector< std::string > &dest_entries, const T &mean)
As above, with the least loaded target taking the call (synchCallJSQ).
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.
void entry_type(const std::string &entry_name, const std::string &type)
Entry.setType: PH1PH2 (the default) or NONE; carried for the code generators only.
void cache_retrieval(const std::string &task_name, bool on=true)
CacheTask.setRetrieval(true): concurrent misses of one item coalesce on a single fetch.
std::size_t host(const std::string &name, double mult, SchedStrategy sched, double repl=1.0)
Host, the other name MATLAB, the JAR and python give a processor: the same element.
Definition lqn_builder.h:61
LqnStruct< T > build() const
Flatten into the struct SolverLN consumes.
void act_think_time(const std::string &act, const Distrib< T > &d)
An activity think time, in series with the host demand.
void loop(const std::string &pre, const std::vector< std::string > &body, const std::string &end, const T &count)
pre -> body, repeated count times in expectation, then -> end.
void activity_phase(const std::string &act, int phase)
Activity phase: 1 before the entry replies, 2 after it (the second phase).
void open_arrival(const std::string &entry_name, const Distrib< T > &d)
An open arrival stream at an entry.
void setup_time(const std::string &task_name, const Distrib< T > &setup, const Distrib< T > &delayoff)
A SetupTask: a server that powers down when idle and pays to restart.
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 set_joint_dependence(const std::string &elem, const CdScaling< T > &eta, const std::vector< T > &peak)
elem.setJointDependence(eta, peak): the non-product-form handle, read at the whole per-operand vector...
void host_class(const std::string &proc_name, bool is_host=true)
Declare a processor as a Host rather than a Processor: the same element to every solver.
void task_priority(const std::string &task_name, int priority)
Task.setPriority, on the lqns scale the .lqnx priority attribute carries: a LARGER value is served fi...
void replies_to(const std::string &act, const std::string &entry_name)
Mark an activity as the one that replies to an entry.
void forward(const std::string &src_entry, const std::string &dest_entry, const T &prob)
Forwarding: whenever src_entry is invoked, with probability prob the request is handed onward to dest...
void and_fork(const std::string &pre, const std::vector< std::string > &posts)
pre -> every post, concurrently.
void cache_access(const std::string &pre, const std::string &hit, const std::string &miss)
ActivityPrecedence.CacheAccess(pre, {hit, miss}).
std::size_t cache_task(const std::string &name, double mult, SchedStrategy sched, const std::string &on_processor, std::size_t nitems, const std::vector< int > &itemcap, ReplacementStrategy replacestrat, double repl=1.0)
A CacheTask: a task whose entries are looked up in a cache of nitems.
Definition lqn_builder.h:99
void set_constraint(const std::string &elem, const Matrix< T > &A, const std::vector< T > &b)
The positional form, elem.setConstraint(A, b).
void add_constraint(const std::string &elem, const std::vector< std::string > &operands, const std::vector< T > &coeffs, const T &cap)
elem.addConstraint(operands, coeffs, cap): sum(coeffs .
void async_call(const std::string &act, const std::string &dest_entry, const T &mean)
An asynchronous call from an activity to an entry of another task.
void and_join(const std::vector< std::string > &pres, const std::string &post, std::size_t quorum=0)
all pres (or quorum of them) -> post.
void serial(const std::string &pre, const std::string &post)
pre -> post, a plain sequence.
void sync_call(const std::string &act, const std::string &dest_entry, const T &mean)
A synchronous call from an activity to an entry of another task.
void call_order(const std::string &act, const std::string &order)
Activity.setCallOrder: STOCHASTIC or DETERMINISTIC, any other value reading as STOCHASTIC.
void set_class_dependence(const std::string &elem, const CdScaling< T > &beta, const std::vector< T > &peak)
elem.setClassDependence(beta, peak): the product-form handle, whose argument is the per-OPERAND popul...
void think_time(const std::string &task_name, const Distrib< T > &d)
Set a task's think time.
Definition lqn_builder.h:87
const LqnModel< T > & model() const
void sync_call_round_robin(const std::string &act, const std::vector< std::string > &dest_entries, const T &mean)
ONE synchronous call per invocation, its destination CYCLING over the targets in the order given (syn...
void add_server_type(const std::string &elem, const std::string &pool, double count, const std::vector< std::string > &compatible, const T &rate)
elem.addServerType(ServerType(pool, count, compatible)): one pool of COUNT identical servers,...
void or_fork(const std::string &pre, const std::vector< std::string > &posts, const std::vector< T > &probs)
pre -> one of the posts, with the given branch probabilities.
void set_load_dependence(const std::string &elem, const std::vector< T > &alpha)
elem.setLoadDependence(alpha): alpha(n) scales the rate of the layer station of ELEM when it holds n ...
void or_join(const std::vector< std::string > &pres, const std::string &post)
any of the pres -> post.
void speed_factor(const std::string &proc_name, double speed)
Processor::setSpeedFactor: the processor serves its activities' demands speed times faster.
std::size_t item_entry(const std::string &name, const std::string &on_task, std::size_t cardinality, const std::vector< T > &popularity)
An ItemEntry: the entry a cache read enters, over cardinality items.
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.
.lqnx -> LqnStruct, a port of matlab/src/lang/layered/@LayeredNetwork/parseXML.m followed by ....
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:736
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:385
LqnStruct< T > lqn_finalize(const LqnModel< T > &m)
Port of @LayeredNetwork/getStruct.m: flatten the model into its struct.
Definition lqn_reader.h:501
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static Distrib immediate()
The point mass at zero.
Definition lang_types.h:986
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:411
std::vector< detail::RawTask< T > > tasks
Definition lqn_reader.h:415
std::vector< detail::RawProc > procs
Definition lqn_reader.h:414
std::vector< detail::RawEntry< T > > entries
Definition lqn_reader.h:416