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