LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pnml.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_PNML_H
6#define LINE_IO_PNML_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * PNML (ISO/IEC 15909-2) place/transition nets, read and written.
12 *
13 * Port of matlab/src/io/pnml_save.m and pnml_load.m,
14 * jar/src/main/java/jline/io/PnmlIO.java and
15 * python/line_solver/io/pnml_io.py. The grammar written is
16 * http://www.pnml.org/version-2009/grammar/ptnet, so that a LINE net can be
17 * read by the tools built around that corpus (GreatSPN, TINA, the Model
18 * Checking Contest harnesses) and a net from that corpus can be analysed here.
19 *
20 * THE P/T GRAMMAR IS UNCOLOURED, so what it can carry is narrower than what
21 * LINE can express, and the difference is REFUSED rather than approximated:
22 * more than one job class, an open class or a Source/Sink, a queueing place, a
23 * firing-rate dependence, and any distribution outside the scalar-parameter
24 * families listed in dist_param_names() below.
25 *
26 * TIMING RIDES IN A TOOLSPECIFIC BLOCK, which is where the grammar puts what it
27 * does not define. Each LINE MODE becomes one PNML transition, so that the arcs
28 * of a mode are the arcs of a transition as the grammar requires; the block
29 * records which LINE transition and mode the PNML transition came from, so the
30 * reader regroups the modes the writer split. A reader that ignores the block
31 * still sees a correct untimed P/T net, and a P/T net with no such block is read
32 * with every transition TIMED and EXPONENTIAL AT RATE 1, the convention of the
33 * stochastic Petri net literature and GreatSPN's own default.
34 *
35 * PARAMETER NAMES ARE MATLAB'S, not this port's argument names. `Distrib::params`
36 * is documented to hold the constructor arguments in MATLAB getParam order, so
37 * the names below are attached positionally to that order; they are what makes a
38 * file written by any of the four codebases readable by the other three.
39 */
40
41#include <algorithm>
42#include <cmath>
43#include <cstddef>
44#include <cstdio>
45#include <fstream>
46#include <limits>
47#include <map>
48#include <string>
49#include <vector>
50
54#include "line/num/number.h"
55#include "line/util/error.h"
56#include "line/util/matrix.h"
57#include "line/util/xml.h"
58
59namespace line {
60namespace io {
61
62namespace pnml_detail {
63
64/**
65 * The parameter names of the scalar-parameter families, in `Distrib::params`
66 * order. An empty vector means the family has no PNML form and the writer
67 * refuses it by name.
68 */
69inline std::vector<std::string> dist_param_names(lang::ProcessType t) {
70 std::vector<std::string> v;
71 switch (t) {
72 case lang::ProcessType::EXP: v.push_back("lambda"); break;
73 case lang::ProcessType::DET: v.push_back("t"); break;
75 v.push_back("alpha");
76 v.push_back("r");
77 break;
79 v.push_back("p");
80 v.push_back("lambda1");
81 v.push_back("lambda2");
82 break;
84 v.push_back("min");
85 v.push_back("max");
86 break;
88 v.push_back("alpha");
89 v.push_back("beta");
90 break;
92 v.push_back("alpha");
93 v.push_back("k");
94 break;
96 v.push_back("alpha");
97 v.push_back("r");
98 break;
100 v.push_back("mu");
101 v.push_back("sigma");
102 break;
103 default: break;
104 }
105 return v;
106}
107
108/** Rebuild a distribution from its name and named parameters. */
109template <class T>
110lang::Distrib<T> dist_from(const std::string& name, const std::map<std::string, double>& p) {
111 typedef lang::Distrib<T> D;
112 struct Need {
113 static double get(const std::map<std::string, double>& m, const std::string& key,
114 const std::string& who) {
115 std::map<std::string, double>::const_iterator it = m.find(key);
116 if (it == m.end())
117 throw InputError(ErrorKind{"Format"}, "pnml: distribution " + who + " is missing the parameter \"" + key + "\"");
118 return it->second;
119 }
120 };
121 if (name == "Immediate") return D::immediate();
122 if (name == "Disabled") return D::disabled_dist();
123 if (name == "Exp") return D::exp_rate(num_traits<T>::from_double(Need::get(p, "lambda", name)));
124 if (name == "Det") return D::det(num_traits<T>::from_double(Need::get(p, "t", name)));
125 if (name == "Erlang")
126 return D::erlang(num_traits<T>::from_double(Need::get(p, "alpha", name)),
127 static_cast<std::size_t>(std::llround(Need::get(p, "r", name))));
128 if (name == "HyperExp")
129 return D::hyperexp(num_traits<T>::from_double(Need::get(p, "p", name)),
130 num_traits<T>::from_double(Need::get(p, "lambda1", name)),
131 num_traits<T>::from_double(Need::get(p, "lambda2", name)));
132 if (name == "Uniform")
133 return D::uniform(num_traits<T>::from_double(Need::get(p, "min", name)),
134 num_traits<T>::from_double(Need::get(p, "max", name)));
135 if (name == "Gamma")
136 return D::gamma_dist(num_traits<T>::from_double(Need::get(p, "alpha", name)),
137 num_traits<T>::from_double(Need::get(p, "beta", name)));
138 if (name == "Pareto")
139 return D::pareto(num_traits<T>::from_double(Need::get(p, "alpha", name)),
140 num_traits<T>::from_double(Need::get(p, "k", name)));
141 if (name == "Weibull")
142 // stored (alpha = scale, r = shape); the factory takes (scale, shape),
143 // so naming both is what keeps a round trip from transposing them.
144 return D::weibull(num_traits<T>::from_double(Need::get(p, "alpha", name)),
145 num_traits<T>::from_double(Need::get(p, "r", name)));
146 if (name == "Lognormal")
147 return D::lognormal(num_traits<T>::from_double(Need::get(p, "mu", name)),
148 num_traits<T>::from_double(Need::get(p, "sigma", name)));
149 throw InputError(ErrorKind{"Format"}, "pnml: the timing block names distribution \"" + name +
150 "\", which the reader does not construct. The families it reads are Exp, Det, "
151 "Erlang, HyperExp, Uniform, Gamma, Pareto, Weibull, Lognormal, Immediate and "
152 "Disabled, which are the ones the writer writes");
153}
154
155/** The five XML entities a name or an attribute value may need. */
156inline std::string escape(const std::string& in) {
157 std::string out;
158 out.reserve(in.size());
159 for (std::size_t i = 0; i < in.size(); ++i) {
160 switch (in[i]) {
161 case '&': out += "&amp;"; break;
162 case '<': out += "&lt;"; break;
163 case '>': out += "&gt;"; break;
164 case '"': out += "&quot;"; break;
165 default: out += in[i]; break;
166 }
167 }
168 return out;
169}
170
171/**
172 * Shortest form that reads back exactly, so an integral count does not acquire a
173 * decimal point and an infinite server count keeps the spelling the reader
174 * expects.
175 */
176inline std::string num_text(double v) {
177 if (std::isinf(v)) return v > 0 ? "Inf" : "-Inf";
178 if (v == std::floor(v) && std::fabs(v) < 9.007199254740992e15) {
179 char buf[32];
180 std::snprintf(buf, sizeof buf, "%lld", static_cast<long long>(v));
181 return std::string(buf);
182 }
183 char buf[64];
184 std::snprintf(buf, sizeof buf, "%.17g", v);
185 return std::string(buf);
186}
187
188inline double text_number(const xml::Element& e, const std::string& label, double fallback) {
189 const std::vector<const xml::Element*> labels = e.child_tags(label);
190 if (labels.empty()) return fallback;
191 std::string txt;
192 const std::vector<const xml::Element*> texts = labels[0]->child_tags("text");
193 txt = texts.empty() ? labels[0]->text : texts[0]->text;
194 // trim
195 std::size_t a = txt.find_first_not_of(" \t\r\n");
196 std::size_t b = txt.find_last_not_of(" \t\r\n");
197 if (a == std::string::npos) return fallback;
198 txt = txt.substr(a, b - a + 1);
199 // Some tools write the marking as "3" and some as "1`3" (a coloured multiset
200 // of one colour); the plain integer is the one the grammar defines.
201 const std::size_t tick = txt.rfind('`');
202 if (tick != std::string::npos) txt = txt.substr(tick + 1);
203 try {
204 return std::stod(txt);
205 } catch (const std::exception&) {
206 throw InputError(ErrorKind{"Format"}, "pnml: label <" + label + "> holds \"" + txt + "\", which is not a number");
207 }
208}
209
210inline double attr_number(const xml::Element& e, const std::string& key, double fallback) {
211 std::string txt = e.attr(key);
212 const std::size_t a = txt.find_first_not_of(" \t\r\n");
213 if (a == std::string::npos) return fallback;
214 const std::size_t b = txt.find_last_not_of(" \t\r\n");
215 txt = txt.substr(a, b - a + 1);
216 if (txt == "Inf" || txt == "inf") return std::numeric_limits<double>::infinity();
217 if (txt == "-Inf" || txt == "-inf") return -std::numeric_limits<double>::infinity();
218 try {
219 return std::stod(txt);
220 } catch (const std::exception&) {
221 throw InputError(ErrorKind{"Format"}, "pnml: attribute " + key + " holds \"" + txt + "\", which is not a number");
222 }
223}
224
225inline std::string element_id(const xml::Element& e) {
226 std::string id = e.attr("id");
227 if (id.empty()) {
228 const std::vector<const xml::Element*> names = e.child_tags("name");
229 if (!names.empty()) {
230 const std::vector<const xml::Element*> texts = names[0]->child_tags("text");
231 id = texts.empty() ? names[0]->text : texts[0]->text;
232 }
233 }
234 if (id.empty()) throw InputError(ErrorKind{"Format"}, "pnml: a place or transition carries neither an id nor a name");
235 return id;
236}
237
238inline bool is_inhibitor(const xml::Element& arc) {
239 const std::vector<const xml::Element*> types = arc.child_tags("type");
240 for (std::size_t i = 0; i < types.size(); ++i) {
241 std::string v = types[i]->attr("value");
242 std::transform(v.begin(), v.end(), v.begin(), ::tolower);
243 if (v == "inhibitor") return true;
244 }
245 std::string v = arc.attr("type");
246 std::transform(v.begin(), v.end(), v.begin(), ::tolower);
247 return v == "inhibitor";
248}
249
250/** The <mode> of a transition's LINE toolspecific block, or null. */
251inline const xml::Element* line_toolspecific(const xml::Element& tr) {
252 const std::vector<const xml::Element*> blocks = tr.child_tags("toolspecific");
253 for (std::size_t i = 0; i < blocks.size(); ++i) {
254 std::string tool = blocks[i]->attr("tool");
255 std::transform(tool.begin(), tool.end(), tool.begin(), ::toupper);
256 if (tool != "LINE") continue;
257 const std::vector<const xml::Element*> modes = blocks[i]->child_tags("mode");
258 if (!modes.empty()) return modes[0];
259 }
260 return 0;
261}
262
263} // namespace pnml_detail
264
265/**
266 * Write the Petri net of a refreshed NetworkStruct to a PNML place/transition
267 * file.
268 *
269 * @param sn struct of a net holding only places and transitions, one closed class
270 * @param path output path
271 */
272template <class T>
273void pnml_save(const qn::NetworkStruct<T>& sn, const std::string& path) {
274 const double inf = std::numeric_limits<double>::infinity();
275 if (sn.classes.size() != 1)
276 throw InputError(ErrorKind{"Format"}, "pnml_save: the PNML place/transition grammar is UNCOLOURED, so it cannot carry a "
277 "net with " + std::to_string(sn.classes.size()) +
278 " job classes: its tokens are indistinguishable. Export a single-class net, or "
279 "use the JSON writer for the full model");
280 if (sn.classes[0].type != qn::JobClassType::CLOSED)
281 throw InputError(ErrorKind{"Format"}, "pnml_save: the PNML place/transition grammar has no unbounded token source, so an "
282 "open class cannot be represented. Close the class, or use the JSON writer");
283
284 std::vector<std::size_t> places, transitions; // 1-based node indices
285 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
286 const lang::NodeType nt = sn.nodes[i].nodetype;
287 if (nt == lang::NodeType::Place) {
288 places.push_back(i + 1);
289 } else if (nt == lang::NodeType::Transition) {
290 transitions.push_back(i + 1);
291 } else {
292 throw InputError(ErrorKind{"Format"}, "pnml_save: node " + sn.nodes[i].name + " is a " +
294 ". A PNML place/transition net holds only places and transitions; a Source, a "
295 "Sink or a queueing station has no counterpart in the grammar");
296 }
297 }
298 if (places.empty())
299 throw InputError(ErrorKind{"Format"}, "pnml_save: the model holds no Place, so there is no Petri net to write");
300
301 // Token count of each place. The recorded marking is authoritative; a place
302 // with none holds the class population when it is the reference station,
303 // which is the default LINE itself applies.
304 std::vector<long long> marking(places.size(), 0);
305 for (std::size_t p = 0; p < places.size(); ++p) {
306 const typename std::map<std::size_t, std::vector<T> >::const_iterator it =
307 sn.initmarking.find(places[p]);
308 if (it != sn.initmarking.end() && !it->second.empty()) {
309 marking[p] = std::llround(num_traits<T>::to_double(it->second[0]));
310 } else if (sn.classes[0].refstat != 0 &&
311 sn.station_to_node[sn.classes[0].refstat - 1] == places[p]) {
312 marking[p] = std::llround(sn.classes[0].population);
313 }
314 }
315
316 // THE DOCUMENT IS BUILT AS TEXT, not through xml::Element, and the reason is
317 // byte identity: the shared serializer puts every element on its own line,
318 // while MATLAB, the JAR and python all write `<name><text>x</text></name>`
319 // inline. Emitting the same bytes as the other three is what lets a parity
320 // check diff the FILES rather than reparse them. Reading still goes through
321 // xml::parse_file, where the layout does not matter.
322 const std::string netname = sn.name.empty() ? std::string("net") : sn.name;
323 std::vector<std::string> sb;
324 sb.push_back("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
325 sb.push_back("<pnml xmlns=\"http://www.pnml.org/version-2009/grammar/pnml\">");
326 sb.push_back(" <net id=\"" + pnml_detail::escape(netname) +
327 "\" type=\"http://www.pnml.org/version-2009/grammar/ptnet\">");
328 sb.push_back(" <name><text>" + pnml_detail::escape(netname) + "</text></name>");
329 // the token class's name, which the P/T grammar cannot carry; read back by pnml_load (else "Class1")
330 sb.push_back(" <toolspecific tool=\"LINE\" version=\"3.0\"><class name=\"" +
331 pnml_detail::escape(sn.classes[0].name) + "\"/></toolspecific>");
332 sb.push_back(" <page id=\"page0\">");
333
334 for (std::size_t p = 0; p < places.size(); ++p) {
335 const std::string nm = pnml_detail::escape(sn.nodes[places[p] - 1].name);
336 sb.push_back(" <place id=\"" + nm + "\">");
337 sb.push_back(" <name><text>" + nm + "</text></name>");
338 sb.push_back(" <initialMarking><text>" + std::to_string(marking[p]) +
339 "</text></initialMarking>");
340 sb.push_back(" </place>");
341 }
342
343 // Arcs are collected and appended after every transition, so the document
344 // reads places, transitions, arcs, as the other three codebases write it.
345 struct ArcRec {
346 std::string source, target;
347 long long weight;
348 bool inhibitor;
349 };
350 std::vector<ArcRec> arcs;
351
352 for (std::size_t t = 0; t < transitions.size(); ++t) {
353 const std::size_t nd = transitions[t];
354 const typename std::map<std::size_t, qn::TransitionParam<T> >::const_iterator it =
355 sn.transparam.find(nd);
356 if (it == sn.transparam.end())
357 throw InputError(ErrorKind{"Format"}, "pnml_save: transition " + sn.nodes[nd - 1].name +
358 " declares no mode, so it has no firing behaviour to write");
359 const qn::TransitionParam<T>& tp = it->second;
360 for (std::size_t m = 0; m < tp.nmodes; ++m) {
361 if (m < tp.firingdep.size() && tp.firingdep[m])
362 throw InputError(ErrorKind{"Format"}, "pnml_save: transition " + sn.nodes[nd - 1].name + " mode " +
363 std::to_string(m + 1) +
364 " declares a marking-dependent firing rate, which no PNML element can carry");
365 const std::string mname =
366 m < tp.modenames.size() && !tp.modenames[m].empty() ? tp.modenames[m]
367 : "Mode" + std::to_string(m + 1);
368 const std::string tid =
369 tp.nmodes == 1 ? sn.nodes[nd - 1].name : sn.nodes[nd - 1].name + "." + mname;
370 const bool immediate =
371 m < tp.timing.size() && tp.timing[m] == lang::TimingStrategy::IMMEDIATE;
372
373 const std::string etid = pnml_detail::escape(tid);
374 sb.push_back(" <transition id=\"" + etid + "\">");
375 sb.push_back(" <name><text>" + etid + "</text></name>");
376 sb.push_back(" <toolspecific tool=\"LINE\" version=\"3.0\">");
377 sb.push_back(
378 " <mode transition=\"" + pnml_detail::escape(sn.nodes[nd - 1].name) +
379 "\" name=\"" + pnml_detail::escape(mname) + "\" timing=\"" +
380 (immediate ? "immediate" : "timed") + "\" servers=\"" +
381 pnml_detail::num_text(m < tp.nmodeservers.size() ? tp.nmodeservers[m] : 1.0) +
382 "\" priority=\"" +
383 pnml_detail::num_text(m < tp.firingprio.size() ? tp.firingprio[m] : 1.0) +
384 "\" weight=\"" +
385 pnml_detail::num_text(m < tp.fireweight.size()
386 ? num_traits<T>::to_double(tp.fireweight[m])
387 : 1.0) +
388 "\">");
389 if (!immediate) {
390 if (m >= tp.firingproc.size() || tp.firingproc[m].disabled)
391 throw InputError(ErrorKind{"Format"}, "pnml_save: transition " + sn.nodes[nd - 1].name + " mode " +
392 std::to_string(m + 1) + " is timed but carries no distribution");
393 const lang::Distrib<T>& d = tp.firingproc[m];
394 const std::string dname = lang::process_to_text(d.type);
395 const std::vector<std::string> pnames = pnml_detail::dist_param_names(d.type);
396 if (pnames.empty())
397 throw InputError(ErrorKind{"Format"},
398 "pnml_save: transition " + sn.nodes[nd - 1].name + " mode " +
399 std::to_string(m + 1) + " holds a " + dname +
400 ", whose parameters are not scalars. The PNML timing block carries scalar "
401 "parameters only; a matrix-parameterized law (PH, APH, MAP, MMPP2, ME, RAP) has "
402 "no PNML representation and writing its mean rate instead would read back as a "
403 "different model");
404 if (d.params.size() < pnames.size())
405 throw InputError(ErrorKind{"Format"}, "pnml_save: transition " + sn.nodes[nd - 1].name + " mode " +
406 std::to_string(m + 1) + " holds a " + dname + " with " +
407 std::to_string(d.params.size()) + " parameters, expected " +
408 std::to_string(pnames.size()));
409 sb.push_back(" <distribution name=\"" + pnml_detail::escape(dname) +
410 "\">");
411 for (std::size_t k = 0; k < pnames.size(); ++k) {
412 sb.push_back(" <parameter name=\"" +
413 pnml_detail::escape(pnames[k]) + "\" value=\"" +
414 pnml_detail::num_text(num_traits<T>::to_double(d.params[k])) +
415 "\"/>");
416 }
417 sb.push_back(" </distribution>");
418 }
419 sb.push_back(" </mode>");
420 sb.push_back(" </toolspecific>");
421 sb.push_back(" </transition>");
422
423 for (std::size_t p = 0; p < places.size(); ++p) {
424 const std::size_t row = places[p] - 1;
425 const double w = m < tp.enabling.size() && row < tp.enabling[m].rows()
426 ? num_traits<T>::to_double(tp.enabling[m](row, 0))
427 : 0.0;
428 if (w > 0) {
429 ArcRec a;
430 a.source = sn.nodes[places[p] - 1].name;
431 a.target = tid;
432 a.weight = std::llround(w);
433 a.inhibitor = false;
434 arcs.push_back(a);
435 }
436 const double h = m < tp.inhibiting.size() && row < tp.inhibiting[m].rows()
437 ? num_traits<T>::to_double(tp.inhibiting[m](row, 0))
438 : inf;
439 if (std::isfinite(h)) {
440 ArcRec a;
441 a.source = sn.nodes[places[p] - 1].name;
442 a.target = tid;
443 a.weight = std::llround(h);
444 a.inhibitor = true;
445 arcs.push_back(a);
446 }
447 const double f = m < tp.firing.size() && row < tp.firing[m].rows()
448 ? num_traits<T>::to_double(tp.firing[m](row, 0))
449 : 0.0;
450 if (f > 0) {
451 ArcRec a;
452 a.source = tid;
453 a.target = sn.nodes[places[p] - 1].name;
454 a.weight = std::llround(f);
455 a.inhibitor = false;
456 arcs.push_back(a);
457 }
458 }
459 }
460 }
461
462 for (std::size_t a = 0; a < arcs.size(); ++a) {
463 sb.push_back(" <arc id=\"a" + std::to_string(a + 1) + "\" source=\"" +
464 pnml_detail::escape(arcs[a].source) + "\" target=\"" +
465 pnml_detail::escape(arcs[a].target) + "\">");
466 if (arcs[a].inhibitor) {
467 // An inhibitor arc is not in the P/T grammar itself; <type
468 // value="inhibitor"/> is the extension GreatSPN, TINA and PIPE all
469 // read, so it is the one written here.
470 sb.push_back(" <type value=\"inhibitor\"/>");
471 }
472 sb.push_back(" <inscription><text>" + std::to_string(arcs[a].weight) +
473 "</text></inscription>");
474 sb.push_back(" </arc>");
475 }
476
477 sb.push_back(" </page>");
478 sb.push_back(" </net>");
479 sb.push_back("</pnml>");
480
481 std::ofstream out(path.c_str());
482 if (!out) throw InputError(ErrorKind{"File"}, "pnml_save: cannot open " + path + " for writing");
483 for (std::size_t i = 0; i < sb.size(); ++i) out << sb[i] << "\n";
484}
485
486/**
487 * Read one net of a PNML place/transition document into a Network.
488 *
489 * @param net_id id of the net to read; empty selects the first
490 */
491template <class T>
492qn::Network<T> pnml_load(const std::string& path, const std::string& net_id = std::string()) {
493 const double inf = std::numeric_limits<double>::infinity();
494 std::unique_ptr<xml::Element> root = xml::parse_file(path);
495 const std::vector<const xml::Element*> nets = root->child_tags("net");
496 if (nets.empty()) throw InputError(ErrorKind{"Format"}, "pnml_load: " + path + " holds no <net> element");
497 const xml::Element* net = 0;
498 for (std::size_t i = 0; i < nets.size(); ++i)
499 if (net_id.empty() || nets[i]->attr("id") == net_id) {
500 net = nets[i];
501 break;
502 }
503 if (net == 0) throw InputError(ErrorKind{"Format"}, "pnml_load: " + path + " holds no net with id \"" + net_id + "\"");
504 const std::string nettype = net->attr("type");
505 if (!nettype.empty() && nettype.find("ptnet") == std::string::npos)
506 throw UnsupportedError(ErrorKind{"Import"}, "pnml_load: net \"" + net->attr("id") + "\" declares type " + nettype +
507 ". Only the place/transition grammar "
508 "(http://www.pnml.org/version-2009/grammar/ptnet) is read: a coloured or a "
509 "symmetric net carries token colours that a single-class LINE net cannot hold");
510
511 // Places, transitions and arcs may sit directly under <net> or under any
512 // <page>; the grammar allows both and tools differ, so the whole subtree is
513 // searched rather than one level of it.
514 const std::vector<const xml::Element*> place_elems = net->by_tag("place");
515 const std::vector<const xml::Element*> trans_elems = net->by_tag("transition");
516 const std::vector<const xml::Element*> arc_elems = net->by_tag("arc");
517 if (place_elems.empty())
518 throw InputError(ErrorKind{"Format"}, "pnml_load: net \"" + net->attr("id") + "\" holds no place");
519
520 std::vector<std::string> place_names;
521 std::vector<long long> place_marking;
522 long long total = 0;
523 for (std::size_t i = 0; i < place_elems.size(); ++i) {
524 place_names.push_back(pnml_detail::element_id(*place_elems[i]));
525 const long long mk =
526 std::llround(pnml_detail::text_number(*place_elems[i], "initialMarking", 0.0));
527 place_marking.push_back(mk);
528 total += mk;
529 }
530 if (total == 0)
531 throw InputError(ErrorKind{"Format"}, "pnml_load: the initial marking of this net is empty. A LINE closed class needs "
532 "tokens to hold, and a net with none has no reachable behaviour to analyse");
533
534 // Each PNML transition is one LINE MODE; the toolspecific block says which
535 // LINE transition it belongs to, so the modes the writer split regroup here.
536 std::vector<std::string> trans_ids, mode_owner, mode_name;
537 std::vector<lang::TimingStrategy> mode_timing;
538 std::vector<double> mode_servers, mode_prio, mode_weight;
539 std::vector<lang::Distrib<T> > mode_dist;
540 for (std::size_t i = 0; i < trans_elems.size(); ++i) {
541 const std::string id = pnml_detail::element_id(*trans_elems[i]);
542 trans_ids.push_back(id);
543 std::string owner = id, name = "Mode1";
545 double servers = 1.0, prio = 1.0, weight = 1.0;
547 const xml::Element* spec = pnml_detail::line_toolspecific(*trans_elems[i]);
548 if (spec != 0) {
549 if (!spec->attr("transition").empty()) owner = spec->attr("transition");
550 if (!spec->attr("name").empty()) name = spec->attr("name");
551 std::string tm = spec->attr("timing");
552 std::transform(tm.begin(), tm.end(), tm.begin(), ::tolower);
553 if (tm == "immediate") timing = lang::TimingStrategy::IMMEDIATE;
554 servers = pnml_detail::attr_number(*spec, "servers", 1.0);
555 prio = pnml_detail::attr_number(*spec, "priority", 1.0);
556 weight = pnml_detail::attr_number(*spec, "weight", 1.0);
557 if (timing != lang::TimingStrategy::IMMEDIATE) {
558 const std::vector<const xml::Element*> ds = spec->child_tags("distribution");
559 if (!ds.empty()) {
560 std::map<std::string, double> params;
561 const std::vector<const xml::Element*> ps = ds[0]->child_tags("parameter");
562 for (std::size_t k = 0; k < ps.size(); ++k)
563 params[ps[k]->attr("name")] =
564 pnml_detail::attr_number(*ps[k], "value",
565 std::numeric_limits<double>::quiet_NaN());
566 dist = pnml_detail::dist_from<T>(ds[0]->attr("name"), params);
567 }
568 } else {
570 }
571 }
572 mode_owner.push_back(owner);
573 mode_name.push_back(name);
574 mode_timing.push_back(timing);
575 mode_servers.push_back(servers);
576 mode_prio.push_back(prio);
577 mode_weight.push_back(weight);
578 mode_dist.push_back(dist);
579 }
580
581 std::vector<std::string> owner_names;
582 std::vector<std::size_t> owner_of(trans_ids.size(), 0);
583 for (std::size_t i = 0; i < trans_ids.size(); ++i) {
584 std::size_t k = owner_names.size();
585 for (std::size_t j = 0; j < owner_names.size(); ++j)
586 if (owner_names[j] == mode_owner[i]) {
587 k = j;
588 break;
589 }
590 if (k == owner_names.size()) owner_names.push_back(mode_owner[i]);
591 owner_of[i] = k;
592 }
593
594 std::string name = net_id.empty() ? net->attr("id") : net_id;
595 if (name.empty()) name = "pnml";
596 qn::Network<T> model(name);
597
598 std::vector<std::size_t> place_node;
599 for (std::size_t i = 0; i < place_names.size(); ++i)
600 place_node.push_back(model.add_place(place_names[i]));
601
602 // The reference station is the first place holding tokens, so the class
603 // starts where the marking says it does.
604 std::size_t ref = 0;
605 for (std::size_t i = 0; i < place_marking.size(); ++i)
606 if (place_marking[i] > 0) {
607 ref = i;
608 break;
609 }
610 // the token class's name rides in a net-level LINE toolspecific (written by pnml_save), else "Class1"
611 std::string class_name = "Class1";
612 const std::vector<const xml::Element*> net_blocks = net->child_tags("toolspecific");
613 for (std::size_t b = 0; b < net_blocks.size(); ++b) {
614 std::string tool = net_blocks[b]->attr("tool");
615 std::transform(tool.begin(), tool.end(), tool.begin(), ::toupper);
616 if (tool != "LINE") continue;
617 const std::vector<const xml::Element*> ces = net_blocks[b]->child_tags("class");
618 for (std::size_t c = 0; c < ces.size(); ++c)
619 if (!ces[c]->attr("name").empty()) class_name = ces[c]->attr("name");
620 }
621 const std::size_t cls =
622 model.add_closed_class(class_name, static_cast<double>(total), place_node[ref], 0);
623
624 // A transition is itself a node, so the arc matrices must be sized against
625 // the FINISHED node count, which is known before any of them is added.
626 const std::size_t nnodes = place_names.size() + owner_names.size();
627
628 // The arcs are read first, because a mode's matrices are built whole and
629 // handed to add_transition rather than set afterwards.
630 std::vector<qn::TransitionParam<T> > params(owner_names.size());
631 std::vector<std::size_t> mode_index(trans_ids.size(), 0);
632 for (std::size_t i = 0; i < trans_ids.size(); ++i) {
633 qn::TransitionParam<T>& tp = params[owner_of[i]];
634 mode_index[i] = tp.nmodes;
635 tp.nmodes++;
636 tp.modenames.push_back(mode_name[i]);
637 tp.timing.push_back(mode_timing[i]);
638 tp.firingproc.push_back(mode_dist[i]);
639 tp.firingphases.push_back(
640 mode_timing[i] == lang::TimingStrategy::IMMEDIATE || mode_dist[i].disabled
641 ? 0
642 : lang::dist_to_map(mode_dist[i]).order());
643 tp.nmodeservers.push_back(mode_servers[i]);
644 tp.firingprio.push_back(mode_prio[i]);
645 tp.fireweight.push_back(num_traits<T>::from_double(mode_weight[i]));
646 tp.enabling.push_back(Matrix<T>(nnodes, 1, num_traits<T>::from_int(0)));
647 tp.inhibiting.push_back(Matrix<T>(nnodes, 1, num_traits<T>::from_double(inf)));
648 tp.firing.push_back(Matrix<T>(nnodes, 1, num_traits<T>::from_int(0)));
649 tp.firingdep.push_back(std::function<T(const std::vector<T>&)>());
650 }
651
652 // Node index of a LINE transition, in the order add_transition will assign:
653 // the places come first, then one node per owner.
654 std::vector<std::size_t> trans_node(owner_names.size(), 0);
655 for (std::size_t i = 0; i < owner_names.size(); ++i)
656 trans_node[i] = place_names.size() + i + 1;
657
659 for (std::size_t a = 0; a < arc_elems.size(); ++a) {
660 const std::string src = arc_elems[a]->attr("source");
661 const std::string tgt = arc_elems[a]->attr("target");
662 const double w = pnml_detail::text_number(*arc_elems[a], "inscription", 1.0);
663 if (!(w > 0))
664 throw InputError(ErrorKind{"Format"}, "pnml_load: arc " + src + " -> " + tgt +
665 " carries a non-positive inscription");
666 std::size_t ip = place_names.size(), it = trans_ids.size();
667 for (std::size_t k = 0; k < place_names.size(); ++k)
668 if (place_names[k] == src) ip = k;
669 for (std::size_t k = 0; k < trans_ids.size(); ++k)
670 if (trans_ids[k] == tgt) it = k;
671 if (ip < place_names.size() && it < trans_ids.size()) {
672 qn::TransitionParam<T>& tp = params[owner_of[it]];
673 const std::size_t row = place_node[ip] - 1;
674 if (pnml_detail::is_inhibitor(*arc_elems[a]))
675 tp.inhibiting[mode_index[it]](row, 0) = num_traits<T>::from_double(w);
676 else
677 tp.enabling[mode_index[it]](row, 0) = num_traits<T>::from_double(w);
678 R.set(cls, cls, place_node[ip], trans_node[owner_of[it]], num_traits<T>::from_int(1));
679 continue;
680 }
681 ip = place_names.size();
682 it = trans_ids.size();
683 for (std::size_t k = 0; k < trans_ids.size(); ++k)
684 if (trans_ids[k] == src) it = k;
685 for (std::size_t k = 0; k < place_names.size(); ++k)
686 if (place_names[k] == tgt) ip = k;
687 if (ip < place_names.size() && it < trans_ids.size()) {
688 qn::TransitionParam<T>& tp = params[owner_of[it]];
689 tp.firing[mode_index[it]](place_node[ip] - 1, 0) = num_traits<T>::from_double(w);
690 R.set(cls, cls, trans_node[owner_of[it]], place_node[ip], num_traits<T>::from_int(1));
691 continue;
692 }
693 throw InputError(ErrorKind{"Format"}, "pnml_load: arc " + src + " -> " + tgt +
694 " connects two places or two transitions, which the place/transition grammar "
695 "does not allow");
696 }
697
698 for (std::size_t i = 0; i < owner_names.size(); ++i) {
699 const std::size_t nd = model.add_transition(owner_names[i], params[i]);
700 if (nd != trans_node[i])
701 throw InternalError(ErrorKind{"Invariant"}, "pnml_load: internal node numbering disagreed with the arc matrices");
702 }
703
704 model.link(R);
705 for (std::size_t i = 0; i < place_node.size(); ++i)
707 place_node[i], std::vector<T>(1, num_traits<T>::from_double(
708 static_cast<double>(place_marking[i]))));
709 return model;
710}
711
712} // namespace io
713} // namespace line
714
715#endif // LINE_IO_PNML_H
InputError(const std::string &what)
Definition error.h:105
InternalError(const std::string &what)
Definition error.h:133
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
A queueing network under construction.
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.
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.
std::size_t add_place(const std::string &nm)
A Place: an SPN token container.
void link(const RoutingMatrix< T > &Pm)
model.link(P): install the routing.
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.
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.
void pnml_save(const qn::NetworkStruct< T > &sn, const std::string &path)
Write the Petri net of a refreshed NetworkStruct to a PNML place/transition file.
Definition pnml.h:273
qn::Network< T > pnml_load(const std::string &path, const std::string &net_id=std::string())
Read one net of a PNML place/transition document into a Network.
Definition pnml.h:492
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
TimingStrategy
SPN transition timing, with the values of MATLAB TimingStrategy.
Definition lang_types.h:368
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:370
@ TIMED
fires after its firing distribution elapses
Definition lang_types.h:369
const char * node_type_to_text(NodeType t)
Name of a node kind, for diagnostics.
Definition lang_types.h:348
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
bool in(const std::string &s, std::initializer_list< const char * > set)
const std::string * get(const Element *el, const std::string &key)
el.get(key), with Python's None as a null pointer.
Definition srvn_writer.h:62
std::string name(const Element *el, const std::string &key="name")
std::vector< int > tick(int a, int q, const std::vector< int > &B, int nph)
i_tick: advance every non-owner phase by one tick; a completed job frees its server for a waiting one...
std::unique_ptr< Element > parse_file(const std::string &path)
Read and parse a file.
Definition xml.h:290
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
The Network constructor API: Queue, Delay, Source, Sink, Router, ClassSwitch, Cache,...
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static Distrib exp_rate(const T &r)
Definition lang_types.h:955
static Distrib immediate()
The point mass at zero.
Definition lang_types.h:986
The parameters of a Transition node: MATLAB sn.nodeparam{ind} for an SPN transition,...
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
std::string attr(const std::string &key) const
Attribute value, or the empty string when absent (org.w3c.dom semantics).
Definition xml.h:63
std::vector< const Element * > child_tags(const std::string &tag) const
Direct children with the given tag, in document order.
Definition xml.h:83
A minimal XML DOM: read for the .lqnx interchange format, write for the JMT .jsimg and ....