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 ignores the two elements and answers the model
39 * without the cold start, which is what it would do with them absent too.
40 *
41 * REPLIES. lqns requires every synchronously-called entry of a non-reference
42 * task to name its reply activity. A model built in code, or read from a
43 * document that left them implicit, has none declared, so the implicit rule of
44 * getStruct.m:641-671 is reproduced here: a leaf activity of the task (no
45 * successor within the same task) replies to the entry reached by walking the
46 * graph backwards, and an entry that declares any reply keeps its own.
47 */
48
49#include <algorithm>
50#include <cmath>
51#include <cstdio>
52#include <cstdlib>
53#include <map>
54#include <set>
55#include <string>
56#include <vector>
57
59#include "line/util/error.h"
60#include "line/util/xml.h"
61
62namespace line {
63namespace lqn {
64
65/** What the schema could not carry, one human-readable line per loss. */
67 std::vector<std::string> dropped;
68};
69
70namespace detail {
71
72/**
73 * Shortest decimal that reads back as the same double.
74 *
75 * num2str, which the reference uses, keeps five significant digits, so a demand
76 * of 1/3 reaches lqns as 0.33333 and the answer differs in the fourth digit
77 * from the one this port computes in-process. The file is a wire format for a
78 * solver, not a display, so it carries the value.
79 */
80inline std::string lqnx_num(double v) {
81 if (std::isinf(v)) return v > 0 ? "inf" : "-inf";
82 char buf[40];
83 for (int prec = 15; prec <= 17; ++prec) {
84 std::snprintf(buf, sizeof(buf), "%.*g", prec, v);
85 if (std::strtod(buf, nullptr) == v) return std::string(buf);
86 }
87 return std::string(buf);
88}
89
90template <class T>
91std::string lqnx_num_of(const T& v) {
92 return lqnx_num(num_traits<T>::to_double(v));
93}
94
95/** The `pre` / `post` element name of a precedence kind. */
96inline const char* precedence_tag(PrecedenceType t) {
97 switch (t) {
98 case PrecedenceType::PRE_SEQ: return "pre";
99 case PrecedenceType::PRE_AND: return "pre-AND";
100 case PrecedenceType::PRE_OR: return "pre-OR";
101 case PrecedenceType::POST_SEQ: return "post";
102 case PrecedenceType::POST_AND: return "post-AND";
103 case PrecedenceType::POST_OR: return "post-OR";
104 case PrecedenceType::POST_LOOP: return "post-LOOP";
105 // `post-CACHE` IS A LINE EXTENSION OF THE SCHEMA, and all three reference
106 // codebases already write and read it: ActivityPrecedenceType.toText
107 // names it, writeXML.m:306 and layered.py:3287 emit it, parseXML.m:449
108 // and layered.py:4179 take it back. Refusing it here left this port the
109 // only one that could not round-trip a layered cache-queueing model
110 // through .lqnx, and its own reader threw on the element.
111 case PrecedenceType::POST_CACHE: return "post-CACHE";
112 default:
113 throw UnsupportedError("lqn writer: precedence with no schema element");
114 }
115}
116
117/** ReplacementStrategy -> the `<cache replacement>` name, spelled as writeXML.m's repl_to_lqnx and the reader take it. */
118inline const char* replacement_to_lqnx(lang::ReplacementStrategy r) {
120 switch (r) {
121 case R::LRU: return "LRU";
122 case R::FIFO: return "FIFO";
123 case R::RR: return "RR";
124 case R::SFIFO: return "SFIFO";
125 case R::HLRU: return "HLRU";
126 case R::CLIMB: return "CLIMB";
127 case R::QLRU: return "QLRU";
128 default: throw UnsupportedError("lqn writer: cache replacement strategy with no .lqnx spelling");
129 }
130}
131
132/**
133 * A call group's `strategy` name, spelled as the JSON interchange spells a
134 * routing strategy: the enum CONSTANT, upper case, not the lower-case name
135 * `lang::routing_to_text` prints in a struct dump.
136 *
137 * Only the two strategies a call group can be built with are named: WRROBIN
138 * would need per-target weights the group API does not take, and the remaining
139 * strategies are not dispatch policies at all, so an unnamed one is an error
140 * rather than a silent PROB.
141 */
142inline const char* callgroup_to_lqnx(lang::RoutingStrategy r) {
143 if (r == lang::RoutingStrategy::RROBIN) return "RROBIN";
144 if (r == lang::RoutingStrategy::JSQ) return "JSQ";
145 throw UnsupportedError("lqn writer: call groups carry RROBIN or JSQ; routing strategy '" +
146 std::string(lang::routing_to_text(r)) + "' cannot be written to .lqnx");
147}
148
149/**
150 * The reply activities of each entry, declared where declared and inferred
151 * where not, keyed by entry name.
152 *
153 * Port of the implicit-reply pass of getStruct.m: a LEAF activity of a task
154 * (one with no successor among that task's own activities) replies, and the
155 * entry it replies to is found by walking `graph` backwards through first
156 * ancestors until an ENTRY is reached. An entry with an explicit reply keeps
157 * it, which is what makes a phase-2 activity possible.
158 */
159template <class T>
160std::map<std::string, std::vector<std::string>> reply_activities(const LqnModel<T>& m,
161 const LqnStruct<T>& sn) {
162 std::map<std::string, std::vector<std::string>> out;
163 std::set<std::string> explicit_reply;
164 for (std::size_t e = 0; e < m.entries.size(); ++e)
165 if (!m.entries[e].reply_activities.empty()) {
166 out[m.entries[e].name] = m.entries[e].reply_activities;
167 explicit_reply.insert(m.entries[e].name);
168 }
169
170 for (std::size_t t = 1; t <= sn.ntasks; ++t) {
171 const std::size_t tidx = sn.tshift + t;
172 for (std::size_t aidx : sn.actsof[tidx]) {
173 bool is_reply = true;
174 const std::vector<std::size_t> post = sn.graph.succ(aidx);
175 for (std::size_t p : post)
176 if (std::find(sn.actsof[tidx].begin(), sn.actsof[tidx].end(), p) !=
177 sn.actsof[tidx].end())
178 is_reply = false;
179 if (!is_reply) continue;
180 // A leaf: walk back to the entry it belongs to, first ancestor only,
181 // exactly as the reference does.
182 std::size_t parent = aidx;
183 std::size_t hops = 0;
184 while (sn.type[parent] != LqnElement::ENTRY && hops++ <= sn.nidx) {
185 const std::vector<std::size_t> anc = sn.graph.pred(parent);
186 if (anc.empty()) break;
187 parent = anc[0];
188 }
189 if (sn.type[parent] != LqnElement::ENTRY) continue;
190 const std::string& ename = sn.names[parent];
191 if (explicit_reply.count(ename)) continue;
192 out[ename].push_back(sn.names[aidx]);
193 }
194 }
195 return out;
196}
197
198} // namespace detail
199
200/**
201 * Write a layered model as a .lqnx document.
202 *
203 * @param m the intermediate model, from the builder or from
204 * read_lqnx_model
205 * @param path file to create
206 * @param model_name the `name` attribute of `<lqn-model>`
207 * @param use_abstract_names rename elements P1/T1/E1/A1, as writeXML's third
208 * argument does, for a document that is compared
209 * rather than read
210 * @param out_text when non-null, the document is SERIALIZED INTO IT
211 * and no file is written (`path` is then ignored).
212 * The tree cannot be returned instead:
213 * `xml::Element` owns its children through
214 * unique_ptr and they hold raw `parent` back
215 * pointers, so a moved tree dangles.
216 * @return what the schema could not carry
217 */
218template <class T>
219LqnWriteReport write_lqnx(const LqnModel<T>& m, const std::string& path,
220 const std::string& model_name = std::string("LQN"),
221 bool use_abstract_names = false,
222 std::string* out_text = NULL) {
223 LqnWriteReport report;
224 const LqnStruct<T> sn = lqn_finalize(m);
225
226 // ---- constructs with no element in the schema are refused by name -----
227 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
228 const detail::RawTask<T>& tk = m.tasks[t];
229 if (!tk.linconrows.empty() || tk.lincon_A.rows() > 0)
230 throw UnsupportedError("write_lqnx: task '" + tk.name +
231 "' declares an admission constraint, which the LQN XML schema "
232 "cannot express; it travels as JSON `admissionConstraints`");
233 }
234 if (!m.proc_linconrows.empty() || !m.proc_lincon.empty())
235 throw UnsupportedError(
236 "write_lqnx: a processor declares an admission constraint, which the LQN XML schema "
237 "cannot express; it travels as JSON `admissionConstraints`");
238
239 // ---- name map, either identity or the abstract P1/T1/E1/A1 -----------
240 //
241 // ONE MAP PER KIND, not the reference's single nodeHashMap. An LQN
242 // routinely names a processor, its task and that task's entry alike (`c0`
243 // throughout the lqngen corpus), and every reference in the document is to
244 // a KNOWN kind: `dest` is an entry, `bound-to-entry` an entry, a precedence
245 // operand an activity. With one map the last declaration wins and the
246 // abstract-name mode would emit `E1` where the processor should be, writing
247 // a document that names elements which do not exist.
248 std::map<std::string, std::string> nm_host, nm_task, nm_entry, nm_act;
249 {
250 std::size_t tctr = 0, ectr = 0, actr = 0;
251 for (std::size_t p = 0; p < m.procs.size(); ++p) {
252 char buf[32];
253 std::snprintf(buf, sizeof(buf), "P%zu", p + 1);
254 nm_host[m.procs[p].name] = use_abstract_names ? buf : m.procs[p].name;
255 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
256 if (m.tasks[t].proc_slot != p) continue;
257 std::snprintf(buf, sizeof(buf), "T%zu", ++tctr);
258 nm_task[m.tasks[t].name] = use_abstract_names ? buf : m.tasks[t].name;
259 for (std::size_t e = 0; e < m.entries.size(); ++e) {
260 if (m.entries[e].task_slot != t) continue;
261 std::snprintf(buf, sizeof(buf), "E%zu", ++ectr);
262 nm_entry[m.entries[e].name] = use_abstract_names ? buf : m.entries[e].name;
263 }
264 for (std::size_t a = 0; a < m.acts.size(); ++a) {
265 if (m.acts[a].task_slot != t) continue;
266 std::snprintf(buf, sizeof(buf), "A%zu", ++actr);
267 nm_act[m.acts[a].name] = use_abstract_names ? buf : m.acts[a].name;
268 }
269 }
270 }
271 }
272 // A name the map has never seen is a dangling reference, not a name to
273 // invent: the document would name an element that does not exist and lqns
274 // would refuse the file with no indication of which model built it.
275 auto lookup = [](const std::map<std::string, std::string>& nm, const std::string& raw,
276 const char* kind) -> const std::string& {
277 const std::map<std::string, std::string>::const_iterator it = nm.find(raw);
278 if (it == nm.end())
279 throw InputError("write_lqnx: '" + raw + "' is referenced as a " + kind +
280 " but no " + kind + " declares it");
281 return it->second;
282 };
283 auto host_name = [&](const std::string& r) -> const std::string& {
284 return lookup(nm_host, r, "processor");
285 };
286 auto task_name = [&](const std::string& r) -> const std::string& {
287 return lookup(nm_task, r, "task");
288 };
289 auto entry_name = [&](const std::string& r) -> const std::string& {
290 return lookup(nm_entry, r, "entry");
291 };
292 auto act_name = [&](const std::string& r) -> const std::string& {
293 return lookup(nm_act, r, "activity");
294 };
295
296 const std::map<std::string, std::vector<std::string>> replies =
297 detail::reply_activities(m, sn);
298
299 // Which entries lqns needs a reply for: called synchronously, or the target
300 // of a forwarding chain. An entry reached only by an asynchronous call has
301 // nobody to reply TO, and a reply-entry declared for it is an error there.
302 std::set<std::string> needs_reply;
303 for (std::size_t e = 1; e <= sn.nentries; ++e) {
304 const std::size_t eidx = sn.eshift + e;
305 bool wanted = sn.issynccaller.any_col(eidx);
306 for (std::size_t c = 1; !wanted && c <= sn.ncalls; ++c)
307 if (sn.calltype[c] == CallType::FWD && sn.callpair_dst[c] == eidx) wanted = true;
308 if (wanted) needs_reply.insert(sn.names[eidx]);
309 }
310
311 xml::Element root;
312 root.name = "lqn-model";
313 root.set_attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
314 root.set_attr("xsi:noNamespaceSchemaLocation", "lqn.xsd");
315 root.set_attr("name", model_name);
316
317 for (std::size_t p = 0; p < m.procs.size(); ++p) {
318 const detail::RawProc& pr = m.procs[p];
319 xml::Element& pe = root.add_child("processor");
320 pe.set_attr("name", host_name(pr.name));
321 pe.set_attr("scheduling", lang::sched_to_lqnx(pr.sched));
322 if (pr.repl > 1.0) pe.set_attr("replication", detail::lqnx_num(pr.repl));
323 if (pr.sched != SchedStrategy::INF) {
324 // An infinite multiplicity on a finite discipline has no spelling;
325 // the reference writes 1, which is what a single server means.
326 const double mult = std::isinf(pr.mult) ? 1.0 : pr.mult;
327 pe.set_attr("multiplicity", detail::lqnx_num(mult));
328 }
329 if (pr.sched == SchedStrategy::PS && pr.quantum > 0.0)
330 pe.set_attr("quantum", detail::lqnx_num(pr.quantum));
331 pe.set_attr("speed-factor", detail::lqnx_num(pr.speed_factor));
332
333 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
334 if (m.tasks[t].proc_slot != p) continue;
335 const detail::RawTask<T>& tk = m.tasks[t];
336 xml::Element& te = pe.add_child("task");
337 te.set_attr("name", task_name(tk.name));
338 te.set_attr("scheduling", lang::sched_to_lqnx(tk.sched));
339 // written only when nonzero, as writeXML.m does; lqns reads it for priority scheduling
340 if (tk.priority != 0) te.set_attr("priority", std::to_string(tk.priority));
341 if (tk.repl > 1.0) te.set_attr("replication", detail::lqnx_num(tk.repl));
342 if (tk.sched != SchedStrategy::INF)
343 te.set_attr("multiplicity",
344 detail::lqnx_num(std::isinf(tk.mult) ? 1.0 : tk.mult));
345 const double think = num_traits<T>::to_double(tk.thinktime.mean);
346 if (tk.sched == SchedStrategy::REF) {
347 te.set_attr("think-time", detail::lqnx_num(tk.thinktime.disabled ? 0.0 : think));
348 } else if (!tk.thinktime.disabled && think > 0.0) {
349 report.dropped.push_back("task '" + tk.name + "' has a think time of " +
350 detail::lqnx_num(think) +
351 ", which the schema accepts on reference tasks only");
352 }
353 // LINE dialect, as writeXML.m writes it: <cache> makes the task a CacheTask on read, one <level> per cache list.
354 // Refusing it made a layered cache model the one layered model this port could not write to .lqnx.
355 if (tk.nitems > 0) {
356 xml::Element& ce = te.add_child("cache");
357 ce.set_attr("items", std::to_string(tk.nitems));
358 ce.set_attr("replacement", detail::replacement_to_lqnx(tk.replacestrat));
359 if (tk.retrieval) ce.set_attr("retrieval", "true");
360 for (std::size_t lv = 0; lv < tk.itemcap.size(); ++lv)
361 ce.add_child("level").set_attr("capacity", std::to_string(tk.itemcap[lv]));
362 }
363 // <setup>/<delay-off> are a LINE extension the reference writes
364 // ahead of fan-out (writeXML.m:170-183), and every reader in the
365 // project takes them; refusing them here made the C++ row the only
366 // one that could not round-trip a SetupTask.
367 if (!tk.setuptime.disabled &&
369 xml::Element& se = te.add_child("setup");
370 se.set_attr("mean", detail::lqnx_num(num_traits<T>::to_double(tk.setuptime.mean)));
371 se.set_attr("scv", detail::lqnx_num(num_traits<T>::to_double(tk.setuptime.scv)));
372 }
373 if (!tk.delayofftime.disabled &&
375 xml::Element& de = te.add_child("delay-off");
376 de.set_attr("mean",
377 detail::lqnx_num(num_traits<T>::to_double(tk.delayofftime.mean)));
378 de.set_attr("scv",
379 detail::lqnx_num(num_traits<T>::to_double(tk.delayofftime.scv)));
380 }
381 // lqn-core.xsd (TaskType) places fan-out and fan-in before the entries.
382 for (std::size_t f = 0; f < tk.fanout.size(); ++f) {
383 xml::Element& fe = te.add_child("fan-out");
384 fe.set_attr("dest", task_name(tk.fanout[f].first));
385 fe.set_attr("value", detail::lqnx_num(tk.fanout[f].second));
386 }
387 for (std::size_t f = 0; f < tk.fanin.size(); ++f) {
388 xml::Element& fe = te.add_child("fan-in");
389 fe.set_attr("source", task_name(tk.fanin[f].first));
390 fe.set_attr("value", detail::lqnx_num(tk.fanin[f].second));
391 }
392
393 for (std::size_t e = 0; e < m.entries.size(); ++e) {
394 if (m.entries[e].task_slot != t) continue;
395 const detail::RawEntry<T>& en = m.entries[e];
396 xml::Element& ee = te.add_child("entry");
397 ee.set_attr("name", entry_name(en.name));
398 ee.set_attr("type", "NONE");
399 if (en.has_arrival && !en.arrival.disabled) {
400 const double mean = num_traits<T>::to_double(en.arrival.mean);
401 if (std::isfinite(mean) && mean > lang::GlobalConstants::FineTol)
402 ee.set_attr("open-arrival-rate", detail::lqnx_num(1.0 / mean));
403 }
404 // LINE dialect: <item-entry> makes the entry an ItemEntry on read. The model keeps the pmf, not the
405 // law, so it is written as a DiscreteSampler over the default support 1..n, which writeXML.m omits.
406 if (en.cardinality > 0) {
407 xml::Element& ie = ee.add_child("item-entry");
408 ie.set_attr("cardinality", std::to_string(en.cardinality));
409 if (!en.popularity.empty()) {
410 xml::Element& pe = ie.add_child("access-popularity");
411 pe.set_attr("name", "DiscreteSampler");
412 for (std::size_t k = 0; k < en.popularity.size(); ++k)
413 pe.add_child("parameter")
414 .set_attr("value", detail::lqnx_num_of<T>(en.popularity[k]));
415 }
416 }
417 for (std::size_t f = 0; f < en.fwd_dest.size(); ++f) {
418 xml::Element& fe = ee.add_child("forwarding");
419 fe.set_attr("dest", entry_name(en.fwd_dest[f]));
420 fe.set_attr("prob", detail::lqnx_num_of<T>(en.fwd_prob[f]));
421 }
422 }
423
424 xml::Element& ta = te.add_child("task-activities");
425 for (std::size_t a = 0; a < m.acts.size(); ++a) {
426 if (m.acts[a].task_slot != t) continue;
427 const detail::RawActivity<T>& ac = m.acts[a];
428 xml::Element& ae = ta.add_child("activity");
429 ae.set_attr("host-demand-mean",
430 detail::lqnx_num(ac.hostdem.disabled
431 ? 0.0
432 : num_traits<T>::to_double(ac.hostdem.mean)));
433 ae.set_attr("host-demand-cvsq",
434 detail::lqnx_num(ac.hostdem.disabled
435 ? 1.0
436 : num_traits<T>::to_double(ac.hostdem.scv)));
437 if (!ac.bound_to_entry.empty())
438 ae.set_attr("bound-to-entry", entry_name(ac.bound_to_entry));
439 ae.set_attr("call-order", ac.call_order);
440 ae.set_attr("name", act_name(ac.name));
441 const double athink = num_traits<T>::to_double(ac.thinktime.mean);
442 if (!ac.thinktime.disabled && athink > lang::GlobalConstants::FineTol)
443 ae.set_attr("think-time", detail::lqnx_num(athink));
444 for (std::size_t c = 0; c < ac.sync_calls.size(); ++c) {
445 xml::Element& ce = ae.add_child("synch-call");
446 ce.set_attr("dest", entry_name(ac.sync_calls[c].dest));
447 ce.set_attr("calls-mean", detail::lqnx_num_of<T>(ac.sync_calls[c].mean));
448 }
449 for (std::size_t c = 0; c < ac.async_calls.size(); ++c) {
450 xml::Element& ce = ae.add_child("asynch-call");
451 ce.set_attr("dest", entry_name(ac.async_calls[c].dest));
452 ce.set_attr("calls-mean", detail::lqnx_num_of<T>(ac.async_calls[c].mean));
453 }
454 // LINE dialect: which of the synch-calls above one dispatcher
455 // issues, and under which strategy. The member calls stay
456 // ordinary synch-calls, so a reader that ignores this element
457 // still sees the same aggregate call means -- which is what
458 // lqns and lqsim, having no dispatcher, should see.
459 for (std::size_t g = 0; g < ac.call_groups.size(); ++g) {
460 xml::Element& ge = ae.add_child("call-group");
461 ge.set_attr("strategy", detail::callgroup_to_lqnx(ac.call_groups[g].first));
462 for (std::size_t d = 0; d < ac.call_groups[g].second.size(); ++d)
463 ge.add_child("dest").set_attr("name",
464 entry_name(ac.call_groups[g].second[d]));
465 }
466 }
467
468 for (std::size_t q = 0; q < tk.precedences.size(); ++q) {
469 const detail::RawPrecedence<T>& pc = tk.precedences[q];
470 xml::Element& pce = ta.add_child("precedence");
471
472 xml::Element& pre = pce.add_child(detail::precedence_tag(pc.pretype));
473 if (pc.pretype == PrecedenceType::PRE_AND && pc.has_quorum)
474 pre.set_attr("quorum", detail::lqnx_num(static_cast<double>(pc.quorum)));
475 for (std::size_t i = 0; i < pc.preacts.size(); ++i)
476 pre.add_child("activity").set_attr("name", act_name(pc.preacts[i]));
477
478 xml::Element& post = pce.add_child(detail::precedence_tag(pc.posttype));
479 if (pc.posttype == PrecedenceType::POST_OR) {
480 for (std::size_t i = 0; i < pc.postacts.size(); ++i) {
481 xml::Element& ae = post.add_child("activity");
482 ae.set_attr("name", act_name(pc.postacts[i]));
483 if (i < pc.postparams.size())
484 ae.set_attr("prob", detail::lqnx_num_of<T>(pc.postparams[i]));
485 }
486 } else if (pc.posttype == PrecedenceType::POST_LOOP) {
487 // The LAST post activity is the loop exit and is named by
488 // the `end` attribute, not by an <activity> of its own --
489 // which is also how the reader takes it apart again.
490 if (pc.postacts.empty())
491 throw InputError("write_lqnx: a post-LOOP names no activity");
492 for (std::size_t i = 0; i + 1 < pc.postacts.size(); ++i) {
493 xml::Element& ae = post.add_child("activity");
494 ae.set_attr("name", act_name(pc.postacts[i]));
495 if (i < pc.postparams.size())
496 ae.set_attr("count", detail::lqnx_num_of<T>(pc.postparams[i]));
497 }
498 post.set_attr("end", act_name(pc.postacts.back()));
499 } else if (pc.posttype == PrecedenceType::POST_CACHE) {
500 // NAME THE BRANCH, do not leave it to position: hit first and
501 // miss second is the builder's order, but a reader that sorts
502 // or a writer that reorders would otherwise swap them
503 // silently. writeXML.m:305-317 sets the same attribute.
504 static const char* const kResult[2] = {"hit", "miss"};
505 for (std::size_t i = 0; i < pc.postacts.size(); ++i) {
506 xml::Element& ae = post.add_child("activity");
507 ae.set_attr("name", act_name(pc.postacts[i]));
508 if (i < 2) ae.set_attr("cache-result", kResult[i]);
509 }
510 } else {
511 for (std::size_t i = 0; i < pc.postacts.size(); ++i)
512 post.add_child("activity").set_attr("name", act_name(pc.postacts[i]));
513 }
514 }
515
516 if (tk.sched != SchedStrategy::REF) {
517 for (std::size_t e = 0; e < m.entries.size(); ++e) {
518 if (m.entries[e].task_slot != t) continue;
519 const std::string& ename = m.entries[e].name;
520 if (!needs_reply.count(ename)) continue;
521 const std::map<std::string, std::vector<std::string>>::const_iterator it =
522 replies.find(ename);
523 if (it == replies.end() || it->second.empty())
524 throw InputError(
525 "write_lqnx: entry '" + ename +
526 "' is called synchronously but no activity replies to it, and none "
527 "can be inferred; declare one with replies_to()");
528 xml::Element& re = ta.add_child("reply-entry");
529 re.set_attr("name", entry_name(ename));
530 for (std::size_t r = 0; r < it->second.size(); ++r)
531 re.add_child("reply-activity").set_attr("name", act_name(it->second[r]));
532 }
533 }
534 }
535 }
536
537 if (out_text != NULL)
538 *out_text = xml::serialize(root);
539 else
540 xml::write_file(path, root);
541 return report;
542}
543
544/**
545 * The `.lqnx` document as a STRING, for a caller with no file to write to --
546 * the CLI's model-generation mode hands the XML back through its JSON envelope,
547 * and a test can compare documents without a temporary directory.
548 */
549template <class T>
550std::string lqnx_to_string(const LqnModel<T>& m,
551 const std::string& model_name = std::string("LQN"),
552 bool use_abstract_names = false) {
553 std::string text;
554 write_lqnx(m, std::string(), model_name, use_abstract_names, &text);
555 return text;
556}
557
558} // namespace lqn
559} // namespace line
560
561#endif // LINE_LANG_LQN_LQN_WRITER_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
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:472
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
std::string sched_to_lqnx(SchedStrategy s)
The scheduling attribute an .lqnx processor or task carries for a strategy.
Definition lang_types.h:306
const char * routing_to_text(RoutingStrategy r)
Definition lang_types.h:404
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:380
LqnStruct< T > lqn_finalize(const LqnModel< T > &m)
Port of @LayeredNetwork/getStruct.m: flatten the model into its struct.
Definition lqn_reader.h:444
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:219
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:550
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
static constexpr double FineTol
Definition lang_types.h:760
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:409
std::vector< detail::RawTask< T > > tasks
Definition lqn_reader.h:413
std::vector< detail::RawActivity< T > > acts
Definition lqn_reader.h:415
std::vector< detail::RawProc > procs
Definition lqn_reader.h:412
std::map< std::size_t, std::pair< Matrix< T >, std::vector< T > > > proc_lincon
Definition lqn_reader.h:425
std::map< std::size_t, std::vector< detail::RawLinConRow< T > > > proc_linconrows
Admission constraints declared on a HOST, by 0-based processor slot.
Definition lqn_reader.h:424
std::vector< detail::RawEntry< T > > entries
Definition lqn_reader.h:414
What the schema could not carry, one human-readable line per loss.
Definition lqn_writer.h:66
std::vector< std::string > dropped
Definition lqn_writer.h:67
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 ....