LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_reader.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_READER_H
6#define LINE_LANG_LQN_LQN_READER_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * .lqnx -> LqnStruct, a port of matlab/src/lang/layered/@@LayeredNetwork/parseXML.m
12 * followed by .../getStruct.m.
13 *
14 * The two MATLAB stages are fused here because the intermediate object graph
15 * (Processor / Task / Entry / Activity handles) exists in MATLAB only to be
16 * flattened by getStruct, and nothing in this port holds a model object. The
17 * ORDER in which the stages walk the document is load-bearing and is preserved
18 * exactly, because it fixes the index assignment that every later array is
19 * keyed on:
20 *
21 * hosts document order of `<processor>`
22 * tasks for each processor, document order of its `<task>` descendants
23 * entries for each task, document order of its `<entry>` descendants
24 * activities for each task: the `<entry-phase-activities>` activities of each
25 * of its entries, in entry order, THEN its `<task-activities>`
26 * activities
27 *
28 * That last ordering is not the document order of `<activity>` elements: MATLAB
29 * processes all entries of a task before its task-activities block, so an
30 * entry-phase activity declared after a task-activities block still receives
31 * the lower index. Reproducing it is what makes an index-by-index comparison
32 * against a MATLAB dump meaningful.
33 *
34 * WHAT IS REFUSED. The reader implements the subset of the .lqnx grammar that
35 * a layered model needs to reach SolverLN: processors, tasks, entries with
36 * phase activities or an activity graph, synchronous and asynchronous calls,
37 * forwarding, sequence / AND / OR / loop precedences, replies and open
38 * arrivals. Constructs outside it (fan-in and fan-out replication, cache
39 * tasks, setup tasks with setup times, service-time distributions declared
40 * by histogram) are rejected by name where they would change the answer, and
41 * ignored where MATLAB also ignores them.
42 */
43
44#include <algorithm>
45#include <cctype>
46#include <cmath>
47#include <cstdio>
48#include <limits>
49#include <map>
50#include <string>
51#include <unordered_map>
52#include <vector>
53
57#include "line/util/decimal.h"
58#include "line/util/error.h"
59#include "line/util/xml.h"
60
61namespace line {
62namespace lqn {
63
64namespace detail {
65
66/** Intermediate objects, the C++ stand-in for the MATLAB handle graph. */
67template <class T>
68struct RawCall {
69 std::string dest;
70 T mean;
71};
72
73template <class T>
74struct RawActivity {
75 std::string name;
76 Distrib<T> hostdem;
77 Distrib<T> thinktime;
78 std::string bound_to_entry;
79 int phase = 1;
80 /** `Activity.setCallOrder`: STOCHASTIC (the default) or DETERMINISTIC; `.lqnx` `call-order`. */
81 std::string call_order = "STOCHASTIC";
82 std::size_t task_slot = 0; ///< 0-based index into the raw task list
83 std::vector<RawCall<T>> sync_calls;
84 std::vector<RawCall<T>> async_calls;
85 /**
86 * Routed call groups declared on this activity: the strategy and the target
87 * entry NAMES, in declaration order. The member calls themselves are
88 * ordinary rows of `sync_calls`; this only records that they are ONE
89 * dispatch. Resolved to entry indices by lqn_finalize.
90 */
91 std::vector<std::pair<lang::RoutingStrategy, std::vector<std::string>>> call_groups;
92};
93
94template <class T>
95struct RawPrecedence {
96 PrecedenceType pretype = PrecedenceType::PRE_SEQ;
97 PrecedenceType posttype = PrecedenceType::POST_SEQ;
98 std::vector<std::string> preacts;
99 std::vector<std::string> postacts;
100 std::vector<T> preparams; ///< PRE_OR branch probabilities, or a PRE_AND quorum
101 std::vector<T> postparams; ///< POST_OR probabilities or POST_LOOP counts
102 bool has_quorum = false;
103 std::size_t quorum = 0;
104};
105
106template <class T>
107struct RawEntry {
108 std::string name;
109 std::size_t task_slot = 0;
110 std::vector<std::string> reply_activities;
111 bool has_arrival = false;
112 Distrib<T> arrival;
113 std::vector<std::string> fwd_dest;
114 std::vector<T> fwd_prob;
115 /** Item entry: cardinality and the popularity pmf over it; 0 = ordinary entry. */
116 std::size_t cardinality = 0;
117 std::vector<T> popularity;
118 /** `Entry.setType`: PH1PH2 (the default) or NONE. No solver reads it and no wire form carries it. */
119 std::string type = "PH1PH2";
120};
121
122/**
123 * One row of an admission constraint, named rather than positional.
124 *
125 * The operands are entries (on a task) or tasks (on a host); they cannot be
126 * resolved to columns until tasksof/entriesof exist, so they are carried by
127 * name and resolved at the end of lqn_finalize, as getStruct.m:221-256 does.
128 */
129template <class T>
130struct RawLinConRow {
131 std::vector<std::string> names;
132 std::vector<T> coeffs;
133 T cap;
134};
135
136/**
137 * One declared server pool, before its operands are resolved to indices.
138 *
139 * `compatible` names tasks (on a host) or entries (on a task); the names are
140 * looked up against the element's own operand list in lqn_finalize, so a pool
141 * may be declared before the operand it names.
142 */
143template <class T>
144struct RawServerPool {
145 std::string name;
146 double count = 1.0;
147 T rate;
148 std::vector<std::string> compatible;
149};
150
151template <class T>
152struct RawTask {
153 std::string name;
154 SchedStrategy sched = SchedStrategy::FCFS;
155 double mult = 1.0;
156 double repl = 1.0;
157 /** `Task.setPriority` on the lqns scale, larger first; orders priority service (lsn.prio, `.lqnx` `priority`). */
158 int priority = 0;
159 Distrib<T> thinktime;
160 std::size_t proc_slot = 0;
161 /** fan-out/fan-in as declared: (peer task NAME, value), resolved once indices exist. */
162 std::vector<std::pair<std::string, double>> fanout;
163 std::vector<std::pair<std::string, double>> fanin;
164 std::vector<RawPrecedence<T>> precedences;
165 std::vector<RawLinConRow<T>> linconrows;
166 Matrix<T> lincon_A;
167 std::vector<T> lincon_b;
168 /** Cache task: item population, per-list capacity, replacement rule. */
169 std::size_t nitems = 0;
170 std::vector<int> itemcap;
171 ReplacementStrategy replacestrat = ReplacementStrategy::RR;
172 /** Delayed-hit retrieval: concurrent misses of one item coalesce on a single fetch. */
173 bool retrieval = false;
174 /** Setup task: the server shuts down when idle and pays to restart. */
175 Distrib<T> setuptime;
176 Distrib<T> delayofftime;
177 /**
178 * Queue-dependent service rates on this task's layer station, over its
179 * entries as operands. Empty where not declared; see LqnStruct.
180 */
181 std::vector<T> lldscaling;
182 CdScaling<T> cdscaling;
183 std::vector<T> cdscalingpeak;
184 CdScaling<T> jdscaling;
185 std::vector<T> jdscalingpeak;
186 std::vector<RawServerPool<T>> pools;
187};
188
189struct RawProc {
190 std::string name;
191 SchedStrategy sched = SchedStrategy::FCFS;
192 double mult = 1.0;
193 double repl = 1.0;
194 /**
195 * `speed-factor` and `quantum`, carried but never read by a solver here.
196 *
197 * getStruct.m does not read either, so SolverLN cannot see them and this
198 * port's LqnStruct has no slot for them. They still have to survive the
199 * intermediate model, because SolverLQNS WRITES a .lqnx back out and lqns
200 * does honour both: a document declaring `speed-factor="2"` would come back
201 * from an unmindful round trip as a processor twice as slow, and nothing in
202 * the answer would say so.
203 */
204 double speed_factor = 1.0;
205 double quantum = 0.0;
206 /** Declared with `Host(...)` rather than `Processor(...)`; the two are the same element to every solver. */
207 bool is_host_class = false;
208};
209
210/**
211 * The host-demand distribution of an activity, following the MATLAB mapping.
212 *
213 * MATLAB uses two slightly different ladders, one in the entry-phase branch and
214 * one in the task-activities branch: the phase branch sends every scv != 1 to
215 * APH, the activity branch splits scv < 1 to APH, scv == 1 to Exp and scv > 1
216 * to HyperExp. The activity ladder is the one taken here, because it is the one
217 * the writer's own `host-demand-cvsq` round-trips through and it is what
218 * `parseXML` applies to every activity outside the phase-indexed form.
219 *
220 * THE LAW IS FITTED, NOT RELABELLED. This used to build an Exp and then
221 * overwrite `type` with APH or HYPEREXP, leaving the Exp's single-entry
222 * `params` and its empty (D0, D1) behind a family that indexes three of them --
223 * so any consumer reading the parameters read past the end. `dist_scale_rate`
224 * does exactly that on the first fixed-point iterate, which aborted line-cli
225 * (`vector::operator[]: __n < size()`) on every layered model carrying a
226 * non-unit cvsq: lqn_multi_solvers died before its first table.
227 */
228template <class T>
229Distrib<T> host_demand(const std::string& mean_s, const std::string& scv_s) {
230 const double mean_d = dbl_from_decimal(mean_s, 0.0);
231 if (!(mean_d > 0.0)) return Distrib<T>::immediate();
232 const T mean = num_from_decimal<T>(mean_s);
233 const double scv_d = dbl_from_decimal(scv_s, 1.0);
234 if (!(scv_d > 0.0)) return Distrib<T>::det(mean);
235 if (scv_d == 1.0) return Distrib<T>::exp_mean(mean);
236 if constexpr (!num_traits<T>::has_transcendental) {
237 // Both fits solve a moment condition through a square root, which the
238 // exact-arithmetic types have no representation for. Named rather than
239 // silently exponential: an activity whose cvsq the document states is
240 // not an Exp, and reporting one would be a different model.
241 throw UnsupportedError(
242 "lqn reader: an activity declares host-demand-cvsq " + scv_s +
243 ", whose APH / HyperExp fit needs a square root that exact arithmetic cannot "
244 "represent; solve this model in double precision");
245 } else {
246 const T scv = num_from_decimal<T>(scv_s);
247 if (scv_d < 1.0) return lang::aph_fit_mean_scv(mean, scv);
248 return lang::hyperexp_fit_mean_scv(mean, scv);
249 }
250}
251
252/**
253 * A `<setup>` / `<delay-off>` mean and SCV back into a distribution.
254 *
255 * Twin of parseXML.m's `time_from_element` and the JAR's `timeFromElement`:
256 * the family is the one the SETTER itself would build, so an SCV of one is the
257 * Exp `setSetupTime(mean)` makes and anything else needs a two-moment APH fit.
258 * That ladder differs from `host_demand` above, which splits scv > 1 to
259 * HyperExp; do not merge them.
260 */
261template <class T>
262Distrib<T> setup_time(const std::string& mean_s, const std::string& scv_s) {
263 const double mean_d = dbl_from_decimal(mean_s, 0.0);
265 const T mean = num_from_decimal<T>(mean_s);
266 const double scv_d = dbl_from_decimal(scv_s, 1.0);
267 if (std::abs(scv_d - 1.0) <= lang::GlobalConstants::FineTol) return Distrib<T>::exp_mean(mean);
268 if constexpr (!num_traits<T>::has_transcendental) {
269 throw UnsupportedError(
270 "lqn reader: a task declares a setup or delay-off scv " + scv_s +
271 ", whose APH fit needs a square root that exact arithmetic cannot represent; "
272 "solve this model in double precision");
273 } else {
274 return lang::aph_fit_mean_scv(mean, num_from_decimal<T>(scv_s));
275 }
276}
277
278/**
279 * `<cache replacement="...">` -> ReplacementStrategy.
280 *
281 * The attribute carries the enum NAME, as MATLAB's writeXML and the JAR's
282 * writeXML both emit it; case is not load-bearing, so both spellings are taken.
283 * An unknown rule is refused rather than defaulted: the replacement rule is
284 * what the hit probability is a function of, so serving CLIMB as RR would
285 * answer a different model without saying so.
286 */
287inline lang::ReplacementStrategy replacement_from_lqnx(const std::string& s) {
289 std::string key;
290 for (std::size_t i = 0; i < s.size(); ++i) {
291 const char c = s[i];
292 if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue;
293 key.push_back(static_cast<char>(std::toupper(static_cast<unsigned char>(c))));
294 }
295 if (key.empty() || key == "FIFO") return R::FIFO;
296 if (key == "RR" || key == "RANDOM") return R::RR;
297 if (key == "SFIFO") return R::SFIFO;
298 if (key == "LRU") return R::LRU;
299 if (key == "HLRU") return R::HLRU;
300 if (key == "CLIMB") return R::CLIMB;
301 if (key == "QLRU") return R::QLRU;
302 throw UnsupportedError("lqn reader: unsupported cache replacement strategy '" + s + "'");
303}
304
305/**
306 * `<item-entry><access-popularity>` -> the item pmf `lqn.itemproc` carries.
307 *
308 * The struct keeps the PMF and not the law, so the two discrete families the
309 * dialect writes are both reduced here, as `pmf_from_json` does on the JSON
310 * side: `DiscreteSampler` states the pmf outright and `Zipf` states (s, n) with
311 * p_i = i^-s / H(s,n). A DiscreteSampler written with its support carries 2*card
312 * parameters (p then x) and only the first half is the pmf, which is the split
313 * the JAR's `readAccessPopularity` makes on the same document.
314 */
315template <class T>
316std::vector<T> popularity_from_lqnx(const xml::Element* item, std::size_t cardinality) {
317 std::vector<T> out;
318 const std::vector<const xml::Element*> pops = item->child_tags("access-popularity");
319 if (pops.empty()) {
320 // An item entry with no popularity is still an item entry: the uniform
321 // law over its items is what the reference falls back to, and an empty
322 // vector would leave the cache with no read at all.
323 for (std::size_t k = 0; k < cardinality; ++k)
324 out.push_back(num_traits<T>::from_double(1.0 / double(cardinality ? cardinality : 1)));
325 return out;
326 }
327 const xml::Element* pe = pops[0];
328 const std::string family = pe->attr("name");
329 std::vector<std::string> raw;
330 for (const xml::Element* par : pe->child_tags("parameter")) raw.push_back(par->attr("value"));
331 if (family == "Zipf") {
332 if (raw.size() != 2)
333 throw InputError(
334 "lqn reader: an access popularity of class Zipf carries exactly two parameters, "
335 "the shape s then the item count n, but this one carries " +
336 std::to_string(raw.size()));
337 const double s = dbl_from_decimal(raw[0], 1.0);
338 const std::size_t n = static_cast<std::size_t>(dbl_from_decimal(raw[1], double(cardinality)));
339 double h = 0.0;
340 for (std::size_t k = 1; k <= n; ++k) h += std::pow(double(k), -s);
341 for (std::size_t k = 1; k <= n; ++k)
342 out.push_back(num_traits<T>::from_double(std::pow(double(k), -s) / h));
343 return out;
344 }
345 if (family.empty() || family == "DiscreteSampler") {
346 const std::size_t n =
347 (cardinality > 0 && raw.size() == 2 * cardinality) ? cardinality : raw.size();
348 for (std::size_t k = 0; k < n; ++k) out.push_back(num_from_decimal<T>(raw[k]));
349 return out;
350 }
351 throw UnsupportedError(
352 "lqn reader: an access popularity is written as '" + family +
353 "', and the discrete families the .lqnx dialect carries are DiscreteSampler and Zipf");
354}
355
356/**
357 * Wire enum name -> RoutingStrategy, the inverse of the writer's
358 * `callgroup_to_lqnx`.
359 */
360inline lang::RoutingStrategy callgroup_from_lqnx(const std::string& name,
361 const std::string& act_name) {
362 std::string key;
363 for (std::size_t i = 0; i < name.size(); ++i) {
364 const char c = name[i];
365 if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue;
366 key += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
367 }
368 if (key == "RROBIN") return lang::RoutingStrategy::RROBIN;
369 if (key == "JSQ") return lang::RoutingStrategy::JSQ;
370 throw InputError("lqn reader: activity '" + act_name +
371 "' declares a call group with an unrecognized strategy '" + name +
372 "'; the dialect spells them RROBIN and JSQ");
373}
374
375/**
376 * Reads the LINE dialect <call-group> children of an activity element.
377 *
378 * The member calls are ordinary synch-call elements and have already been read,
379 * so only the grouping is recorded; issuing them again would double the call
380 * rate.
381 */
382template <class T>
383void read_call_groups(const xml::Element* ae, RawActivity<T>& ac) {
384 const std::vector<const xml::Element*> groups = ae->child_tags("call-group");
385 for (std::size_t g = 0; g < groups.size(); ++g) {
386 std::vector<std::string> dests;
387 const std::vector<const xml::Element*> de = groups[g]->child_tags("dest");
388 for (std::size_t d = 0; d < de.size(); ++d) dests.push_back(de[d]->attr("name"));
389 ac.call_groups.push_back(
390 std::make_pair(callgroup_from_lqnx(groups[g]->attr("strategy"), ac.name), dests));
391 }
392}
393
394} // namespace detail
395
396/**
397 * The intermediate model, and the second stage that flattens it.
398 *
399 * MATLAB reaches the LayeredNetworkStruct along two routes -- parseXML from a
400 * .lqnx file, and the Processor/Task/Entry/Activity constructors used directly
401 * from a script -- and both end in the SAME getStruct. The split here mirrors
402 * that: `LqnModel` is the flat intermediate both routes fill, and
403 * `lqn_finalize` is getStruct. It is not a convenience; the .lqnx interchange
404 * is LOSSY for models a script can express (it cannot carry a think time on a
405 * non-reference task, because lqns rejects one there), so a port that could
406 * only read files could not represent every model the reference can.
407 */
408template <class T>
409struct LqnModel {
410 /** `LayeredNetwork.getName()`; empty when unnamed. Carried for the model.json writer. */
411 std::string name;
412 std::vector<detail::RawProc> procs;
413 std::vector<detail::RawTask<T>> tasks;
414 std::vector<detail::RawEntry<T>> entries;
415 std::vector<detail::RawActivity<T>> acts;
416 /**
417 * Admission constraints declared on a HOST, by 0-based processor slot.
418 *
419 * Kept beside RawProc rather than inside it because RawProc is not a
420 * template and the coefficients are T; tasks carry their own rows.
421 * `proc_lincon` is the positional (A,b) form, `proc_linconrows` the named
422 * one; a host may use either, exactly as a task may.
423 */
424 std::map<std::size_t, std::vector<detail::RawLinConRow<T>>> proc_linconrows;
425 std::map<std::size_t, std::pair<Matrix<T>, std::vector<T>>> proc_lincon;
426 /**
427 * Queue-dependent service rates and compatibility pools declared on a HOST,
428 * by 0-based processor slot.
429 *
430 * Kept beside RawProc for the same reason as proc_lincon: RawProc is not a
431 * template and the scalings are T. A task carries its own in RawTask. The
432 * operands of a host are its TASKS, in declaration order.
433 */
434 std::map<std::size_t, std::vector<T>> proc_lldscaling;
435 std::map<std::size_t, CdScaling<T>> proc_cdscaling;
436 std::map<std::size_t, std::vector<T>> proc_cdscalingpeak;
437 std::map<std::size_t, CdScaling<T>> proc_jdscaling;
438 std::map<std::size_t, std::vector<T>> proc_jdscalingpeak;
439 std::map<std::size_t, std::vector<detail::RawServerPool<T>>> proc_pools;
440};
441
442/** Port of @@LayeredNetwork/getStruct.m: flatten the model into its struct. */
443template <class T>
445 const T zero = num_traits<T>::from_int(0);
446 const T one = num_traits<T>::from_int(1);
447 const std::vector<detail::RawProc>& procs = m.procs;
448 const std::vector<detail::RawTask<T>>& tasks = m.tasks;
449 const std::vector<detail::RawEntry<T>>& entries = m.entries;
450 const std::vector<detail::RawActivity<T>>& acts = m.acts;
451
452 // Stage 2: getStruct
453 LqnStruct<T> l;
454 l.nhosts = procs.size();
455 l.ntasks = tasks.size();
456 l.nentries = entries.size();
457 l.nacts = acts.size();
458 l.hshift = 0;
459 l.tshift = l.nhosts;
460 l.eshift = l.nhosts + l.ntasks;
461 l.ashift = l.eshift + l.nentries;
462 l.nidx = l.ashift + l.nacts;
463 l.cshift = l.nidx;
464
465 const std::size_t N = l.nidx;
466 const std::size_t NT = l.tshift + l.ntasks;
467 l.names.assign(N + 1, {});
468 l.hashnames.assign(N + 1, {});
469 l.type.assign(N + 1, LqnElement::HOST);
470 l.parent.assign(N + 1, 0);
471 l.sched.assign(NT + 1, SchedStrategy::NONE);
472 l.mult.assign(NT + 1, 0.0);
473 l.maxmult.assign(NT + 1, 0.0);
474 l.repl.assign(NT + 1, 1.0);
475 l.prio.assign(NT + 1, 0);
476 l.lldscaling.assign(NT + 1, {});
477 l.cdscaling.assign(NT + 1, CdScaling<T>());
478 l.cdscalingpeak.assign(NT + 1, {});
479 l.jdscaling.assign(NT + 1, CdScaling<T>());
480 l.jdscalingpeak.assign(NT + 1, {});
481 l.pools.assign(NT + 1, ServerPools<T>());
482 l.isref.assign(NT + 1, false);
483 l.iscache.assign(NT + 1, false);
484 l.hasretrieval.assign(NT + 1, false);
485 l.hassetup.assign(NT + 1, false);
486 l.nitems.assign(N + 1, 0);
487 l.itemcap.assign(NT + 1, {});
488 l.replacestrat.assign(NT + 1, ReplacementStrategy::RR);
489 l.itemproc.assign(N + 1, {});
490 l.setuptime.assign(NT + 1, Distrib<T>::disabled_dist());
491 l.delayofftime.assign(NT + 1, Distrib<T>::disabled_dist());
492 l.hostdem.assign(N + 1, Distrib<T>::disabled_dist());
493 l.think.assign(N + 1, Distrib<T>::disabled_dist());
494 l.actthink.assign(N + 1, Distrib<T>::disabled_dist());
495 l.has_arrival.assign(N + 1, false);
496 l.arrival.assign(N + 1, Distrib<T>::disabled_dist());
497 l.tasksof.assign(l.nhosts + 1, {});
498 l.entriesof.assign(NT + 1, {});
499 l.actsof.assign(l.ashift + 1, {});
500 l.callsof.assign(N + 1, {});
501 l.precedences.assign(NT + 1, {});
502 l.actpretype.assign(N + 1, PrecedenceType::NONE);
503 l.actposttype.assign(N + 1, PrecedenceType::NONE);
504 l.actquorum.assign(N + 1, 0);
505 l.actphase.assign(l.nacts + 1, 1);
506 l.graph.resize(N);
507 l.taskgraph.resize(NT);
508 l.iscaller.resize(N);
509 l.issynccaller.resize(N);
511
512 std::unordered_map<std::string, std::size_t> byhash;
513
514 for (std::size_t p = 0; p < l.nhosts; ++p) {
515 const std::size_t idx = p + 1;
516 l.sched[idx] = procs[p].sched;
517 l.mult[idx] = procs[p].mult;
518 l.repl[idx] = procs[p].repl;
519 l.names[idx] = procs[p].name;
520 l.hashnames[idx] = "P:" + procs[p].name;
521 l.type[idx] = LqnElement::HOST;
522 // A host keeps its rate dependence in a side map on the model, since
523 // RawProc is not a template; the operands are its tasks.
524 if (m.proc_lldscaling.count(p)) l.lldscaling[idx] = m.proc_lldscaling.at(p);
525 if (m.proc_cdscaling.count(p)) l.cdscaling[idx] = m.proc_cdscaling.at(p);
526 if (m.proc_cdscalingpeak.count(p)) l.cdscalingpeak[idx] = m.proc_cdscalingpeak.at(p);
527 if (m.proc_jdscaling.count(p)) l.jdscaling[idx] = m.proc_jdscaling.at(p);
528 if (m.proc_jdscalingpeak.count(p)) l.jdscalingpeak[idx] = m.proc_jdscalingpeak.at(p);
529 byhash[l.hashnames[idx]] = idx;
530 }
531 for (std::size_t t = 0; t < l.ntasks; ++t) {
532 const std::size_t idx = l.tshift + t + 1;
533 l.sched[idx] = tasks[t].sched;
535 l.think[idx] = tasks[t].thinktime;
536 l.mult[idx] = tasks[t].mult;
537 l.repl[idx] = tasks[t].repl;
538 l.prio[idx] = tasks[t].priority;
539 l.lldscaling[idx] = tasks[t].lldscaling;
540 l.cdscaling[idx] = tasks[t].cdscaling;
541 l.cdscalingpeak[idx] = tasks[t].cdscalingpeak;
542 l.jdscaling[idx] = tasks[t].jdscaling;
543 l.jdscalingpeak[idx] = tasks[t].jdscalingpeak;
544 l.names[idx] = tasks[t].name;
545 // A cache task is C:, as getStruct.m prefixes it; the reference gives a
546 // reference task R: and every other task T:.
547 l.nitems[idx] = tasks[t].nitems;
548 l.itemcap[idx] = tasks[t].itemcap;
549 l.replacestrat[idx] = tasks[t].replacestrat;
550 l.iscache[idx] = tasks[t].nitems > 0;
551 l.hasretrieval[idx] = l.iscache[idx] && tasks[t].retrieval;
552 l.setuptime[idx] = tasks[t].setuptime;
553 l.delayofftime[idx] = tasks[t].delayofftime;
554 // LQN2QN.m:1252-1275 gates the feature on a setup that is declared, not
555 // Immediate, and above tolerance -- a sub-tolerance setup is no setup.
556 l.hassetup[idx] = !tasks[t].setuptime.disabled &&
557 num_traits<T>::to_double(tasks[t].setuptime.mean) >
559 l.hashnames[idx] = (l.iscache[idx] ? "C:"
560 : l.hassetup[idx] ? "T:"
561 : tasks[t].sched == SchedStrategy::REF ? "R:"
562 : "T:") +
563 tasks[t].name;
564 l.parent[idx] = tasks[t].proc_slot + 1;
565 l.graph.set(idx, l.parent[idx], one);
566 l.type[idx] = LqnElement::TASK;
567 byhash[l.hashnames[idx]] = idx;
568 }
569 // a task inherits its host's replication when it declares none of its own
570 for (std::size_t t = 0; t < l.ntasks; ++t) {
571 const std::size_t tidx = l.tshift + t + 1;
572 l.repl[tidx] = std::max(l.repl[tidx], l.repl[l.parent[tidx]]);
573 }
574 // fan-out/fan-in resolve now that every task carries an element index. A
575 // peer that names no task in the model is dropped, as getStruct.m and the
576 // Python and JAR struct builders drop it: the declaration is not a call, so
577 // an unresolved name removes an edge that never existed.
578 {
579 std::unordered_map<std::string, std::size_t> task_by_name;
580 for (std::size_t t = 0; t < l.ntasks; ++t) {
581 const std::size_t tidx = l.tshift + t + 1;
582 task_by_name[l.names[tidx]] = tidx;
583 }
584 for (std::size_t t = 0; t < l.ntasks; ++t) {
585 const std::size_t tidx = l.tshift + t + 1;
586 for (std::size_t k = 0; k < tasks[t].fanout.size(); ++k) {
587 const std::unordered_map<std::string, std::size_t>::const_iterator it =
588 task_by_name.find(tasks[t].fanout[k].first);
589 if (it != task_by_name.end())
590 l.fanout[std::make_pair(tidx, it->second)] = tasks[t].fanout[k].second;
591 }
592 for (std::size_t k = 0; k < tasks[t].fanin.size(); ++k) {
593 const std::unordered_map<std::string, std::size_t>::const_iterator it =
594 task_by_name.find(tasks[t].fanin[k].first);
595 if (it != task_by_name.end())
596 l.fanin[std::make_pair(tidx, it->second)] = tasks[t].fanin[k].second;
597 }
598 }
599 }
600 for (std::size_t p = 1; p <= l.nhosts; ++p)
601 for (std::size_t idx = 1; idx <= NT; ++idx)
602 if (l.type[idx] == LqnElement::TASK && l.parent[idx] == p) l.tasksof[p].push_back(idx);
603
604 for (std::size_t e = 0; e < l.nentries; ++e) {
605 const std::size_t idx = l.eshift + e + 1;
606 l.names[idx] = entries[e].name;
607 // An item entry is I:, and carries the item population and its pmf
608 l.nitems[idx] = entries[e].cardinality;
609 l.itemproc[idx] = entries[e].popularity;
610 l.hashnames[idx] = (entries[e].cardinality > 0 ? "I:" : "E:") + entries[e].name;
612 l.has_arrival[idx] = entries[e].has_arrival;
613 l.arrival[idx] = entries[e].arrival;
614 const std::size_t tidx = l.tshift + entries[e].task_slot + 1;
615 l.parent[idx] = tidx;
616 l.graph.set(tidx, idx, one);
617 l.entriesof[tidx].push_back(idx);
618 l.type[idx] = LqnElement::ENTRY;
619 byhash[l.hashnames[idx]] = idx;
620 }
621 for (std::size_t a = 0; a < l.nacts; ++a) {
622 const std::size_t idx = l.ashift + a + 1;
623 l.names[idx] = acts[a].name;
624 l.hashnames[idx] = "A:" + acts[a].name;
625 l.hostdem[idx] = acts[a].hostdem;
626 l.actthink[idx] = acts[a].thinktime;
627 const std::size_t tidx = l.tshift + acts[a].task_slot + 1;
628 l.parent[idx] = tidx;
629 l.actsof[tidx].push_back(idx);
630 l.type[idx] = LqnElement::ACTIVITY;
631 l.actphase[a + 1] = acts[a].phase;
632 byhash[l.hashnames[idx]] = idx;
633 }
634
635 auto find_entry = [&](const std::string& name) -> std::size_t {
636 auto it = byhash.find("E:" + name);
637 if (it != byhash.end()) return it->second;
638 it = byhash.find("I:" + name);
639 return it == byhash.end() ? 0 : it->second;
640 };
641 auto find_act = [&](const std::string& name) -> std::size_t {
642 auto it = byhash.find("A:" + name);
643 return it == byhash.end() ? 0 : it->second;
644 };
645
646 // ---- calls, activity binding and precedences, task by task -------------
647 std::vector<std::pair<std::size_t, std::size_t>> loop_back_edges;
648 std::unordered_map<std::string, std::string> bound_entry_to_act;
649 std::size_t cidx = 0;
650
651 auto add_call = [&](std::size_t src, std::size_t dst_e, CallType ct, const T& mean,
652 const std::string& arrow) {
653 ++cidx;
654 l.callpair_src.push_back(src);
655 l.callpair_dst.push_back(dst_e);
656 l.calltype.push_back(ct);
657 l.callproc_mean.push_back(mean);
658 l.callnames.push_back(l.names[src] + arrow + l.names[dst_e]);
659 l.callhashnames.push_back(l.hashnames[src] + arrow + l.hashnames[dst_e]);
660 };
661 // slot 0 of the call arrays is unused, matching the 1-based element arrays
662 l.callpair_src.push_back(0);
663 l.callpair_dst.push_back(0);
664 l.calltype.push_back(CallType::NONE);
665 l.callproc_mean.push_back(zero);
666 l.callnames.push_back({});
667 l.callhashnames.push_back({});
668
669 for (std::size_t t = 0; t < l.ntasks; ++t) {
670 const std::size_t tidx = l.tshift + t + 1;
671 for (std::size_t a = 0; a < l.nacts; ++a) {
672 if (acts[a].task_slot != t) continue;
673 const std::size_t aidx = l.ashift + a + 1;
674
675 if (!acts[a].bound_to_entry.empty()) {
676 const std::size_t eidx = find_entry(acts[a].bound_to_entry);
677 if (eidx > 0) {
678 l.graph.set(eidx, aidx, one);
679 auto it = bound_entry_to_act.find(acts[a].bound_to_entry);
680 if (it != bound_entry_to_act.end())
681 throw InputError("lqn reader: activities '" + it->second + "' and '" +
682 acts[a].name + "' are both bound to entry '" +
683 acts[a].bound_to_entry + "'");
684 bound_entry_to_act[acts[a].bound_to_entry] = acts[a].name;
685 }
686 }
687
688 for (const auto& c : acts[a].sync_calls) {
689 const std::size_t te = find_entry(c.dest);
690 if (te == 0)
691 throw InputError("lqn reader: activity '" + acts[a].name +
692 "' calls unknown entry '" + c.dest + "'");
693 const std::size_t tt = l.parent[te];
694 if (tidx == tt)
695 throw InputError("lqn reader: an entry on a task cannot call another entry on "
696 "the same task ('" + acts[a].name + "' -> '" + c.dest + "')");
697 add_call(aidx, te, CallType::SYNC, c.mean, "=>");
698 l.callsof[aidx].push_back(cidx);
699 l.iscaller.set(tidx, tt);
700 l.iscaller.set(aidx, tt);
701 l.iscaller.set(tidx, te);
702 l.iscaller.set(aidx, te);
703 l.issynccaller.set(tidx, tt);
704 l.issynccaller.set(aidx, tt);
705 l.issynccaller.set(tidx, te);
706 l.issynccaller.set(aidx, te);
707 l.taskgraph.set(tidx, tt, one);
708 l.graph.set(aidx, te, one);
709 }
710 for (const auto& c : acts[a].async_calls) {
711 const std::size_t te = find_entry(c.dest);
712 if (te == 0)
713 throw InputError("lqn reader: activity '" + acts[a].name +
714 "' has an async call to unknown entry '" + c.dest + "'");
715 const std::size_t tt = l.parent[te];
716 if (tidx == tt)
717 throw InputError("lqn reader: async self-call from '" + acts[a].name + "'");
718 add_call(aidx, te, CallType::ASYNC, c.mean, "->");
719 l.callsof[aidx].push_back(cidx);
720 l.iscaller.set(aidx, tt);
721 l.iscaller.set(aidx, te);
722 l.iscaller.set(tidx, tt);
723 l.iscaller.set(tidx, te);
724 l.isasynccaller.set(tidx, tt);
725 l.isasynccaller.set(tidx, te);
726 l.isasynccaller.set(aidx, tt);
727 l.isasynccaller.set(aidx, te);
728 l.taskgraph.set(tidx, tt, one);
729 l.graph.set(aidx, te, one);
730 }
731 // Routed call groups, resolved from target names to entry indices.
732 // A group with fewer than two of its targets resolvable is not a
733 // dispatch decision and is dropped, which is what the reference
734 // does when it filters the group at layer-build time.
735 for (const auto& g : acts[a].call_groups) {
736 LqnCallGroup grp;
737 grp.caller = aidx;
738 grp.strategy = g.first;
739 for (const std::string& nm : g.second) {
740 const std::size_t te = find_entry(nm);
741 if (te == 0)
742 throw InputError("lqn reader: activity '" + acts[a].name +
743 "' dispatches a call group to unknown entry '" + nm +
744 "'");
745 grp.targets.push_back(te);
746 }
747 if (grp.targets.size() >= 2) l.callgroups.push_back(grp);
748 }
749 }
750
751 for (const auto& pr : tasks[t].precedences) {
752 // Keep the DECLARED precedence: the arc expansion below turns a loop
753 // count into a back-edge probability, which method 'srvn.ph' cannot
754 // invert -- see LqnStruct::precedences
755 {
756 LqnPrecedence<T> kept;
757 kept.pretype = pr.pretype;
758 kept.posttype = pr.posttype;
759 kept.preparams = pr.preparams;
760 kept.postparams = pr.postparams;
761 // An AND-join quorum is declared as has_quorum/quorum, not as a
762 // preparam. The workflow composition reads it from pre_params, and
763 // a partial join is the one thing it must refuse, so carry it.
764 if (pr.pretype == PrecedenceType::PRE_AND && pr.has_quorum &&
765 kept.preparams.empty())
766 kept.preparams.push_back(num_traits<T>::from_double(double(pr.quorum)));
767 bool resolved = true;
768 for (const std::string& nm : pr.preacts) {
769 const std::size_t ai = find_act(nm);
770 if (ai == 0) { resolved = false; break; }
771 kept.preacts.push_back(ai);
772 }
773 for (const std::string& nm : pr.postacts) {
774 const std::size_t ai = find_act(nm);
775 if (ai == 0) { resolved = false; break; }
776 kept.postacts.push_back(ai);
777 }
778 if (resolved) l.precedences[tidx].push_back(kept);
779 }
780 std::size_t quorum_count = 0;
781 if (pr.pretype == PrecedenceType::PRE_AND) {
782 if (pr.preacts.empty())
783 throw InputError("lqn reader: PRE_AND precedence with no pre activities in "
784 "task '" + tasks[t].name + "'");
785 quorum_count = (pr.has_quorum && pr.quorum >= 1 && pr.quorum <= pr.preacts.size())
786 ? pr.quorum
787 : pr.preacts.size();
788 }
789 for (std::size_t pa = 0; pa < pr.preacts.size(); ++pa) {
790 const std::size_t preaidx = find_act(pr.preacts[pa]);
791 if (preaidx == 0)
792 throw InputError("lqn reader: precedence names unknown activity '" +
793 pr.preacts[pa] + "'");
794 switch (pr.posttype) {
795 case PrecedenceType::POST_OR:
796 for (std::size_t po = 0; po < pr.postacts.size(); ++po) {
797 const std::size_t postaidx = find_act(pr.postacts[po]);
798 l.graph.set(preaidx, postaidx, pr.postparams[po]);
799 l.actpretype[preaidx] = pr.pretype;
800 l.actposttype[postaidx] = pr.posttype;
801 }
802 break;
803 case PrecedenceType::POST_AND:
804 for (std::size_t po = 0; po < pr.postacts.size(); ++po) {
805 const std::size_t postaidx = find_act(pr.postacts[po]);
806 l.graph.set(preaidx, postaidx, one);
807 l.actpretype[preaidx] = pr.pretype;
808 l.actposttype[postaidx] = pr.posttype;
809 }
810 break;
811 case PrecedenceType::POST_LOOP: {
812 // postacts = [body..., end]; postparams[0] is the count
813 const T counts = pr.postparams.empty() ? one : pr.postparams[0];
814 const std::size_t enda = pr.postacts.size() - 1;
815 const std::size_t loopentry = find_act(pr.preacts[0]);
816 const std::size_t loopstart = find_act(pr.postacts[0]);
817 const std::size_t loopend = find_act(pr.postacts[enda]);
818 if (counts < one) {
819 l.graph.set(loopentry, loopstart, counts);
820 l.graph.set(loopentry, loopend, T(one - counts));
821 std::size_t cur = loopstart;
822 for (std::size_t po = 1; po + 1 < pr.postacts.size(); ++po) {
823 const std::size_t pi = find_act(pr.postacts[po]);
824 l.graph.set(cur, pi, one);
825 l.actposttype[pi] = pr.posttype;
826 cur = pi;
827 }
828 l.graph.set(cur, loopend, one);
829 l.actposttype[loopstart] = pr.posttype;
830 } else {
831 std::size_t cur = loopentry;
832 for (std::size_t po = 0; po + 1 < pr.postacts.size(); ++po) {
833 const std::size_t pi = find_act(pr.postacts[po]);
834 l.graph.set(cur, pi, one);
835 l.actposttype[pi] = pr.posttype;
836 cur = pi;
837 }
838 loop_back_edges.emplace_back(cur, loopstart);
839 l.graph.set(cur, loopstart, T(one - one / counts));
840 l.graph.set(cur, loopend, T(one / counts));
841 }
842 l.actposttype[loopend] = pr.posttype;
843 break;
844 }
845 default:
846 for (std::size_t po = 0; po < pr.postacts.size(); ++po) {
847 const std::size_t postaidx = find_act(pr.postacts[po]);
848 if (postaidx == 0)
849 throw InputError("lqn reader: precedence names unknown activity '" +
850 pr.postacts[po] + "'");
851 l.graph.set(preaidx, postaidx, one);
852 l.actpretype[preaidx] = pr.pretype;
853 l.actposttype[postaidx] = pr.posttype;
854 if (quorum_count > 0) l.actquorum[postaidx] = quorum_count;
855 }
856 break;
857 }
858 }
859 }
860 }
861
862 // ---- forwarding calls, after every ordinary call ------------------------
863 for (std::size_t e = 0; e < l.nentries; ++e) {
864 const std::size_t eidx = l.eshift + e + 1;
865 const std::size_t src_t = l.parent[eidx];
866 for (std::size_t f = 0; f < entries[e].fwd_dest.size(); ++f) {
867 const std::size_t te = find_entry(entries[e].fwd_dest[f]);
868 if (te == 0)
869 throw InputError("lqn reader: entry '" + entries[e].name +
870 "' forwards to unknown entry '" + entries[e].fwd_dest[f] + "'");
871 if (l.parent[te] == src_t)
872 throw InputError("lqn reader: entry '" + entries[e].name +
873 "' forwards to an entry on the same task");
874 add_call(eidx, te, CallType::FWD, entries[e].fwd_prob[f], "~>");
875 l.taskgraph.set(src_t, l.parent[te], one);
876 l.graph.set(eidx, te, one);
877 }
878 }
879 l.ncalls = cidx;
880
881 // ---- admission constraints, once tasksof/entriesof exist ---------------
882 // getStruct.m:221-256. The columns of a host's constraint are its tasks and
883 // of a task's its entries, so neither can be resolved before those lists.
884 l.lincon_A.assign(l.tshift + l.ntasks + 1, Matrix<T>());
885 l.lincon_b.assign(l.tshift + l.ntasks + 1, std::vector<T>());
886 for (std::size_t cidx2 = 1; cidx2 <= l.tshift + l.ntasks; ++cidx2) {
887 const bool ishost = cidx2 <= l.nhosts;
888 const std::vector<std::size_t>& colIdx =
889 ishost ? l.tasksof[cidx2] : l.entriesof[cidx2];
890 const char* colwhat = ishost ? "tasks on this host" : "entries of this task";
891 const Matrix<T>* rawA = nullptr;
892 const std::vector<T>* rawb = nullptr;
893 const std::vector<detail::RawLinConRow<T>>* rows = nullptr;
894 if (ishost) {
895 typename std::map<std::size_t, std::vector<detail::RawLinConRow<T>>>::const_iterator
896 it = m.proc_linconrows.find(cidx2 - 1);
897 if (it != m.proc_linconrows.end()) rows = &it->second;
898 typename std::map<std::size_t,
899 std::pair<Matrix<T>, std::vector<T>>>::const_iterator ip =
900 m.proc_lincon.find(cidx2 - 1);
901 if (ip != m.proc_lincon.end()) {
902 rawA = &ip->second.first;
903 rawb = &ip->second.second;
904 }
905 } else {
906 const detail::RawTask<T>& rt = tasks[cidx2 - l.tshift - 1];
907 rows = &rt.linconrows;
908 rawA = &rt.lincon_A;
909 rawb = &rt.lincon_b;
910 }
911 const std::size_t ncols = colIdx.size();
912 std::vector<std::vector<T>> Arows;
913 std::vector<T> brows;
914 if (rawA != nullptr && rawA->rows() > 0) {
915 if (rawA->cols() != ncols)
916 throw InputError("lqn reader: admission constraint on '" + l.names[cidx2] +
917 "' has " + std::to_string(rawA->cols()) + " columns but there are " +
918 std::to_string(ncols) + " " + colwhat);
919 for (std::size_t k = 0; k < rawA->rows(); ++k) {
920 std::vector<T> row(ncols, zero);
921 for (std::size_t j = 0; j < ncols; ++j) row[j] = (*rawA)(k, j);
922 Arows.push_back(row);
923 brows.push_back(k < rawb->size() ? (*rawb)[k] : zero);
924 }
925 }
926 if (rows != nullptr) {
927 for (std::size_t r = 0; r < rows->size(); ++r) {
928 std::vector<T> row(ncols, zero);
929 for (std::size_t k = 0; k < (*rows)[r].names.size(); ++k) {
930 std::size_t pos = ncols;
931 for (std::size_t j = 0; j < ncols; ++j)
932 if (l.names[colIdx[j]] == (*rows)[r].names[k]) pos = j;
933 if (pos == ncols)
934 throw InputError("lqn reader: admission constraint on '" + l.names[cidx2] +
935 "' names '" + (*rows)[r].names[k] +
936 "', which is not one of the " + colwhat);
937 row[pos] = T(row[pos] + (*rows)[r].coeffs[k]);
938 }
939 Arows.push_back(row);
940 brows.push_back((*rows)[r].cap);
941 }
942 }
943 if (Arows.empty()) continue;
944 Matrix<T> A(Arows.size(), ncols, zero);
945 for (std::size_t k = 0; k < Arows.size(); ++k)
946 for (std::size_t j = 0; j < ncols; ++j) A(k, j) = Arows[k][j];
947 l.lincon_A[cidx2] = A;
948 l.lincon_b[cidx2] = brows;
949 }
950
951 // ---- compatibility pools, once tasksof/entriesof exist ------------------
952 // A pool names the operands it may serve, and the operands of an element are
953 // its tasks (a host) or its entries (a task), so the names cannot become
954 // columns before those lists exist. Same staging as the constraints above.
955 for (std::size_t pidx = 1; pidx <= l.tshift + l.ntasks; ++pidx) {
956 const bool ishost = pidx <= l.nhosts;
957 const std::vector<std::size_t>& colIdx =
958 ishost ? l.tasksof[pidx] : l.entriesof[pidx];
959 const char* colwhat = ishost ? "tasks on this host" : "entries of this task";
960 const std::vector<detail::RawServerPool<T>>* raw = nullptr;
961 if (ishost) {
962 typename std::map<std::size_t,
963 std::vector<detail::RawServerPool<T>>>::const_iterator it =
964 m.proc_pools.find(pidx - 1);
965 if (it != m.proc_pools.end()) raw = &it->second;
966 } else {
967 raw = &tasks[pidx - l.tshift - 1].pools;
968 }
969 if (raw == nullptr || raw->empty()) continue;
970 const std::size_t ncols = colIdx.size();
971 if (ncols == 0)
972 throw InputError("lqn reader: server pools on '" + l.names[pidx] +
973 "' but the element has no operand to serve");
975 sp.compat = Matrix<T>(raw->size(), ncols, zero);
976 for (std::size_t t2 = 0; t2 < raw->size(); ++t2) {
977 const detail::RawServerPool<T>& rp = (*raw)[t2];
978 sp.names.push_back(rp.name);
979 sp.counts.push_back(rp.count);
980 sp.rates.push_back(rp.rate);
981 for (std::size_t k = 0; k < rp.compatible.size(); ++k) {
982 bool found = false;
983 for (std::size_t j = 0; j < ncols; ++j) {
984 if (l.names[colIdx[j]] == rp.compatible[k]) {
985 sp.compat(t2, j) = one;
986 found = true;
987 break;
988 }
989 }
990 if (!found)
991 throw InputError("lqn reader: server pool '" + rp.name + "' on '" +
992 l.names[pidx] + "' names '" + rp.compatible[k] +
993 "', which is not one of the " + colwhat);
994 }
995 }
996 // A pool nobody can reach is a declaration error, not a zero column to
997 // carry: the operand would be served at rate zero and never complete.
998 for (std::size_t j = 0; j < ncols; ++j) {
999 bool served = false;
1000 for (std::size_t t2 = 0; t2 < sp.npools(); ++t2)
1001 if (sp.compat(t2, j) != zero) served = true;
1002 if (!served)
1003 throw InputError("lqn reader: '" + l.names[colIdx[j]] + "' on '" + l.names[pidx] +
1004 "' is compatible with no server pool, so it can never be served");
1005 }
1006 l.pools[pidx] = sp;
1007 }
1008
1009 // ---- every entry must have a bound activity ---------------------------
1010 for (std::size_t e = 1; e <= l.nentries; ++e) {
1011 const std::size_t eidx = l.eshift + e;
1012 bool bound = false;
1013 for (std::size_t s : l.graph.succ(eidx))
1014 if (s > l.ashift) bound = true;
1015 // the message is getStruct.m's, verbatim: this refusal is pinned to the
1016 // same wording in MATLAB, the JAR and python, so a harness can compare it
1017 if (!bound) throw InputError("An entry does not have any boundTo activity.");
1018 }
1019
1020 // ---- a replying activity must have no PHASE 1 successor ---------------
1021 // getStruct.m's guard, absent from this port until 2026-08-15. An activity
1022 // that replies ends phase 1 of its entry, so a successor still marked phase
1023 // 1 is a graph the struct cannot represent: the reply would be read as the
1024 // end of the entry and the tail served as though it did not exist. A phase
1025 // 2 successor is the legitimate case, post-reply processing.
1026 for (std::size_t e = 0; e < entries.size(); ++e) {
1027 for (const std::string& rname : entries[e].reply_activities) {
1028 const std::size_t aidx = find_act(rname);
1029 if (aidx == 0) continue;
1030 for (std::size_t succ : l.graph.succ(aidx)) {
1031 if (succ <= l.ashift) continue;
1032 if (l.actphase[succ - l.ashift] == 1)
1033 throw InputError("Unsupported replyTo in non-terminal activity.");
1034 }
1035 }
1036 }
1037
1038 // infinite-server multiplicity correction rationale: see _kb/04-networkstruct.md (cpp port notes)
1039 for (std::size_t tidx = 1; tidx <= NT; ++tidx) {
1040 if (l.sched[tidx] != SchedStrategy::INF) continue;
1041 if (l.type[tidx] != LqnElement::TASK) continue;
1042 double s = 0.0;
1043 for (std::size_t c = 1; c <= NT; ++c)
1044 if (l.taskgraph.get(c, tidx) != zero) s += l.mult[c];
1045 l.mult[tidx] = s;
1046 }
1047
1048 for (std::size_t idx = 1; idx <= NT; ++idx) l.isref[idx] = l.sched[idx] == SchedStrategy::REF;
1049
1050 // ---- the dag ----------------------------------------------------------
1051 l.dag = l.graph;
1052 for (std::size_t i = 1; i <= N; ++i) {
1053 if (l.type[i] != LqnElement::TASK || l.isref[i]) continue;
1054 std::vector<std::size_t> to_flip;
1055 for (const auto& e : l.dag.row[i])
1056 if (l.type[e.first] == LqnElement::ENTRY && e.second != zero)
1057 to_flip.push_back(e.first);
1058 for (std::size_t j : to_flip) {
1059 l.dag.erase(i, j);
1060 l.dag.set(j, i, one);
1061 }
1062 }
1063 for (const auto& be : loop_back_edges) l.dag.erase(be.first, be.second);
1064
1065 // ---- entry-to-activity reachability -----------------------------------
1066 for (std::size_t e = 1; e <= l.nentries; ++e) {
1067 const std::size_t eidx = l.eshift + e;
1068 const std::size_t tidx = l.parent[eidx];
1069 std::vector<bool> visited(N + 1, false);
1070 std::vector<std::size_t> stack{eidx};
1071 visited[eidx] = true;
1072 while (!stack.empty()) {
1073 const std::size_t v = stack.back();
1074 stack.pop_back();
1075 for (std::size_t w : l.graph.succ(v))
1076 if (!visited[w]) {
1077 visited[w] = true;
1078 stack.push_back(w);
1079 }
1080 }
1081 std::vector<std::size_t> found;
1082 for (std::size_t i = 1; i <= N; ++i)
1083 if (visited[i] && l.type[i] == LqnElement::ACTIVITY && l.parent[i] == tidx)
1084 found.push_back(i);
1085 l.actsof[eidx] = found;
1086 }
1087
1088 // ---- sustainable multiplicities ---------------------------------------
1089 {
1091 in.dag = Matrix<double>(N, N, 0.0);
1092 for (std::size_t i = 1; i <= N; ++i)
1093 for (const auto& ed : l.dag.row[i])
1094 if (ed.second != zero) in.dag(i - 1, ed.first - 1) = 1.0;
1095 in.mult.resize(N);
1096 in.type.resize(N);
1097 in.isref.assign(N, false);
1098 in.entry_has_arrival.assign(N, false);
1099 // A SETUP TASK KEEPS ITS SPARE CAPACITY. lsn_max_multiplicity.m:69-72
1100 // exempts it from the min against its inflow, because the servers a
1101 // caller cannot keep busy are exactly the ones that power down and pay
1102 // the setup -- trimming them away deletes the effect being modelled.
1103 // Leaving this vector empty silently built every setup layer with one
1104 // server, which is the whole layer, not a detail of it.
1105 in.hassetup.assign(N, false);
1106 for (std::size_t i = 1; i <= N; ++i) {
1107 const double m = i <= NT ? l.mult[i] : std::numeric_limits<double>::infinity();
1108 in.mult[i - 1] = std::isinf(m) ? lsn::Multiplicity<double>::inf()
1110 switch (l.type[i]) {
1111 case LqnElement::HOST: in.type[i - 1] = lsn::LsnElementType::HOST; break;
1112 case LqnElement::TASK: in.type[i - 1] = lsn::LsnElementType::TASK; break;
1113 case LqnElement::ENTRY: in.type[i - 1] = lsn::LsnElementType::ENTRY; break;
1114 default: in.type[i - 1] = lsn::LsnElementType::ACTIVITY; break;
1115 }
1116 if (i <= NT) in.isref[i - 1] = l.isref[i];
1117 if (i <= NT) in.hassetup[i - 1] = l.hassetup[i];
1118 in.entry_has_arrival[i - 1] = l.has_arrival[i];
1119 }
1120 const std::vector<lsn::Multiplicity<double>> mm = lsn::lsn_max_multiplicity(in);
1121 for (std::size_t i = 1; i <= NT; ++i)
1122 l.maxmult[i] = mm[i - 1].infinite ? std::numeric_limits<double>::infinity()
1123 : mm[i - 1].value;
1124 }
1125
1126 // ---- an entry must not be called both synchronously and asynchronously --
1127 for (std::size_t e = 1; e <= l.nentries; ++e) {
1128 const std::size_t eidx = l.eshift + e;
1129 bool sync = false, async = false;
1130 for (std::size_t c = 1; c <= l.ncalls; ++c) {
1131 if (l.callpair_dst[c] != eidx) continue;
1132 if (l.calltype[c] == CallType::SYNC) sync = true;
1133 if (l.calltype[c] == CallType::ASYNC) async = true;
1134 }
1135 if (sync && async)
1136 throw InputError("lqn reader: entry '" + l.names[eidx] +
1137 "' is called both synchronously and asynchronously");
1138 }
1139
1140 return l;
1141}
1142
1143/**
1144 * Read a .lqnx model into the INTERMEDIATE form, before getStruct flattens it.
1145 *
1146 * `read_lqnx` is this followed by `lqn_finalize`, and is what a solver wants.
1147 * The intermediate form is what a WRITER wants: `write_lqnx` (lqn_writer.h)
1148 * emits declarations -- precedence blocks, reply entries, fan-out -- that the
1149 * struct records only in flattened form, so a round trip through the struct
1150 * alone could not reproduce the document. SolverLQNS hands the file it writes
1151 * to an external binary, which will reject a document whose precedence blocks
1152 * were guessed, so the declarative form is not optional there.
1153 *
1154 * @param path file to read
1155 * @return the intermediate model, exactly as the document declares it
1156 */
1157namespace detail {
1158
1159/** Renders a number as the MATLAB, JAR and Python readers do, so messages agree. */
1160inline std::string fmt_num(double v) {
1161 char buf[32];
1162 std::snprintf(buf, sizeof(buf), "%g", v);
1163 return std::string(buf);
1164}
1165
1166/** Reads a numeric attribute; NaN when the text is present but not a number. */
1167inline double attr_num(const std::string& s, double dflt) {
1168 if (s.empty()) return dflt;
1169 try {
1170 return std::stod(s);
1171 } catch (const std::exception&) {
1172 return std::numeric_limits<double>::quiet_NaN();
1173 }
1174}
1175
1176/** Case-insensitive equality, for the scheduling attribute. */
1177inline bool iequals(const std::string& a, const std::string& b) {
1178 if (a.size() != b.size()) return false;
1179 for (std::size_t i = 0; i < a.size(); ++i)
1180 if (std::tolower(static_cast<unsigned char>(a[i])) !=
1181 std::tolower(static_cast<unsigned char>(b[i])))
1182 return false;
1183 return true;
1184}
1185
1186/** `Activity.setCallOrder`: STOCHASTIC or DETERMINISTIC in any case, anything else STOCHASTIC. */
1187inline std::string call_order_from_text(const std::string& s) {
1188 if (iequals(s, "DETERMINISTIC")) return "DETERMINISTIC";
1189 return "STOCHASTIC";
1190}
1191
1192/**
1193 * Reject a structurally inconsistent LQN document.
1194 *
1195 * Run on the parsed document before any object is built, so that a defective
1196 * input is named at its source instead of surfacing as a downstream failure.
1197 * The same checks, in the same order and with the same messages, are applied by
1198 * the MATLAB, JAR and Python readers.
1199 *
1200 * @param doc root element of the parsed document
1201 */
1202inline void validate_input_model(const xml::Element& doc) {
1203 const double tol = 1e-6;
1204 std::vector<std::string> proc_names;
1205 std::vector<std::string> task_names;
1206 std::vector<std::string> entry_names;
1207 std::vector<std::string> entry_owner; // task owning entry_names[k]
1208 std::vector<bool> is_ref_entry;
1209 std::vector<std::string> call_dests;
1210 std::vector<std::string> reply_entries;
1211 bool has_ref_task = false;
1212 bool has_open_arrival = false;
1213
1214 for (const xml::Element* pe : doc.by_tag("processor")) {
1215 const std::string proc_name = pe->attr("name");
1216 if (std::find(proc_names.begin(), proc_names.end(), proc_name) != proc_names.end())
1217 throw InputError("Duplicate processor name \"" + proc_name + "\".");
1218 proc_names.push_back(proc_name);
1219
1220 for (const xml::Element* te : pe->by_tag("task")) {
1221 const std::string task_name = te->attr("name");
1222 if (std::find(task_names.begin(), task_names.end(), task_name) != task_names.end())
1223 throw InputError("Duplicate task name \"" + task_name + "\".");
1224 task_names.push_back(task_name);
1225 const bool is_ref = iequals(te->attr("scheduling"), "ref");
1226 has_ref_task = has_ref_task || is_ref;
1227
1228 const std::vector<const xml::Element*> entry_els = te->by_tag("entry");
1229 if (entry_els.empty())
1230 throw InputError("Task \"" + task_name + "\" has no entries.");
1231 for (const xml::Element* ee : entry_els) {
1232 const std::string entry_name = ee->attr("name");
1233 if (std::find(entry_names.begin(), entry_names.end(), entry_name) !=
1234 entry_names.end())
1235 throw InputError("Duplicate entry name \"" + entry_name + "\".");
1236 entry_names.push_back(entry_name);
1237 entry_owner.push_back(task_name);
1238 is_ref_entry.push_back(is_ref);
1239
1240 const double arrival_rate =
1241 attr_num(ee->attr("open-arrival-rate"), std::numeric_limits<double>::quiet_NaN());
1242 if (arrival_rate > 0.0) {
1243 has_open_arrival = true;
1244 if (is_ref)
1245 throw InputError("Entry \"" + entry_name + "\" belongs to reference task \"" +
1246 task_name + "\" and cannot have open arrivals.");
1247 }
1248
1249 const std::vector<const xml::Element*> fwd_els = ee->by_tag("forwarding");
1250 if (is_ref && !fwd_els.empty())
1251 throw InputError("Entry \"" + entry_name + "\" belongs to reference task \"" +
1252 task_name + "\" and cannot forward requests.");
1253 double fwd_total = 0.0;
1254 for (const xml::Element* fe : fwd_els) {
1255 const double prob = attr_num(fe->attr("prob"), 1.0);
1256 if (std::isnan(prob) || prob < 0.0 || prob > 1.0)
1257 throw InputError("Forwarding from entry \"" + entry_name + "\" to entry \"" +
1258 fe->attr("dest") + "\" has an invalid probability of " +
1259 fmt_num(prob) + ".");
1260 fwd_total += prob;
1261 }
1262 if (fwd_total > 1.0 + tol)
1263 throw InputError("Entry \"" + entry_name +
1264 "\" has a total forwarding probability of " +
1265 fmt_num(fwd_total) + ".");
1266 }
1267
1268 // activity names are unique within their task; a name under a pre or post list is a reference, not a declaration
1269 std::vector<std::string> act_names;
1270 for (const xml::Element* ae : te->by_tag("activity")) {
1271 if (ae->parent == nullptr) continue;
1272 if (ae->parent->name != "task-activities" &&
1273 ae->parent->name != "entry-phase-activities")
1274 continue;
1275 const std::string act_name = ae->attr("name");
1276 if (std::find(act_names.begin(), act_names.end(), act_name) != act_names.end())
1277 throw InputError("Duplicate activity name \"" + act_name + "\" in task \"" +
1278 task_name + "\".");
1279 act_names.push_back(act_name);
1280 }
1281
1282 for (const xml::Element* ce : te->by_tag("synch-call"))
1283 call_dests.push_back(ce->attr("dest"));
1284 for (const xml::Element* ce : te->by_tag("asynch-call"))
1285 call_dests.push_back(ce->attr("dest"));
1286 for (const xml::Element* fe : te->by_tag("forwarding"))
1287 call_dests.push_back(fe->attr("dest"));
1288
1289 for (const xml::Element* oe : te->by_tag("post-OR")) {
1290 double branch_total = 0.0;
1291 for (const xml::Element* be : oe->by_tag("activity")) {
1292 const double prob = attr_num(be->attr("prob"), 1.0);
1293 if (std::isnan(prob) || prob < 0.0 || prob > 1.0)
1294 throw InputError("Activity \"" + be->attr("name") + "\" in task \"" +
1295 task_name + "\" has an invalid branch probability of " +
1296 fmt_num(prob) + ".");
1297 branch_total += prob;
1298 }
1299 if (std::fabs(branch_total - 1.0) > tol)
1300 throw InputError("Branch probabilities of an OR-fork in task \"" + task_name +
1301 "\" sum to " + fmt_num(branch_total) + " instead of 1.");
1302 }
1303
1304 for (const xml::Element* re : te->by_tag("reply-entry"))
1305 reply_entries.push_back(re->attr("name"));
1306 }
1307 }
1308
1309 for (const std::string& dest : call_dests) {
1310 const std::vector<std::string>::const_iterator it =
1311 std::find(entry_names.begin(), entry_names.end(), dest);
1312 if (it == entry_names.end()) continue;
1313 const std::size_t idx = static_cast<std::size_t>(it - entry_names.begin());
1314 if (is_ref_entry[idx])
1315 throw InputError("Entry \"" + entry_names[idx] + "\" belongs to reference task \"" +
1316 entry_owner[idx] + "\" and cannot receive requests.");
1317 }
1318
1319 for (const std::string& reply_name : reply_entries) {
1320 const std::vector<std::string>::const_iterator it =
1321 std::find(entry_names.begin(), entry_names.end(), reply_name);
1322 if (it == entry_names.end()) continue;
1323 const std::size_t idx = static_cast<std::size_t>(it - entry_names.begin());
1324 if (is_ref_entry[idx])
1325 throw InputError("Entry \"" + entry_names[idx] + "\" belongs to reference task \"" +
1326 entry_owner[idx] + "\" and cannot be replied to.");
1327 }
1328
1329 if (!has_ref_task && !has_open_arrival)
1330 throw InputError("The model has no reference task and no open arrivals.");
1331}
1332
1333} // namespace detail
1334
1335template <class T>
1336LqnModel<T> read_lqnx_model(const std::string& path) {
1337 const T one = num_traits<T>::from_int(1);
1338 LqnModel<T> m;
1339 std::vector<detail::RawProc>& procs = m.procs;
1340 std::vector<detail::RawTask<T>>& tasks = m.tasks;
1341 std::vector<detail::RawEntry<T>>& entries = m.entries;
1342 std::vector<detail::RawActivity<T>>& acts = m.acts;
1343
1344
1345 std::unique_ptr<xml::Element> doc = xml::parse_file(path);
1346 detail::validate_input_model(*doc);
1347
1348 // Stage 1: parseXML
1349
1350 const std::vector<const xml::Element*> proc_els = doc->by_tag("processor");
1351 for (const xml::Element* pe : proc_els) {
1352 detail::RawProc pr;
1353 pr.name = pe->attr("name");
1354 const std::string psched = pe->attr("scheduling");
1355 pr.sched = lang::sched_from_lqnx(psched.empty() ? std::string("fcfs") : psched);
1356 pr.repl = dbl_from_decimal(pe->attr("replication"), 1.0);
1357 pr.speed_factor = dbl_from_decimal(pe->attr("speed-factor"), 1.0);
1358 pr.quantum = dbl_from_decimal(pe->attr("quantum"), 0.0);
1359 if (pr.sched == SchedStrategy::INF) {
1360 // A finite multiplicity on an inf-scheduled processor is discarded,
1361 // as in MATLAB, which warns and overrides it.
1362 pr.mult = std::numeric_limits<double>::infinity();
1363 } else {
1364 pr.mult = dbl_from_decimal(pe->attr("multiplicity"), 1.0);
1365 }
1366 const std::size_t proc_slot = procs.size();
1367 procs.push_back(pr);
1368
1369 for (const xml::Element* te : pe->by_tag("task")) {
1370 detail::RawTask<T> tk;
1371 tk.name = te->attr("name");
1372 const std::string tsched = te->attr("scheduling");
1373 tk.sched = lang::sched_from_lqnx(tsched.empty() ? std::string("fcfs") : tsched);
1374 tk.repl = dbl_from_decimal(te->attr("replication"), 1.0);
1375 // `priority` is Task.setPriority, read as parseXML.m reads it: a non-numeric value is ignored
1376 const double prio_d = dbl_from_decimal(te->attr("priority"), 0.0);
1377 if (!std::isnan(prio_d)) tk.priority = static_cast<int>(prio_d);
1378 if (tk.sched == SchedStrategy::INF) {
1379 tk.mult = std::numeric_limits<double>::infinity();
1380 } else {
1381 tk.mult = dbl_from_decimal(te->attr("multiplicity"), 1.0);
1382 }
1383 const std::string think_s = te->attr("think-time");
1384 const double think_d = dbl_from_decimal(think_s, 0.0);
1385 tk.thinktime = think_d > 0.0 ? Distrib<T>::exp_mean(num_from_decimal<T>(think_s))
1387 tk.proc_slot = proc_slot;
1388 // fan-out names a callee task, fan-in a caller task; both carry the
1389 // count of peer replicas one replica of THIS task addresses. Stored
1390 // by name because the callee's element index does not exist yet.
1391 for (const xml::Element* fe : te->by_tag("fan-out"))
1392 tk.fanout.push_back(
1393 std::make_pair(fe->attr("dest"), dbl_from_decimal(fe->attr("value"), 1.0)));
1394 for (const xml::Element* fe : te->by_tag("fan-in"))
1395 tk.fanin.push_back(
1396 std::make_pair(fe->attr("source"), dbl_from_decimal(fe->attr("value"), 1.0)));
1397 // <setup> and <delay-off> are a LINE extension to the schema that
1398 // MATLAB, the JAR and Python all write and read. Skipping them here
1399 // dropped the cold start entirely: lqn_setup came back with the bare
1400 // host demand at the entry (E2 RespT 0.3333 against 1) because
1401 // `hassetup` stayed false and `setup_charge` returned 0.
1402 // child_tags and not by_tag: parseXML.m reads them as DIRECT
1403 // children, and a descendant search would read a nested task's.
1404 for (const xml::Element* se : te->child_tags("setup"))
1405 tk.setuptime = detail::setup_time<T>(se->attr("mean"), se->attr("scv"));
1406 for (const xml::Element* se : te->child_tags("delay-off"))
1407 tk.delayofftime = detail::setup_time<T>(se->attr("mean"), se->attr("scv"));
1408 // <cache> is the LINE extension that makes this a CacheTask, exactly
1409 // as in parseXML.m and the JAR's readXML: `items` is the item
1410 // population, each <level> one cache list. IGNORING IT DOES NOT
1411 // DEGRADE THE MODEL GRACEFULLY -- with `nitems` left at 0 the layer
1412 // builder never marks the host layer a cache layer, so no Cache node
1413 // is added and the hit/miss branch keeps the even split `link()`
1414 // offers, independent of capacity, item count and replacement rule
1415 // (lcq_threehosts came back with hit 0.5 against 0.48331).
1416 for (const xml::Element* ce : te->child_tags("cache")) {
1417 const std::string items_s = ce->attr("items");
1418 const double items_d = dbl_from_decimal(items_s, 1.0);
1419 tk.nitems = items_d > 0.0 ? static_cast<std::size_t>(items_d) : 1;
1420 tk.replacestrat = detail::replacement_from_lqnx(ce->attr("replacement"));
1421 // `retrieval="true"` is CacheTask.setRetrieval, read as parseXML.m reads it
1422 const std::string rs = ce->attr("retrieval");
1423 tk.retrieval = detail::iequals(rs, "true") || rs == "1";
1424 tk.itemcap.clear();
1425 for (const xml::Element* le : ce->child_tags("level")) {
1426 const double cap_d = dbl_from_decimal(le->attr("capacity"), 1.0);
1427 tk.itemcap.push_back(cap_d > 0.0 ? static_cast<int>(cap_d) : 1);
1428 }
1429 // A <cache> with no <level> is a single list of one item, the
1430 // same reading the JAR takes; the capacity is what the cache
1431 // HOLDS, so an empty vector would make it hold nothing.
1432 if (tk.itemcap.empty()) tk.itemcap.push_back(1);
1433 }
1434 const std::size_t task_slot = tasks.size();
1435 tasks.push_back(tk);
1436
1437 for (const xml::Element* ee : te->by_tag("entry")) {
1438 detail::RawEntry<T> en;
1439 en.name = ee->attr("name");
1440 en.task_slot = task_slot;
1441 const std::string arr_s = ee->attr("open-arrival-rate");
1442 if (!arr_s.empty()) {
1443 const double rate_d = dbl_from_decimal(arr_s, 0.0);
1444 if (rate_d > 0.0) {
1445 en.has_arrival = true;
1446 en.arrival = Distrib<T>::exp_rate(num_from_decimal<T>(arr_s));
1447 }
1448 }
1449 for (const xml::Element* fe : ee->by_tag("forwarding")) {
1450 en.fwd_dest.push_back(fe->attr("dest"));
1451 const std::string ps = fe->attr("prob");
1452 en.fwd_prob.push_back(ps.empty() ? one : num_from_decimal<T>(ps));
1453 }
1454 // <item-entry> is what makes this an ItemEntry, the twin of the
1455 // <cache> test on the task above; the cache layer reads the item
1456 // pmf off `lqn.itemproc`, so dropping it leaves the read
1457 // unpopulated even when the cache task itself was recognised.
1458 for (const xml::Element* ie2 : ee->child_tags("item-entry")) {
1459 const double card_d = dbl_from_decimal(ie2->attr("cardinality"), 1.0);
1460 en.cardinality = card_d > 0.0 ? static_cast<std::size_t>(card_d) : 1;
1461 en.popularity = detail::popularity_from_lqnx<T>(ie2, en.cardinality);
1462 }
1463 const std::size_t entry_slot = entries.size();
1464 entries.push_back(en);
1465
1466 const std::vector<const xml::Element*> epa = ee->by_tag("entry-phase-activities");
1467 if (!epa.empty()) {
1468 // phase-indexed activity-name rationale: see _kb/04-networkstruct.md (cpp port notes)
1469 std::map<int, std::string> by_phase;
1470 for (const xml::Element* ae : epa[0]->by_tag("activity")) {
1471 const int phase = static_cast<int>(dbl_from_decimal(ae->attr("phase"), 1.0));
1472 detail::RawActivity<T> ac;
1473 ac.name = ae->attr("name");
1474 ac.hostdem = detail::host_demand<T>(ae->attr("host-demand-mean"),
1475 ae->attr("host-demand-cvsq"));
1476 const std::string att = ae->attr("think-time");
1477 const double att_d = dbl_from_decimal(att, 0.0);
1478 ac.thinktime = att_d > 0.0
1481 ac.bound_to_entry = phase == 1 ? entries[entry_slot].name : std::string();
1482 ac.phase = phase;
1483 ac.call_order = detail::call_order_from_text(ae->attr("call-order"));
1484 ac.task_slot = task_slot;
1485 for (const xml::Element* ce : ae->by_tag("synch-call"))
1486 ac.sync_calls.push_back(
1487 {ce->attr("dest"), num_from_decimal<T>(ce->attr("calls-mean"))});
1488 for (const xml::Element* ce : ae->by_tag("asynch-call"))
1489 ac.async_calls.push_back(
1490 {ce->attr("dest"), num_from_decimal<T>(ce->attr("calls-mean"))});
1491 detail::read_call_groups<T>(ae, ac);
1492 by_phase[phase] = ac.name;
1493 acts.push_back(ac);
1494 }
1495 // An entry declaring only later phases (LQNS phase-2-only entries) gets an Immediate phase-1
1496 // activity '<entry>_ph1', bound to the entry and replying at once, appended after the declared
1497 // ones as MATLAB parseXML does; the later phases then run after the reply
1498 // If the document already declares an activity of that name, the first free '<entry>_ph1_<n>' is used
1499 if (!by_phase.empty() && !by_phase.count(1)) {
1500 const std::string& en = entries[entry_slot].name;
1501 std::vector<std::string> taken;
1502 for (const xml::Element* de : doc->by_tag("activity")) taken.push_back(de->attr("name"));
1503 std::string ph1 = en + "_ph1";
1504 int suffix = 0;
1505 while (std::find(taken.begin(), taken.end(), ph1) != taken.end())
1506 ph1 = en + "_ph1_" + std::to_string(++suffix);
1507 if (suffix > 0)
1508 std::fprintf(stderr, "[LINE] Warning: parseXML: Entry %s declares no phase-1 activity and %s_ph1 is "
1509 "already an activity; its synthetic phase-1 activity is named %s.\n",
1510 en.c_str(), en.c_str(), ph1.c_str());
1511 detail::RawActivity<T> ac;
1512 ac.name = ph1;
1513 ac.hostdem = Distrib<T>::immediate();
1514 ac.thinktime = Distrib<T>::immediate();
1515 ac.bound_to_entry = entries[entry_slot].name;
1516 ac.phase = 1;
1517 ac.task_slot = task_slot;
1518 by_phase[1] = ac.name;
1519 acts.push_back(ac);
1520 }
1521 // implicit precedence between consecutive DECLARED phases (90-A01: e1_ph2 -> e1_ph3)
1522 for (auto it = by_phase.begin(); it != by_phase.end(); ++it) {
1523 auto nx = std::next(it);
1524 if (nx == by_phase.end()) break;
1525 detail::RawPrecedence<T> pr;
1526 pr.pretype = PrecedenceType::PRE_SEQ;
1527 pr.posttype = PrecedenceType::POST_SEQ;
1528 pr.preacts.push_back(it->second);
1529 pr.postacts.push_back(nx->second);
1530 tasks[task_slot].precedences.push_back(pr);
1531 }
1532 if (!by_phase.empty() && by_phase.count(1))
1533 entries[entry_slot].reply_activities.push_back(by_phase[1]);
1534 }
1535 }
1536
1537 const std::vector<const xml::Element*> tal = te->by_tag("task-activities");
1538 if (!tal.empty()) {
1539 const xml::Element* ta = tal[0];
1540 for (const xml::Element* ae : ta->by_tag("activity")) {
1541 // descendant-search scope rationale: see _kb/04-networkstruct.md (cpp port notes)
1542 if (ae->parent != ta) continue;
1543 detail::RawActivity<T> ac;
1544 ac.name = ae->attr("name");
1545 ac.hostdem = detail::host_demand<T>(ae->attr("host-demand-mean"),
1546 ae->attr("host-demand-cvsq"));
1547 const std::string att = ae->attr("think-time");
1548 const double att_d = dbl_from_decimal(att, 0.0);
1549 ac.thinktime = att_d > 0.0 ? Distrib<T>::exp_mean(num_from_decimal<T>(att))
1551 ac.bound_to_entry = ae->attr("bound-to-entry");
1552 ac.phase = 1;
1553 ac.call_order = detail::call_order_from_text(ae->attr("call-order"));
1554 ac.task_slot = task_slot;
1555 for (const xml::Element* ce : ae->by_tag("synch-call"))
1556 ac.sync_calls.push_back(
1557 {ce->attr("dest"), num_from_decimal<T>(ce->attr("calls-mean"))});
1558 for (const xml::Element* ce : ae->by_tag("asynch-call"))
1559 ac.async_calls.push_back(
1560 {ce->attr("dest"), num_from_decimal<T>(ce->attr("calls-mean"))});
1561 detail::read_call_groups<T>(ae, ac);
1562 acts.push_back(ac);
1563 }
1564
1565 for (const xml::Element* pe2 : ta->by_tag("precedence")) {
1566 detail::RawPrecedence<T> pr;
1567 const xml::Element* pre = nullptr;
1568 if (!pe2->child_tags("pre").empty()) {
1569 pre = pe2->child_tags("pre")[0];
1570 pr.pretype = PrecedenceType::PRE_SEQ;
1571 } else if (!pe2->child_tags("pre-AND").empty()) {
1572 pre = pe2->child_tags("pre-AND")[0];
1573 pr.pretype = PrecedenceType::PRE_AND;
1574 } else if (!pe2->child_tags("pre-OR").empty()) {
1575 pre = pe2->child_tags("pre-OR")[0];
1576 pr.pretype = PrecedenceType::PRE_OR;
1577 } else {
1578 throw InputError("lqn reader: <precedence> without a pre element");
1579 }
1580 for (const xml::Element* ae : pre->by_tag("activity")) {
1581 pr.preacts.push_back(ae->attr("name"));
1582 if (pr.pretype == PrecedenceType::PRE_OR)
1583 pr.preparams.push_back(num_from_decimal<T>(ae->attr("prob")));
1584 }
1585 if (pr.pretype == PrecedenceType::PRE_SEQ && pr.preacts.size() > 1)
1586 pr.preacts.resize(1);
1587 if (pr.pretype == PrecedenceType::PRE_AND) {
1588 const std::string q = pre->attr("quorum");
1589 if (!q.empty()) {
1590 pr.has_quorum = true;
1591 pr.quorum = static_cast<std::size_t>(dbl_from_decimal(q, 0.0) + 0.5);
1592 }
1593 }
1594
1595 const xml::Element* post = nullptr;
1596 if (!pe2->child_tags("post").empty()) {
1597 post = pe2->child_tags("post")[0];
1598 pr.posttype = PrecedenceType::POST_SEQ;
1599 } else if (!pe2->child_tags("post-AND").empty()) {
1600 post = pe2->child_tags("post-AND")[0];
1601 pr.posttype = PrecedenceType::POST_AND;
1602 } else if (!pe2->child_tags("post-OR").empty()) {
1603 post = pe2->child_tags("post-OR")[0];
1604 pr.posttype = PrecedenceType::POST_OR;
1605 } else if (!pe2->child_tags("post-LOOP").empty()) {
1606 post = pe2->child_tags("post-LOOP")[0];
1607 pr.posttype = PrecedenceType::POST_LOOP;
1608 } else if (!pe2->child_tags("post-CACHE").empty()) {
1609 post = pe2->child_tags("post-CACHE")[0];
1610 pr.posttype = PrecedenceType::POST_CACHE;
1611 } else {
1612 // minOccurs="0" on the post choice in lqn-core.xsd: a
1613 // precedence carrying only a pre element declares a
1614 // TERMINAL activity and no successor, so it contributes
1615 // no edge and is dropped rather than refused.
1616 continue;
1617 }
1618 for (const xml::Element* ae : post->by_tag("activity")) {
1619 pr.postacts.push_back(ae->attr("name"));
1620 if (pr.posttype == PrecedenceType::POST_OR)
1621 pr.postparams.push_back(num_from_decimal<T>(ae->attr("prob")));
1622 if (pr.posttype == PrecedenceType::POST_LOOP)
1623 pr.postparams.push_back(num_from_decimal<T>(ae->attr("count")));
1624 }
1625 if (pr.posttype == PrecedenceType::POST_CACHE) {
1626 // THE BRANCH IS NAMED WHERE THE WRITER NAMED IT. `hit`
1627 // then `miss` is what the builder and SolverLN key on, so
1628 // a file whose activities are in the other order must be
1629 // reordered rather than read positionally. A file written
1630 // before `cache-result` existed carries no attribute, and
1631 // there document order IS the order, as layered.py:4204
1632 // also falls back to.
1633 std::vector<std::string> hit, miss, unlabelled;
1634 const std::vector<const xml::Element*> aes = post->by_tag("activity");
1635 for (std::size_t k = 0; k < aes.size(); ++k) {
1636 const std::string res = aes[k]->attr("cache-result");
1637 if (res == "hit")
1638 hit.push_back(pr.postacts[k]);
1639 else if (res == "miss")
1640 miss.push_back(pr.postacts[k]);
1641 else
1642 unlabelled.push_back(pr.postacts[k]);
1643 }
1644 if (!hit.empty() || !miss.empty()) {
1645 pr.postacts = hit;
1646 pr.postacts.insert(pr.postacts.end(), miss.begin(), miss.end());
1647 pr.postacts.insert(pr.postacts.end(), unlabelled.begin(),
1648 unlabelled.end());
1649 }
1650 if (pr.postacts.size() < 2)
1651 throw InputError(
1652 "lqn reader: a <post-CACHE> branches on a hit and a miss, so it "
1653 "names two activities");
1654 }
1655 if (pr.posttype == PrecedenceType::POST_LOOP)
1656 pr.postacts.push_back(post->attr("end"));
1657 tasks[task_slot].precedences.push_back(pr);
1658 }
1659
1660 for (const xml::Element* re : ta->by_tag("reply-entry")) {
1661 const std::string ename = re->attr("name");
1662 std::size_t slot = entries.size();
1663 for (std::size_t s = 0; s < entries.size(); ++s)
1664 if (entries[s].name == ename) {
1665 slot = s;
1666 break;
1667 }
1668 if (slot == entries.size())
1669 throw InputError("lqn reader: <reply-entry> names unknown entry '" + ename +
1670 "'");
1671 for (const xml::Element* ra : re->by_tag("reply-activity"))
1672 entries[slot].reply_activities.push_back(ra->attr("name"));
1673 }
1674 }
1675 }
1676 }
1677
1678 return m;
1679}
1680
1681/**
1682 * Read a .lqnx model.
1683 *
1684 * @param path file to read
1685 * @return the flattened struct SolverLN consumes
1686 */
1687template <class T>
1688LqnStruct<T> read_lqnx(const std::string& path) {
1689 return lqn_finalize(read_lqnx_model<T>(path));
1690}
1691
1692
1693} // namespace lqn
1694} // namespace line
1695
1696#endif // LINE_LANG_LQN_LQN_READER_H
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
UnsupportedError(const std::string &what)
Definition error.h:51
Decimal literal -> T, without a detour through double when T is exact.
The moment fitters the reference distributions carry as STATIC FACTORIES: Erlang.fitMeanAndOrder,...
The exception types the port throws.
LayeredNetworkStruct, the flattened description of a layered queueing network.
Maximum sustainable multiplicity (concurrency level) of every element of a layered software network.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
PrecedenceType
Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType.
Definition lang_types.h:472
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
SchedStrategy sched_from_lqnx(const std::string &s)
Parse the scheduling attribute of an .lqnx processor or task.
Definition lang_types.h:277
CallType
Call kinds, with the values of MATLAB CallType.
Definition lang_types.h:469
Distrib< T > aph_fit_mean_scv(const T &mean, const T &scv)
APH.fitMeanAndSCV(MEAN, SCV), through mam::aph_fit_mean_scv.
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,...
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:731
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:380
LqnModel< T > read_lqnx_model(const std::string &path)
LqnStruct< T > lqn_finalize(const LqnModel< T > &m)
Port of @LayeredNetwork/getStruct.m: flatten the model into its struct.
Definition lqn_reader.h:444
LqnStruct< T > read_lqnx(const std::string &path)
Read a .lqnx model.
std::vector< Multiplicity< T > > lsn_max_multiplicity(const LsnInput< T > &lsn)
Maximum sustainable multiplicity (concurrency level) of every element of a layered software network.
std::unique_ptr< Element > parse_file(const std::string &path)
Read and parse a file.
Definition xml.h:290
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
double dbl_from_decimal(const std::string &s, double fallback)
Parse a decimal literal as a plain double (multiplicities, populations, tolerances).
Definition decimal.h:120
T num_from_decimal(const std::string &s)
Parse a decimal literal into T.
Definition decimal.h:110
static constexpr double FineTol
Definition lang_types.h:760
void set(std::size_t i, std::size_t j)
Definition lqn_struct.h:159
void resize(std::size_t nn)
Definition lqn_struct.h:155
static Distrib exp_rate(const T &r)
Definition lang_types.h:945
static Distrib disabled_dist()
Definition lang_types.h:988
static Distrib det(const T &m)
Definition lang_types.h:989
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:977
static Distrib exp_mean(const T &m)
Definition lang_types.h:930
One routed call group: an activity, the strategy that picks among its targets, and the target ENTRIES...
Definition lqn_struct.h:201
std::vector< std::size_t > targets
absolute entry indices, in declaration order
Definition lqn_struct.h:204
lang::RoutingStrategy strategy
Definition lqn_struct.h:203
std::size_t caller
absolute index of the dispatching activity
Definition lqn_struct.h:202
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:409
std::vector< detail::RawTask< T > > tasks
Definition lqn_reader.h:413
std::vector< detail::RawActivity< T > > acts
Definition lqn_reader.h:415
std::map< std::size_t, std::vector< T > > proc_jdscalingpeak
Definition lqn_reader.h:438
std::map< std::size_t, std::vector< detail::RawServerPool< T > > > proc_pools
Definition lqn_reader.h:439
std::vector< detail::RawProc > procs
Definition lqn_reader.h:412
std::map< std::size_t, CdScaling< T > > proc_jdscaling
Definition lqn_reader.h:437
std::map< std::size_t, std::vector< T > > proc_cdscalingpeak
Definition lqn_reader.h:436
std::map< std::size_t, std::pair< Matrix< T >, std::vector< T > > > proc_lincon
Definition lqn_reader.h:425
std::map< std::size_t, std::vector< detail::RawLinConRow< T > > > proc_linconrows
Admission constraints declared on a HOST, by 0-based processor slot.
Definition lqn_reader.h:424
std::map< std::size_t, CdScaling< T > > proc_cdscaling
Definition lqn_reader.h:435
std::string name
LayeredNetwork.getName(); empty when unnamed.
Definition lqn_reader.h:411
std::map< std::size_t, std::vector< T > > proc_lldscaling
Queue-dependent service rates and compatibility pools declared on a HOST, by 0-based processor slot.
Definition lqn_reader.h:434
std::vector< detail::RawEntry< T > > entries
Definition lqn_reader.h:414
One activity precedence of a task, with its activities resolved to indices.
Definition lqn_struct.h:185
std::vector< std::size_t > preacts
absolute activity indices
Definition lqn_struct.h:188
std::vector< T > preparams
PRE_OR shares, or a PRE_AND quorum.
Definition lqn_struct.h:190
std::vector< T > postparams
POST_OR probabilities or the POST_LOOP count.
Definition lqn_struct.h:191
PrecedenceType pretype
Definition lqn_struct.h:186
std::vector< std::size_t > postacts
absolute activity indices
Definition lqn_struct.h:189
PrecedenceType posttype
Definition lqn_struct.h:187
BoolGraph isasynccaller
Definition lqn_struct.h:320
std::vector< Distrib< T > > hostdem
(nidx+1) host demand per activity (Immediate elsewhere)
Definition lqn_struct.h:299
std::vector< std::vector< T > > jdscalingpeak
(tshift+ntasks+1)
Definition lqn_struct.h:242
std::vector< int > actphase
(nacts+1) phase of each activity, 1-based by act
Definition lqn_struct.h:369
std::vector< CallType > calltype
(ncalls+1)
Definition lqn_struct.h:312
std::vector< std::size_t > callpair_dst
(ncalls+1) called entry
Definition lqn_struct.h:311
std::vector< std::vector< LqnPrecedence< T > > > precedences
Activity precedences of each task, as DECLARED, indexed by the task's absolute index.
Definition lqn_struct.h:348
std::vector< std::string > callhashnames
(ncalls+1)
Definition lqn_struct.h:315
std::vector< Distrib< T > > setuptime
Setup tasks: the server powers down when idle and pays to restart.
Definition lqn_struct.h:296
std::map< std::pair< std::size_t, std::size_t >, double > fanout
Fan-out and fan-in, keyed by task element index, absent = 0.
Definition lqn_struct.h:256
std::vector< bool > hassetup
(tshift+ntasks+1)
Definition lqn_struct.h:269
std::vector< LqnElement > type
(nidx+1)
Definition lqn_struct.h:214
std::vector< std::size_t > nitems
Cache tasks and item entries.
Definition lqn_struct.h:283
std::vector< Distrib< T > > actthink
(nidx+1) activity think time
Definition lqn_struct.h:301
std::vector< std::vector< T > > lldscaling
Queue-dependent service rates declared on a layer server (a host or a task), by element index,...
Definition lqn_struct.h:238
std::vector< double > mult
(tshift+ntasks+1) declared multiplicity, may be Inf
Definition lqn_struct.h:217
std::vector< SchedStrategy > sched
(tshift+ntasks+1)
Definition lqn_struct.h:216
std::vector< std::size_t > callpair_src
(ncalls+1) calling activity (entry for FWD)
Definition lqn_struct.h:310
std::vector< std::size_t > parent
(nidx+1) host of a task, task of an entry/activity
Definition lqn_struct.h:215
std::vector< std::vector< T > > lincon_b
Definition lqn_struct.h:334
std::vector< ServerPools< T > > pools
(tshift+ntasks+1)
Definition lqn_struct.h:243
std::vector< T > callproc_mean
(ncalls+1) mean number of calls
Definition lqn_struct.h:313
std::vector< std::size_t > actquorum
(nidx+1) AND-join quorum, on the join target
Definition lqn_struct.h:368
std::vector< bool > iscache
(tshift+ntasks+1)
Definition lqn_struct.h:267
std::vector< bool > has_arrival
(nidx+1) entry with an open arrival
Definition lqn_struct.h:302
std::vector< std::vector< std::size_t > > callsof
(nidx+1) call indices issued by an activity
Definition lqn_struct.h:308
std::size_t nentries
Definition lqn_struct.h:209
std::vector< std::string > hashnames
(nidx+1) name prefixed by kind: P:/T:/R:/E:/A:
Definition lqn_struct.h:213
std::vector< std::string > callnames
(ncalls+1)
Definition lqn_struct.h:314
std::vector< CdScaling< T > > cdscaling
(tshift+ntasks+1)
Definition lqn_struct.h:239
std::vector< std::vector< int > > itemcap
(tshift+ntasks+1)
Definition lqn_struct.h:284
std::vector< std::vector< std::size_t > > entriesof
(tshift+ntasks+1)
Definition lqn_struct.h:306
std::vector< bool > hasretrieval
(tshift+ntasks+1) cache task with delayed-hit retrieval
Definition lqn_struct.h:268
std::vector< double > repl
(tshift+ntasks+1) replication
Definition lqn_struct.h:219
std::vector< std::string > names
(nidx+1) declared name
Definition lqn_struct.h:212
std::vector< Matrix< T > > lincon_A
Admission constraint A n <= b on the layer station of a host or task.
Definition lqn_struct.h:333
SparseGraph< T > dag
graph with entry-task edges reversed and loop back-edges removed
Definition lqn_struct.h:318
std::vector< Distrib< T > > think
(nidx+1) task think time
Definition lqn_struct.h:300
std::vector< std::vector< std::size_t > > tasksof
(nhosts+1)
Definition lqn_struct.h:305
std::vector< bool > isref
(tshift+ntasks+1)
Definition lqn_struct.h:266
std::vector< PrecedenceType > actpretype
(nidx+1)
Definition lqn_struct.h:366
std::vector< std::vector< std::size_t > > actsof
(ashift+1) by task and by entry
Definition lqn_struct.h:307
std::map< std::pair< std::size_t, std::size_t >, double > fanin
Definition lqn_struct.h:257
std::vector< PrecedenceType > actposttype
(nidx+1)
Definition lqn_struct.h:367
std::vector< LqnCallGroup > callgroups
Synchronous calls DISPATCHED AS A GROUP, lsn.callgroups.
Definition lqn_struct.h:364
std::vector< std::vector< T > > cdscalingpeak
(tshift+ntasks+1)
Definition lqn_struct.h:240
std::vector< Distrib< T > > delayofftime
Definition lqn_struct.h:297
std::vector< int > prio
(tshift+ntasks+1) task priority, larger first (lqns); 0 on hosts (getStruct.m)
Definition lqn_struct.h:220
std::vector< Distrib< T > > arrival
(nidx+1) open arrival process of an entry
Definition lqn_struct.h:303
std::vector< std::vector< T > > itemproc
(nidx+1) popularity pmf
Definition lqn_struct.h:286
std::vector< CdScaling< T > > jdscaling
(tshift+ntasks+1)
Definition lqn_struct.h:241
std::vector< ReplacementStrategy > replacestrat
(tshift+ntasks+1)
Definition lqn_struct.h:285
std::vector< double > maxmult
(tshift+ntasks+1) sustainable multiplicity
Definition lqn_struct.h:218
SparseGraph< T > graph
element call/precedence graph, edge weights are branch shares
Definition lqn_struct.h:317
SparseGraph< T > taskgraph
task-to-task calls
Definition lqn_struct.h:319
Heterogeneous server pools declared on a layer server, the twin of the nservertypes / servertypenames...
Definition lqn_struct.h:79
Matrix< T > compat
(npools x noperands), nonzero = eligible
Definition lqn_struct.h:83
std::vector< double > counts
(npools) servers held by each pool
Definition lqn_struct.h:81
std::vector< std::string > names
(npools) declared pool name
Definition lqn_struct.h:80
std::vector< T > rates
(npools) per-pool rate multiplier
Definition lqn_struct.h:82
std::size_t npools() const
Definition lqn_struct.h:86
The plain-data fields of a layered software network read by the algorithm.
std::vector< bool > hassetup
(n) setup task flag; may be empty
std::vector< bool > entry_has_arrival
(n) entry with an open arrival; may be empty
std::vector< LsnElementType > type
(n) element kind
std::vector< bool > isref
(n) reference task flag
Matrix< T > dag
(n x n) call graph; an edge is a strictly positive entry
std::vector< Multiplicity< T > > mult
(n) declared multiplicity; short vectors are padded with Inf
static Multiplicity finite(const T &v)
static Multiplicity inf()
std::vector< const Element * > by_tag(const std::string &tag) const
Descendant-or-self search excluding self, in document order.
Definition xml.h:76
std::string attr(const std::string &key) const
Attribute value, or the empty string when absent (org.w3c.dom semantics).
Definition xml.h:63
std::vector< const Element * > child_tags(const std::string &tag) const
Direct children with the given tag, in document order.
Definition xml.h:83
A minimal XML DOM: read for the .lqnx interchange format, write for the JMT .jsimg and ....