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 REFUSED. Fan-in and fan-out replication (`fanIn`/`fanOut`) and the
30 * per-element admission constraints have no `LqnBuilder` counterpart that the
31 * layer decomposition honours, and a call multiplicity silently applied at 1
32 * would understate every visit through the replicated task. They are named
33 * rather than dropped, as everywhere else on this boundary.
34 */
35
36#include <map>
37#include <string>
38#include <vector>
39
41#include <cctype>
42#include <fstream>
43
47#include "line/util/error.h"
48
49namespace line {
50namespace io {
51
52namespace detail {
53
54/** The `scheduling` name of a host or task, upper case as the writers emit it. */
55inline lang::SchedStrategy lqn_sched_from_json(const std::string& s) {
56 typedef lang::SchedStrategy S;
57 if (s == "INF" || s == "inf") return S::INF;
58 if (s == "FCFS" || s == "fcfs") return S::FCFS;
59 if (s == "PS" || s == "ps") return S::PS;
60 if (s == "HOL" || s == "hol") return S::HOL;
61 if (s == "REF" || s == "ref") return S::REF;
62 if (s == "SIRO" || s == "siro") return S::SIRO;
63 if (s == "LCFS" || s == "lcfs") return S::LCFS;
64 if (s == "LCFSPR" || s == "lcfspr") return S::LCFSPR;
65 throw UnsupportedError("lqn_json_reader: unsupported scheduling discipline '" + s + "'");
66}
67
68/**
69 * The wire's infinite multiplicity.
70 *
71 * The writers emit `Integer.MAX_VALUE` as a literal rather than a float
72 * infinity, because an infinity would not survive an integer-valued export;
73 * both spellings are accepted here and both mean the same thing.
74 */
75inline double lqn_mult_from_json(const json& v) {
76 if (v.is_string()) {
77 const std::string s = v.get<std::string>();
78 if (s == "Infinity" || s == "inf") return std::numeric_limits<double>::infinity();
79 return std::atof(s.c_str());
80 }
81 const double m = v.get<double>();
82 return m >= 2147483647.0 ? std::numeric_limits<double>::infinity() : m;
83}
84
85} // namespace detail
86
87/**
88 * Build an `LqnStruct<T>` from a parsed model.json envelope carrying a
89 * LayeredNetwork.
90 */
91template <class T>
92lqn::LqnStruct<T> build_lqn_from_json(const detail::json& root) {
93 using detail::json;
94 const json& model = root.contains("model") ? root.at("model") : root;
95 const std::string mtype = model.value("type", std::string("LayeredNetwork"));
96 if (mtype != "LayeredNetwork")
97 throw UnsupportedError("lqn_json_reader: model type '" + mtype +
98 "' is not a LayeredNetwork; a Network model is read by "
99 "network_reader.h");
100
102
103 // -- hosts (processors) --------------------------------------------------
104 // `hosts` is the LINE spelling and `processors` the LQNS one; the schema
105 // declares both and a writer may use either.
106 const json empty = json::array();
107 const json& hosts = model.contains("hosts")
108 ? model.at("hosts")
109 : (model.contains("processors") ? model.at("processors") : empty);
110 for (const json& h : hosts) {
111 const std::string name = h.at("name").get<std::string>();
112 if (h.contains("admissionConstraints"))
113 throw UnsupportedError(
114 "lqn_json_reader: host '" + name +
115 "' carries admission constraints, which this reader does not rebuild");
116 const lang::SchedStrategy sched =
117 detail::lqn_sched_from_json(h.value("scheduling", std::string("PS")));
118 const double mult = h.contains("multiplicity")
119 ? detail::lqn_mult_from_json(h.at("multiplicity"))
120 : 1.0;
121 b.processor(name, mult, sched, h.value("replication", 1.0));
122 }
123
124 // -- tasks ---------------------------------------------------------------
125 for (const json& t : model.at("tasks")) {
126 const std::string name = t.at("name").get<std::string>();
127 if (t.contains("fanIn") || t.contains("fanOut"))
128 throw UnsupportedError(
129 "lqn_json_reader: task '" + name +
130 "' declares fan-in or fan-out replication, which the layer decomposition in this "
131 "port does not honour; a call multiplicity applied at 1 would understate every "
132 "visit through the replicated task");
133 if (t.contains("admissionConstraints"))
134 throw UnsupportedError(
135 "lqn_json_reader: task '" + name +
136 "' carries admission constraints, which this reader does not rebuild");
137 const std::string on = t.at("host").get<std::string>();
138 const lang::SchedStrategy sched =
139 detail::lqn_sched_from_json(t.value("scheduling", std::string("FCFS")));
140 const double mult = t.contains("multiplicity")
141 ? detail::lqn_mult_from_json(t.at("multiplicity"))
142 : 1.0;
143 const double repl = t.value("replication", 1.0);
144 const std::string kind = t.value("taskType", std::string());
145 if (kind == "CacheTask") {
146 std::vector<int> cap;
147 if (t.at("cacheCapacity").is_array())
148 cap = t.at("cacheCapacity").get<std::vector<int> >();
149 else
150 cap.push_back(t.at("cacheCapacity").get<int>());
151 b.cache_task(name, mult, sched, on, t.at("totalItems").get<std::size_t>(), cap,
152 detail::replacement_from_json(
153 t.value("replacementStrategy", std::string("FIFO"))),
154 repl);
155 } else {
156 b.task(name, mult, sched, on, repl);
157 }
158 // A think time is accepted on ANY task here, as the reference API
159 // allows and as .lqnx cannot express.
160 if (t.contains("thinkTime"))
161 b.think_time(name, detail::dist_from_json<T>(t.at("thinkTime")));
162 else if (t.contains("thinkTimeMean") && t.at("thinkTimeMean").get<double>() > 0)
164 t.at("thinkTimeMean").get<double>())));
165 // BOTH SPELLINGS ARE ACCEPTED, the object form PREFERRED. MATLAB,
166 // Python and this port all write `setupTime`/`delayOffTime` as full
167 // distributions, so that is the canonical wire form; the JAR writes
168 // `setupTimeMean`+`setupTimeSCV` instead. Documents already written in
169 // the field carry only the scalars, and without this fallback both
170 // fields vanish on read and a SetupTask arrives with no setup time at
171 // all -- silently, which is the failure mode `.lqnx` already has. An
172 // SCV cannot be recovered from a mean, so the scalar path reconstructs
173 // an exponential and claims no more, exactly as `thinkTimeMean` does.
174 const bool has_setup_obj = t.contains("setupTime");
175 const bool has_setup_mean =
176 t.contains("setupTimeMean") && t.at("setupTimeMean").get<double>() > 0;
177 if (has_setup_obj || has_setup_mean) {
178 const bool has_off_obj = t.contains("delayOffTime");
179 const bool has_off_mean =
180 t.contains("delayOffTimeMean") && t.at("delayOffTimeMean").get<double>() > 0;
181 if (!has_off_obj && !has_off_mean)
182 throw InputError("lqn_json_reader: task '" + name +
183 "' declares a setup time with no delay-off time; a server that "
184 "never shuts down pays the setup at most once");
185 const lang::Distrib<T> su =
186 has_setup_obj ? detail::dist_from_json<T>(t.at("setupTime"))
188 t.at("setupTimeMean").get<double>()));
189 const lang::Distrib<T> doff =
190 has_off_obj ? detail::dist_from_json<T>(t.at("delayOffTime"))
192 t.at("delayOffTimeMean").get<double>()));
193 b.setup_time(name, su, doff);
194 }
195 }
196
197 // -- entries -------------------------------------------------------------
198 for (const json& e : model.at("entries")) {
199 const std::string name = e.at("name").get<std::string>();
200 const std::string on = e.at("task").get<std::string>();
201 if (e.value("entryType", std::string()) == "ItemEntry") {
202 const std::size_t card = e.at("totalItems").get<std::size_t>();
203 // The popularity crosses as a DISTRIBUTION (a Zipf, typically) and
204 // the builder wants the pmf over the items, which is the same
205 // object the cache node's `pread` is.
206 std::vector<T> pop;
207 if (e.contains("accessProb"))
208 pop = detail::pmf_from_json<T>(e.at("accessProb"), card);
209 if (pop.empty())
210 pop.assign(card, num_traits<T>::from_double(1.0 / double(card)));
211 b.item_entry(name, on, card, pop);
212 } else {
213 b.entry(name, on);
214 }
215 if (e.contains("arrival"))
216 b.open_arrival(name, detail::dist_from_json<T>(e.at("arrival")));
217 }
218
219 // -- activities ----------------------------------------------------------
220 for (const json& a : model.at("activities")) {
221 const std::string name = a.at("name").get<std::string>();
222 const std::string on = a.at("task").get<std::string>();
223 // An activity with no host demand is an IMMEDIATE one; the writers omit
224 // the key in exactly that case, so the absence is the value.
225 const lang::Distrib<T> dem = a.contains("hostDemand")
226 ? detail::dist_from_json<T>(a.at("hostDemand"))
228 b.activity(name, dem, on);
229 if (a.contains("boundToEntry"))
230 b.bound_to(name, a.at("boundToEntry").get<std::string>());
231 else if (a.contains("boundTo"))
232 b.bound_to(name, a.at("boundTo").get<std::string>());
233 if (a.contains("repliesTo")) b.replies_to(name, a.at("repliesTo").get<std::string>());
234 const char* kCallKey[2] = {"synchCalls", "asynchCalls"};
235 for (int which = 0; which < 2; ++which) {
236 if (!a.contains(kCallKey[which])) continue;
237 for (const json& c : a.at(kCallKey[which])) {
238 // `dest` is the LINE spelling and `entry` the schema's.
239 const std::string dest = c.contains("dest")
240 ? c.at("dest").get<std::string>()
241 : c.at("entry").get<std::string>();
242 const T mean = num_traits<T>::from_double(c.value("mean", 1.0));
243 if (which == 0) b.sync_call(name, dest, mean);
244 else b.async_call(name, dest, mean);
245 }
246 }
247 }
248
249 // Entry forwarding is declared on the ENTRY and resolved after every entry
250 // exists, since it names one.
251 for (const json& e : model.at("entries")) {
252 if (!e.contains("forwarding")) continue;
253 const std::string name = e.at("name").get<std::string>();
254 for (const json& f : e.at("forwarding"))
255 b.forward(name, f.at("dest").get<std::string>(),
256 num_traits<T>::from_double(f.value("prob", 1.0)));
257 }
258
259 // -- precedences ---------------------------------------------------------
260 //
261 // The TYPED form the writers emit: one `type` naming the pre/post pair,
262 // plus the activity list with the pre-activities first. The EXPLICIT form
263 // (`preType`/`postType` with separate lists) is the schema's other spelling
264 // and is mapped onto the same builder calls.
265 if (model.contains("precedences")) {
266 for (const json& p : model.at("precedences")) {
267 const std::string kind = p.value("type", std::string());
268 if (kind.empty()) {
269 // Explicit form.
270 const std::string pre = p.at("preType").get<std::string>();
271 const std::string post = p.at("postType").get<std::string>();
272 std::vector<std::string> pres, posts;
273 for (const json& x : p.at("preActs")) pres.push_back(x.get<std::string>());
274 for (const json& x : p.at("postActs")) posts.push_back(x.get<std::string>());
275 if (pre == "PRE_SEQ" && post == "POST_SEQ") b.serial(pres.at(0), posts.at(0));
276 else if (pre == "PRE_SEQ" && post == "POST_AND") b.and_fork(pres.at(0), posts);
277 else if (pre == "PRE_AND" && post == "POST_SEQ") b.and_join(pres, posts.at(0));
278 else if (pre == "PRE_OR" && post == "POST_SEQ") b.or_join(pres, posts.at(0));
279 else if (pre == "PRE_SEQ" && post == "POST_OR") {
280 std::vector<T> probs;
281 if (p.contains("postParams"))
282 probs = detail::num_vec_from_json<T>(p.at("postParams"));
283 b.or_fork(pres.at(0), posts, probs);
284 } else if (post == "POST_LOOP") {
285 // `postActs` is the loop BODY followed by the activity the
286 // loop exits to, which is how `ActivityPrecedence.Loop`
287 // stores it and how the builder takes it back apart.
288 if (posts.size() < 2)
289 throw InputError(
290 "lqn_json_reader: a loop's postActs is the body followed by the "
291 "activity it exits to, so it holds at least two entries");
292 const T count = p.contains("postParams")
293 ? detail::num_vec_from_json<T>(p.at("postParams")).at(0)
295 b.loop(pres.at(0), std::vector<std::string>(posts.begin(), posts.end() - 1),
296 posts.back(), count);
297 } else if (post == "POST_CACHE") {
298 b.cache_access(pres.at(0), posts.at(0), posts.at(1));
299 } else {
300 throw UnsupportedError("lqn_json_reader: precedence " + pre + " -> " + post +
301 " has no builder counterpart");
302 }
303 continue;
304 }
305 std::vector<std::string> acts;
306 for (const json& x : p.at("activities")) acts.push_back(x.get<std::string>());
307 if (kind == "Serial") {
308 // A serial chain is written as one list, and the builder takes
309 // it a pair at a time.
310 for (std::size_t i = 0; i + 1 < acts.size(); ++i) b.serial(acts[i], acts[i + 1]);
311 } else if (kind == "AndFork") {
312 b.and_fork(acts.at(0), std::vector<std::string>(acts.begin() + 1, acts.end()));
313 } else if (kind == "AndJoin") {
314 b.and_join(std::vector<std::string>(acts.begin(), acts.end() - 1), acts.back());
315 } else if (kind == "OrFork") {
316 std::vector<T> probs;
317 if (p.contains("probabilities"))
318 probs = detail::num_vec_from_json<T>(p.at("probabilities"));
319 b.or_fork(acts.at(0), std::vector<std::string>(acts.begin() + 1, acts.end()),
320 probs);
321 } else if (kind == "OrJoin") {
322 b.or_join(std::vector<std::string>(acts.begin(), acts.end() - 1), acts.back());
323 } else if (kind == "Loop") {
324 // The trigger is written separately from the body, so `acts` is
325 // the body alone.
326 if (!p.contains("preActivity"))
327 throw InputError(
328 "lqn_json_reader: a Loop precedence carries its trigger as 'preActivity'");
329 if (acts.size() < 2)
330 throw InputError(
331 "lqn_json_reader: a Loop precedence lists the body followed by the "
332 "activity it exits to, so it holds at least two entries");
333 b.loop(p.at("preActivity").get<std::string>(),
334 std::vector<std::string>(acts.begin(), acts.end() - 1), acts.back(),
335 num_traits<T>::from_double(p.value("loopCount", 1.0)));
336 } else if (kind == "CacheAccess") {
337 if (acts.size() < 3)
338 throw InputError(
339 "lqn_json_reader: a CacheAccess precedence names the read activity plus "
340 "its hit and miss branches");
341 b.cache_access(acts.at(0), acts.at(1), acts.at(2));
342 } else {
343 throw UnsupportedError("lqn_json_reader: precedence type '" + kind +
344 "' has no builder counterpart");
345 }
346 }
347 }
348 return b.build();
349}
350
351/**
352 * True when a file is a LayeredNetwork model.json rather than an .lqnx.
353 *
354 * Decided on the CONTENT, not the extension: `linemodel_save` writes `.json`
355 * for both model kinds and the two readers cannot be told apart by name. The
356 * first non-space character settles it -- `{` is JSON, `<` is XML -- and the
357 * `type` field settles which JSON.
358 */
359inline bool is_layered_json(const std::string& path) {
360 std::ifstream in(path.c_str());
361 if (!in) return false;
362 char c = 0;
363 while (in.get(c) && std::isspace(static_cast<unsigned char>(c))) {}
364 if (c != '{') return false;
365 in.seekg(0);
366 detail::json root;
367 try {
368 in >> root;
369 } catch (const detail::json::parse_error&) {
370 return false;
371 }
372 const detail::json& model = root.contains("model") ? root.at("model") : root;
373 return model.value("type", std::string()) == "LayeredNetwork";
374}
375
376/** Parse a LayeredNetwork model.json file into an `LqnStruct<T>`. */
377template <class T>
378lqn::LqnStruct<T> read_lqn_json(const std::string& path) {
379 std::ifstream in(path.c_str());
380 if (!in) throw InputError("lqn_json_reader: cannot open " + path);
381 detail::json root;
382 try {
383 in >> root;
384 } catch (const detail::json::parse_error& e) {
385 throw InputError("lqn_json_reader: malformed JSON in " + path + ": " + e.what());
386 }
387 return build_lqn_from_json<T>(root);
388}
389
390/**
391 * Read a layered model from either interchange: the LINE `model.json` or the
392 * LQNS `.lqnx`.
393 *
394 * Every layered entry point goes through here, so a caller never has to know
395 * which of the two it was handed -- and, more to the point, a `model.json`
396 * written by `linemodel_save` for a LayeredNetwork stops being unreadable
397 * merely because the XML reader was the only one wired up.
398 */
399template <class T>
400lqn::LqnStruct<T> read_layered_model(const std::string& path) {
401 return is_layered_json(path) ? read_lqn_json<T>(path) : lqn::read_lqnx<T>(path);
402}
403
404} // namespace io
405} // namespace line
406
407#endif // LINE_IO_LQN_JSON_READER_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
void bound_to(const std::string &act, const std::string &entry_name)
Bind an activity to an entry: it is the entry's first activity.
std::size_t activity(const std::string &name, const Distrib< T > &hostdem, const std::string &on_task)
Add an activity on a task, with its host demand.
LqnStruct< T > build() const
Flatten into the struct SolverLN consumes.
void loop(const std::string &pre, const std::vector< std::string > &body, const std::string &end, const T &count)
pre -> body, repeated count times in expectation, then -> end.
void open_arrival(const std::string &entry_name, const Distrib< T > &d)
An open arrival stream at an entry.
void setup_time(const std::string &task_name, const Distrib< T > &setup, const Distrib< T > &delayoff)
A SetupTask: a server that powers down when idle and pays to restart.
std::size_t processor(const std::string &name, double mult, SchedStrategy sched, double repl=1.0)
Add a processor.
Definition lqn_builder.h:49
void replies_to(const std::string &act, const std::string &entry_name)
Mark an activity as the one that replies to an entry.
void forward(const std::string &src_entry, const std::string &dest_entry, const T &prob)
Forwarding: whenever src_entry is invoked, with probability prob the request is handed onward to dest...
void and_fork(const std::string &pre, const std::vector< std::string > &posts)
pre -> every post, concurrently.
void cache_access(const std::string &pre, const std::string &hit, const std::string &miss)
ActivityPrecedence.CacheAccess(pre, {hit, miss}).
std::size_t cache_task(const std::string &name, double mult, SchedStrategy sched, const std::string &on_processor, std::size_t nitems, const std::vector< int > &itemcap, ReplacementStrategy replacestrat, double repl=1.0)
A CacheTask: a task whose entries are looked up in a cache of nitems.
Definition lqn_builder.h:94
void async_call(const std::string &act, const std::string &dest_entry, const T &mean)
An asynchronous call from an activity to an entry of another task.
void and_join(const std::vector< std::string > &pres, const std::string &post, std::size_t quorum=0)
all pres (or quorum of them) -> post.
void serial(const std::string &pre, const std::string &post)
pre -> post, a plain sequence.
void sync_call(const std::string &act, const std::string &dest_entry, const T &mean)
A synchronous call from an activity to an entry of another task.
void think_time(const std::string &task_name, const Distrib< T > &d)
Set a task's think time.
Definition lqn_builder.h:82
void or_fork(const std::string &pre, const std::vector< std::string > &posts, const std::vector< T > &probs)
pre -> one of the posts, with the given branch probabilities.
void or_join(const std::vector< std::string > &pres, const std::string &post)
any of the pres -> post.
std::size_t item_entry(const std::string &name, const std::string &on_task, std::size_t cardinality, const std::vector< T > &popularity)
An ItemEntry: the entry a cache read enters, over cardinality items.
std::size_t entry(const std::string &name, const std::string &on_task)
Add an entry on a task.
std::size_t task(const std::string &name, double mult, SchedStrategy sched, const std::string &on_processor, double repl=1.0)
Add a task on a processor.
Definition lqn_builder.h:61
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::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 either interchange: the LINE model.json or the LQNS .lqnx.
lqn::LqnStruct< T > build_lqn_from_json(const detail::json &root)
Build an LqnStruct<T> from a parsed model.json envelope carrying a LayeredNetwork.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
LqnStruct< T > read_lqnx(const std::string &path)
Read a .lqnx model.
Reader for the LINE model.json interchange (a Network model) into a qn::Network<T> built through the ...
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:846
static Distrib exp_mean(const T &m)
Definition lang_types.h:799