LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_writer.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_WRITER_H
6#define LINE_LANG_LQN_LQN_WRITER_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * LqnModel -> .lqnx, a port of matlab/src/lang/layered/@@LayeredNetwork/writeXML.m.
12 *
13 * WHY IT WRITES THE INTERMEDIATE MODEL AND NOT THE STRUCT. getStruct flattens a
14 * precedence block into edges of `graph`, so an AND-fork and two independent
15 * sequences leave the same trace there, and a POST_LOOP loses its counts to the
16 * branch shares. lqns rejects a document whose activity graph does not name its
17 * blocks, so a writer working from the struct would have to guess them; the
18 * reference writes from the handle graph for the same reason, and `LqnModel` is
19 * this port's stand-in for it (lqn_reader.h).
20 *
21 * WHAT THE SCHEMA CANNOT CARRY, and what this does about it:
22 *
23 * - A think time on a NON-reference task. lqns rejects `think-time` there
24 * outright ('Task "X" is not a reference task'), so the attribute is dropped
25 * and the caller is told through `LqnWriteReport::dropped`, never silently.
26 * See _kb, "lqnx cannot carry non-ref think time".
27 * - PRE_OR branch shares. An OR-JOIN takes whichever branch arrives, so the
28 * schema puts no `prob` on a `pre-OR` activity; writeXML.m omits them too.
29 * The reader accepts them when present, so a document that carries them
30 * round-trips through THIS port and not through the reference.
31 * - Cache tasks, item entries and admission constraints. They reach this port
32 * through the JSON interchange or the builder, and the LQN schema has no
33 * element for any of them. A model that declares one is REFUSED by name
34 * rather than written as a plain task, because lqns would answer the
35 * resulting document and the answer would describe a different model.
36 * A SETUP TASK IS NOT IN THAT LIST: `<setup>`/`<delay-off>` are a LINE
37 * extension that every codebase here writes and reads, so the model survives
38 * the round trip. lqns does NOT ignore them, it stops with "Unexpected
39 * element <setup>", so SolverLQNS refuses the model by name before writing
40 * (lqns_method_refusal in solver_lqns.h).
41 *
42 * REPLIES. lqns requires every synchronously-called entry of a non-reference
43 * task to name its reply activity. A model built in code, or read from a
44 * document that left them implicit, has none declared, so the implicit rule of
45 * getStruct.m:641-671 is reproduced here: a leaf activity of the task (no
46 * successor within the same task) replies to the entry reached by walking the
47 * graph backwards, and an entry that declares any reply keeps its own.
48 */
49
50#include <algorithm>
51#include <cmath>
52#include <cstdio>
53#include <cstdlib>
54#include <map>
55#include <set>
56#include <string>
57#include <vector>
58
60#include "line/util/error.h"
61#include "line/util/xml.h"
62
63namespace line {
64namespace lqn {
65
66/** What the schema could not carry, one human-readable line per loss. */
68 std::vector<std::string> dropped;
69};
70
71namespace detail {
72
73/**
74 * Shortest decimal that reads back as the same double.
75 *
76 * num2str, which the reference uses, keeps five significant digits, so a demand
77 * of 1/3 reaches lqns as 0.33333 and the answer differs in the fourth digit
78 * from the one this port computes in-process. The file is a wire format for a
79 * solver, not a display, so it carries the value.
80 */
81inline std::string lqnx_num(double v) {
82 if (std::isinf(v)) return v > 0 ? "inf" : "-inf";
83 char buf[40];
84 for (int prec = 15; prec <= 17; ++prec) {
85 std::snprintf(buf, sizeof(buf), "%.*g", prec, v);
86 if (std::strtod(buf, nullptr) == v) return std::string(buf);
87 }
88 return std::string(buf);
89}
90
91template <class T>
92std::string lqnx_num_of(const T& v) {
93 return lqnx_num(num_traits<T>::to_double(v));
94}
95
96/** The `pre` / `post` element name of a precedence kind. */
97inline const char* precedence_tag(PrecedenceType t) {
98 switch (t) {
99 case PrecedenceType::PRE_SEQ: return "pre";
100 case PrecedenceType::PRE_AND: return "pre-AND";
101 case PrecedenceType::PRE_OR: return "pre-OR";
102 case PrecedenceType::POST_SEQ: return "post";
103 case PrecedenceType::POST_AND: return "post-AND";
104 case PrecedenceType::POST_OR: return "post-OR";
105 case PrecedenceType::POST_LOOP: return "post-LOOP";
106 // `post-CACHE` IS A LINE EXTENSION OF THE SCHEMA, and all three reference
107 // codebases already write and read it: ActivityPrecedenceType.toText
108 // names it, writeXML.m:306 and layered.py:3287 emit it, parseXML.m:449
109 // and layered.py:4179 take it back. Refusing it here left this port the
110 // only one that could not round-trip a layered cache-queueing model
111 // through .lqnx, and its own reader threw on the element.
112 case PrecedenceType::POST_CACHE: return "post-CACHE";
113 default:
114 throw UnsupportedError(ErrorKind{"Export"}, "lqn writer: precedence with no schema element");
115 }
116}
117
118/** ReplacementStrategy -> the `<cache replacement>` name, spelled as writeXML.m's repl_to_lqnx and the reader take it. */
119inline const char* replacement_to_lqnx(lang::ReplacementStrategy r) {
121 switch (r) {
122 case R::LRU: return "LRU";
123 case R::FIFO: return "FIFO";
124 case R::RR: return "RR";
125 case R::SFIFO: return "SFIFO";
126 case R::HLRU: return "HLRU";
127 case R::CLIMB: return "CLIMB";
128 case R::QLRU: return "QLRU";
129 default: throw UnsupportedError(ErrorKind{"Export"}, "lqn writer: cache replacement strategy with no .lqnx spelling");
130 }
131}
132
133/**
134 * A call group's `strategy` name, spelled as the JSON interchange spells a
135 * routing strategy: the enum CONSTANT, upper case, not the lower-case name
136 * `lang::routing_to_text` prints in a struct dump.
137 *
138 * Only the two strategies a call group can be built with are named: WRROBIN
139 * would need per-target weights the group API does not take, and the remaining
140 * strategies are not dispatch policies at all, so an unnamed one is an error
141 * rather than a silent PROB.
142 */
143inline const char* callgroup_to_lqnx(lang::RoutingStrategy r) {
144 if (r == lang::RoutingStrategy::RROBIN) return "RROBIN";
145 if (r == lang::RoutingStrategy::JSQ) return "JSQ";
146 throw UnsupportedError(ErrorKind{"Export"}, "lqn writer: call groups carry RROBIN or JSQ; routing strategy '" +
147 std::string(lang::routing_to_text(r)) + "' cannot be written to .lqnx");
148}
149
150/**
151 * The reply activities of each entry, declared where declared and inferred
152 * where not, keyed by entry name.
153 *
154 * Port of the implicit-reply pass of getStruct.m: a LEAF activity of a task
155 * (one with no successor among that task's own activities) replies, and the
156 * entry it replies to is found by walking `graph` backwards through first
157 * ancestors until an ENTRY is reached. An entry with an explicit reply keeps
158 * it, which is what makes a phase-2 activity possible.
159 */
160template <class T>
161std::map<std::string, std::vector<std::string>> reply_activities(const LqnModel<T>& m,
162 const LqnStruct<T>& sn) {
163 std::map<std::string, std::vector<std::string>> out;
164 std::set<std::string> explicit_reply;
165 for (std::size_t e = 0; e < m.entries.size(); ++e)
166 if (!m.entries[e].reply_activities.empty()) {
167 out[m.entries[e].name] = m.entries[e].reply_activities;
168 explicit_reply.insert(m.entries[e].name);
169 }
170
171 for (std::size_t t = 1; t <= sn.ntasks; ++t) {
172 const std::size_t tidx = sn.tshift + t;
173 for (std::size_t aidx : sn.actsof[tidx]) {
174 bool is_reply = true;
175 const std::vector<std::size_t> post = sn.graph.succ(aidx);
176 for (std::size_t p : post)
177 if (std::find(sn.actsof[tidx].begin(), sn.actsof[tidx].end(), p) !=
178 sn.actsof[tidx].end())
179 is_reply = false;
180 if (!is_reply) continue;
181 // A leaf: walk back to the entry it belongs to, first ancestor only,
182 // exactly as the reference does.
183 std::size_t parent = aidx;
184 std::size_t hops = 0;
185 while (sn.type[parent] != LqnElement::ENTRY && hops++ <= sn.nidx) {
186 const std::vector<std::size_t> anc = sn.graph.pred(parent);
187 if (anc.empty()) break;
188 parent = anc[0];
189 }
190 if (sn.type[parent] != LqnElement::ENTRY) continue;
191 const std::string& ename = sn.names[parent];
192 if (explicit_reply.count(ename)) continue;
193 out[ename].push_back(sn.names[aidx]);
194 }
195 }
196 return out;
197}
198
199} // namespace detail
200
201/**
202 * Write a layered model as a .lqnx document.
203 *
204 * @param m the intermediate model, from the builder or from
205 * read_lqnx_model
206 * @param path file to create
207 * @param model_name the `name` attribute of `<lqn-model>`
208 * @param use_abstract_names rename elements P1/T1/E1/A1, as writeXML's third
209 * argument does, for a document that is compared
210 * rather than read
211 * @param out_text when non-null, the document is SERIALIZED INTO IT
212 * and no file is written (`path` is then ignored).
213 * The tree cannot be returned instead:
214 * `xml::Element` owns its children through
215 * unique_ptr and they hold raw `parent` back
216 * pointers, so a moved tree dangles.
217 * @return what the schema could not carry
218 */
219template <class T>
220LqnWriteReport write_lqnx(const LqnModel<T>& m, const std::string& path,
221 const std::string& model_name = std::string("LQN"),
222 bool use_abstract_names = false,
223 std::string* out_text = NULL) {
224 LqnWriteReport report;
225 const LqnStruct<T> sn = lqn_finalize(m);
226
227 // ---- constructs with no element in the schema are refused by name -----
228 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
229 const detail::RawTask<T>& tk = m.tasks[t];
230 if (!tk.linconrows.empty() || tk.lincon_A.rows() > 0)
231 throw UnsupportedError(ErrorKind{"Export"}, "write_lqnx: task '" + tk.name +
232 "' declares an admission constraint, which the LQN XML schema "
233 "cannot express; it travels as JSON `admissionConstraints`");
234 }
235 if (!m.proc_linconrows.empty() || !m.proc_lincon.empty())
236 throw UnsupportedError(ErrorKind{"Export"},
237 "write_lqnx: a processor declares an admission constraint, which the LQN XML schema "
238 "cannot express; it travels as JSON `admissionConstraints`");
239
240 // A service-rate dependence or a compatibility graph has no attribute in the schema either, but MATLAB writeXML
241 // WARNS and omits it rather than refusing; reported the same way here, beside the dropped think times.
242 const auto dropped_server = [&](const std::string& kind, const std::string& name, bool pools, bool rate) {
243 if (pools)
244 report.dropped.push_back(kind + " " + name + " declares heterogeneous server pools with a "
245 "class-compatibility graph, which the LQN XML schema cannot represent: they are "
246 "omitted from " + path + " and the " + (kind == "Host" ? "host" : "task") +
247 " reads back as a homogeneous multiserver.");
248 else if (rate)
249 report.dropped.push_back(kind + " " + name + " declares a service-rate dependence, which the LQN XML "
250 "schema cannot represent: it is omitted from " + path + ".");
251 };
252 for (std::size_t p = 0; p < m.procs.size(); ++p) {
253 const auto pl = m.proc_pools.find(p);
254 const auto ll = m.proc_lldscaling.find(p);
255 const auto cd = m.proc_cdscaling.find(p);
256 const auto jd = m.proc_jdscaling.find(p);
257 dropped_server("Host", m.procs[p].name, pl != m.proc_pools.end() && !pl->second.empty(),
258 (ll != m.proc_lldscaling.end() && !ll->second.empty()) ||
259 (cd != m.proc_cdscaling.end() && static_cast<bool>(cd->second)) ||
260 (jd != m.proc_jdscaling.end() && static_cast<bool>(jd->second)));
261 }
262 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
263 const detail::RawTask<T>& tk = m.tasks[t];
264 dropped_server("Task", tk.name, !tk.pools.empty(),
265 !tk.lldscaling.empty() || static_cast<bool>(tk.cdscaling) || static_cast<bool>(tk.jdscaling));
266 }
267
268 // ---- name map, either identity or the abstract P1/T1/E1/A1 -----------
269 //
270 // ONE MAP PER KIND, not the reference's single nodeHashMap. An LQN
271 // routinely names a processor, its task and that task's entry alike (`c0`
272 // throughout the lqngen corpus), and every reference in the document is to
273 // a KNOWN kind: `dest` is an entry, `bound-to-entry` an entry, a precedence
274 // operand an activity. With one map the last declaration wins and the
275 // abstract-name mode would emit `E1` where the processor should be, writing
276 // a document that names elements which do not exist.
277 std::map<std::string, std::string> nm_host, nm_task, nm_entry, nm_act;
278 {
279 std::size_t tctr = 0, ectr = 0, actr = 0;
280 for (std::size_t p = 0; p < m.procs.size(); ++p) {
281 char buf[32];
282 std::snprintf(buf, sizeof(buf), "P%zu", p + 1);
283 nm_host[m.procs[p].name] = use_abstract_names ? buf : m.procs[p].name;
284 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
285 if (m.tasks[t].proc_slot != p) continue;
286 std::snprintf(buf, sizeof(buf), "T%zu", ++tctr);
287 nm_task[m.tasks[t].name] = use_abstract_names ? buf : m.tasks[t].name;
288 for (std::size_t e = 0; e < m.entries.size(); ++e) {
289 if (m.entries[e].task_slot != t) continue;
290 std::snprintf(buf, sizeof(buf), "E%zu", ++ectr);
291 nm_entry[m.entries[e].name] = use_abstract_names ? buf : m.entries[e].name;
292 }
293 for (std::size_t a = 0; a < m.acts.size(); ++a) {
294 if (m.acts[a].task_slot != t) continue;
295 std::snprintf(buf, sizeof(buf), "A%zu", ++actr);
296 nm_act[m.acts[a].name] = use_abstract_names ? buf : m.acts[a].name;
297 }
298 }
299 }
300 }
301 // A name the map has never seen is a dangling reference, not a name to
302 // invent: the document would name an element that does not exist and lqns
303 // would refuse the file with no indication of which model built it.
304 auto lookup = [](const std::map<std::string, std::string>& nm, const std::string& raw,
305 const char* kind) -> const std::string& {
306 const std::map<std::string, std::string>::const_iterator it = nm.find(raw);
307 if (it == nm.end())
308 throw InputError("write_lqnx: '" + raw + "' is referenced as a " + kind +
309 " but no " + kind + " declares it");
310 return it->second;
311 };
312 auto host_name = [&](const std::string& r) -> const std::string& {
313 return lookup(nm_host, r, "processor");
314 };
315 auto task_name = [&](const std::string& r) -> const std::string& {
316 return lookup(nm_task, r, "task");
317 };
318 auto entry_name = [&](const std::string& r) -> const std::string& {
319 return lookup(nm_entry, r, "entry");
320 };
321 auto act_name = [&](const std::string& r) -> const std::string& {
322 return lookup(nm_act, r, "activity");
323 };
324
325 const std::map<std::string, std::vector<std::string>> replies =
326 detail::reply_activities(m, sn);
327
328 // Which entries lqns needs a reply for: called synchronously, or the target
329 // of a forwarding chain. An entry reached only by an asynchronous call has
330 // nobody to reply TO, and a reply-entry declared for it is an error there.
331 std::set<std::string> needs_reply;
332 for (std::size_t e = 1; e <= sn.nentries; ++e) {
333 const std::size_t eidx = sn.eshift + e;
334 bool wanted = sn.issynccaller.any_col(eidx);
335 for (std::size_t c = 1; !wanted && c <= sn.ncalls; ++c)
336 if (sn.calltype[c] == CallType::FWD && sn.callpair_dst[c] == eidx) wanted = true;
337 if (wanted) needs_reply.insert(sn.names[eidx]);
338 }
339
340 // One <activity>: under task-activities, or (with its phase) under an entry's entry-phase-activities.
341 auto write_activity = [&](xml::Element& parent, const detail::RawActivity<T>& ac, bool in_phase) {
342 xml::Element& ae = parent.add_child("activity");
343 ae.set_attr("host-demand-mean",
344 detail::lqnx_num(ac.hostdem.disabled
345 ? 0.0
346 : num_traits<T>::to_double(ac.hostdem.mean)));
347 ae.set_attr("host-demand-cvsq",
348 detail::lqnx_num(ac.hostdem.disabled
349 ? 1.0
350 : num_traits<T>::to_double(ac.hostdem.scv)));
351 if (!in_phase && !ac.bound_to_entry.empty())
352 ae.set_attr("bound-to-entry", entry_name(ac.bound_to_entry));
353 ae.set_attr("call-order", ac.call_order);
354 ae.set_attr("name", act_name(ac.name));
355 if (in_phase) ae.set_attr("phase", std::to_string(ac.phase));
356 const double athink = num_traits<T>::to_double(ac.thinktime.mean);
357 if (!ac.thinktime.disabled && athink > lang::GlobalConstants::FineTol)
358 ae.set_attr("think-time", detail::lqnx_num(athink));
359 for (std::size_t c = 0; c < ac.sync_calls.size(); ++c) {
360 xml::Element& ce = ae.add_child("synch-call");
361 ce.set_attr("dest", entry_name(ac.sync_calls[c].dest));
362 ce.set_attr("calls-mean", detail::lqnx_num_of<T>(ac.sync_calls[c].mean));
363 }
364 for (std::size_t c = 0; c < ac.async_calls.size(); ++c) {
365 xml::Element& ce = ae.add_child("asynch-call");
366 ce.set_attr("dest", entry_name(ac.async_calls[c].dest));
367 ce.set_attr("calls-mean", detail::lqnx_num_of<T>(ac.async_calls[c].mean));
368 }
369 // LINE dialect: which of the synch-calls above one dispatcher
370 // issues, and under which strategy. The member calls stay
371 // ordinary synch-calls, so a reader that ignores this element
372 // still sees the same aggregate call means -- which is what
373 // lqns and lqsim, having no dispatcher, should see.
374 for (std::size_t g = 0; g < ac.call_groups.size(); ++g) {
375 xml::Element& ge = ae.add_child("call-group");
376 ge.set_attr("strategy", detail::callgroup_to_lqnx(ac.call_groups[g].first));
377 for (std::size_t d = 0; d < ac.call_groups[g].second.size(); ++d)
378 ge.add_child("dest").set_attr("name",
379 entry_name(ac.call_groups[g].second[d]));
380 }
381 };
382
383 xml::Element root;
384 root.name = "lqn-model";
385 root.set_attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
386 root.set_attr("xsi:noNamespaceSchemaLocation", "lqn.xsd");
387 root.set_attr("name", model_name);
388
389 for (std::size_t p = 0; p < m.procs.size(); ++p) {
390 const detail::RawProc& pr = m.procs[p];
391 xml::Element& pe = root.add_child("processor");
392 pe.set_attr("name", host_name(pr.name));
393 pe.set_attr("scheduling", lang::sched_to_lqnx(pr.sched));
394 if (pr.repl > 1.0) pe.set_attr("replication", detail::lqnx_num(pr.repl));
395 if (pr.sched != SchedStrategy::INF) {
396 // An infinite multiplicity on a finite discipline has no spelling;
397 // the reference writes 1, which is what a single server means.
398 const double mult = std::isinf(pr.mult) ? 1.0 : pr.mult;
399 pe.set_attr("multiplicity", detail::lqnx_num(mult));
400 }
401 // always for a PS host, as MATLAB writeXML does: lqsim refuses a PS processor with no quantum
402 if (pr.sched == SchedStrategy::PS || pr.sched == SchedStrategy::PSPRIO)
403 pe.set_attr("quantum", detail::lqnx_num(pr.quantum));
404 pe.set_attr("speed-factor", detail::lqnx_num(pr.speed_factor));
405
406 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
407 if (m.tasks[t].proc_slot != p) continue;
408 const detail::RawTask<T>& tk = m.tasks[t];
409 xml::Element& te = pe.add_child("task");
410 te.set_attr("name", task_name(tk.name));
411 te.set_attr("scheduling", lang::sched_to_lqnx(tk.sched));
412 // written only when nonzero, as writeXML.m does; lqns reads it for priority scheduling
413 if (tk.priority != 0) te.set_attr("priority", std::to_string(tk.priority));
414 if (tk.repl > 1.0) te.set_attr("replication", detail::lqnx_num(tk.repl));
415 if (tk.sched != SchedStrategy::INF)
416 te.set_attr("multiplicity",
417 detail::lqnx_num(std::isinf(tk.mult) ? 1.0 : tk.mult));
418 const double think = num_traits<T>::to_double(tk.thinktime.mean);
419 if (tk.sched == SchedStrategy::REF) {
420 te.set_attr("think-time", detail::lqnx_num(tk.thinktime.disabled ? 0.0 : think));
421 } else if (!tk.thinktime.disabled && think > 0.0) {
422 report.dropped.push_back("task '" + tk.name + "' has a think time of " +
423 detail::lqnx_num(think) +
424 ", which the schema accepts on reference tasks only");
425 }
426 // LINE dialect, as writeXML.m writes it: <cache> makes the task a CacheTask on read, one <level> per cache list.
427 // Refusing it made a layered cache model the one layered model this port could not write to .lqnx.
428 if (tk.nitems > 0) {
429 xml::Element& ce = te.add_child("cache");
430 ce.set_attr("items", std::to_string(tk.nitems));
431 ce.set_attr("replacement", detail::replacement_to_lqnx(tk.replacestrat));
432 if (tk.retrieval) ce.set_attr("retrieval", "true");
433 for (std::size_t lv = 0; lv < tk.itemcap.size(); ++lv)
434 ce.add_child("level").set_attr("capacity", std::to_string(tk.itemcap[lv]));
435 }
436 // <setup>/<delay-off> are a LINE extension the reference writes
437 // ahead of fan-out (writeXML.m:170-183), and every reader in the
438 // project takes them; refusing them here made the C++ row the only
439 // one that could not round-trip a SetupTask.
440 if (!tk.setuptime.disabled &&
442 xml::Element& se = te.add_child("setup");
443 se.set_attr("mean", detail::lqnx_num(num_traits<T>::to_double(tk.setuptime.mean)));
444 se.set_attr("scv", detail::lqnx_num(num_traits<T>::to_double(tk.setuptime.scv)));
445 }
446 if (!tk.delayofftime.disabled &&
448 xml::Element& de = te.add_child("delay-off");
449 de.set_attr("mean",
450 detail::lqnx_num(num_traits<T>::to_double(tk.delayofftime.mean)));
451 de.set_attr("scv",
452 detail::lqnx_num(num_traits<T>::to_double(tk.delayofftime.scv)));
453 }
454 // lqn-core.xsd (TaskType) places fan-out and fan-in before the entries.
455 for (std::size_t f = 0; f < tk.fanout.size(); ++f) {
456 xml::Element& fe = te.add_child("fan-out");
457 fe.set_attr("dest", task_name(tk.fanout[f].first));
458 fe.set_attr("value", detail::lqnx_num(tk.fanout[f].second));
459 }
460 for (std::size_t f = 0; f < tk.fanin.size(); ++f) {
461 xml::Element& fe = te.add_child("fan-in");
462 fe.set_attr("source", task_name(tk.fanin[f].first));
463 fe.set_attr("value", detail::lqnx_num(tk.fanin[f].second));
464 }
465
466 // An entry whose activities are one phase chain (bound phase-1 activity, then sequential successors of
467 // strictly increasing phase, nothing else touching them) is written as entry-phase-activities, as the JAR
468 // and Python writeXML do. lqn-core.xsd gives `phase` to ActivityPhasesType only: written as task
469 // activities, a phase-2 activity read back as phase 1 after the reply, which the reader refuses.
470 std::map<std::string, std::vector<std::size_t>> phase_chains; // entry name -> acts slots, by phase
471 std::set<std::size_t> phase_acts, phase_precs;
472 {
473 std::map<std::string, std::size_t> slot_of;
474 for (std::size_t a = 0; a < m.acts.size(); ++a)
475 if (m.acts[a].task_slot == t) slot_of[m.acts[a].name] = a;
476 auto is_seq = [&](const detail::RawPrecedence<T>& pc) {
477 return pc.pretype == PrecedenceType::PRE_SEQ && pc.posttype == PrecedenceType::POST_SEQ &&
478 pc.preacts.size() == 1 && pc.postacts.size() == 1;
479 };
480 for (std::size_t e = 0; e < m.entries.size(); ++e) {
481 if (m.entries[e].task_slot != t) continue;
482 const detail::RawEntry<T>& en = m.entries[e];
483 std::vector<std::size_t> bound;
484 for (std::size_t a = 0; a < m.acts.size(); ++a)
485 if (m.acts[a].task_slot == t && m.acts[a].bound_to_entry == en.name) bound.push_back(a);
486 if (bound.size() != 1) continue;
487 std::vector<std::size_t> chain(1, bound[0]);
488 std::set<std::size_t> chain_precs;
489 for (bool grew = true; grew;) {
490 grew = false;
491 for (std::size_t q = 0; q < tk.precedences.size(); ++q) {
492 const detail::RawPrecedence<T>& pc = tk.precedences[q];
493 if (chain_precs.count(q) || !is_seq(pc) || pc.preacts[0] != m.acts[chain.back()].name)
494 continue;
495 const std::map<std::string, std::size_t>::const_iterator nx = slot_of.find(pc.postacts[0]);
496 if (nx == slot_of.end() || std::find(chain.begin(), chain.end(), nx->second) != chain.end())
497 continue;
498 chain.push_back(nx->second);
499 chain_precs.insert(q);
500 grew = true;
501 break;
502 }
503 }
504 bool ok = m.acts[chain.back()].phase >= 2 && m.acts[chain.back()].phase <= 3;
505 for (std::size_t i = 1; ok && i < chain.size(); ++i)
506 ok = m.acts[chain[i]].phase > m.acts[chain[i - 1]].phase &&
507 m.acts[chain[i]].bound_to_entry.empty();
508 std::set<std::string> names;
509 for (std::size_t a : chain) names.insert(m.acts[a].name);
510 for (std::size_t q = 0; ok && q < tk.precedences.size(); ++q) {
511 if (chain_precs.count(q)) continue;
512 const detail::RawPrecedence<T>& pc = tk.precedences[q];
513 for (const std::string& nmv : pc.preacts) ok = ok && !names.count(nmv);
514 for (const std::string& nmv : pc.postacts) ok = ok && !names.count(nmv);
515 }
516 // the phase-1 activity is the implicit reply; any other reply makes the chain inexpressible
517 bool replied = false;
518 for (std::size_t e2 = 0; ok && e2 < m.entries.size(); ++e2)
519 for (const std::string& r : m.entries[e2].reply_activities) {
520 if (!names.count(r) && e2 != e) continue;
521 if (e2 != e || r != m.acts[chain[0]].name) ok = false;
522 else replied = true;
523 }
524 if (ok && !replied && tk.sched != SchedStrategy::REF && m.acts[chain[0]].phase == 1) ok = false;
525 if (!ok) continue;
526 phase_chains[en.name] = chain;
527 phase_acts.insert(chain.begin(), chain.end());
528 phase_precs.insert(chain_precs.begin(), chain_precs.end());
529 }
530 }
531
532 for (std::size_t e = 0; e < m.entries.size(); ++e) {
533 if (m.entries[e].task_slot != t) continue;
534 const detail::RawEntry<T>& en = m.entries[e];
535 xml::Element& ee = te.add_child("entry");
536 ee.set_attr("name", entry_name(en.name));
537 ee.set_attr("type", phase_chains.count(en.name) ? "PH1PH2" : "NONE");
538 if (en.has_arrival && !en.arrival.disabled) {
539 const double mean = num_traits<T>::to_double(en.arrival.mean);
540 if (std::isfinite(mean) && mean > lang::GlobalConstants::FineTol)
541 ee.set_attr("open-arrival-rate", detail::lqnx_num(1.0 / mean));
542 }
543 // LINE dialect: <item-entry> makes the entry an ItemEntry on read. The model keeps the pmf, not the
544 // law, so it is written as a DiscreteSampler over the default support 1..n, which writeXML.m omits.
545 if (en.cardinality > 0) {
546 xml::Element& ie = ee.add_child("item-entry");
547 ie.set_attr("cardinality", std::to_string(en.cardinality));
548 if (!en.popularity.empty()) {
549 xml::Element& pe = ie.add_child("access-popularity");
550 pe.set_attr("name", "DiscreteSampler");
551 for (std::size_t k = 0; k < en.popularity.size(); ++k)
552 pe.add_child("parameter")
553 .set_attr("value", detail::lqnx_num_of<T>(en.popularity[k]));
554 }
555 }
556 for (std::size_t f = 0; f < en.fwd_dest.size(); ++f) {
557 xml::Element& fe = ee.add_child("forwarding");
558 fe.set_attr("dest", entry_name(en.fwd_dest[f]));
559 fe.set_attr("prob", detail::lqnx_num_of<T>(en.fwd_prob[f]));
560 }
561 const typename std::map<std::string, std::vector<std::size_t>>::const_iterator ch =
562 phase_chains.find(en.name);
563 if (ch != phase_chains.end()) {
564 xml::Element& epa = ee.add_child("entry-phase-activities");
565 for (std::size_t a : ch->second) write_activity(epa, m.acts[a], true);
566 }
567 }
568
569 // every activity in entry-phase-activities leaves no task-activities to write
570 bool any_task_act = false;
571 for (std::size_t a = 0; a < m.acts.size(); ++a)
572 if (m.acts[a].task_slot == t && !phase_acts.count(a)) any_task_act = true;
573 for (std::size_t e = 0; !any_task_act && e < m.entries.size(); ++e)
574 if (m.entries[e].task_slot == t && !phase_chains.count(m.entries[e].name)) any_task_act = true;
575 if (!any_task_act) continue;
576 xml::Element& ta = te.add_child("task-activities");
577 for (std::size_t a = 0; a < m.acts.size(); ++a) {
578 if (m.acts[a].task_slot != t || phase_acts.count(a)) continue;
579 write_activity(ta, m.acts[a], false);
580 }
581
582 for (std::size_t q = 0; q < tk.precedences.size(); ++q) {
583 if (phase_precs.count(q)) continue;
584 const detail::RawPrecedence<T>& pc = tk.precedences[q];
585 xml::Element& pce = ta.add_child("precedence");
586
587 xml::Element& pre = pce.add_child(detail::precedence_tag(pc.pretype));
588 // only a genuine quorum k < n: a join waiting for all is the LQN default and lqns rejects the attribute
589 if (pc.pretype == PrecedenceType::PRE_AND && pc.has_quorum && pc.quorum >= 1 && pc.quorum < pc.preacts.size())
590 pre.set_attr("quorum", detail::lqnx_num(static_cast<double>(pc.quorum)));
591 for (std::size_t i = 0; i < pc.preacts.size(); ++i)
592 pre.add_child("activity").set_attr("name", act_name(pc.preacts[i]));
593
594 xml::Element& post = pce.add_child(detail::precedence_tag(pc.posttype));
595 if (pc.posttype == PrecedenceType::POST_OR) {
596 for (std::size_t i = 0; i < pc.postacts.size(); ++i) {
597 xml::Element& ae = post.add_child("activity");
598 ae.set_attr("name", act_name(pc.postacts[i]));
599 if (i < pc.postparams.size())
600 ae.set_attr("prob", detail::lqnx_num_of<T>(pc.postparams[i]));
601 }
602 } else if (pc.posttype == PrecedenceType::POST_LOOP) {
603 // The LAST post activity is the loop exit and is named by
604 // the `end` attribute, not by an <activity> of its own --
605 // which is also how the reader takes it apart again.
606 if (pc.postacts.empty())
607 throw InputError("write_lqnx: a post-LOOP names no activity");
608 for (std::size_t i = 0; i + 1 < pc.postacts.size(); ++i) {
609 xml::Element& ae = post.add_child("activity");
610 ae.set_attr("name", act_name(pc.postacts[i]));
611 if (i < pc.postparams.size())
612 ae.set_attr("count", detail::lqnx_num_of<T>(pc.postparams[i]));
613 }
614 post.set_attr("end", act_name(pc.postacts.back()));
615 } else if (pc.posttype == PrecedenceType::POST_CACHE) {
616 // NAME THE BRANCH, do not leave it to position: hit first and
617 // miss second is the builder's order, but a reader that sorts
618 // or a writer that reorders would otherwise swap them
619 // silently. writeXML.m:305-317 sets the same attribute.
620 static const char* const kResult[2] = {"hit", "miss"};
621 for (std::size_t i = 0; i < pc.postacts.size(); ++i) {
622 xml::Element& ae = post.add_child("activity");
623 ae.set_attr("name", act_name(pc.postacts[i]));
624 if (i < 2) ae.set_attr("cache-result", kResult[i]);
625 }
626 } else {
627 for (std::size_t i = 0; i < pc.postacts.size(); ++i)
628 post.add_child("activity").set_attr("name", act_name(pc.postacts[i]));
629 }
630 }
631
632 if (tk.sched != SchedStrategy::REF) {
633 for (std::size_t e = 0; e < m.entries.size(); ++e) {
634 if (m.entries[e].task_slot != t) continue;
635 const std::string& ename = m.entries[e].name;
636 if (!needs_reply.count(ename) || phase_chains.count(ename)) continue;
637 const std::map<std::string, std::vector<std::string>>::const_iterator it =
638 replies.find(ename);
639 if (it == replies.end() || it->second.empty())
640 throw InputError(
641 "write_lqnx: entry '" + ename +
642 "' is called synchronously but no activity replies to it, and none "
643 "can be inferred; declare one with replies_to()");
644 xml::Element& re = ta.add_child("reply-entry");
645 re.set_attr("name", entry_name(ename));
646 for (std::size_t r = 0; r < it->second.size(); ++r)
647 re.add_child("reply-activity").set_attr("name", act_name(it->second[r]));
648 }
649 }
650 }
651 }
652
653 if (out_text != NULL)
654 *out_text = xml::serialize(root);
655 else
656 xml::write_file(path, root);
657 return report;
658}
659
660/**
661 * The `.lqnx` document as a STRING, for a caller with no file to write to --
662 * the CLI's model-generation mode hands the XML back through its JSON envelope,
663 * and a test can compare documents without a temporary directory.
664 */
665template <class T>
666std::string lqnx_to_string(const LqnModel<T>& m,
667 const std::string& model_name = std::string("LQN"),
668 bool use_abstract_names = false) {
669 std::string text;
670 write_lqnx(m, std::string(), model_name, use_abstract_names, &text);
671 return text;
672}
673
674} // namespace lqn
675} // namespace line
676
677#endif // LINE_LANG_LQN_LQN_WRITER_H
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
The exception types the port throws.
.lqnx -> LqnStruct, a port of matlab/src/lang/layered/@LayeredNetwork/parseXML.m followed by ....
PrecedenceType
Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType.
Definition lang_types.h:477
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
std::string sched_to_lqnx(SchedStrategy s)
The scheduling attribute an .lqnx processor or task carries for a strategy.
Definition lang_types.h:311
const char * routing_to_text(RoutingStrategy r)
Definition lang_types.h:409
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:385
LqnStruct< T > lqn_finalize(const LqnModel< T > &m)
Port of @LayeredNetwork/getStruct.m: flatten the model into its struct.
Definition lqn_reader.h:501
LqnWriteReport write_lqnx(const LqnModel< T > &m, const std::string &path, const std::string &model_name=std::string("LQN"), bool use_abstract_names=false, std::string *out_text=NULL)
Write a layered model as a .lqnx document.
Definition lqn_writer.h:220
std::string lqnx_to_string(const LqnModel< T > &m, const std::string &model_name=std::string("LQN"), bool use_abstract_names=false)
The .lqnx document as a STRING, for a caller with no file to write to – the CLI's model-generation mo...
Definition lqn_writer.h:666
void write_file(const std::string &path, const Element &root)
Serialize to a file, creating or truncating it.
Definition xml.h:376
std::string serialize(const Element &root)
Serialize a document: the XML declaration MATLAB's xmlwrite emits, then the root subtree.
Definition xml.h:369
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static constexpr double FineTol
Definition lang_types.h:765
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:411
std::vector< detail::RawTask< T > > tasks
Definition lqn_reader.h:415
std::vector< detail::RawActivity< T > > acts
Definition lqn_reader.h:417
std::map< std::size_t, std::vector< detail::RawServerPool< T > > > proc_pools
Definition lqn_reader.h:441
std::vector< detail::RawProc > procs
Definition lqn_reader.h:414
std::map< std::size_t, CdScaling< T > > proc_jdscaling
Definition lqn_reader.h:439
std::map< std::size_t, std::pair< Matrix< T >, std::vector< T > > > proc_lincon
Definition lqn_reader.h:427
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:426
std::map< std::size_t, CdScaling< T > > proc_cdscaling
Definition lqn_reader.h:437
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:436
std::vector< detail::RawEntry< T > > entries
Definition lqn_reader.h:416
What the schema could not carry, one human-readable line per loss.
Definition lqn_writer.h:67
std::vector< std::string > dropped
Definition lqn_writer.h:68
std::string name
Definition xml.h:55
Element & add_child(const std::string &tag)
createElement + appendChild in one step; the child is owned here.
Definition xml.h:107
Element & set_attr(const std::string &key, const std::string &value)
setAttribute: replace the value in place when the key already exists, otherwise append.
Definition xml.h:96
A minimal XML DOM: read for the .lqnx interchange format, write for the JMT .jsimg and ....