62namespace pnml_detail {
70 std::vector<std::string> v;
80 v.push_back(
"lambda1");
81 v.push_back(
"lambda2");
101 v.push_back(
"sigma");
110lang::Distrib<T> dist_from(
const std::string& name,
const std::map<std::string, double>& p) {
111 typedef lang::Distrib<T> D;
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);
117 throw InputError(
"pnml: distribution " + who +
" is missing the parameter \"" + key +
"\"");
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)));
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")
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");
161inline std::string escape(
const std::string& in) {
163 out.reserve(in.size());
164 for (std::size_t i = 0; i < in.size(); ++i) {
166 case '&': out +=
"&";
break;
167 case '<': out +=
"<";
break;
168 case '>': out +=
">";
break;
169 case '"': out +=
""";
break;
170 default: out += in[i];
break;
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) {
180 std::snprintf(buf,
sizeof buf,
"%lld",
static_cast<long long>(v));
181 return std::string(buf);
184 std::snprintf(buf,
sizeof buf,
"%.17g", v);
185 return std::string(buf);
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;
192 const std::vector<const xml::Element*> texts = labels[0]->child_tags(
"text");
193 txt = texts.empty() ? labels[0]->text : texts[0]->text;
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);
201 const std::size_t tick = txt.rfind(
'`');
202 if (tick != std::string::npos) txt = txt.substr(tick + 1);
204 return std::stod(txt);
205 }
catch (
const std::exception&) {
206 throw InputError(
"pnml: label <" + label +
"> holds \"" + txt +
"\", which is not a number");
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();
219 return std::stod(txt);
220 }
catch (
const std::exception&) {
221 throw InputError(
"pnml: attribute " + key +
" holds \"" + txt +
"\", which is not a number");
225inline std::string element_id(
const xml::Element& e) {
226 std::string
id = e.attr(
"id");
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;
234 if (
id.empty())
throw InputError(
"pnml: a place or transition carries neither an id nor a name");
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;
245 std::string v = arc.attr(
"type");
246 std::transform(v.begin(), v.end(), v.begin(), ::tolower);
247 return v ==
"inhibitor";
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];
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");
284 std::vector<std::size_t> places, transitions;
285 for (std::size_t i = 0; i <
sn.nodes.size(); ++i) {
288 places.push_back(i + 1);
290 transitions.push_back(i + 1);
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");
299 throw InputError(
"pnml_save: the model holds no Place, so there is no Petri net to write");
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()) {
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);
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\">");
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>");
343 std::string source, target;
347 std::vector<ArcRec> arcs;
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");
357 for (std::size_t m = 0; m < tp.
nmodes; ++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 =
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 =
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\">");
375 " <mode transition=\"" + pnml_detail::escape(
sn.nodes[nd - 1].name) +
376 "\" name=\"" + pnml_detail::escape(mname) +
"\" timing=\"" +
377 (immediate ?
"immediate" :
"timed") +
"\" servers=\"" +
382 pnml_detail::num_text(m < tp.
fireweight.size()
388 throw InputError(
"pnml_save: transition " +
sn.nodes[nd - 1].name +
" mode " +
389 std::to_string(m + 1) +
" is timed but carries no distribution");
392 const std::vector<std::string> pnames = pnml_detail::dist_param_names(d.
type);
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 "
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) +
408 for (std::size_t k = 0; k < pnames.size(); ++k) {
409 sb.push_back(
" <parameter name=\"" +
410 pnml_detail::escape(pnames[k]) +
"\" value=\"" +
414 sb.push_back(
" </distribution>");
416 sb.push_back(
" </mode>");
417 sb.push_back(
" </toolspecific>");
418 sb.push_back(
" </transition>");
420 for (std::size_t p = 0; p < places.size(); ++p) {
421 const std::size_t row = places[p] - 1;
427 a.source =
sn.nodes[places[p] - 1].name;
429 a.weight = std::llround(w);
436 if (std::isfinite(h)) {
438 a.source =
sn.nodes[places[p] - 1].name;
440 a.weight = std::llround(h);
444 const double f = m < tp.
firing.size() && row < tp.
firing[m].rows()
450 a.target =
sn.nodes[places[p] - 1].name;
451 a.weight = std::llround(f);
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) {
467 sb.push_back(
" <type value=\"inhibitor\"/>");
469 sb.push_back(
" <inscription><text>" + std::to_string(arcs[a].weight) +
470 "</text></inscription>");
471 sb.push_back(
" </arc>");
474 sb.push_back(
" </page>");
475 sb.push_back(
" </net>");
476 sb.push_back(
"</pnml>");
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";
491 const double inf = std::numeric_limits<double>::infinity();
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");
496 for (std::size_t i = 0; i < nets.size(); ++i)
497 if (net_id.empty() || nets[i]->attr(
"id") == net_id) {
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");
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");
518 std::vector<std::string> place_names;
519 std::vector<long long> place_marking;
521 for (std::size_t i = 0; i < place_elems.size(); ++i) {
522 place_names.push_back(pnml_detail::element_id(*place_elems[i]));
524 std::llround(pnml_detail::text_number(*place_elems[i],
"initialMarking", 0.0));
525 place_marking.push_back(mk);
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");
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]);
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);
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);
556 const std::vector<const xml::Element*> ds = spec->
child_tags(
"distribution");
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);
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);
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]) {
588 if (k == owner_names.size()) owner_names.push_back(mode_owner[i]);
592 std::string name = net_id.empty() ? net->
attr(
"id") : net_id;
593 if (name.empty()) name =
"pnml";
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]));
603 for (std::size_t i = 0; i < place_marking.size(); ++i)
604 if (place_marking[i] > 0) {
608 const std::size_t cls =
609 model.
add_closed_class(
"Class1",
static_cast<double>(total), place_node[ref], 0);
613 const std::size_t nnodes = place_names.size() + owner_names.size();
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) {
621 mode_index[i] = tp.
nmodes;
624 tp.
timing.push_back(mode_timing[i]);
636 tp.
firingdep.push_back(std::function<T(
const std::vector<T>&)>());
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;
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);
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()) {
660 const std::size_t row = place_node[ip] - 1;
661 if (pnml_detail::is_inhibitor(*arc_elems[a]))
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()) {
680 throw InputError(
"pnml_load: arc " + src +
" -> " + tgt +
681 " connects two places or two transitions, which the place/transition grammar "
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");
692 for (std::size_t i = 0; i < place_node.size(); ++i)
695 static_cast<double>(place_marking[i]))));
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.
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.
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.
mam::Map< T > dist_to_map(const Distrib< T > &d)
TimingStrategy
SPN transition timing, with the values of MATLAB TimingStrategy.
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
@ TIMED
fires after its firing distribution elapses
const char * node_type_to_text(NodeType t)
Name of a node kind, for diagnostics.
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
NodeType
Node kinds, with the values of MATLAB NodeType.
std::unique_ptr< Element > parse_file(const std::string &path)
Read and parse a file.
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)
std::vector< T > params
Constructor arguments, in MATLAB getParam order.
static Distrib immediate()
The Immediate singleton.
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.
std::string attr(const std::string &key) const
Attribute value, or the empty string when absent (org.w3c.dom semantics).
std::vector< const Element * > child_tags(const std::string &tag) const
Direct children with the given tag, in document order.
A minimal XML DOM: read for the .lqnx interchange format, write for the JMT .jsimg and ....