LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
jsim_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_JSIM_READER_H
6#define LINE_IO_JSIM_READER_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * Read a JMT `.jsim` / `.jsimg` / `.jsimw` model into a `qn::Network`.
12 *
13 * Port of `matlab/src/io/JSIM2LINE.m` (and `jline.io.M2M.JSIM2LINE`), the
14 * inverse of `jmt_writer.h`. It is what lets `line-cli -i jsimg` solve a model
15 * a user drew in JMT's GUI, which until now only the MATLAB and JAR front ends
16 * could do.
17 *
18 * THE MODEL IS UNDER `<sim>` AND NOTHING ELSE IS READ. A `.jsimg` also carries
19 * a sibling `<jmodel>` block holding GUI data -- station coordinates, class
20 * colours -- and it repeats every `<userClass name=...>`. Reading the document
21 * root instead of `<sim>` therefore doubles every class, silently, on exactly
22 * the files a user is most likely to import.
23 *
24 * THE SECTION ORDER IS THE INTERFACE. A JMT node is three `<section>` elements
25 * -- input, service, output -- and every parameter inside one is found by
26 * POSITION, not by name: `saveGetStrategy` writes the put strategy fourth
27 * because the reader counts to four. This reader therefore addresses parameters
28 * positionally where the reference does, and by `name` attribute where a name
29 * exists, which is what makes it survive the two JMT XML dialects (the
30 * retrial-carrying 1.2.0 form shifts the put strategy from parameter 4 to 5,
31 * and the reference tells them apart by reading parameter 3's name).
32 *
33 * WHAT IS NOT READ IS REFUSED BY NAME rather than dropped. A `.jsimg` can
34 * describe blocking regions, cache sections and measures this reader does not
35 * reconstruct; importing such a file and solving the remainder would answer a
36 * question about a different model. The refusals name the section.
37 */
38
39#include <algorithm>
40#include <cmath>
41#include <cstddef>
42#include <cstdlib>
43#include <limits>
44#include <fstream>
45#include <map>
46#include <memory>
47#include <string>
48#include <vector>
49
52#include "line/num/number.h"
53#include "line/util/error.h"
54#include "line/util/matrix.h"
55#include "line/util/xml.h"
56
57#include <unistd.h>
58
59namespace line {
60namespace io {
61
62namespace jsim_detail {
63
64using xml::Element;
65
66inline double to_num(const std::string& s) { return std::atof(s.c_str()); }
67
68/** The text of an element, trimmed; empty when the element is absent. */
69inline std::string text_of(const Element* e) {
70 if (!e) return std::string();
71 std::string t = e->text;
72 std::size_t a = 0, b = t.size();
73 while (a < b && (t[a] == ' ' || t[a] == '\n' || t[a] == '\t' || t[a] == '\r')) ++a;
74 while (b > a && (t[b - 1] == ' ' || t[b - 1] == '\n' || t[b - 1] == '\t' || t[b - 1] == '\r'))
75 --b;
76 return t.substr(a, b - a);
77}
78
79/**
80 * `<value>` of a parameter, which JMT writes as a child element rather than as
81 * text on the parameter itself.
82 */
83inline std::string value_of(const Element* e) {
84 if (!e) return std::string();
85 const std::vector<const Element*> v = e->child_tags("value");
86 if (!v.empty()) return text_of(v[0]);
87 return text_of(e);
88}
89
90/** Direct `<parameter>` children, in document order. */
91inline std::vector<const Element*> params(const Element* e) {
92 return e ? e->child_tags("parameter") : std::vector<const Element*>();
93}
94
95/** Direct `<subParameter>` children, in document order. */
96inline std::vector<const Element*> subs(const Element* e) {
97 return e ? e->child_tags("subParameter") : std::vector<const Element*>();
98}
99
100/** The first direct child `<parameter>`/`<subParameter>` carrying `name`. */
101inline const Element* named(const std::vector<const Element*>& v, const std::string& name) {
102 for (std::size_t i = 0; i < v.size(); ++i)
103 if (v[i]->attr("name") == name) return v[i];
104 return nullptr;
105}
106
107/** Element i of a list, or null -- positional access that cannot read past the end. */
108inline const Element* at(const std::vector<const Element*>& v, std::size_t i) {
109 return i < v.size() ? v[i] : nullptr;
110}
111
112/** `\` and `/` become `_`, as the reference renames a node on import. */
113inline std::string sanitize(const std::string& s) {
114 std::string out = s;
115 for (std::size_t i = 0; i < out.size(); ++i)
116 if (out[i] == '/' || out[i] == '\\') out[i] = '_';
117 return out;
118}
119
120/**
121 * A JMT distribution block -- the `<subParameter name="...">` pair naming the
122 * law and its parameters -- lowered to a `Distrib`.
123 *
124 * THE INVERSE OF `jmt_append_distribution`, and of the JMT GUI's own writer,
125 * which are the same format. `dist` is the element whose `name` attribute is
126 * the display name ("Exponential", "Burst (MAP)", ...) and `par` the
127 * `distrPar` block beside it.
128 *
129 * A LAW THIS READER DOES NOT KNOW IS REFUSED BY NAME. The reference falls back
130 * to matching two moments with an APH, which silently replaces the user's
131 * distribution with a different one of the same mean and SCV; that is a
132 * reasonable default in an interactive session and a wrong answer in a CLI, so
133 * the name is reported instead.
134 */
135template <class T>
136lang::Distrib<T> read_distribution(const Element* dist, const Element* par,
137 const std::string& who) {
138 if (!dist) return lang::Distrib<T>::disabled_dist();
139 const std::string name = dist->attr("name");
140 const std::string cp = dist->attr("classPath");
141 if (name == "DisabledServiceTimeStrategy" || cp.find("DisabledServiceTime") != std::string::npos)
143 if (name == "ZeroServiceTimeStrategy" || cp.find("ZeroServiceTime") != std::string::npos)
145
146 const std::vector<const Element*> p = subs(par);
147 auto num = [&](std::size_t i) -> T {
148 return num_traits<T>::from_double(to_num(value_of(at(p, i))));
149 };
150 auto bykey = [&](const char* key, std::size_t fallback) -> T {
151 const Element* e = named(p, key);
152 return e ? num_traits<T>::from_double(to_num(value_of(e))) : num(fallback);
153 };
154
155 if (name == "Exponential") return lang::Distrib<T>::exp_rate(bykey("lambda", 0));
156 if (name == "Deterministic") return lang::Distrib<T>::det(bykey("t", 0));
157 if (name == "Erlang") {
158 // JMT's `alpha` is the PHASE rate and `r` the phase count, which is
159 // exactly `Distrib::erlang`'s pair -- no conversion, and in particular
160 // NOT the mean.
161 const T a = bykey("alpha", 0);
162 const Element* re = named(p, "r");
163 const double r = re ? to_num(value_of(re)) : to_num(value_of(at(p, 1)));
164 if (!(r >= 1.0))
165 throw InputError(who + ": an Erlang needs at least one phase, got r=" +
166 std::to_string(r));
167 return lang::Distrib<T>::erlang(a, static_cast<std::size_t>(r));
168 }
169 if (name == "Hyperexponential")
170 return lang::Distrib<T>::hyperexp(bykey("p", 0), bykey("lambda1", 1), bykey("lambda2", 2));
171 if (name == "Coxian") {
172 // `Coxian([mu1 mu2], [phi1 1])` of the reference: the third parameter is
173 // the completion probability of phase 1, and phase 2 always completes.
174 std::vector<T> mu, phi;
175 mu.push_back(bykey("lambda0", 0));
176 mu.push_back(bykey("lambda1", 1));
177 phi.push_back(bykey("phi0", 2));
178 phi.push_back(num_traits<T>::from_int(1));
179 return lang::Distrib<T>::coxian(mu, phi);
180 }
181 if (name == "Pareto") return lang::Distrib<T>::pareto(bykey("alpha", 0), bykey("k", 1));
182 if (name == "Gamma") return lang::Distrib<T>::gamma_dist(bykey("alpha", 0), bykey("beta", 1));
183 if (name == "Uniform") return lang::Distrib<T>::uniform(bykey("min", 0), bykey("max", 1));
184 if (name == "Weibull") {
185 // The constructor takes (scale, shape) and JMT writes alpha=scale,
186 // r=shape; the reference's own comment records that the two are
187 // inverted relative to the JMT parameter order.
188 return lang::Distrib<T>::weibull(bykey("alpha", 0), bykey("r", 1));
189 }
190 if (name == "Lognormal")
191 return lang::Distrib<T>::lognormal(bykey("mu", 0), bykey("sigma", 1));
192 if (name == "Replayer" || name == "Trace") {
193 const Element* fe = named(p, "fileName");
194 const std::string path = fe ? value_of(fe) : value_of(at(p, 0));
195 if (path.empty())
196 throw InputError(who + ": a Replayer names no trace file");
197 std::vector<T> samples;
198 {
199 std::ifstream tr(path.c_str());
200 double v = 0.0;
201 while (tr >> v) samples.push_back(num_traits<T>::from_double(v));
202 }
203 if (samples.empty())
204 throw InputError(who + ": the Replayer trace '" + path +
205 "' is missing or empty; the model cannot be reconstructed without "
206 "the samples it replays");
207 lang::Distrib<T> d = lang::Distrib<T>::replayer(samples);
208 d.trace_file = path;
209 return d;
210 }
211 if (name == "Burst (MMPP2)") {
212 const double l0 = to_num(value_of(at(p, 0))), l1 = to_num(value_of(at(p, 1)));
213 const double s0 = to_num(value_of(at(p, 2))), s1 = to_num(value_of(at(p, 3)));
214 Matrix<T> D0(2, 2, num_traits<T>::from_int(0)), D1(2, 2, num_traits<T>::from_int(0));
215 D1(0, 0) = num_traits<T>::from_double(l0);
216 D1(1, 1) = num_traits<T>::from_double(l1);
217 D0(0, 0) = num_traits<T>::from_double(-(l0 + s0));
218 D0(0, 1) = num_traits<T>::from_double(s0);
219 D0(1, 0) = num_traits<T>::from_double(s1);
220 D0(1, 1) = num_traits<T>::from_double(-(l1 + s1));
222 }
223 // The two matrix forms: a square block of rows, each row an array of
224 // `<value>` scalars. `jmt_object_array` writes the same nesting for both.
225 auto read_matrix = [&](const Element* blk) {
226 std::vector<std::vector<double> > rows;
227 const std::vector<const Element*> rr = subs(blk);
228 for (std::size_t i = 0; i < rr.size(); ++i) {
229 std::vector<double> row;
230 const std::vector<const Element*> cc = subs(rr[i]);
231 for (std::size_t j = 0; j < cc.size(); ++j) row.push_back(to_num(value_of(cc[j])));
232 if (!row.empty()) rows.push_back(row);
233 }
234 Matrix<T> M(rows.size(), rows.empty() ? 0 : rows[0].size(), num_traits<T>::from_int(0));
235 for (std::size_t i = 0; i < rows.size(); ++i)
236 for (std::size_t j = 0; j < rows[i].size(); ++j)
237 M(i, j) = num_traits<T>::from_double(rows[i][j]);
238 return M;
239 };
240 if (name == "Burst (MAP)") {
241 const Element* d0 = named(p, "D0") ? named(p, "D0") : at(p, 0);
242 const Element* d1 = named(p, "D1") ? named(p, "D1") : at(p, 1);
243 return lang::Distrib<T>::map_dist(read_matrix(d0), read_matrix(d1),
245 }
246 if (name == "Phase-Type") {
247 const Element* av = named(p, "alpha") ? named(p, "alpha") : at(p, 0);
248 const Element* Tb = named(p, "T") ? named(p, "T") : at(p, 1);
249 // `alpha` is an array wrapping a `vector` array of scalars, one level
250 // deeper than `T`'s rows.
251 std::vector<T> alpha;
252 const std::vector<const Element*> a1 = subs(av);
253 const std::vector<const Element*> a2 = a1.empty() ? a1 : subs(a1[0]);
254 for (std::size_t i = 0; i < a2.size(); ++i)
255 alpha.push_back(num_traits<T>::from_double(to_num(value_of(a2[i]))));
256 const Matrix<T> A = read_matrix(Tb);
257 // APH when the sub-diagonal is empty, general PH otherwise -- the
258 // reference's own test, and the flag decides which representation the
259 // struct records rather than which numbers it holds.
260 bool acyclic = true;
261 for (std::size_t i = 0; i < A.rows(); ++i)
262 for (std::size_t j = 0; j < i; ++j)
263 if (num_traits<T>::to_double(A(i, j)) > 0.0) acyclic = false;
264 return lang::Distrib<T>::phase_type(alpha, A, acyclic);
265 }
266 throw UnsupportedError(who + ": JMT distribution '" + name +
267 "' has no counterpart in this reader. The MATLAB importer matches its "
268 "first two moments with an APH instead; that substitutes a different "
269 "law for the user's, which a solve would then report as the model's "
270 "answer");
271}
272
273/** The (dist, par) pair inside a strategy wrapper, as the writer emits it. */
274template <class T>
275lang::Distrib<T> read_strategy_dist(const Element* wrapper, const std::string& who) {
276 if (!wrapper) return lang::Distrib<T>::disabled_dist();
277 const std::string nm = wrapper->attr("name");
278 if (nm == "DisabledServiceTimeStrategy") return lang::Distrib<T>::disabled_dist();
279 if (nm == "ZeroServiceTimeStrategy") return lang::Distrib<T>::immediate();
280 const std::vector<const Element*> ss = subs(wrapper);
281 if (ss.empty()) return lang::Distrib<T>::disabled_dist();
282 return read_distribution<T>(at(ss, 0), at(ss, 1), who);
283}
284
285} // namespace jsim_detail
286
287/**
288 * Read a JSIM document into a Network.
289 *
290 * @param path the `.jsim` / `.jsimg` / `.jsimw` file
291 * @param name the model name; the document's own when empty
292 */
293template <class T>
294qn::Network<T> read_jsim(const std::string& path, const std::string& name = std::string()) {
295 using namespace jsim_detail;
296 std::unique_ptr<Element> doc = xml::parse_file(path);
297 if (!doc) throw InputError("read_jsim: cannot parse '" + path + "' as XML");
298
299 // `<sim>` is the model; a `.jsimg` wraps it beside the GUI's `<jmodel>`.
300 const Element* sim = doc->name == "sim" ? doc.get() : nullptr;
301 if (!sim) {
302 const std::vector<const Element*> s = doc->by_tag("sim");
303 if (s.empty())
304 throw InputError("read_jsim: '" + path +
305 "' carries no <sim> element, so it is not a JSIM model");
306 sim = s[0];
307 }
308
309 qn::Network<T> net(name.empty() ? sim->attr("name") : name);
310 const std::vector<const Element*> xnodes = sim->child_tags("node");
311 const std::vector<const Element*> xclasses = sim->child_tags("userClass");
312 if (xnodes.empty()) throw InputError("read_jsim: the model declares no node");
313 if (xclasses.empty()) throw InputError("read_jsim: the model declares no job class");
314 const std::size_t N = xnodes.size(), K = xclasses.size();
315
316 std::vector<std::string> orig_name(N), node_name(N);
317 for (std::size_t i = 0; i < N; ++i) {
318 orig_name[i] = xnodes[i]->attr("name");
319 node_name[i] = sanitize(orig_name[i]);
320 }
321 // The three sections of each node, and the class names of each section.
322 std::vector<std::vector<const Element*> > sec(N);
323 for (std::size_t i = 0; i < N; ++i) sec[i] = xnodes[i]->child_tags("section");
324
325 auto secclass = [&](std::size_t i, std::size_t k) -> std::string {
326 const Element* e = at(sec[i], k);
327 return e ? e->attr("className") : std::string();
328 };
329
330 // ---- pass 1: create the nodes -----------------------------------------
331 std::vector<std::size_t> nidx(N, 0); ///< 1-based node index in the Network
332 std::vector<lang::SchedStrategy> sched(N, lang::SchedStrategy::FCFS);
333 std::vector<bool> is_station(N, false), is_source(N, false), is_delay(N, false);
334 std::vector<std::vector<T> > schedparam(N);
335 std::size_t forknode = 0;
336
337 for (std::size_t i = 0; i < N; ++i) {
338 const std::string in = secclass(i, 0), svc = secclass(i, 1), out = secclass(i, 2);
339 if (in == "JobSink") {
340 nidx[i] = net.add_sink(node_name[i]);
341 continue;
342 }
343 if (in == "RandomSource") {
344 nidx[i] = net.add_source(node_name[i]);
345 is_station[i] = is_source[i] = true;
346 continue;
347 }
348 if (in == "Join") {
349 if (!forknode)
350 throw UnsupportedError(
351 "read_jsim: a Join appears before any Fork; the importer supports at most one "
352 "fork-join pair and reads them in document order, as the reference does");
353 nidx[i] = net.add_join(node_name[i], forknode);
354 continue;
355 }
356 if (in == "Storage") {
357 nidx[i] = net.add_place(node_name[i]);
358 is_station[i] = true;
359 continue;
360 }
361 if (in == "Enabling") {
362 // Built in pass 3, once the mode structure has been read: the
363 // constructor takes the whole TransitionParam and there is no
364 // setter to fill it afterwards.
365 continue;
366 }
367 if (in != "Queue" && in != "Buffer")
368 throw UnsupportedError("read_jsim: node '" + orig_name[i] +
369 "' has input section '" + in +
370 "', which this reader does not build");
371
372 if (out == "Fork") {
373 const std::vector<const Element*> op = params(at(sec[i], 2));
374 const double tpl = op.empty() ? 1.0 : to_num(value_of(op[0]));
375 nidx[i] = net.add_fork(node_name[i], tpl);
376 forknode = nidx[i];
377 continue;
378 }
379 if (svc == "ServiceTunnel") {
380 nidx[i] = net.add_router(node_name[i]);
381 continue;
382 }
383 if (svc == "ClassSwitch" || svc == "StatelessClassSwitcher") {
384 // The node is created HERE, in file order, and its matrix installed
385 // in pass 3 once the classes exist: the matrix is indexed by class
386 // and a `.jsimg` lists its nodes first. Creating the node later
387 // instead would renumber every node after it, which the reference's
388 // own commented-out attempt to "create the cs elements last" was
389 // uncertain about for exactly that reason.
390 nidx[i] = net.add_class_switch(node_name[i]);
391 continue;
392 }
393
394 // ---- a queueing station: read the put strategy for the discipline --
395 const std::vector<const Element*> ip = params(at(sec[i], 0));
396 // The 1.2.0 dialect inserts `retrialDistributions` at position 3, which
397 // pushes the put strategy from parameter 4 to parameter 5. The
398 // reference tells the two apart by reading parameter 3's NAME, and so
399 // does this: counting positions without the test reads the retrial
400 // block as a scheduling strategy.
401 const std::size_t putpos =
402 (ip.size() > 2 && ip[2]->attr("name") == "retrialDistributions") ? 4 : 3;
403 const std::vector<const Element*> put = subs(at(ip, putpos));
404 const std::string putname = put.empty() ? std::string() : put[0]->attr("name");
405 if (putname == "TailStrategy") sched[i] = lang::SchedStrategy::FCFS;
406 else if (putname == "TailStrategyPriority") sched[i] = lang::SchedStrategy::HOL;
407 else if (putname == "HeadStrategy") sched[i] = lang::SchedStrategy::LCFS;
408 else if (putname == "RandStrategy") sched[i] = lang::SchedStrategy::SIRO;
409 else if (putname == "SJFStrategy") sched[i] = lang::SchedStrategy::SJF;
410 else if (putname == "SEPTStrategy") sched[i] = lang::SchedStrategy::SEPT;
411 else if (putname == "LJFStrategy") sched[i] = lang::SchedStrategy::LJF;
412 else if (putname == "LEPTStrategy") sched[i] = lang::SchedStrategy::LEPT;
413
414 if (svc == "Delay" || svc == "InfiniteServer") {
415 nidx[i] = net.add_delay(node_name[i]);
416 is_station[i] = is_delay[i] = true;
417 } else if (svc == "PSServer" || svc == "SharedServer") {
418 // The PS family names its discipline in the PREEMPTIVE strategy
419 // block of the service section, not in the buffer's put strategy.
420 const std::vector<const Element*> sp = params(at(sec[i], 1));
421 std::string ps = "EPSStrategy";
422 if (sp.size() > 3) {
423 const std::vector<const Element*> ss = subs(sp[3]);
424 if (!ss.empty()) ps = ss[0]->attr("name");
425 }
426 if (ps == "EPSStrategy") sched[i] = lang::SchedStrategy::PS;
427 else if (ps == "DPSStrategy") sched[i] = lang::SchedStrategy::DPS;
428 else if (ps == "GPSStrategy") sched[i] = lang::SchedStrategy::GPS;
429 else if (ps == "EPSStrategyPriority") sched[i] = lang::SchedStrategy::PSPRIO;
430 else if (ps == "DPSStrategyPriority") sched[i] = lang::SchedStrategy::DPSPRIO;
431 else if (ps == "GPSStrategyPriority") sched[i] = lang::SchedStrategy::GPSPRIO;
432 // The per-class weights sit in the parameter after it.
433 if (sp.size() > 4)
434 for (const Element* w : subs(sp[4]))
435 schedparam[i].push_back(num_traits<T>::from_double(to_num(value_of(w))));
436 nidx[i] = net.add_queue(node_name[i], sched[i]);
437 is_station[i] = true;
438 } else if (svc == "Server" || svc == "PreemptiveServer") {
439 nidx[i] = net.add_queue(node_name[i], sched[i]);
440 is_station[i] = true;
441 } else {
442 throw UnsupportedError("read_jsim: node '" + orig_name[i] + "' has service section '" +
443 svc + "', which this reader does not build");
444 }
445
446 // Buffer size and, for a Server, the number of servers.
447 if (!ip.empty()) {
448 const double cap = to_num(value_of(ip[0]));
449 // JMT writes -1 for "unbounded", which is Inf here and NOT a
450 // capacity of -1: a negative buffer would refuse every arrival.
451 net.set_capacity(nidx[i], cap < 0 ? std::numeric_limits<double>::infinity() : cap);
452 }
453 if (svc != "Delay" && svc != "InfiniteServer") {
454 const std::vector<const Element*> sp = params(at(sec[i], 1));
455 const Element* ns = named(sp, "maxJobs");
456 if (!ns && !sp.empty()) ns = sp[0];
457 if (ns) {
458 const double c = to_num(value_of(ns));
460 nidx[i], c < 0 ? std::numeric_limits<double>::infinity() : c);
461 }
462 }
463 }
464
465 // ---- pass 2: the job classes ------------------------------------------
466 // JMT reads a HIGHER priority value as MORE important and LINE reads a
467 // LOWER one as more important, so the values are reflected about the
468 // maximum rather than copied. Copying them would invert every priority
469 // ordering in the imported model, which no metric would flag as wrong.
470 int maxprio = 0;
471 for (std::size_t r = 0; r < K; ++r)
472 maxprio = std::max(maxprio, static_cast<int>(to_num(xclasses[r]->attr("priority"))));
473
474 std::map<std::string, std::size_t> node_by_name;
475 for (std::size_t i = 0; i < N; ++i)
476 if (nidx[i]) {
477 node_by_name[node_name[i]] = nidx[i];
478 node_by_name[orig_name[i]] = nidx[i];
479 }
480
481 std::vector<std::size_t> cidx(K, 0);
482 std::vector<bool> cs_referenced(K, false);
483 for (std::size_t r = 0; r < K; ++r) {
484 const std::string cname = xclasses[r]->attr("name");
485 const std::string type = xclasses[r]->attr("type");
486 const int prio = maxprio - static_cast<int>(to_num(xclasses[r]->attr("priority")));
487 const std::string refsrc = xclasses[r]->attr("referenceSource");
488 if (type == "closed") {
489 const std::map<std::string, std::size_t>::const_iterator it =
490 node_by_name.find(refsrc);
491 if (it == node_by_name.end())
492 throw InputError("read_jsim: class '" + cname + "' names reference source '" +
493 refsrc + "', which is not a node of the model");
494 cidx[r] = net.add_closed_class(cname, to_num(xclasses[r]->attr("customers")),
495 it->second, prio);
496 } else {
497 cidx[r] = net.add_open_class(cname, prio);
498 // 'ClassSwitch' / 'StatelessClassSwitcher' is JMT's marker for an
499 // open class that enters by class switching only; its Source
500 // arrival is DISABLED rather than absent, which is a different
501 // model from one whose arrival was simply not written.
502 if (refsrc == "ClassSwitch" || refsrc == "StatelessClassSwitcher")
503 cs_referenced[r] = true;
504 }
505 }
506
507 // ---- pass 3: what needs the classes: ClassSwitch matrices, Petri nets --
508 for (std::size_t i = 0; i < N; ++i) {
509 const std::string svc3 = secclass(i, 1);
510 if (nidx[i] && (svc3 == "ClassSwitch" || svc3 == "StatelessClassSwitcher")) {
511 const std::vector<const Element*> sp = params(at(sec[i], 1));
513 if (!sp.empty()) {
514 // `<parameter name="matrix">` holds one `<subParameter name="row">`
515 // per class, each holding one `<subParameter name="cell">` per
516 // class. TWO levels, not three: descending once more reads the
517 // FIRST row's cells as the whole matrix, which leaves every
518 // other row zero -- and a zero row means "this class switches
519 // into nothing", so the classes it fed become unreachable and
520 // the model is rejected several frames later for a reason that
521 // names neither the matrix nor this node.
522 const std::vector<const Element*> rows = subs(sp[0]);
523 for (std::size_t r = 0; r < rows.size() && r < K; ++r) {
524 const std::vector<const Element*> cols = subs(rows[r]);
525 for (std::size_t c = 0; c < cols.size() && c < K; ++c)
526 C(r, c) = num_traits<T>::from_double(to_num(value_of(cols[c])));
527 }
528 }
529 net.set_class_switch_matrix(nidx[i], C);
530 }
531 const std::string in = secclass(i, 0);
532 if (in == "Storage") {
533 const std::vector<const Element*> ip = params(at(sec[i], 0));
534 if (!ip.empty()) {
535 const double cap = to_num(value_of(ip[0]));
536 net.set_capacity(nidx[i],
537 cap < 0 ? std::numeric_limits<double>::infinity() : cap);
538 }
539 if (ip.size() > 1) {
540 const std::vector<const Element*> pc = subs(ip[1]);
541 for (std::size_t c = 0; c < pc.size() && c < K; ++c) {
542 const double v = to_num(value_of(pc[c]));
543 net.set_class_capacity(nidx[i], cidx[c],
544 v < 0 ? std::numeric_limits<double>::infinity() : v);
545 }
546 }
547 if (ip.size() > 2) {
548 const std::vector<const Element*> dr = subs(ip[2]);
549 for (std::size_t c = 0; c < dr.size() && c < K; ++c) {
550 const std::string rule = value_of(dr[c]);
551 if (rule == "BAS blocking")
552 net.set_drop_rule(nidx[i], cidx[c], lang::DropStrategy::BAS);
553 else if (rule == "drop")
554 net.set_drop_rule(nidx[i], cidx[c], lang::DropStrategy::DROP);
555 else if (rule == "waiting queue")
556 net.set_drop_rule(nidx[i], cidx[c], lang::DropStrategy::WAITQ);
557 }
558 }
559 continue;
560 }
561 if (in != "Enabling") continue;
562
563 // ---- a Transition: enabling, timing and firing sections ------------
564 const std::vector<const Element*> ep = params(at(sec[i], 0));
565 const std::vector<const Element*> tp = params(at(sec[i], 1));
566 const std::vector<const Element*> fp = params(at(sec[i], 2));
567 if (ep.size() < 2 || tp.size() < 5 || fp.empty())
568 throw InputError("read_jsim: transition '" + orig_name[i] +
569 "' is missing one of the enabling, timing or firing parameters");
570 const std::vector<const Element*> enmodes = subs(ep[0]);
571 const std::vector<const Element*> inmodes = subs(ep[1]);
572 const std::vector<const Element*> names = subs(tp[0]);
573 const std::vector<const Element*> nserv = subs(tp[1]);
574 const std::vector<const Element*> timing = subs(tp[2]);
575 const std::vector<const Element*> fprio = subs(tp[3]);
576 const std::vector<const Element*> fweight = subs(tp[4]);
577 const std::vector<const Element*> fmodes = subs(fp[0]);
578 const std::size_t M = enmodes.size();
579
581 par.nmodes = M;
582 // The arcs are indexed by PLACE, which is the whole node index space
583 // here: a transition names its neighbours by node name, and a name that
584 // is not a Place would silently become an arc to a queue.
585 const std::size_t inf_marker = 0;
586 (void)inf_marker;
587 const double dinf = std::numeric_limits<double>::infinity();
588 par.enabling.assign(M, Matrix<T>(N, K, num_traits<T>::from_int(0)));
589 par.inhibiting.assign(M, Matrix<T>(N, K, num_traits<T>::from_double(dinf)));
590 par.firing.assign(M, Matrix<T>(N, K, num_traits<T>::from_int(0)));
591
592 // ONE VALUE PER CLASS in the file, and one per (place, class) in the
593 // struct: a COLOURED net is what JSIM writes and what this reads back,
594 // so the per-class values are kept apart instead of being collapsed onto
595 // the place. `cidx` maps the document's class order onto the struct's.
596 auto read_arcs = [&](const Element* mode, Matrix<T>& row, bool inhibiting) {
597 const std::vector<const Element*> lvl1 = subs(mode);
598 if (lvl1.empty()) return;
599 for (const Element* arc : subs(lvl1[0])) {
600 const std::vector<const Element*> ap = subs(arc);
601 if (ap.size() < 2) continue;
602 const std::string target = value_of(ap[0]);
603 const std::map<std::string, std::size_t>::const_iterator it =
604 node_by_name.find(target);
605 if (it == node_by_name.end()) continue;
606 const std::vector<const Element*> per = subs(ap[1]);
607 for (std::size_t c = 0; c < per.size() && c < K; ++c) {
608 const double w = to_num(value_of(per[c]));
609 const std::size_t rr = cidx[c] ? cidx[c] - 1 : c;
610 if (inhibiting) {
611 // JMT writes 0 or -1 for "no inhibitor arc"; a threshold
612 // of 0 would inhibit at zero tokens, i.e. always, and
613 // deadlock the transition -- the reference's own note.
614 row(it->second - 1, rr) = (w <= 0.0)
617 } else {
618 row(it->second - 1, rr) = (w < 0.0)
621 }
622 }
623 }
624 };
625
626 for (std::size_t m = 0; m < M; ++m) {
627 par.modenames.push_back(m < names.size() ? value_of(names[m])
628 : "mode" + std::to_string(m + 1));
629 read_arcs(enmodes[m], par.enabling[m], false);
630 if (m < inmodes.size()) read_arcs(inmodes[m], par.inhibiting[m], true);
631 if (m < fmodes.size()) read_arcs(fmodes[m], par.firing[m], false);
632
633 const double ns = m < nserv.size() ? to_num(value_of(nserv[m])) : 1.0;
634 par.nmodeservers.push_back(ns < 0 ? dinf : ns);
635 par.firingprio.push_back(m < fprio.size() ? to_num(value_of(fprio[m])) : 0.0);
637 m < fweight.size() ? to_num(value_of(fweight[m])) : 1.0));
638
639 const Element* tm = at(timing, m);
640 const std::string cp = tm ? tm->attr("classPath") : std::string();
641 if (cp.find("ZeroServiceTimeStrategy") != std::string::npos) {
644 par.firingphases.push_back(1);
645 } else {
646 par.timing.push_back(lang::TimingStrategy::TIMED);
647 const std::vector<const Element*> ts = subs(tm);
648 const lang::Distrib<T> d = read_distribution<T>(
649 at(ts, 0), at(ts, 1), "read_jsim (transition '" + orig_name[i] + "')");
650 par.firingproc.push_back(d);
651 par.firingphases.push_back(d.phases());
652 }
653 }
654 par.firingdep.assign(M, std::function<T(const std::vector<T>&)>());
655 nidx[i] = net.add_transition(node_name[i], par);
656 node_by_name[node_name[i]] = nidx[i];
657 node_by_name[orig_name[i]] = nidx[i];
658 }
659
660 // ---- pass 4: arrival and service processes -----------------------------
661 for (std::size_t i = 0; i < N; ++i) {
662 if (!nidx[i] || !is_station[i]) continue;
663 if (is_source[i]) {
664 const std::vector<const Element*> sp = params(at(sec[i], 0));
665 if (sp.empty()) continue;
666 const std::vector<const Element*> per = subs(sp[0]);
667 for (std::size_t r = 0; r < K; ++r) {
668 if (cs_referenced[r]) {
669 net.set_arrival(nidx[i], cidx[r], lang::Distrib<T>::disabled_dist());
670 continue;
671 }
672 net.set_arrival(nidx[i], cidx[r],
673 read_strategy_dist<T>(at(per, r),
674 "read_jsim (arrival at '" + orig_name[i] +
675 "')"));
676 }
677 continue;
678 }
679 const std::string svc = secclass(i, 1);
680 if (svc == "ClassSwitch" || svc == "StatelessClassSwitcher") continue;
681 // The service strategy is the LAST parameter block whose subParameters
682 // are per class: parameter 0 for a Delay (which has no server count),
683 // parameter 2 for a Server. Located by name rather than by position so
684 // an extra block -- server visits, heterogeneous policy -- does not
685 // shift it.
686 const std::vector<const Element*> sp = params(at(sec[i], 1));
687 const Element* strat = named(sp, "ServiceStrategy");
688 if (!strat) strat = is_delay[i] ? at(sp, 0) : at(sp, 2);
689 if (!strat) continue;
690 const std::vector<const Element*> per = subs(strat);
691 for (std::size_t r = 0; r < K; ++r) {
692 net.set_service(nidx[i], cidx[r],
693 read_strategy_dist<T>(at(per, r), "read_jsim (service at '" +
694 orig_name[i] + "')"));
695 if (r < schedparam[i].size()) net.set_sched_param(nidx[i], cidx[r], schedparam[i][r]);
696 }
697 }
698
699 // ---- pass 5: links and routing ----------------------------------------
700 std::vector<std::vector<bool> > conn(N, std::vector<bool>(N, false));
701 std::map<std::string, std::size_t> pos_by_name;
702 for (std::size_t i = 0; i < N; ++i) {
703 pos_by_name[orig_name[i]] = i;
704 pos_by_name[node_name[i]] = i;
705 }
706 for (const Element* c : sim->child_tags("connection")) {
707 const std::map<std::string, std::size_t>::const_iterator a =
708 pos_by_name.find(c->attr("source"));
709 const std::map<std::string, std::size_t>::const_iterator b =
710 pos_by_name.find(c->attr("target"));
711 if (a == pos_by_name.end() || b == pos_by_name.end()) continue;
712 conn[a->second][b->second] = true;
713 }
714
716 const T one = num_traits<T>::from_int(1);
717 for (std::size_t from = 0; from < N; ++from) {
718 if (!nidx[from]) continue;
719 const std::string in = secclass(from, 0);
720 if (in == "JobSink" || in == "Storage" || in == "Enabling") continue;
721 const std::vector<const Element*> op = params(at(sec[from], 2));
722 const std::vector<const Element*> per = op.empty() ? std::vector<const Element*>()
723 : subs(op[0]);
724 std::vector<std::size_t> targets;
725 for (std::size_t j = 0; j < N; ++j)
726 if (conn[from][j] && nidx[j]) targets.push_back(j);
727
728 for (std::size_t r = 0; r < K; ++r) {
729 const Element* st = at(per, r);
730 const std::string rs = st ? st->attr("name") : std::string("Random");
731 if (rs == "Disabled") {
732 net.set_routing(nidx[from], cidx[r], lang::RoutingStrategy::DISABLED);
733 continue;
734 }
735 if (rs == "Probabilities" || rs == "Weighted Round Robin") {
736 const bool wrr = rs != "Probabilities";
737 net.set_routing(nidx[from], cidx[r],
740 // `<subParameter name="EmpiricalEntryArray">` holds one entry
741 // per destination, each a (name, value) pair.
742 const std::vector<const Element*> arr = subs(st);
743 const std::vector<const Element*> entries =
744 arr.empty() ? arr : subs(arr[0]);
745 std::map<std::size_t, double> w;
746 for (const Element* e : entries) {
747 const std::vector<const Element*> kv = subs(e);
748 if (kv.size() < 2) continue;
749 const std::map<std::string, std::size_t>::const_iterator it =
750 pos_by_name.find(value_of(kv[0]));
751 if (it == pos_by_name.end() || !nidx[it->second]) continue;
752 w[it->second] = to_num(value_of(kv[1]));
753 }
754 if (wrr) {
755 std::map<std::size_t, double> wt;
756 for (std::map<std::size_t, double>::const_iterator it = w.begin();
757 it != w.end(); ++it)
758 wt[nidx[it->first]] = it->second;
759 net.set_routing_weights(nidx[from], cidx[r], wt);
760 // A weighted round robin still has to reach its
761 // destinations, so the links are declared with equal shares;
762 // the WEIGHTS above are what the refresh reads.
763 for (std::size_t j = 0; j < targets.size(); ++j)
764 P.set(cidx[r], cidx[r], nidx[from], nidx[targets[j]],
766 static_cast<double>(targets.size())));
767 } else {
768 for (std::map<std::size_t, double>::const_iterator it = w.begin();
769 it != w.end(); ++it)
770 P.set(cidx[r], cidx[r], nidx[from], nidx[it->first],
771 num_traits<T>::from_double(it->second));
772 }
773 continue;
774 }
775 // The state-dependent strategies declare only WHERE a job may go;
776 // the choice is made at run time, so every reachable link carries an
777 // equal share and the strategy is what a solver reads.
779 if (rs == "Round Robin") strat = lang::RoutingStrategy::RROBIN;
780 else if (rs == "Join the Shortest Queue (JSQ)") strat = lang::RoutingStrategy::JSQ;
781 else if (rs == "Power of k") strat = lang::RoutingStrategy::SQ;
782 else if (rs != "Random")
783 throw UnsupportedError("read_jsim: node '" + orig_name[from] +
784 "' uses routing strategy '" + rs +
785 "', which this reader does not build");
786 net.set_routing(nidx[from], cidx[r], strat);
787 if (strat == lang::RoutingStrategy::SQ) {
788 int kk = 2; // JMT's default when <k> is absent
789 for (const Element* pj : subs(st)) {
790 if (pj->attr("name") == "k") kk = static_cast<int>(to_num(value_of(pj)));
791 if (pj->attr("name") == "withMemory") {
792 const std::string wm = value_of(pj);
793 if (wm == "true" || wm == "1")
794 throw UnsupportedError(
795 "read_jsim: node '" + orig_name[from] +
796 "' selects Power-of-k WITH MEMORY, which is Anselmi & Dufour's "
797 "SQ(d,N) and not the memoryless SQ(d) this port implements; "
798 "importing it as SQ(d) would answer about a different policy");
799 }
800 }
801 net.set_routing_param(nidx[from], cidx[r], kk);
802 }
803 for (std::size_t j = 0; j < targets.size(); ++j)
804 P.set(cidx[r], cidx[r], nidx[from], nidx[targets[j]],
805 targets.size() == 1
806 ? one
808 static_cast<double>(targets.size())));
809 }
810 }
811 net.link(P);
812
813 // ---- pass 6: the preload marking --------------------------------------
814 //
815 // THE REFERENCE CALLS `initFromMarginal` AND THIS PORT HAS NO SUCH ENTRY.
816 // Its stateful nodes derive their initial state from the class populations
817 // and reference stations (`default_init_state`), and the one declared
818 // marking it carries is a Place's token count -- which is the case that
819 // MATTERS, because an SPN with no tokens is a dead net and its answer is
820 // "nothing ever fires", not an approximation of the modelled one.
821 //
822 // A preload that places CLOSED jobs anywhere but their reference station is
823 // therefore refused rather than dropped: dropping it solves a model whose
824 // population sits somewhere else, which no metric would flag.
825 const std::vector<const Element*> pre = sim->child_tags("preload");
826 if (!pre.empty()) {
827 for (const Element* sp : pre[0]->child_tags("stationPopulations")) {
828 const std::map<std::string, std::size_t>::const_iterator it =
829 pos_by_name.find(sp->attr("stationName"));
830 if (it == pos_by_name.end() || !nidx[it->second]) continue;
831 const std::size_t pos = it->second;
832 const bool is_place = secclass(pos, 0) == "Storage";
833 std::vector<T> tokens(K, num_traits<T>::from_int(0));
834 for (const Element* cp : sp->child_tags("classPopulation")) {
835 const std::string cn = cp->attr("refClass");
836 const double pop = to_num(cp->attr("population"));
837 for (std::size_t r = 0; r < K; ++r) {
838 if (xclasses[r]->attr("name") != cn) continue;
839 tokens[r] = num_traits<T>::from_double(pop);
840 if (is_place || pop == 0.0) continue;
841 // A closed class whose preload sits at its own reference
842 // station is exactly what the default derivation produces,
843 // so it is not a divergence and needs no marking.
844 const std::string type = xclasses[r]->attr("type");
845 if (type == "closed" && xclasses[r]->attr("referenceSource") == orig_name[pos])
846 continue;
847 throw UnsupportedError(
848 "read_jsim: the <preload> block places " + std::to_string(pop) +
849 " job(s) of class '" + cn + "' at '" + orig_name[pos] +
850 "', which is not that class's reference station. This port derives a "
851 "queueing station's initial state from the class populations and "
852 "reference stations and has no initFromMarginal to override it, so the "
853 "marking is refused rather than dropped -- dropping it would solve a "
854 "model whose population starts somewhere else");
855 }
856 }
857 if (is_place) net.set_initial_marking(nidx[pos], tokens);
858 }
859 }
860 return net;
861}
862
863/**
864 * Write a piped XML model document to a temporary file and return its path.
865 *
866 * Shared by the JMT and PNML paths: both readers walk a DOM built by
867 * `xml::parse_file`, which takes a path, and a document arriving on stdin has
868 * none. Staging it is the whole of the difference; the caller removes the file.
869 * The messages name no reader, since either may be the caller.
870 */
871inline std::string jsim_stage_stdin(const std::string& text) {
872 if (text.empty())
873 throw InputError("stdin carried no model document");
874 const char* tmpdir = std::getenv("TMPDIR");
875 std::string path = std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") + "/line-cli-jsim-XXXXXX";
876 std::vector<char> buf(path.begin(), path.end());
877 buf.push_back('\0');
878 const int fd = ::mkstemp(&buf[0]);
879 if (fd < 0) throw InputError("cannot create a temporary file for the piped model");
880 ::close(fd);
881 path.assign(&buf[0]);
882 std::ofstream out(path.c_str());
883 out << text;
884 out.close();
885 return path;
886}
887
888} // namespace io
889} // namespace line
890
891#endif // LINE_IO_JSIM_READER_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A queueing network under construction.
void set_drop_rule(std::size_t node, std::size_t cls, DropStrategy rule)
station.setDropRule(class, rule).
void set_class_capacity(std::size_t node, std::size_t cls, double k)
station.setChainCapacity(class, k).
std::size_t add_source(const std::string &nm)
The external arrival station.
std::size_t add_fork(const std::string &nm, double tasks_per_link=1.0)
A Fork node.
std::size_t add_open_class(const std::string &nm, int prio=0)
An open class.
std::size_t add_delay(const std::string &nm)
An infinite-server station (a Delay, MATLAB's Delay / DelayStation).
std::size_t add_router(const std::string &nm)
A stateless routing node.
void set_initial_marking(std::size_t node, const std::vector< T > &tokens)
Place.setState(marking): the initial token count of the place, per class.
void set_number_of_servers(std::size_t node, double n)
queue.setNumberOfServers(n).
std::size_t add_queue(const std::string &nm, SchedStrategy sched=SchedStrategy::FCFS)
A queueing station.
void set_class_switch_matrix(std::size_t node, const Matrix< T > &C)
Install the switching matrix of a ClassSwitch created without one.
void set_routing(std::size_t node, std::size_t cls, RoutingStrategy rs)
node.setRouting(class, strategy).
std::size_t add_closed_class(const std::string &nm, double njobs, std::size_t refstat_node, int prio=0)
A closed class of the given population, referencing a station node.
void set_sched_param(std::size_t node, std::size_t cls, const T &weight)
The DPS / GPS weight of a class at a station.
std::size_t add_class_switch(const std::string &nm, const Matrix< T > &C)
A ClassSwitch node carrying the (nclasses x nclasses) switching matrix.
std::size_t add_sink(const std::string &nm)
The external departure node.
std::size_t add_place(const std::string &nm)
A Place: an SPN token container.
void set_routing_weights(std::size_t node, std::size_t cls, const std::map< std::size_t, double > &weights)
The per-destination weights of a WRROBIN dispatcher, per (node, class).
void set_capacity(std::size_t node, double k)
station.setCapacity(k), the K of Kendall's notation.
void link(const RoutingMatrix< T > &Pm)
model.link(P): install the routing.
void set_routing_param(std::size_t node, std::size_t cls, int d)
The d of a power-of-d (SQ) dispatcher, per (node, class).
void set_service(std::size_t node, std::size_t cls, const Distrib< T > &d)
station.setService(class, dist).
std::size_t add_join(const std::string &nm, std::size_t fork_node)
A Join node, which IS a station: it serves at an infinite rate, and the synchronisation delay is supp...
std::size_t add_transition(const std::string &nm, const TransitionParam< T > &par)
A Transition: the firing rules of an SPN, as Transition in MATLAB.
void set_arrival(std::size_t node, std::size_t cls, const Distrib< T > &d)
source.setArrival(class, dist): the same table, at the Source.
The routing matrix a model script fills in, MATLAB's P cell array.
void set(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense matrix and non-owning view.
std::string jsim_stage_stdin(const std::string &text)
Write a piped XML model document to a temporary file and return its path.
qn::Network< T > read_jsim(const std::string &path, const std::string &name=std::string())
Read a JSIM document into a Network.
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:363
@ TIMED
fires after its firing distribution elapses
Definition lang_types.h:362
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:389
std::unique_ptr< Element > parse_file(const std::string &path)
Read and parse a file.
Definition xml.h:290
The Network constructor API: Queue, Delay, Source, Sink, Router, ClassSwitch, Cache,...
Number-type abstraction for the templated API port.
static Distrib replayer(const std::vector< T > &samples)
Replayer / Trace: the samples, with their empirical first two moments.
static Distrib exp_rate(const T &r)
Definition lang_types.h:814
static Distrib phase_type(const std::vector< T > &alpha, const Matrix< T > &A, bool acyclic)
PH / APH given by (alpha, A): D0 = A and D1 = (-A e) alpha.
static Distrib weibull(const T &scale, const T &shape)
static Distrib disabled_dist()
Definition lang_types.h:857
static Distrib pareto(const T &shape, const T &scale)
Pareto(shape, scale), with the MATLAB parameter order (alpha, k).
static Distrib gamma_dist(const T &shape, const T &scale)
Gamma(shape, scale), Weibull(scale, shape) and Lognormal(mu, sigma).
static Distrib map_dist(const Matrix< T > &D0, const Matrix< T > &D1, ProcessType tag)
A MAP given by its two matrices; the moments are those of its stationary phase.
static Distrib det(const T &m)
Definition lang_types.h:858
static Distrib uniform(const T &a, const T &b)
Uniform(a, b).
static Distrib lognormal(const T &logmean, const T &logsigma)
static Distrib hyperexp(const T &p, const T &lambda1, const T &lambda2)
Definition lang_types.h:956
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:846
static Distrib erlang(const T &phase_rate, std::size_t r)
Erlang(alpha, r): r phases of rate alpha, as MATLAB's Erlang(phaseRate, nphases).
Definition lang_types.h:873
std::size_t phases() const
The order of the representation, MATLAB's sn.phases.
static Distrib coxian(const std::vector< T > &mu, const std::vector< T > &phi)
Coxian(mu, phi): phase i completes with probability phi(i) and otherwise moves to phase i+1.
Definition lang_types.h:987
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
std::vector< double > firingprio
firing priority per mode
std::vector< lang::TimingStrategy > timing
immediate or timed
std::vector< std::string > modenames
std::vector< lang::Distrib< T > > firingproc
firing distribution per mode
std::vector< double > nmodeservers
servers per mode, may be infinite
std::vector< T > fireweight
weight among simultaneously enabled modes
std::vector< Matrix< T > > firing
firing[m](p,r): class-r tokens mode m moves to/from place p when it fires.
std::vector< Matrix< T > > enabling
enabling[m](p,r): class-r tokens of place p (0-based node) mode m needs.
std::vector< std::function< T(const std::vector< T > &)> > firingdep
Marking-dependent firing-rate multiplier g_m(marking); an empty entry is the unit multiplier.
std::vector< Matrix< T > > inhibiting
inhibiting[m](p,r): class-r tokens of p that BLOCK mode m (Inf = never).
std::vector< std::size_t > firingphases
phase count per mode, 0 when non-Markovian
A minimal XML DOM: read for the .lqnx interchange format, write for the JMT .jsimg and ....