LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_json_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_IO_LQN_JSON_READER_H
6#define LINE_IO_LQN_JSON_READER_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * `model.json` with `type: "LayeredNetwork"` -> `LqnStruct`, via `LqnBuilder`.
12 *
13 * WHY THIS EXISTS BESIDE `lqn_reader.h`. The .lqnx reader covers the LQNS file
14 * format, which is what an external tool writes; this covers the LINE
15 * interchange format, which is what `linemodel_save.m`, `save_model` and the
16 * JAR `LineModelIO` write for a `LayeredNetwork`. The two are not
17 * interchangeable in either direction: .lqnx cannot carry a non-reference
18 * task's think time, a CacheTask, an ItemEntry or a SetupTask, and model.json
19 * carries all four. Without this reader a layered model could reach the C++
20 * port only by being re-exported through XML, losing exactly those constructs.
21 *
22 * ORDERING IS PART OF THE FORMAT. `LqnBuilder` assigns indices in call order,
23 * and every later array -- the layer decomposition, the visit matrix, the
24 * result tables -- is keyed on them. The document's own order is used
25 * throughout (hosts, then tasks, then entries, then activities), which is the
26 * order the writers emit and therefore the order MATLAB's own `getStruct`
27 * produced before serialization.
28 *
29 * WHAT IS READ, following `linemodel_load.m::json2layered`: fan-in and fan-out
30 * (`fanIn`/`fanOut`, a map from the peer task to the count) and the named
31 * admission constraints of hosts and tasks, which `lqn_finalize` resolves as it
32 * does for the .lqnx reader; a host's `quantum` (0.001 when absent, MATLAB's
33 * default) and `speedFactor`; the activity `thinkTime` the JAR writes. An
34 * explicit-form precedence may spell its types as the JAR and the Workflow wire
35 * do (`pre`, `post-OR`), as Python's aliases (`and-fork`, `or-join`, `loop`) or
36 * as the enum names (`PRE_SEQ`); an unknown spelling is refused by name, where
37 * MATLAB would read it as `pre`.
38 */
39
40#include <map>
41#include <string>
42#include <vector>
43
45#include <cctype>
46#include <fstream>
47
52#include "line/util/error.h"
53
54namespace line {
55namespace io {
56
57namespace detail {
58
59/** The `scheduling` name of a host or task, upper case as the writers emit it. */
60inline lang::SchedStrategy lqn_sched_from_json(const std::string& s) {
61 typedef lang::SchedStrategy S;
62 if (s == "INF" || s == "inf") return S::INF;
63 if (s == "FCFS" || s == "fcfs") return S::FCFS;
64 if (s == "PS" || s == "ps") return S::PS;
65 if (s == "HOL" || s == "hol") return S::HOL;
66 if (s == "REF" || s == "ref") return S::REF;
67 if (s == "SIRO" || s == "siro") return S::SIRO;
68 if (s == "LCFS" || s == "lcfs") return S::LCFS;
69 if (s == "LCFSPR" || s == "lcfspr") return S::LCFSPR;
70 throw UnsupportedError(ErrorKind{"Import"}, "lqn_json_reader: unsupported scheduling discipline '" + s + "'");
71}
72
73/**
74 * The wire's infinite multiplicity.
75 *
76 * The writers emit `Integer.MAX_VALUE` as a literal rather than a float
77 * infinity, because an infinity would not survive an integer-valued export;
78 * both spellings are accepted here and both mean the same thing.
79 */
80inline double lqn_mult_from_json(const json& v) {
81 if (v.is_string()) {
82 const std::string s = v.get<std::string>();
83 if (s == "Infinity" || s == "inf") return std::numeric_limits<double>::infinity();
84 return std::atof(s.c_str());
85 }
86 const double m = v.get<double>();
87 return m >= 2147483647.0 ? std::numeric_limits<double>::infinity() : m;
88}
89
90/**
91 * Explicit-form precedence type -> `PrecedenceType`, over the JAR spelling
92 * (`ActivityPrecedenceType.java`, also the Workflow wire), Python's aliases and the enum names.
93 */
94inline lang::PrecedenceType lqn_prectype_from_json(const std::string& s, bool pre) {
95 typedef lang::PrecedenceType P;
96 if (pre) {
97 if (s == "pre" || s == "PRE_SEQ") return P::PRE_SEQ;
98 if (s == "pre-AND" || s == "and-join" || s == "PRE_AND") return P::PRE_AND;
99 if (s == "pre-OR" || s == "or-join" || s == "PRE_OR") return P::PRE_OR;
100 } else {
101 if (s == "post" || s == "POST_SEQ") return P::POST_SEQ;
102 if (s == "post-AND" || s == "and-fork" || s == "POST_AND") return P::POST_AND;
103 if (s == "post-OR" || s == "or-fork" || s == "POST_OR") return P::POST_OR;
104 if (s == "post-LOOP" || s == "loop" || s == "POST_LOOP") return P::POST_LOOP;
105 if (s == "post-CACHE" || s == "POST_CACHE") return P::POST_CACHE;
106 }
107 throw UnsupportedError(ErrorKind{"Import"}, std::string("lqn_json_reader: unknown ") + (pre ? "preType" : "postType") +
108 " '" + s + "'; the wire spells pre, pre-AND, pre-OR, post, post-AND, "
109 "post-OR, post-LOOP or post-CACHE");
110}
111
112/** `apply_lincon`: replay named admission-constraint rows onto a host or task. */
113template <class T>
114void lqn_apply_lincon(lqn::LqnBuilder<T>& b, const std::string& elem, const json& rows) {
115 for (const json& row : rows) {
116 std::vector<std::string> ops;
117 if (row.at("operands").is_string()) ops.push_back(row.at("operands").get<std::string>());
118 else
119 for (const json& o : row.at("operands")) ops.push_back(o.get<std::string>());
120 // A row with no coefficients weighs every operand by one, as the JAR's addConstraintByName does.
121 std::vector<T> coeffs = row.contains("coeffs") ? num_vec_from_json<T>(row.at("coeffs"))
122 : std::vector<T>(ops.size(), num_traits<T>::from_int(1));
123 b.add_constraint(elem, ops, coeffs, num_traits<T>::from_double(row.at("cap").get<double>()));
124 }
125}
126
127/** `serverTypes`: replay the heterogeneous pools of a host or task (see `lqn_pools_to_json`). */
128template <class T>
129void lqn_apply_pools(lqn::LqnBuilder<T>& b, const std::string& elem, const json& pools) {
130 for (const json& pj : pools) {
131 std::vector<std::string> ops;
132 if (pj.at("operands").is_string()) ops.push_back(pj.at("operands").get<std::string>());
133 else
134 for (const json& o : pj.at("operands")) ops.push_back(o.get<std::string>());
135 b.add_server_type(elem, pj.at("name").get<std::string>(), pj.at("count").get<double>(), ops,
136 num_traits<T>::from_double(pj.value("rate", 1.0)));
137 }
138}
139
140/** A `fanIn`/`fanOut` map {peer task: count} as the declared (name, value) pairs. */
141inline std::vector<std::pair<std::string, double>> lqn_fan_from_json(const json& obj) {
142 std::vector<std::pair<std::string, double>> out;
143 for (json::const_iterator it = obj.begin(); it != obj.end(); ++it)
144 out.push_back(std::make_pair(it.key(), it.value().get<double>()));
145 return out;
146}
147
148/**
149 * The AND-join quorum of a precedence object, 0 for none.
150 *
151 * `preParams: [q]` is the wire key in both the explicit and the typed form. Only
152 * a ONE-element list is a quorum: a list with one entry per branch is not. A
153 * quorum of every branch is the plain join.
154 */
155template <class T>
156std::size_t lqn_quorum_from_json(const json& p, std::size_t nbranches) {
157 if (!p.contains("preParams")) return 0;
158 const json& v = p.at("preParams");
159 double qd = 0.0;
160 if (v.is_number()) qd = v.get<double>();
161 else if (v.is_array() && v.size() == 1 && v[0].is_number()) qd = v[0].get<double>();
162 else return 0;
163 return (qd > 0 && qd < double(nbranches)) ? std::size_t(qd + 0.5) : 0;
164}
165
166} // namespace detail
167
168/**
169 * Build the intermediate `LqnModel<T>` from a parsed model.json envelope carrying
170 * a LayeredNetwork. The struct loses the declared reply activities, which the
171 * source-code generators in code_gen.h need, so the model is exposed as well.
172 */
173template <class T>
175 using detail::json;
176 const json& model = root.contains("model") ? root.at("model") : root;
177 const std::string mtype = model.value("type", std::string("LayeredNetwork"));
178 if (mtype != "LayeredNetwork")
179 throw UnsupportedError(ErrorKind{"Import"}, "lqn_json_reader: model type '" + mtype +
180 "' is not a LayeredNetwork; a Network model is read by "
181 "network_reader.h");
182
184
185 // -- hosts (processors) --------------------------------------------------
186 // `hosts` is the LINE spelling and `processors` the LQNS one; the schema
187 // declares both and a writer may use either.
188 const json empty = json::array();
189 const json& hosts = model.contains("hosts")
190 ? model.at("hosts")
191 : (model.contains("processors") ? model.at("processors") : empty);
192 std::vector<std::pair<double, double>> quantum_speed; // per host, patched onto the model below
193 std::vector<std::pair<std::string, const json*>> host_rows;
194 std::vector<std::pair<std::string, const json*>> host_pools, task_pools; // operands resolve at build()
195 for (const json& h : hosts) {
196 const std::string name = h.at("name").get<std::string>();
197 const lang::SchedStrategy sched =
198 detail::lqn_sched_from_json(h.value("scheduling", std::string("PS")));
199 const double mult = h.contains("multiplicity")
200 ? detail::lqn_mult_from_json(h.at("multiplicity"))
201 : 1.0;
202 b.processor(name, mult, sched, h.value("replication", 1.0));
203 quantum_speed.push_back(
204 std::make_pair(h.value("quantum", 0.001), h.value("speedFactor", 1.0)));
205 if (h.contains("admissionConstraints"))
206 host_rows.push_back(std::make_pair(name, &h.at("admissionConstraints")));
207 if (h.contains("serverTypes")) host_pools.push_back(std::make_pair(name, &h.at("serverTypes")));
208 }
209
210 // -- tasks ---------------------------------------------------------------
211 std::vector<std::vector<std::pair<std::string, double>>> fan_in, fan_out;
212 std::vector<std::pair<std::string, const json*>> task_rows; // applied once every entry exists
213 for (const json& t : model.at("tasks")) {
214 const std::string name = t.at("name").get<std::string>();
215 fan_in.push_back(t.contains("fanIn") ? detail::lqn_fan_from_json(t.at("fanIn"))
216 : std::vector<std::pair<std::string, double>>());
217 fan_out.push_back(t.contains("fanOut") ? detail::lqn_fan_from_json(t.at("fanOut"))
218 : std::vector<std::pair<std::string, double>>());
219 if (t.contains("admissionConstraints"))
220 task_rows.push_back(std::make_pair(name, &t.at("admissionConstraints")));
221 if (t.contains("serverTypes")) task_pools.push_back(std::make_pair(name, &t.at("serverTypes")));
222 const std::string on = t.at("host").get<std::string>();
223 const lang::SchedStrategy sched =
224 detail::lqn_sched_from_json(t.value("scheduling", std::string("FCFS")));
225 const double mult = t.contains("multiplicity")
226 ? detail::lqn_mult_from_json(t.at("multiplicity"))
227 : 1.0;
228 const double repl = t.value("replication", 1.0);
229 const std::string kind = t.value("taskType", std::string());
230 if (kind == "CacheTask") {
231 std::vector<int> cap;
232 if (t.at("cacheCapacity").is_array())
233 cap = t.at("cacheCapacity").get<std::vector<int> >();
234 else
235 cap.push_back(t.at("cacheCapacity").get<int>());
236 b.cache_task(name, mult, sched, on, t.at("totalItems").get<std::size_t>(), cap,
237 detail::replacement_from_json(
238 t.value("replacementStrategy", std::string("FIFO"))),
239 repl);
240 } else {
241 b.task(name, mult, sched, on, repl);
242 }
243 // `priority` is Task.setPriority; the JAR writes it only when nonzero (LineModelIO)
244 if (t.contains("priority")) b.task_priority(name, static_cast<int>(t.at("priority").get<double>()));
245 // A think time is accepted on ANY task here, as the reference API
246 // allows and as .lqnx cannot express.
247 if (t.contains("thinkTime"))
248 b.think_time(name, detail::dist_from_json<T>(t.at("thinkTime")));
249 else if (t.contains("thinkTimeMean") && t.at("thinkTimeMean").get<double>() > 0)
251 t.at("thinkTimeMean").get<double>())));
252 // BOTH SPELLINGS ARE ACCEPTED, the object form PREFERRED. MATLAB,
253 // Python and this port all write `setupTime`/`delayOffTime` as full
254 // distributions, so that is the canonical wire form; the JAR writes
255 // `setupTimeMean`+`setupTimeSCV` instead. Documents already written in
256 // the field carry only the scalars, and without this fallback both
257 // fields vanish on read and a SetupTask arrives with no setup time at
258 // all -- silently, which is the failure mode `.lqnx` already has. An
259 // SCV cannot be recovered from a mean, so the scalar path reconstructs
260 // an exponential and claims no more, exactly as `thinkTimeMean` does.
261 const bool has_setup_obj = t.contains("setupTime");
262 const bool has_setup_mean =
263 t.contains("setupTimeMean") && t.at("setupTimeMean").get<double>() > 0;
264 if (has_setup_obj || has_setup_mean) {
265 const bool has_off_obj = t.contains("delayOffTime");
266 const bool has_off_mean =
267 t.contains("delayOffTimeMean") && t.at("delayOffTimeMean").get<double>() > 0;
268 if (!has_off_obj && !has_off_mean)
269 throw InputError(ErrorKind{"Format"}, "lqn_json_reader: task '" + name +
270 "' declares a setup time with no delay-off time; a server that "
271 "never shuts down pays the setup at most once");
272 const lang::Distrib<T> su =
273 has_setup_obj ? detail::dist_from_json<T>(t.at("setupTime"))
275 t.at("setupTimeMean").get<double>()));
276 const lang::Distrib<T> doff =
277 has_off_obj ? detail::dist_from_json<T>(t.at("delayOffTime"))
279 t.at("delayOffTimeMean").get<double>()));
280 b.setup_time(name, su, doff);
281 }
282 }
283
284 // -- entries -------------------------------------------------------------
285 for (const json& e : model.at("entries")) {
286 const std::string name = e.at("name").get<std::string>();
287 const std::string on = e.at("task").get<std::string>();
288 if (e.value("entryType", std::string()) == "ItemEntry") {
289 const std::size_t card = e.at("totalItems").get<std::size_t>();
290 // The popularity crosses as a DISTRIBUTION (a Zipf, typically) and
291 // the builder wants the pmf over the items, which is the same
292 // object the cache node's `pread` is.
293 std::vector<T> pop;
294 if (e.contains("accessProb"))
295 pop = detail::pmf_from_json<T>(e.at("accessProb"), card);
296 if (pop.empty())
297 pop.assign(card, num_traits<T>::from_double(1.0 / double(card)));
298 b.item_entry(name, on, card, pop);
299 } else {
300 b.entry(name, on);
301 }
302 if (e.contains("arrival"))
303 b.open_arrival(name, detail::dist_from_json<T>(e.at("arrival")));
304 }
305
306 // -- activities ----------------------------------------------------------
307 for (const json& a : model.at("activities")) {
308 const std::string name = a.at("name").get<std::string>();
309 const std::string on = a.at("task").get<std::string>();
310 // An activity with no host demand is an IMMEDIATE one; the writers omit
311 // the key in exactly that case, so the absence is the value.
312 const lang::Distrib<T> dem = a.contains("hostDemand")
313 ? detail::dist_from_json<T>(a.at("hostDemand"))
315 b.activity(name, dem, on);
316 if (a.contains("thinkTime")) b.act_think_time(name, detail::dist_from_json<T>(a.at("thinkTime")));
317 if (a.contains("phase")) b.activity_phase(name, a.at("phase").get<int>());
318 if (a.contains("callOrder")) b.call_order(name, a.at("callOrder").get<std::string>());
319 if (a.contains("boundToEntry"))
320 b.bound_to(name, a.at("boundToEntry").get<std::string>());
321 else if (a.contains("boundTo"))
322 b.bound_to(name, a.at("boundTo").get<std::string>());
323 if (a.contains("repliesTo")) b.replies_to(name, a.at("repliesTo").get<std::string>());
324 const char* kCallKey[2] = {"synchCalls", "asynchCalls"};
325 for (int which = 0; which < 2; ++which) {
326 if (!a.contains(kCallKey[which])) continue;
327 for (const json& c : a.at(kCallKey[which])) {
328 // `dest` is the LINE spelling and `entry` the schema's.
329 const std::string dest = c.contains("dest")
330 ? c.at("dest").get<std::string>()
331 : c.at("entry").get<std::string>();
332 const T mean = num_traits<T>::from_double(c.value("mean", 1.0));
333 if (which == 0) b.sync_call(name, dest, mean);
334 else b.async_call(name, dest, mean);
335 }
336 }
337 // Routed call groups: the members are the synchCalls read above, so only
338 // the grouping is recorded; lqn_finalize resolves the entry names.
339 if (a.contains("callGroups")) {
340 for (const json& g : a.at("callGroups")) {
341 const lang::RoutingStrategy rs = lqn::detail::callgroup_from_lqnx(
342 g.value("strategy", std::string()), name);
343 std::vector<std::string> dests;
344 for (const json& d : g.at("dests")) dests.push_back(d.get<std::string>());
345 b.record_call_group(name, dests, rs);
346 }
347 }
348 }
349
350 // Entry forwarding is declared on the ENTRY and resolved after every entry
351 // exists, since it names one.
352 for (const json& e : model.at("entries")) {
353 if (!e.contains("forwarding")) continue;
354 const std::string name = e.at("name").get<std::string>();
355 for (const json& f : e.at("forwarding"))
356 b.forward(name, f.at("dest").get<std::string>(),
357 num_traits<T>::from_double(f.value("prob", 1.0)));
358 }
359
360 // -- precedences ---------------------------------------------------------
361 //
362 // The TYPED form the writers emit: one `type` naming the pre/post pair,
363 // plus the activity list with the pre-activities first. The EXPLICIT form
364 // (`preType`/`postType` with separate lists) is the schema's other spelling
365 // and is mapped onto the same builder calls.
366 if (model.contains("precedences")) {
367 for (const json& p : model.at("precedences")) {
368 const std::string kind = p.value("type", std::string());
369 if (kind.empty()) {
370 // Explicit form, in any of the three spellings lqn_prectype_from_json accepts.
371 typedef lang::PrecedenceType P;
372 const P pre = detail::lqn_prectype_from_json(p.value("preType", std::string("pre")), true);
373 const P post =
374 detail::lqn_prectype_from_json(p.value("postType", std::string("post")), false);
375 std::vector<std::string> pres, posts;
376 if (p.contains("preActs"))
377 for (const json& x : p.at("preActs")) pres.push_back(x.get<std::string>());
378 if (p.contains("postActs"))
379 for (const json& x : p.at("postActs")) posts.push_back(x.get<std::string>());
380 const std::vector<T> post_params = p.contains("postParams")
381 ? detail::num_vec_from_json<T>(p.at("postParams"))
382 : std::vector<T>();
383 if (pre == P::PRE_SEQ && post == P::POST_SEQ) {
384 b.serial(pres.at(0), posts.at(0));
385 } else if (pre == P::PRE_SEQ && post == P::POST_AND) {
386 b.and_fork(pres.at(0), posts);
387 } else if (pre == P::PRE_AND && post == P::POST_SEQ) {
388 b.and_join(pres, posts.at(0), detail::lqn_quorum_from_json<T>(p, pres.size()));
389 } else if (pre == P::PRE_OR && post == P::POST_SEQ) {
390 b.or_join(pres, posts.at(0));
391 } else if (pre == P::PRE_SEQ && post == P::POST_OR) {
392 // probabilities, else postParams, else uniform: linemodel_load.m's order
393 std::vector<T> probs;
394 if (p.contains("probabilities"))
395 probs = detail::num_vec_from_json<T>(p.at("probabilities"));
396 if (probs.empty()) probs = post_params;
397 if (probs.empty())
398 probs.assign(posts.size(), num_traits<T>::from_double(1.0 / double(posts.size())));
399 b.or_fork(pres.at(0), posts, probs);
400 } else if (pre == P::PRE_SEQ && post == P::POST_LOOP) {
401 // `postActs` is the loop BODY followed by the activity the
402 // loop exits to, which is how `ActivityPrecedence.Loop`
403 // stores it and how the builder takes it back apart.
404 if (posts.size() < 2)
405 throw InputError(ErrorKind{"Format"},
406 "lqn_json_reader: a loop's postActs is the body followed by the "
407 "activity it exits to, so it holds at least two entries");
408 T count = num_traits<T>::from_double(p.value("loopCount", 1.0));
409 if (!p.contains("loopCount") && !post_params.empty()) count = post_params[0];
410 b.loop(pres.at(0), std::vector<std::string>(posts.begin(), posts.end() - 1),
411 posts.back(), count);
412 } else if (pre == P::PRE_SEQ && post == P::POST_CACHE) {
413 if (posts.size() < 2)
414 throw InputError(ErrorKind{"Format"}, "lqn_json_reader: a post-CACHE precedence lists the hit and miss branches");
415 b.cache_access(pres.at(0), posts.at(0), posts.at(1));
416 } else {
417 throw UnsupportedError(ErrorKind{"Import"}, "lqn_json_reader: precedence " + p.value("preType", std::string("pre")) +
418 " -> " + p.value("postType", std::string("post")) +
419 " has no builder counterpart");
420 }
421 continue;
422 }
423 std::vector<std::string> acts;
424 for (const json& x : p.at("activities")) acts.push_back(x.get<std::string>());
425 if (kind == "Serial") {
426 // A serial chain is written as one list, and the builder takes
427 // it a pair at a time.
428 for (std::size_t i = 0; i + 1 < acts.size(); ++i) b.serial(acts[i], acts[i + 1]);
429 } else if (kind == "AndFork") {
430 b.and_fork(acts.at(0), std::vector<std::string>(acts.begin() + 1, acts.end()));
431 } else if (kind == "AndJoin") {
432 b.and_join(std::vector<std::string>(acts.begin(), acts.end() - 1), acts.back(),
433 detail::lqn_quorum_from_json<T>(p, acts.size() - 1));
434 } else if (kind == "OrFork") {
435 std::vector<T> probs;
436 if (p.contains("probabilities"))
437 probs = detail::num_vec_from_json<T>(p.at("probabilities"));
438 b.or_fork(acts.at(0), std::vector<std::string>(acts.begin() + 1, acts.end()),
439 probs);
440 } else if (kind == "OrJoin") {
441 b.or_join(std::vector<std::string>(acts.begin(), acts.end() - 1), acts.back());
442 } else if (kind == "Loop") {
443 // The trigger is written separately from the body, so `acts` is
444 // the body alone.
445 if (!p.contains("preActivity"))
446 throw InputError(ErrorKind{"Format"},
447 "lqn_json_reader: a Loop precedence carries its trigger as 'preActivity'");
448 if (acts.size() < 2)
449 throw InputError(ErrorKind{"Format"},
450 "lqn_json_reader: a Loop precedence lists the body followed by the "
451 "activity it exits to, so it holds at least two entries");
452 b.loop(p.at("preActivity").get<std::string>(),
453 std::vector<std::string>(acts.begin(), acts.end() - 1), acts.back(),
454 num_traits<T>::from_double(p.value("loopCount", 1.0)));
455 } else if (kind == "CacheAccess") {
456 if (acts.size() < 3)
457 throw InputError(ErrorKind{"Format"},
458 "lqn_json_reader: a CacheAccess precedence names the read activity plus "
459 "its hit and miss branches");
460 b.cache_access(acts.at(0), acts.at(1), acts.at(2));
461 } else {
462 throw UnsupportedError(ErrorKind{"Import"}, "lqn_json_reader: precedence type '" + kind +
463 "' has no builder counterpart");
464 }
465 }
466 }
467 for (const auto& r : host_rows) detail::lqn_apply_lincon(b, r.first, *r.second);
468 for (const auto& r : task_rows) detail::lqn_apply_lincon(b, r.first, *r.second);
469 for (const auto& r : host_pools) detail::lqn_apply_pools(b, r.first, *r.second);
470 for (const auto& r : task_pools) detail::lqn_apply_pools(b, r.first, *r.second);
471 lqn::LqnModel<T> m = b.model();
472 for (std::size_t i = 0; i < quantum_speed.size() && i < m.procs.size(); ++i) {
473 m.procs[i].quantum = quantum_speed[i].first;
474 m.procs[i].speed_factor = quantum_speed[i].second;
475 }
476 for (std::size_t i = 0; i < m.tasks.size() && i < fan_in.size(); ++i) {
477 m.tasks[i].fanin = fan_in[i];
478 m.tasks[i].fanout = fan_out[i];
479 }
480 if (model.contains("name") && model.at("name").is_string()) m.name = model.at("name").get<std::string>();
481 return m;
482}
483
484/**
485 * Build an `LqnStruct<T>` from a parsed model.json envelope carrying a
486 * LayeredNetwork.
487 */
488template <class T>
492
493/**
494 * True when a DOCUMENT ALREADY IN MEMORY is a LayeredNetwork model.json.
495 *
496 * The path form below reads the file twice, which is right for a file and
497 * impossible for a stream: standard input can be drained only once, so a caller
498 * holding piped text has nothing to hand the path form. Without this the CLI
499 * could not tell a layered model from a flat one on stdin, and refused every
500 * piped LayeredNetwork with a message about the SOLVER rather than the reader.
501 */
502inline bool is_layered_json_text(const std::string& text) {
503 std::size_t i = 0;
504 while (i < text.size() && std::isspace(static_cast<unsigned char>(text[i]))) ++i;
505 if (i >= text.size() || text[i] != '{') return false;
506 detail::json root;
507 try {
508 root = detail::json::parse(text);
509 } catch (const detail::json::parse_error&) {
510 return false;
511 }
512 const detail::json& model = root.contains("model") ? root.at("model") : root;
513 return model.value("type", std::string()) == "LayeredNetwork";
514}
515
516/**
517 * True when a file is a LayeredNetwork model.json rather than an .lqnx.
518 *
519 * Decided on the CONTENT, not the extension: `linemodel_save` writes `.json`
520 * for both model kinds and the two readers cannot be told apart by name. The
521 * first non-space character settles it -- `{` is JSON, `<` is XML -- and the
522 * `type` field settles which JSON.
523 */
524inline bool is_layered_json(const std::string& path) {
525 std::ifstream in(path.c_str());
526 if (!in) return false;
527 char c = 0;
528 while (in.get(c) && std::isspace(static_cast<unsigned char>(c))) {}
529 if (c != '{') return false;
530 in.seekg(0);
531 detail::json root;
532 try {
533 in >> root;
534 } catch (const detail::json::parse_error&) {
535 return false;
536 }
537 const detail::json& model = root.contains("model") ? root.at("model") : root;
538 return model.value("type", std::string()) == "LayeredNetwork";
539}
540
541/** Parse a LayeredNetwork model.json file into its intermediate `LqnModel<T>`. */
542template <class T>
543lqn::LqnModel<T> read_lqn_json_model(const std::string& path) {
544 std::ifstream in(path.c_str());
545 if (!in) throw InputError(ErrorKind{"File"}, "lqn_json_reader: cannot open " + path);
546 detail::json root;
547 try {
548 in >> root;
549 } catch (const detail::json::parse_error& e) {
550 throw InputError(ErrorKind{"Format"}, "lqn_json_reader: malformed JSON in " + path + ": " + e.what());
551 }
552 return build_lqn_model_from_json<T>(root);
553}
554
555/** Parse a LayeredNetwork model.json file into an `LqnStruct<T>`. */
556template <class T>
557lqn::LqnStruct<T> read_lqn_json(const std::string& path) {
558 std::ifstream in(path.c_str());
559 if (!in) throw InputError(ErrorKind{"File"}, "lqn_json_reader: cannot open " + path);
560 detail::json root;
561 try {
562 in >> root;
563 } catch (const detail::json::parse_error& e) {
564 throw InputError(ErrorKind{"Format"}, "lqn_json_reader: malformed JSON in " + path + ": " + e.what());
565 }
566 return build_lqn_from_json<T>(root);
567}
568
569/**
570 * Read a layered model from any of its interchanges: the LINE `model.json`, the
571 * LQNS `.lqnx`, or the LQNS SRVN text (`.in`, or a `.lqn` that is not XML).
572 *
573 * Every layered entry point goes through here, so a caller never has to know
574 * which of the two it was handed -- and, more to the point, a `model.json`
575 * written by `linemodel_save` for a LayeredNetwork stops being unreadable
576 * merely because the XML reader was the only one wired up.
577 */
578template <class T>
579lqn::LqnStruct<T> read_layered_model(const std::string& path) {
580 if (lqn::is_srvn_path(path)) return lqn::read_srvn<T>(path);
581 return is_layered_json(path) ? read_lqn_json<T>(path) : lqn::read_lqnx<T>(path);
582}
583
584} // namespace io
585} // namespace line
586
587#endif // LINE_IO_LQN_JSON_READER_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.
Build a layered queueing network in code, as the MATLAB constructors do.
.lqnx -> LqnStruct, a port of matlab/src/lang/layered/@LayeredNetwork/parseXML.m followed by ....
LayeredNetworkStruct, the flattened description of a layered queueing network.
bool is_layered_json(const std::string &path)
True when a file is a LayeredNetwork model.json rather than an .lqnx.
lqn::LqnModel< T > read_lqn_json_model(const std::string &path)
Parse a LayeredNetwork model.json file into its intermediate LqnModel<T>.
lqn::LqnStruct< T > read_lqn_json(const std::string &path)
Parse a LayeredNetwork model.json file into an LqnStruct<T>.
lqn::LqnStruct< T > read_layered_model(const std::string &path)
Read a layered model from any of its interchanges: the LINE model.json, the LQNS ....
lqn::LqnModel< T > build_lqn_model_from_json(const detail::json &root)
Build the intermediate LqnModel<T> from a parsed model.json envelope carrying a LayeredNetwork.
lqn::LqnStruct< T > build_lqn_from_json(const detail::json &root)
Build an LqnStruct<T> from a parsed model.json envelope carrying a LayeredNetwork.
bool is_layered_json_text(const std::string &text)
True when a DOCUMENT ALREADY IN MEMORY is a LayeredNetwork model.json.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
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
LqnStruct< T > lqn_finalize(const LqnModel< T > &m)
Port of @LayeredNetwork/getStruct.m: flatten the model into its struct.
Definition lqn_reader.h:501
LqnStruct< T > read_lqnx(const std::string &path)
Read a .lqnx model.
LqnStruct< T > read_srvn(const std::string &path)
read_srvn_model followed by lqn_finalize: the struct SolverLN consumes, as read_lqnx is.
bool is_srvn_path(const std::string &path)
Whether path should be read as SRVN: a .in file, or a .lqn file that is not XML.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Reader for the LINE model.json interchange (a Network model) into a qn::Network<T> built through the ...
SRVN input format (the LQNS .in / .lqn text grammar) for layered models, the C++ twin of python/line_...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static Distrib immediate()
The point mass at zero.
Definition lang_types.h:986
static Distrib exp_mean(const T &m)
Definition lang_types.h:940
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::RawProc > procs
Definition lqn_reader.h:414
std::string name
LayeredNetwork.getName(); empty when unnamed.
Definition lqn_reader.h:413