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/**
118 * A call group's `strategy` name, spelled as the JSON interchange spells a
119 * routing strategy: the enum CONSTANT, upper case, not the lower-case name
120 * `lang::routing_to_text` prints in a struct dump.
121 *
122 * Only the two strategies a call group can be built with are named: WRROBIN
123 * would need per-target weights the group API does not take, and the remaining
124 * strategies are not dispatch policies at all, so an unnamed one is an error
125 * rather than a silent PROB.
126 */
127inline const char* callgroup_to_lqnx(lang::RoutingStrategy r) {
128 if (r == lang::RoutingStrategy::RROBIN) return "RROBIN";
129 if (r == lang::RoutingStrategy::JSQ) return "JSQ";
130 throw UnsupportedError("lqn writer: call groups carry RROBIN or JSQ; routing strategy '" +
131 std::string(lang::routing_to_text(r)) + "' cannot be written to .lqnx");
132}
133
134/**
135 * The reply activities of each entry, declared where declared and inferred
136 * where not, keyed by entry name.
137 *
138 * Port of the implicit-reply pass of getStruct.m: a LEAF activity of a task
139 * (one with no successor among that task's own activities) replies, and the
140 * entry it replies to is found by walking `graph` backwards through first
141 * ancestors until an ENTRY is reached. An entry with an explicit reply keeps
142 * it, which is what makes a phase-2 activity possible.
143 */
144template <class T>
145std::map<std::string, std::vector<std::string>> reply_activities(const LqnModel<T>& m,
146 const LqnStruct<T>& sn) {
147 std::map<std::string, std::vector<std::string>> out;
148 std::set<std::string> explicit_reply;
149 for (std::size_t e = 0; e < m.entries.size(); ++e)
150 if (!m.entries[e].reply_activities.empty()) {
151 out[m.entries[e].name] = m.entries[e].reply_activities;
152 explicit_reply.insert(m.entries[e].name);
153 }
154
155 for (std::size_t t = 1; t <= sn.ntasks; ++t) {
156 const std::size_t tidx = sn.tshift + t;
157 for (std::size_t aidx : sn.actsof[tidx]) {
158 bool is_reply = true;
159 const std::vector<std::size_t> post = sn.graph.succ(aidx);
160 for (std::size_t p : post)
161 if (std::find(sn.actsof[tidx].begin(), sn.actsof[tidx].end(), p) !=
162 sn.actsof[tidx].end())
163 is_reply = false;
164 if (!is_reply) continue;
165 // A leaf: walk back to the entry it belongs to, first ancestor only,
166 // exactly as the reference does.
167 std::size_t parent = aidx;
168 std::size_t hops = 0;
169 while (sn.type[parent] != LqnElement::ENTRY && hops++ <= sn.nidx) {
170 const std::vector<std::size_t> anc = sn.graph.pred(parent);
171 if (anc.empty()) break;
172 parent = anc[0];
173 }
174 if (sn.type[parent] != LqnElement::ENTRY) continue;
175 const std::string& ename = sn.names[parent];
176 if (explicit_reply.count(ename)) continue;
177 out[ename].push_back(sn.names[aidx]);
178 }
179 }
180 return out;
181}
182
183} // namespace detail
184
185/**
186 * Write a layered model as a .lqnx document.
187 *
188 * @param m the intermediate model, from the builder or from
189 * read_lqnx_model
190 * @param path file to create
191 * @param model_name the `name` attribute of `<lqn-model>`
192 * @param use_abstract_names rename elements P1/T1/E1/A1, as writeXML's third
193 * argument does, for a document that is compared
194 * rather than read
195 * @return what the schema could not carry
196 */
197template <class T>
198LqnWriteReport write_lqnx(const LqnModel<T>& m, const std::string& path,
199 const std::string& model_name = std::string("LQN"),
200 bool use_abstract_names = false) {
201 LqnWriteReport report;
202 const LqnStruct<T> sn = lqn_finalize(m);
203
204 // ---- constructs with no element in the schema are refused by name -----
205 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
206 const detail::RawTask<T>& tk = m.tasks[t];
207 if (tk.nitems > 0)
208 throw UnsupportedError("write_lqnx: task '" + tk.name +
209 "' is a CacheTask, which the LQN XML schema cannot express; "
210 "solve it with SolverLN, which models the cache directly");
211 if (!tk.linconrows.empty() || tk.lincon_A.rows() > 0)
212 throw UnsupportedError("write_lqnx: task '" + tk.name +
213 "' declares an admission constraint, which the LQN XML schema "
214 "cannot express; it travels as JSON `admissionConstraints`");
215 }
216 for (std::size_t e = 0; e < m.entries.size(); ++e)
217 if (m.entries[e].cardinality > 0)
218 throw UnsupportedError("write_lqnx: entry '" + m.entries[e].name +
219 "' is an ItemEntry, which the LQN XML schema cannot express");
220 if (!m.proc_linconrows.empty() || !m.proc_lincon.empty())
221 throw UnsupportedError(
222 "write_lqnx: a processor declares an admission constraint, which the LQN XML schema "
223 "cannot express; it travels as JSON `admissionConstraints`");
224
225 // ---- name map, either identity or the abstract P1/T1/E1/A1 -----------
226 //
227 // ONE MAP PER KIND, not the reference's single nodeHashMap. An LQN
228 // routinely names a processor, its task and that task's entry alike (`c0`
229 // throughout the lqngen corpus), and every reference in the document is to
230 // a KNOWN kind: `dest` is an entry, `bound-to-entry` an entry, a precedence
231 // operand an activity. With one map the last declaration wins and the
232 // abstract-name mode would emit `E1` where the processor should be, writing
233 // a document that names elements which do not exist.
234 std::map<std::string, std::string> nm_host, nm_task, nm_entry, nm_act;
235 {
236 std::size_t tctr = 0, ectr = 0, actr = 0;
237 for (std::size_t p = 0; p < m.procs.size(); ++p) {
238 char buf[32];
239 std::snprintf(buf, sizeof(buf), "P%zu", p + 1);
240 nm_host[m.procs[p].name] = use_abstract_names ? buf : m.procs[p].name;
241 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
242 if (m.tasks[t].proc_slot != p) continue;
243 std::snprintf(buf, sizeof(buf), "T%zu", ++tctr);
244 nm_task[m.tasks[t].name] = use_abstract_names ? buf : m.tasks[t].name;
245 for (std::size_t e = 0; e < m.entries.size(); ++e) {
246 if (m.entries[e].task_slot != t) continue;
247 std::snprintf(buf, sizeof(buf), "E%zu", ++ectr);
248 nm_entry[m.entries[e].name] = use_abstract_names ? buf : m.entries[e].name;
249 }
250 for (std::size_t a = 0; a < m.acts.size(); ++a) {
251 if (m.acts[a].task_slot != t) continue;
252 std::snprintf(buf, sizeof(buf), "A%zu", ++actr);
253 nm_act[m.acts[a].name] = use_abstract_names ? buf : m.acts[a].name;
254 }
255 }
256 }
257 }
258 // A name the map has never seen is a dangling reference, not a name to
259 // invent: the document would name an element that does not exist and lqns
260 // would refuse the file with no indication of which model built it.
261 auto lookup = [](const std::map<std::string, std::string>& nm, const std::string& raw,
262 const char* kind) -> const std::string& {
263 const std::map<std::string, std::string>::const_iterator it = nm.find(raw);
264 if (it == nm.end())
265 throw InputError("write_lqnx: '" + raw + "' is referenced as a " + kind +
266 " but no " + kind + " declares it");
267 return it->second;
268 };
269 auto host_name = [&](const std::string& r) -> const std::string& {
270 return lookup(nm_host, r, "processor");
271 };
272 auto task_name = [&](const std::string& r) -> const std::string& {
273 return lookup(nm_task, r, "task");
274 };
275 auto entry_name = [&](const std::string& r) -> const std::string& {
276 return lookup(nm_entry, r, "entry");
277 };
278 auto act_name = [&](const std::string& r) -> const std::string& {
279 return lookup(nm_act, r, "activity");
280 };
281
282 const std::map<std::string, std::vector<std::string>> replies =
283 detail::reply_activities(m, sn);
284
285 // Which entries lqns needs a reply for: called synchronously, or the target
286 // of a forwarding chain. An entry reached only by an asynchronous call has
287 // nobody to reply TO, and a reply-entry declared for it is an error there.
288 std::set<std::string> needs_reply;
289 for (std::size_t e = 1; e <= sn.nentries; ++e) {
290 const std::size_t eidx = sn.eshift + e;
291 bool wanted = sn.issynccaller.any_col(eidx);
292 for (std::size_t c = 1; !wanted && c <= sn.ncalls; ++c)
293 if (sn.calltype[c] == CallType::FWD && sn.callpair_dst[c] == eidx) wanted = true;
294 if (wanted) needs_reply.insert(sn.names[eidx]);
295 }
296
297 xml::Element root;
298 root.name = "lqn-model";
299 root.set_attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
300 root.set_attr("xsi:noNamespaceSchemaLocation", "lqn.xsd");
301 root.set_attr("name", model_name);
302
303 for (std::size_t p = 0; p < m.procs.size(); ++p) {
304 const detail::RawProc& pr = m.procs[p];
305 xml::Element& pe = root.add_child("processor");
306 pe.set_attr("name", host_name(pr.name));
307 pe.set_attr("scheduling", lang::sched_to_lqnx(pr.sched));
308 if (pr.repl > 1.0) pe.set_attr("replication", detail::lqnx_num(pr.repl));
309 if (pr.sched != SchedStrategy::INF) {
310 // An infinite multiplicity on a finite discipline has no spelling;
311 // the reference writes 1, which is what a single server means.
312 const double mult = std::isinf(pr.mult) ? 1.0 : pr.mult;
313 pe.set_attr("multiplicity", detail::lqnx_num(mult));
314 }
315 if (pr.sched == SchedStrategy::PS && pr.quantum > 0.0)
316 pe.set_attr("quantum", detail::lqnx_num(pr.quantum));
317 pe.set_attr("speed-factor", detail::lqnx_num(pr.speed_factor));
318
319 for (std::size_t t = 0; t < m.tasks.size(); ++t) {
320 if (m.tasks[t].proc_slot != p) continue;
321 const detail::RawTask<T>& tk = m.tasks[t];
322 xml::Element& te = pe.add_child("task");
323 te.set_attr("name", task_name(tk.name));
324 te.set_attr("scheduling", lang::sched_to_lqnx(tk.sched));
325 if (tk.repl > 1.0) te.set_attr("replication", detail::lqnx_num(tk.repl));
326 if (tk.sched != SchedStrategy::INF)
327 te.set_attr("multiplicity",
328 detail::lqnx_num(std::isinf(tk.mult) ? 1.0 : tk.mult));
329 const double think = num_traits<T>::to_double(tk.thinktime.mean);
330 if (tk.sched == SchedStrategy::REF) {
331 te.set_attr("think-time", detail::lqnx_num(tk.thinktime.disabled ? 0.0 : think));
332 } else if (!tk.thinktime.disabled && think > 0.0) {
333 report.dropped.push_back("task '" + tk.name + "' has a think time of " +
334 detail::lqnx_num(think) +
335 ", which the schema accepts on reference tasks only");
336 }
337 // <setup>/<delay-off> are a LINE extension the reference writes
338 // ahead of fan-out (writeXML.m:170-183), and every reader in the
339 // project takes them; refusing them here made the C++ row the only
340 // one that could not round-trip a SetupTask.
341 if (!tk.setuptime.disabled &&
343 xml::Element& se = te.add_child("setup");
344 se.set_attr("mean", detail::lqnx_num(num_traits<T>::to_double(tk.setuptime.mean)));
345 se.set_attr("scv", detail::lqnx_num(num_traits<T>::to_double(tk.setuptime.scv)));
346 }
347 if (!tk.delayofftime.disabled &&
349 xml::Element& de = te.add_child("delay-off");
350 de.set_attr("mean",
351 detail::lqnx_num(num_traits<T>::to_double(tk.delayofftime.mean)));
352 de.set_attr("scv",
353 detail::lqnx_num(num_traits<T>::to_double(tk.delayofftime.scv)));
354 }
355 // lqn-core.xsd (TaskType) places fan-out and fan-in before the entries.
356 for (std::size_t f = 0; f < tk.fanout.size(); ++f) {
357 xml::Element& fe = te.add_child("fan-out");
358 fe.set_attr("dest", task_name(tk.fanout[f].first));
359 fe.set_attr("value", detail::lqnx_num(tk.fanout[f].second));
360 }
361 for (std::size_t f = 0; f < tk.fanin.size(); ++f) {
362 xml::Element& fe = te.add_child("fan-in");
363 fe.set_attr("source", task_name(tk.fanin[f].first));
364 fe.set_attr("value", detail::lqnx_num(tk.fanin[f].second));
365 }
366
367 for (std::size_t e = 0; e < m.entries.size(); ++e) {
368 if (m.entries[e].task_slot != t) continue;
369 const detail::RawEntry<T>& en = m.entries[e];
370 xml::Element& ee = te.add_child("entry");
371 ee.set_attr("name", entry_name(en.name));
372 ee.set_attr("type", "NONE");
373 if (en.has_arrival && !en.arrival.disabled) {
374 const double mean = num_traits<T>::to_double(en.arrival.mean);
375 if (std::isfinite(mean) && mean > lang::GlobalConstants::FineTol)
376 ee.set_attr("open-arrival-rate", detail::lqnx_num(1.0 / mean));
377 }
378 for (std::size_t f = 0; f < en.fwd_dest.size(); ++f) {
379 xml::Element& fe = ee.add_child("forwarding");
380 fe.set_attr("dest", entry_name(en.fwd_dest[f]));
381 fe.set_attr("prob", detail::lqnx_num_of<T>(en.fwd_prob[f]));
382 }
383 }
384
385 xml::Element& ta = te.add_child("task-activities");
386 for (std::size_t a = 0; a < m.acts.size(); ++a) {
387 if (m.acts[a].task_slot != t) continue;
388 const detail::RawActivity<T>& ac = m.acts[a];
389 xml::Element& ae = ta.add_child("activity");
390 ae.set_attr("host-demand-mean",
391 detail::lqnx_num(ac.hostdem.disabled
392 ? 0.0
393 : num_traits<T>::to_double(ac.hostdem.mean)));
394 ae.set_attr("host-demand-cvsq",
395 detail::lqnx_num(ac.hostdem.disabled
396 ? 1.0
397 : num_traits<T>::to_double(ac.hostdem.scv)));
398 if (!ac.bound_to_entry.empty())
399 ae.set_attr("bound-to-entry", entry_name(ac.bound_to_entry));
400 ae.set_attr("call-order", "STOCHASTIC");
401 ae.set_attr("name", act_name(ac.name));
402 const double athink = num_traits<T>::to_double(ac.thinktime.mean);
403 if (!ac.thinktime.disabled && athink > lang::GlobalConstants::FineTol)
404 ae.set_attr("think-time", detail::lqnx_num(athink));
405 for (std::size_t c = 0; c < ac.sync_calls.size(); ++c) {
406 xml::Element& ce = ae.add_child("synch-call");
407 ce.set_attr("dest", entry_name(ac.sync_calls[c].dest));
408 ce.set_attr("calls-mean", detail::lqnx_num_of<T>(ac.sync_calls[c].mean));
409 }
410 for (std::size_t c = 0; c < ac.async_calls.size(); ++c) {
411 xml::Element& ce = ae.add_child("asynch-call");
412 ce.set_attr("dest", entry_name(ac.async_calls[c].dest));
413 ce.set_attr("calls-mean", detail::lqnx_num_of<T>(ac.async_calls[c].mean));
414 }
415 // LINE dialect: which of the synch-calls above one dispatcher
416 // issues, and under which strategy. The member calls stay
417 // ordinary synch-calls, so a reader that ignores this element
418 // still sees the same aggregate call means -- which is what
419 // lqns and lqsim, having no dispatcher, should see.
420 for (std::size_t g = 0; g < ac.call_groups.size(); ++g) {
421 xml::Element& ge = ae.add_child("call-group");
422 ge.set_attr("strategy", detail::callgroup_to_lqnx(ac.call_groups[g].first));
423 for (std::size_t d = 0; d < ac.call_groups[g].second.size(); ++d)
424 ge.add_child("dest").set_attr("name",
425 entry_name(ac.call_groups[g].second[d]));
426 }
427 }
428
429 for (std::size_t q = 0; q < tk.precedences.size(); ++q) {
430 const detail::RawPrecedence<T>& pc = tk.precedences[q];
431 xml::Element& pce = ta.add_child("precedence");
432
433 xml::Element& pre = pce.add_child(detail::precedence_tag(pc.pretype));
434 if (pc.pretype == PrecedenceType::PRE_AND && pc.has_quorum)
435 pre.set_attr("quorum", detail::lqnx_num(static_cast<double>(pc.quorum)));
436 for (std::size_t i = 0; i < pc.preacts.size(); ++i)
437 pre.add_child("activity").set_attr("name", act_name(pc.preacts[i]));
438
439 xml::Element& post = pce.add_child(detail::precedence_tag(pc.posttype));
440 if (pc.posttype == PrecedenceType::POST_OR) {
441 for (std::size_t i = 0; i < pc.postacts.size(); ++i) {
442 xml::Element& ae = post.add_child("activity");
443 ae.set_attr("name", act_name(pc.postacts[i]));
444 if (i < pc.postparams.size())
445 ae.set_attr("prob", detail::lqnx_num_of<T>(pc.postparams[i]));
446 }
447 } else if (pc.posttype == PrecedenceType::POST_LOOP) {
448 // The LAST post activity is the loop exit and is named by
449 // the `end` attribute, not by an <activity> of its own --
450 // which is also how the reader takes it apart again.
451 if (pc.postacts.empty())
452 throw InputError("write_lqnx: a post-LOOP names no activity");
453 for (std::size_t i = 0; i + 1 < pc.postacts.size(); ++i) {
454 xml::Element& ae = post.add_child("activity");
455 ae.set_attr("name", act_name(pc.postacts[i]));
456 if (i < pc.postparams.size())
457 ae.set_attr("count", detail::lqnx_num_of<T>(pc.postparams[i]));
458 }
459 post.set_attr("end", act_name(pc.postacts.back()));
460 } else if (pc.posttype == PrecedenceType::POST_CACHE) {
461 // NAME THE BRANCH, do not leave it to position: hit first and
462 // miss second is the builder's order, but a reader that sorts
463 // or a writer that reorders would otherwise swap them
464 // silently. writeXML.m:305-317 sets the same attribute.
465 static const char* const kResult[2] = {"hit", "miss"};
466 for (std::size_t i = 0; i < pc.postacts.size(); ++i) {
467 xml::Element& ae = post.add_child("activity");
468 ae.set_attr("name", act_name(pc.postacts[i]));
469 if (i < 2) ae.set_attr("cache-result", kResult[i]);
470 }
471 } else {
472 for (std::size_t i = 0; i < pc.postacts.size(); ++i)
473 post.add_child("activity").set_attr("name", act_name(pc.postacts[i]));
474 }
475 }
476
477 if (tk.sched != SchedStrategy::REF) {
478 for (std::size_t e = 0; e < m.entries.size(); ++e) {
479 if (m.entries[e].task_slot != t) continue;
480 const std::string& ename = m.entries[e].name;
481 if (!needs_reply.count(ename)) continue;
482 const std::map<std::string, std::vector<std::string>>::const_iterator it =
483 replies.find(ename);
484 if (it == replies.end() || it->second.empty())
485 throw InputError(
486 "write_lqnx: entry '" + ename +
487 "' is called synchronously but no activity replies to it, and none "
488 "can be inferred; declare one with replies_to()");
489 xml::Element& re = ta.add_child("reply-entry");
490 re.set_attr("name", entry_name(ename));
491 for (std::size_t r = 0; r < it->second.size(); ++r)
492 re.add_child("reply-activity").set_attr("name", act_name(it->second[r]));
493 }
494 }
495 }
496 }
497
498 xml::write_file(path, root);
499 return report;
500}
501
502} // namespace lqn
503} // namespace line
504
505#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:470
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:389
std::string sched_to_lqnx(SchedStrategy s)
The scheduling attribute an .lqnx processor or task carries for a strategy.
Definition lang_types.h:304
const char * routing_to_text(RoutingStrategy r)
Definition lang_types.h:402
LqnStruct< T > lqn_finalize(const LqnModel< T > &m)
Port of @LayeredNetwork/getStruct.m: flatten the model into its struct.
Definition lqn_reader.h:432
LqnWriteReport write_lqnx(const LqnModel< T > &m, const std::string &path, const std::string &model_name=std::string("LQN"), bool use_abstract_names=false)
Write a layered model as a .lqnx document.
Definition lqn_writer.h:198
void write_file(const std::string &path, const Element &root)
Serialize to a file, creating or truncating it.
Definition xml.h:376
static constexpr double FineTol
Definition lang_types.h:668
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::vector< detail::RawProc > procs
Definition lqn_reader.h:400
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::vector< detail::RawEntry< T > > entries
Definition lqn_reader.h:402
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 ....