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("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("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/**
156 * Shortest form that reads back exactly, so an integral count does not acquire a
157 * decimal point and an infinite server count keeps the spelling the reader
158 * expects.
159 */
160/** The five XML entities a name or an attribute value may need. */
161inline std::string escape(const std::string& in) {
162 std::string out;
163 out.reserve(in.size());
164 for (std::size_t i = 0; i < in.size(); ++i) {
165 switch (in[i]) {
166 case '&': out += "&amp;"; break;
167 case '<': out += "&lt;"; break;
168 case '>': out += "&gt;"; break;
169 case '"': out += "&quot;"; break;
170 default: out += in[i]; break;
171 }
172 }
173 return out;
174}
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("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("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("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("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("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("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("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 sb.push_back(" <page id=\"page0\">");
330
331 for (std::size_t p = 0; p < places.size(); ++p) {
332 const std::string nm = pnml_detail::escape(sn.nodes[places[p] - 1].name);
333 sb.push_back(" <place id=\"" + nm + "\">");
334 sb.push_back(" <name><text>" + nm + "</text></name>");
335 sb.push_back(" <initialMarking><text>" + std::to_string(marking[p]) +
336 "</text></initialMarking>");
337 sb.push_back(" </place>");
338 }
339
340 // Arcs are collected and appended after every transition, so the document
341 // reads places, transitions, arcs, as the other three codebases write it.
342 struct ArcRec {
343 std::string source, target;
344 long long weight;
345 bool inhibitor;
346 };
347 std::vector<ArcRec> arcs;
348
349 for (std::size_t t = 0; t < transitions.size(); ++t) {
350 const std::size_t nd = transitions[t];
351 const typename std::map<std::size_t, qn::TransitionParam<T> >::const_iterator it =
352 sn.transparam.find(nd);
353 if (it == sn.transparam.end())
354 throw InputError("pnml_save: transition " + sn.nodes[nd - 1].name +
355 " declares no mode, so it has no firing behaviour to write");
356 const qn::TransitionParam<T>& tp = it->second;
357 for (std::size_t m = 0; m < tp.nmodes; ++m) {
358 if (m < tp.firingdep.size() && tp.firingdep[m])
359 throw InputError("pnml_save: transition " + sn.nodes[nd - 1].name + " mode " +
360 std::to_string(m + 1) +
361 " declares a marking-dependent firing rate, which no PNML element can carry");
362 const std::string mname =
363 m < tp.modenames.size() && !tp.modenames[m].empty() ? tp.modenames[m]
364 : "Mode" + std::to_string(m + 1);
365 const std::string tid =
366 tp.nmodes == 1 ? sn.nodes[nd - 1].name : sn.nodes[nd - 1].name + "." + mname;
367 const bool immediate =
368 m < tp.timing.size() && tp.timing[m] == lang::TimingStrategy::IMMEDIATE;
369
370 const std::string etid = pnml_detail::escape(tid);
371 sb.push_back(" <transition id=\"" + etid + "\">");
372 sb.push_back(" <name><text>" + etid + "</text></name>");
373 sb.push_back(" <toolspecific tool=\"LINE\" version=\"3.0\">");
374 sb.push_back(
375 " <mode transition=\"" + pnml_detail::escape(sn.nodes[nd - 1].name) +
376 "\" name=\"" + pnml_detail::escape(mname) + "\" timing=\"" +
377 (immediate ? "immediate" : "timed") + "\" servers=\"" +
378 pnml_detail::num_text(m < tp.nmodeservers.size() ? tp.nmodeservers[m] : 1.0) +
379 "\" priority=\"" +
380 pnml_detail::num_text(m < tp.firingprio.size() ? tp.firingprio[m] : 1.0) +
381 "\" weight=\"" +
382 pnml_detail::num_text(m < tp.fireweight.size()
384 : 1.0) +
385 "\">");
386 if (!immediate) {
387 if (m >= tp.firingproc.size() || tp.firingproc[m].disabled)
388 throw InputError("pnml_save: transition " + sn.nodes[nd - 1].name + " mode " +
389 std::to_string(m + 1) + " is timed but carries no distribution");
390 const lang::Distrib<T>& d = tp.firingproc[m];
391 const std::string dname = lang::process_to_text(d.type);
392 const std::vector<std::string> pnames = pnml_detail::dist_param_names(d.type);
393 if (pnames.empty())
394 throw InputError(
395 "pnml_save: transition " + sn.nodes[nd - 1].name + " mode " +
396 std::to_string(m + 1) + " holds a " + dname +
397 ", whose parameters are not scalars. The PNML timing block carries scalar "
398 "parameters only; a matrix-parameterized law (PH, APH, MAP, MMPP2, ME, RAP) has "
399 "no PNML representation and writing its mean rate instead would read back as a "
400 "different model");
401 if (d.params.size() < pnames.size())
402 throw InputError("pnml_save: transition " + sn.nodes[nd - 1].name + " mode " +
403 std::to_string(m + 1) + " holds a " + dname + " with " +
404 std::to_string(d.params.size()) + " parameters, expected " +
405 std::to_string(pnames.size()));
406 sb.push_back(" <distribution name=\"" + pnml_detail::escape(dname) +
407 "\">");
408 for (std::size_t k = 0; k < pnames.size(); ++k) {
409 sb.push_back(" <parameter name=\"" +
410 pnml_detail::escape(pnames[k]) + "\" value=\"" +
411 pnml_detail::num_text(num_traits<T>::to_double(d.params[k])) +
412 "\"/>");
413 }
414 sb.push_back(" </distribution>");
415 }
416 sb.push_back(" </mode>");
417 sb.push_back(" </toolspecific>");
418 sb.push_back(" </transition>");
419
420 for (std::size_t p = 0; p < places.size(); ++p) {
421 const std::size_t row = places[p] - 1;
422 const double w = m < tp.enabling.size() && row < tp.enabling[m].rows()
423 ? num_traits<T>::to_double(tp.enabling[m](row, 0))
424 : 0.0;
425 if (w > 0) {
426 ArcRec a;
427 a.source = sn.nodes[places[p] - 1].name;
428 a.target = tid;
429 a.weight = std::llround(w);
430 a.inhibitor = false;
431 arcs.push_back(a);
432 }
433 const double h = m < tp.inhibiting.size() && row < tp.inhibiting[m].rows()
434 ? num_traits<T>::to_double(tp.inhibiting[m](row, 0))
435 : inf;
436 if (std::isfinite(h)) {
437 ArcRec a;
438 a.source = sn.nodes[places[p] - 1].name;
439 a.target = tid;
440 a.weight = std::llround(h);
441 a.inhibitor = true;
442 arcs.push_back(a);
443 }
444 const double f = m < tp.firing.size() && row < tp.firing[m].rows()
445 ? num_traits<T>::to_double(tp.firing[m](row, 0))
446 : 0.0;
447 if (f > 0) {
448 ArcRec a;
449 a.source = tid;
450 a.target = sn.nodes[places[p] - 1].name;
451 a.weight = std::llround(f);
452 a.inhibitor = false;
453 arcs.push_back(a);
454 }
455 }
456 }
457 }
458
459 for (std::size_t a = 0; a < arcs.size(); ++a) {
460 sb.push_back(" <arc id=\"a" + std::to_string(a + 1) + "\" source=\"" +
461 pnml_detail::escape(arcs[a].source) + "\" target=\"" +
462 pnml_detail::escape(arcs[a].target) + "\">");
463 if (arcs[a].inhibitor) {
464 // An inhibitor arc is not in the P/T grammar itself; <type
465 // value="inhibitor"/> is the extension GreatSPN, TINA and PIPE all
466 // read, so it is the one written here.
467 sb.push_back(" <type value=\"inhibitor\"/>");
468 }
469 sb.push_back(" <inscription><text>" + std::to_string(arcs[a].weight) +
470 "</text></inscription>");
471 sb.push_back(" </arc>");
472 }
473
474 sb.push_back(" </page>");
475 sb.push_back(" </net>");
476 sb.push_back("</pnml>");
477
478 std::ofstream out(path.c_str());
479 if (!out) throw InputError("pnml_save: cannot open " + path + " for writing");
480 for (std::size_t i = 0; i < sb.size(); ++i) out << sb[i] << "\n";
481}
482
483/**
484 * Read one net of a PNML place/transition document into a Network.
485 *
486 * @param path input path
487 * @param net_id id of the net to read; empty selects the first
488 */
489template <class T>
490qn::Network<T> pnml_load(const std::string& path, const std::string& net_id = std::string()) {
491 const double inf = std::numeric_limits<double>::infinity();
492 std::unique_ptr<xml::Element> root = xml::parse_file(path);
493 const std::vector<const xml::Element*> nets = root->child_tags("net");
494 if (nets.empty()) throw InputError("pnml_load: " + path + " holds no <net> element");
495 const xml::Element* net = 0;
496 for (std::size_t i = 0; i < nets.size(); ++i)
497 if (net_id.empty() || nets[i]->attr("id") == net_id) {
498 net = nets[i];
499 break;
500 }
501 if (net == 0) throw InputError("pnml_load: " + path + " holds no net with id \"" + net_id + "\"");
502 const std::string nettype = net->attr("type");
503 if (!nettype.empty() && nettype.find("ptnet") == std::string::npos)
504 throw InputError("pnml_load: net \"" + net->attr("id") + "\" declares type " + nettype +
505 ". Only the place/transition grammar "
506 "(http://www.pnml.org/version-2009/grammar/ptnet) is read: a coloured or a "
507 "symmetric net carries token colours that a single-class LINE net cannot hold");
508
509 // Places, transitions and arcs may sit directly under <net> or under any
510 // <page>; the grammar allows both and tools differ, so the whole subtree is
511 // searched rather than one level of it.
512 const std::vector<const xml::Element*> place_elems = net->by_tag("place");
513 const std::vector<const xml::Element*> trans_elems = net->by_tag("transition");
514 const std::vector<const xml::Element*> arc_elems = net->by_tag("arc");
515 if (place_elems.empty())
516 throw InputError("pnml_load: net \"" + net->attr("id") + "\" holds no place");
517
518 std::vector<std::string> place_names;
519 std::vector<long long> place_marking;
520 long long total = 0;
521 for (std::size_t i = 0; i < place_elems.size(); ++i) {
522 place_names.push_back(pnml_detail::element_id(*place_elems[i]));
523 const long long mk =
524 std::llround(pnml_detail::text_number(*place_elems[i], "initialMarking", 0.0));
525 place_marking.push_back(mk);
526 total += mk;
527 }
528 if (total == 0)
529 throw InputError("pnml_load: the initial marking of this net is empty. A LINE closed class needs "
530 "tokens to hold, and a net with none has no reachable behaviour to analyse");
531
532 // Each PNML transition is one LINE MODE; the toolspecific block says which
533 // LINE transition it belongs to, so the modes the writer split regroup here.
534 std::vector<std::string> trans_ids, mode_owner, mode_name;
535 std::vector<lang::TimingStrategy> mode_timing;
536 std::vector<double> mode_servers, mode_prio, mode_weight;
537 std::vector<lang::Distrib<T> > mode_dist;
538 for (std::size_t i = 0; i < trans_elems.size(); ++i) {
539 const std::string id = pnml_detail::element_id(*trans_elems[i]);
540 trans_ids.push_back(id);
541 std::string owner = id, name = "Mode1";
543 double servers = 1.0, prio = 1.0, weight = 1.0;
545 const xml::Element* spec = pnml_detail::line_toolspecific(*trans_elems[i]);
546 if (spec != 0) {
547 if (!spec->attr("transition").empty()) owner = spec->attr("transition");
548 if (!spec->attr("name").empty()) name = spec->attr("name");
549 std::string tm = spec->attr("timing");
550 std::transform(tm.begin(), tm.end(), tm.begin(), ::tolower);
551 if (tm == "immediate") timing = lang::TimingStrategy::IMMEDIATE;
552 servers = pnml_detail::attr_number(*spec, "servers", 1.0);
553 prio = pnml_detail::attr_number(*spec, "priority", 1.0);
554 weight = pnml_detail::attr_number(*spec, "weight", 1.0);
555 if (timing != lang::TimingStrategy::IMMEDIATE) {
556 const std::vector<const xml::Element*> ds = spec->child_tags("distribution");
557 if (!ds.empty()) {
558 std::map<std::string, double> params;
559 const std::vector<const xml::Element*> ps = ds[0]->child_tags("parameter");
560 for (std::size_t k = 0; k < ps.size(); ++k)
561 params[ps[k]->attr("name")] =
562 pnml_detail::attr_number(*ps[k], "value",
563 std::numeric_limits<double>::quiet_NaN());
564 dist = pnml_detail::dist_from<T>(ds[0]->attr("name"), params);
565 }
566 } else {
568 }
569 }
570 mode_owner.push_back(owner);
571 mode_name.push_back(name);
572 mode_timing.push_back(timing);
573 mode_servers.push_back(servers);
574 mode_prio.push_back(prio);
575 mode_weight.push_back(weight);
576 mode_dist.push_back(dist);
577 }
578
579 std::vector<std::string> owner_names;
580 std::vector<std::size_t> owner_of(trans_ids.size(), 0);
581 for (std::size_t i = 0; i < trans_ids.size(); ++i) {
582 std::size_t k = owner_names.size();
583 for (std::size_t j = 0; j < owner_names.size(); ++j)
584 if (owner_names[j] == mode_owner[i]) {
585 k = j;
586 break;
587 }
588 if (k == owner_names.size()) owner_names.push_back(mode_owner[i]);
589 owner_of[i] = k;
590 }
591
592 std::string name = net_id.empty() ? net->attr("id") : net_id;
593 if (name.empty()) name = "pnml";
594 qn::Network<T> model(name);
595
596 std::vector<std::size_t> place_node;
597 for (std::size_t i = 0; i < place_names.size(); ++i)
598 place_node.push_back(model.add_place(place_names[i]));
599
600 // The reference station is the first place holding tokens, so the class
601 // starts where the marking says it does.
602 std::size_t ref = 0;
603 for (std::size_t i = 0; i < place_marking.size(); ++i)
604 if (place_marking[i] > 0) {
605 ref = i;
606 break;
607 }
608 const std::size_t cls =
609 model.add_closed_class("Class1", static_cast<double>(total), place_node[ref], 0);
610
611 // A transition is itself a node, so the arc matrices must be sized against
612 // the FINISHED node count, which is known before any of them is added.
613 const std::size_t nnodes = place_names.size() + owner_names.size();
614
615 // The arcs are read first, because a mode's matrices are built whole and
616 // handed to add_transition rather than set afterwards.
617 std::vector<qn::TransitionParam<T> > params(owner_names.size());
618 std::vector<std::size_t> mode_index(trans_ids.size(), 0);
619 for (std::size_t i = 0; i < trans_ids.size(); ++i) {
620 qn::TransitionParam<T>& tp = params[owner_of[i]];
621 mode_index[i] = tp.nmodes;
622 tp.nmodes++;
623 tp.modenames.push_back(mode_name[i]);
624 tp.timing.push_back(mode_timing[i]);
625 tp.firingproc.push_back(mode_dist[i]);
626 tp.firingphases.push_back(
627 mode_timing[i] == lang::TimingStrategy::IMMEDIATE || mode_dist[i].disabled
628 ? 0
629 : lang::dist_to_map(mode_dist[i]).order());
630 tp.nmodeservers.push_back(mode_servers[i]);
631 tp.firingprio.push_back(mode_prio[i]);
632 tp.fireweight.push_back(num_traits<T>::from_double(mode_weight[i]));
633 tp.enabling.push_back(Matrix<T>(nnodes, 1, num_traits<T>::from_int(0)));
634 tp.inhibiting.push_back(Matrix<T>(nnodes, 1, num_traits<T>::from_double(inf)));
635 tp.firing.push_back(Matrix<T>(nnodes, 1, num_traits<T>::from_int(0)));
636 tp.firingdep.push_back(std::function<T(const std::vector<T>&)>());
637 }
638
639 // Node index of a LINE transition, in the order add_transition will assign:
640 // the places come first, then one node per owner.
641 std::vector<std::size_t> trans_node(owner_names.size(), 0);
642 for (std::size_t i = 0; i < owner_names.size(); ++i)
643 trans_node[i] = place_names.size() + i + 1;
644
646 for (std::size_t a = 0; a < arc_elems.size(); ++a) {
647 const std::string src = arc_elems[a]->attr("source");
648 const std::string tgt = arc_elems[a]->attr("target");
649 const double w = pnml_detail::text_number(*arc_elems[a], "inscription", 1.0);
650 if (!(w > 0))
651 throw InputError("pnml_load: arc " + src + " -> " + tgt +
652 " carries a non-positive inscription");
653 std::size_t ip = place_names.size(), it = trans_ids.size();
654 for (std::size_t k = 0; k < place_names.size(); ++k)
655 if (place_names[k] == src) ip = k;
656 for (std::size_t k = 0; k < trans_ids.size(); ++k)
657 if (trans_ids[k] == tgt) it = k;
658 if (ip < place_names.size() && it < trans_ids.size()) {
659 qn::TransitionParam<T>& tp = params[owner_of[it]];
660 const std::size_t row = place_node[ip] - 1;
661 if (pnml_detail::is_inhibitor(*arc_elems[a]))
662 tp.inhibiting[mode_index[it]](row, 0) = num_traits<T>::from_double(w);
663 else
664 tp.enabling[mode_index[it]](row, 0) = num_traits<T>::from_double(w);
665 R.set(cls, cls, place_node[ip], trans_node[owner_of[it]], num_traits<T>::from_int(1));
666 continue;
667 }
668 ip = place_names.size();
669 it = trans_ids.size();
670 for (std::size_t k = 0; k < trans_ids.size(); ++k)
671 if (trans_ids[k] == src) it = k;
672 for (std::size_t k = 0; k < place_names.size(); ++k)
673 if (place_names[k] == tgt) ip = k;
674 if (ip < place_names.size() && it < trans_ids.size()) {
675 qn::TransitionParam<T>& tp = params[owner_of[it]];
676 tp.firing[mode_index[it]](place_node[ip] - 1, 0) = num_traits<T>::from_double(w);
677 R.set(cls, cls, trans_node[owner_of[it]], place_node[ip], num_traits<T>::from_int(1));
678 continue;
679 }
680 throw InputError("pnml_load: arc " + src + " -> " + tgt +
681 " connects two places or two transitions, which the place/transition grammar "
682 "does not allow");
683 }
684
685 for (std::size_t i = 0; i < owner_names.size(); ++i) {
686 const std::size_t nd = model.add_transition(owner_names[i], params[i]);
687 if (nd != trans_node[i])
688 throw InputError("pnml_load: internal node numbering disagreed with the arc matrices");
689 }
690
691 model.link(R);
692 for (std::size_t i = 0; i < place_node.size(); ++i)
694 place_node[i], std::vector<T>(1, num_traits<T>::from_double(
695 static_cast<double>(place_marking[i]))));
696 return model;
697}
698
699} // namespace io
700} // namespace line
701
702#endif // LINE_IO_PNML_H
InputError(const std::string &what)
Definition error.h:39
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.
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Definition http.h:349
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:490
mam::Map< T > dist_to_map(const Distrib< T > &d)
TimingStrategy
SPN transition timing, with the values of MATLAB TimingStrategy.
Definition lang_types.h:361
@ 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
const char * node_type_to_text(NodeType t)
Name of a node kind, for diagnostics.
Definition lang_types.h:341
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:483
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:560
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
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,...
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
static Distrib exp_rate(const T &r)
Definition lang_types.h:814
std::vector< T > params
Constructor arguments, in MATLAB getParam order.
Definition lang_types.h:734
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:846
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
std::vector< const Element * > by_tag(const std::string &tag) const
Descendant-or-self search excluding self, in document order.
Definition xml.h:76
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 ....