LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
jmva_reader.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_IO_JMVA_READER_H
6#define LINE_IO_JMVA_READER_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * Port of `matlab/src/io/JMVA2LINE.m` (and `jline.io.M2M.JMVA2LINE`): a JMT
12 * `.jmva` product-form model read into a `qn::Network`, the inverse of
13 * `jmva_writer.h`.
14 *
15 * WHAT A JMVA DOCUMENT CAN SAY IS LESS THAN A NETWORK. It carries service
16 * times and visit ratios per station and class, not routing, so the model is
17 * rebuilt the way the reference rebuilds it: each station's service becomes an
18 * exponential of mean `servicetime * visits` (the DEMAND), and each class is
19 * routed serially through the stations it visits, in document-type order
20 * (delay stations, then load-independent, then load-dependent), open classes
21 * from a `Source` to a `Sink`. Every station-level mean of the product-form
22 * solution depends on the demands alone, so this is exact for what the format
23 * carries; per-visit response times are those of a visit ratio of one.
24 *
25 * STATION ORDER IS BY TYPE, NOT BY DOCUMENT POSITION, as in the reference:
26 * `<delaystation>` elements first, then `<listation>`, then `<ldstation>`.
27 *
28 * LOAD-DEPENDENT STATIONS follow MATLAB, not the JAR (which refuses them). The
29 * per-class `<servicetimes customerclass=...>` body is the semicolon (or comma)
30 * separated list d(1);d(2);... of per-population service times, with a leading
31 * d(0) = 0 dropped when present. The station is served at d(1) * visits and
32 * load-scaled by alpha(n) = d(1) / d(n), which is how `jmva_writer.h` encodes a
33 * multiserver queue. The `servers` attribute becomes the server count. A
34 * `<servicetime>` scalar under an `<ldstation>`, the spelling `lqn2ps -Ojmva`
35 * emits for a multiserver station, is read as d(1) alone: `servers` servers and
36 * no load dependence.
37 *
38 * TWO DELIBERATE READINGS where the reference indexes by position:
39 * - servicetime and visit entries are matched to a class by their
40 * `customerclass` attribute, not by position. The reference takes the r-th
41 * `<visit>` for the class named by the r-th `<servicetime>`, which agrees
42 * wherever both lists are in class order and is wrong otherwise.
43 * - a numeric attribute or body that does not parse (a SPEX variable such as
44 * `population="$N"`) is refused by name. The reference's `Str2Num` leaves
45 * it a string and then fails later in arithmetic with no mention of where.
46 *
47 * REFERENCE STATION. The reference reads `<ReferenceStation>` and then ignores
48 * it, giving every closed class the FIRST station created; the JAR does the
49 * same. This port reproduces that, so system-level metrics agree across the
50 * codebases on the same file.
51 */
52
53#include <cstdlib>
54#include <memory>
55#include <string>
56#include <vector>
57
60#include "line/num/number.h"
61#include "line/util/error.h"
62#include "line/util/xml.h"
63
64namespace line {
65namespace io {
66
67namespace jmva_detail {
68
69using xml::Element;
70
71/** Strict decimal parse: the whole (trimmed) text must be a number. */
72inline double to_double_strict(const std::string& s, const std::string& what) {
73 std::size_t b = 0, e = s.size();
74 while (b < e && (s[b] == ' ' || s[b] == '\t' || s[b] == '\n' || s[b] == '\r')) ++b;
75 while (e > b && (s[e - 1] == ' ' || s[e - 1] == '\t' || s[e - 1] == '\n' || s[e - 1] == '\r'))
76 --e;
77 const std::string t = s.substr(b, e - b);
78 if (t.empty()) throw InputError("jmva2line: " + what + " is empty");
79 char* end = nullptr;
80 const double v = std::strtod(t.c_str(), &end);
81 if (end == t.c_str() || *end != '\0')
82 throw InputError("jmva2line: " + what + " is '" + t +
83 "', which is not a number (a SPEX template variable must be "
84 "substituted before import)");
85 return v;
86}
87
88/** The first direct child with the given tag, or null. */
89inline const Element* child(const Element* e, const std::string& tag) {
90 if (!e) return nullptr;
91 const std::vector<const Element*> v = e->child_tags(tag);
92 return v.empty() ? nullptr : v[0];
93}
94
95/** The entry of `tag` under `parent` whose `customerclass` is `cls`, or null. */
96inline const Element* by_class(const Element* parent, const std::string& tag,
97 const std::string& cls) {
98 if (!parent) return nullptr;
99 for (const Element* e : parent->child_tags(tag))
100 if (e->attr("customerclass") == cls) return e;
101 return nullptr;
102}
103
104/** `str2double(strsplit(s, ';'))` (or ','), NaN entries dropped as the reference does. */
105inline std::vector<double> split_demands(const std::string& s) {
106 const char sep = s.find(';') != std::string::npos ? ';' : ',';
107 std::vector<double> out;
108 std::size_t start = 0;
109 while (start <= s.size()) {
110 std::size_t end = s.find(sep, start);
111 if (end == std::string::npos) end = s.size();
112 const std::string tok = s.substr(start, end - start);
113 char* stop = nullptr;
114 const double v = std::strtod(tok.c_str(), &stop);
115 bool blank = true;
116 for (char c : tok)
117 if (!(c == ' ' || c == '\t' || c == '\n' || c == '\r')) blank = false;
118 if (!blank) {
119 while (*stop == ' ' || *stop == '\t' || *stop == '\n' || *stop == '\r') ++stop;
120 if (stop == tok.c_str() || *stop != '\0')
121 throw InputError("jmva2line: load-dependent service time list '" + s +
122 "' holds the non-numeric entry '" + tok + "'");
123 out.push_back(v);
124 }
125 start = end + 1;
126 }
127 return out;
128}
129
130/** Basename without directory or extension, MATLAB's `[~,fname] = fileparts`. */
131inline std::string file_stem(const std::string& path) {
132 const std::size_t slash = path.find_last_of("/\\");
133 std::string f = slash == std::string::npos ? path : path.substr(slash + 1);
134 const std::size_t dot = f.find_last_of('.');
135 if (dot != std::string::npos && dot > 0) f = f.substr(0, dot);
136 return f;
137}
138
139} // namespace jmva_detail
140
141/**
142 * Port of `JMVA2LINE(filename, modelName)`.
143 *
144 * @param path the `.jmva` file
145 * @param name the model name; empty takes the file's basename, as the reference does
146 * @return the network, linked and ready for `get_struct()`
147 */
148template <class T>
149qn::Network<T> jmva2line(const std::string& path, const std::string& name = std::string()) {
150 using namespace jmva_detail;
151 typedef lang::Distrib<T> D;
152 std::unique_ptr<Element> doc = xml::parse_file(path);
153 if (!doc) throw InputError("jmva2line: cannot parse '" + path + "' as XML");
154
155 // `xDoc = xDoc.sim` when present: the parameters may sit under <sim>.
156 const Element* root = doc.get();
157 if (const Element* sim = child(root, "sim")) root = sim;
158 const Element* params = child(root, "parameters");
159 if (!params) throw InputError("jmva2line: '" + path + "' carries no <parameters> element");
160 const Element* xstations = child(params, "stations");
161 const Element* xclasses = child(params, "classes");
162 if (!xstations) throw InputError("jmva2line: '" + path + "' carries no <stations> element");
163 if (!xclasses) throw InputError("jmva2line: '" + path + "' carries no <classes> element");
164
165 qn::Network<T> model(name.empty() ? file_stem(path) : name);
166
167 const std::vector<const Element*> xdelay = xstations->child_tags("delaystation");
168 const std::vector<const Element*> xli = xstations->child_tags("listation");
169 const std::vector<const Element*> xld = xstations->child_tags("ldstation");
170
171 // ---- stations, by type ----------------------------------------------
172 std::vector<std::size_t> nodes; // node index of each station, creation order
173 std::vector<const Element*> xnode; // its XML element
174 for (const Element* e : xdelay) {
175 nodes.push_back(model.add_delay(e->attr("name")));
176 xnode.push_back(e);
177 }
178 for (const Element* e : xli) {
179 nodes.push_back(model.add_queue(e->attr("name"), lang::SchedStrategy::PS));
180 xnode.push_back(e);
181 }
182 for (const Element* e : xld) {
183 const std::size_t nd = model.add_queue(e->attr("name"), lang::SchedStrategy::PS);
184 double nservers = 1.0;
185 if (e->has_attr("servers"))
186 nservers = to_double_strict(e->attr("servers"),
187 "the 'servers' of ldstation '" + e->attr("name") + "'");
188 model.set_number_of_servers(nd, nservers);
189 nodes.push_back(nd);
190 xnode.push_back(e);
191 }
192 const std::size_t nInf = xdelay.size(), nLI = xli.size();
193 const std::size_t M = nodes.size();
194
195 // ---- classes ----------------------------------------------------------
196 std::vector<std::string> cname;
197 std::vector<std::size_t> cls;
198 const std::vector<const Element*> xopen = xclasses->child_tags("openclass");
199 const std::vector<const Element*> xclosed = xclasses->child_tags("closedclass");
200 std::size_t source = 0, sink = 0;
201 if (!xopen.empty()) {
202 source = model.add_source("Source");
203 sink = model.add_sink("Sink");
204 for (const Element* e : xopen) {
205 const std::string nm = e->attr("name");
206 const std::size_t r = model.add_open_class(nm, 0);
207 const double rate = to_double_strict(e->attr("rate"), "the rate of open class '" + nm + "'");
208 model.set_arrival(source, r, D::exp_rate(num_traits<T>::from_double(rate)));
209 cname.push_back(nm);
210 cls.push_back(r);
211 }
212 }
213 if (!xclosed.empty() && M == 0)
214 throw InputError("jmva2line: '" + path + "' declares closed classes but no station");
215 for (const Element* e : xclosed) {
216 const std::string nm = e->attr("name");
217 const double pop =
218 to_double_strict(e->attr("population"), "the population of closed class '" + nm + "'");
219 // The reference's refstat: the first station created (see the file comment).
220 const std::size_t r = model.add_closed_class(nm, pop, nodes[0], 0);
221 cname.push_back(nm);
222 cls.push_back(r);
223 }
224 const std::size_t K = cls.size();
225 const std::size_t nOpen = xopen.size();
226
227 // ---- service ----------------------------------------------------------
228 std::vector<std::vector<bool>> visited(M, std::vector<bool>(K, false));
229 for (std::size_t i = 0; i < M; ++i) {
230 const Element* st = xnode[i];
231 const std::string sname = st->attr("name");
232 const Element* xst = child(st, "servicetimes");
233 const Element* xvis = child(st, "visits");
234 const bool is_ld = i >= nInf + nLI;
235 for (std::size_t r = 0; r < K; ++r) {
236 const Element* v = by_class(xvis, "visit", cname[r]);
237 if (!v)
238 throw InputError("jmva2line: station '" + sname + "' gives no visit for class '" +
239 cname[r] + "'");
240 const double visits =
241 to_double_strict(v->text, "the visits of class '" + cname[r] + "' at '" + sname + "'");
242 if (!is_ld) {
243 const Element* s = by_class(xst, "servicetime", cname[r]);
244 if (!s)
245 throw InputError("jmva2line: station '" + sname +
246 "' gives no service time for class '" + cname[r] + "'");
247 const double stime = to_double_strict(
248 s->text, "the service time of class '" + cname[r] + "' at '" + sname + "'");
249 if (visits > 0.0) {
250 model.set_service(nodes[i], cls[r],
251 D::exp_mean(num_traits<T>::from_double(stime * visits)));
252 visited[i][r] = true;
253 } else {
254 model.set_service(nodes[i], cls[r], D::disabled_dist());
255 }
256 continue;
257 }
258 // Load-dependent: the per-population list, or the scalar lqn2ps form.
259 std::vector<double> demands;
260 if (const Element* s = by_class(xst, "servicetimes", cname[r])) {
261 demands = split_demands(s->text);
262 if (demands.empty())
263 for (const Element* c : s->child_tags("servicetime"))
264 demands.push_back(to_double_strict(
265 c->text, "a service time of class '" + cname[r] + "' at '" + sname + "'"));
266 } else if (const Element* s1 = by_class(xst, "servicetime", cname[r])) {
267 demands.push_back(to_double_strict(
268 s1->text, "the service time of class '" + cname[r] + "' at '" + sname + "'"));
269 } else {
270 throw InputError("jmva2line: ldstation '" + sname +
271 "' gives no service times for class '" + cname[r] + "'");
272 }
273 if (demands.size() > 1 && demands[0] == 0.0) demands.erase(demands.begin());
274 if (visits > 0.0 && !demands.empty() && demands[0] > 0.0) {
275 const double base = demands[0] * visits;
276 model.set_service(nodes[i], cls[r], D::exp_mean(num_traits<T>::from_double(base)));
277 visited[i][r] = true;
278 if (demands.size() > 1) {
279 std::vector<T> alpha(demands.size());
280 for (std::size_t n = 0; n < demands.size(); ++n)
282 demands[n] > 0.0 ? demands[0] / demands[n] : 0.0);
283 // As the reference: setLoadDependence is per STATION, so the
284 // last class with a list is the one that sets it.
285 model.set_load_dependence(nodes[i], alpha);
286 }
287 } else {
288 model.set_service(nodes[i], cls[r], D::disabled_dist());
289 }
290 }
291 }
292
293 // ---- routing: serial through the visited stations -------------------
295 for (std::size_t r = 0; r < K; ++r) {
296 std::vector<std::size_t> path_nodes;
297 if (r < nOpen) path_nodes.push_back(source);
298 for (std::size_t i = 0; i < M; ++i)
299 if (visited[i][r]) path_nodes.push_back(nodes[i]);
300 if (r < nOpen) path_nodes.push_back(sink);
301 if (path_nodes.empty()) continue;
302 P.set(cls[r], model.serial_routing(path_nodes));
303 }
304 model.link(P);
305 return model;
306}
307
308} // namespace io
309} // namespace line
310
311#endif // LINE_IO_JMVA_READER_H
InputError(const std::string &what)
Definition error.h:39
A queueing network under construction.
void set_load_dependence(std::size_t node, const std::vector< T > &alpha)
station.setLoadDependence(alpha): the rate multiplier at population 1, 2, ... The vector is indexed f...
std::size_t add_source(const std::string &nm)
The external arrival station.
std::size_t add_open_class(const std::string &nm, int prio=0)
An open class.
std::size_t add_delay(const std::string &nm)
An infinite-server station (a Delay, MATLAB's Delay / DelayStation).
RoutingMatrix< T > init_routing_matrix() const
An empty routing matrix, MATLAB's model.initRoutingMatrix.
void set_number_of_servers(std::size_t node, double n)
queue.setNumberOfServers(n).
RoutingMatrix< T > serial_routing(const std::vector< std::size_t > &nodes) const
model.serialRouting(nodes): a unit-probability path through nodes.
std::size_t add_queue(const std::string &nm, SchedStrategy sched=SchedStrategy::FCFS)
A queueing station.
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_sink(const std::string &nm)
The external departure node.
void link(const RoutingMatrix< T > &Pm)
model.link(P): install the routing.
void set_service(std::size_t node, std::size_t cls, const Distrib< T > &d)
station.setService(class, dist).
void set_arrival(std::size_t node, std::size_t cls, const Distrib< T > &d)
source.setArrival(class, dist): the same table, at the Source.
The routing matrix a model script fills in, MATLAB's P cell array.
void set(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
std::string file_stem(const std::string &path)
Basename without directory or extension, MATLAB's [~,fname] = fileparts.
const Element * child(const Element *e, const std::string &tag)
The first direct child with the given tag, or null.
Definition jmva_reader.h:89
double to_double_strict(const std::string &s, const std::string &what)
Strict decimal parse: the whole (trimmed) text must be a number.
Definition jmva_reader.h:72
const Element * by_class(const Element *parent, const std::string &tag, const std::string &cls)
The entry of tag under parent whose customerclass is cls, or null.
Definition jmva_reader.h:96
std::vector< double > split_demands(const std::string &s)
str2double(strsplit(s, ';')) (or ','), NaN entries dropped as the reference does.
qn::Network< T > jmva2line(const std::string &path, const std::string &name=std::string())
Port of JMVA2LINE(filename, modelName).
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,...
Number-type abstraction for the templated API port.
std::string text
Character data of a text-only element, as createTextNode supplies it.
Definition xml.h:60
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
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 ....