LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
jmva_writer.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_WRITER_H
6#define LINE_IO_JMVA_WRITER_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * Port of `@@JMTIO/writeJMVA.m`: the CHAIN-level product-form model in the JMVA
12 * interchange format.
13 *
14 * The format predates this port by way of JMT's MVA panel, and `qnsolver` of the
15 * LQNS distribution reads the same grammar, which is why the writer lives in
16 * `io/` rather than under either solver: SolverJMT and the qns methods of
17 * SolverLQNS both call it, and the document it writes is the JMT one, field for
18 * field.
19 *
20 * WHAT IT WRITES IS ALREADY AGGREGATED. Classes become CHAINS, service times and
21 * visits come from `sn_get_demands_chain`, and a station that is neither a Queue
22 * nor a Delay is dropped -- a Source contributes its arrival rate to the open
23 * chain's `rate` attribute and nothing else, and the Sink is not a station. The
24 * caller therefore gets chain-level results back and has to de-aggregate them;
25 * nothing in this file is per-class.
26 *
27 * A MULTISERVER QUEUE IS WRITTEN AS A LOAD-DEPENDENT STATION, not as a station
28 * with `servers` set: the format's own multiserver support is thinner than the
29 * rate vector S/min(n,c), and the reference has always spelt the rates out. The
30 * `servers` attribute is still emitted as "1" because the schema requires it.
31 */
32
33#include <algorithm>
34#include <cmath>
35#include <cstdio>
36#include <cstdlib>
37#include <fstream>
38#include <limits>
39#include <string>
40#include <vector>
41
44#include "line/util/error.h"
45
46namespace line {
47namespace io {
48
49namespace detail {
50
51/**
52 * Shortest decimal text that reads back as the same double.
53 *
54 * MATLAB writes these through `num2str`, which keeps about five significant
55 * digits, and the JAR through `String.valueOf`. Neither is a deliberate choice
56 * of precision, and truncating a service demand before handing it to an external
57 * solver loses accuracy the port has no way to recover, so the round-trip form
58 * is used here.
59 */
60inline std::string num_text(double x) {
61 if (std::isnan(x)) return "0";
62 if (x == std::floor(x) && std::fabs(x) < 1e15) {
63 char buf[32];
64 std::snprintf(buf, sizeof(buf), "%.0f", x);
65 return std::string(buf);
66 }
67 char buf[64];
68 for (int prec = 15; prec <= 17; ++prec) {
69 std::snprintf(buf, sizeof(buf), "%.*g", prec, x);
70 if (std::strtod(buf, nullptr) == x) break;
71 }
72 return std::string(buf);
73}
74
75/** XML character-data and attribute-value escaping. */
76inline std::string xml_escape(const std::string& s) {
77 std::string out;
78 out.reserve(s.size());
79 for (std::size_t i = 0; i < s.size(); ++i) {
80 switch (s[i]) {
81 case '&': out += "&amp;"; break;
82 case '<': out += "&lt;"; break;
83 case '>': out += "&gt;"; break;
84 case '"': out += "&quot;"; break;
85 case '\'': out += "&apos;"; break;
86 default: out += s[i];
87 }
88 }
89 return out;
90}
91
92/** `sprintf('Chain%02d', c)` of the reference, with c 1-based. */
93inline std::string chain_name(std::size_t c1) {
94 char buf[32];
95 std::snprintf(buf, sizeof(buf), "Chain%02zu", c1);
96 return std::string(buf);
97}
98
99/**
100 * The `algType` name the reference maps `options.method` to, and whether that
101 * algorithm admits a multiserver station.
102 *
103 * The `jmva.*` names are JMT's own solvers and reach this writer through
104 * SolverJMT; SolverLQNS passes a multiserver approximation name, which falls
105 * through to plain MVA because the algorithm there is chosen by `qnsolver -m`
106 * and not by the document.
107 */
108inline std::string alg_type_name(const std::string& method, bool* multiserver_ok) {
109 *multiserver_ok = true;
110 if (method == "jmva.recal") { *multiserver_ok = false; return "RECAL"; }
111 if (method == "jmva.comom") { *multiserver_ok = false; return "CoMoM"; }
112 if (method == "jmva.chow") { *multiserver_ok = false; return "Chow"; }
113 if (method == "jmva.bs" || method == "jmva.amva") {
114 *multiserver_ok = false;
115 return "Bard-Schweitzer";
116 }
117 if (method == "jmva.aql") { *multiserver_ok = false; return "AQL"; }
118 if (method == "jmva.lin") { *multiserver_ok = false; return "Linearizer"; }
119 if (method == "jmva.dmlin") {
120 *multiserver_ok = false;
121 return "De Souza-Muntz Linearizer";
122 }
123 return "MVA";
124}
125
126} // namespace detail
127
128/**
129 * Port of `writeJMVA(sn, outputFileName, options)`.
130 *
131 * @param L the refreshed struct
132 * @param path the file to write
133 * @param method `options.method`, which selects the `algType` name
134 * @param samples `options.samples`, the `maxSamples` attribute
135 * @return the path written, so the caller can chain it as the reference does
136 */
137template <class T>
138std::string write_jmva(const qn::NetworkStruct<T>& L, const std::string& path,
139 const std::string& method, std::size_t samples) {
140 const std::size_t M = L.nstations, K = L.nclasses, C = L.nchains;
141
142 bool multiserver_ok = true;
143 const std::string algname = detail::alg_type_name(method, &multiserver_ok);
144 if (!multiserver_ok) {
145 for (std::size_t i = 0; i < M; ++i) {
146 const double c = L.stations[i].nservers;
147 if (std::isfinite(c) && c > 1.0)
148 throw UnsupportedError("writeJMVA: " + method +
149 " does not support multi-server stations");
150 }
151 }
152
154 auto dv = [](const Matrix<T>& A, std::size_t i, std::size_t j) {
155 return num_traits<T>::to_double(A(i, j));
156 };
157
158 // The reference indexes sn.rates with a logical over NODES to pick the
159 // Source row, which only lands on the right row because the Source is
160 // created before any node that is not a station. The station's own node type
161 // is the index-safe spelling of the same test and agrees wherever the
162 // reference works.
163 std::vector<bool> is_source_station(M, false);
164 std::size_t nsources = 0;
165 for (std::size_t i = 0; i < M; ++i)
166 if (L.stations[i].nodetype == qn::NodeType::Source) {
167 is_source_station[i] = true;
168 ++nsources;
169 }
170
171 std::ofstream f(path.c_str());
172 if (!f) throw InputError("writeJMVA: cannot open '" + path + "' for writing");
173 f << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
174 f << "<model xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
175 << " xsi:noNamespaceSchemaLocation=\"JMTmodel.xsd\">\n";
176 f << " <parameters>\n";
177
178 // ---- classes: one per CHAIN ------------------------------------------
179 f << " <classes number=\"" << C << "\">\n";
180 for (std::size_t c = 0; c < C; ++c) {
181 double sum_njobs = 0.0;
182 for (std::size_t k : L.inchain[c]) sum_njobs += L.classes[k - 1].population;
183 if (std::isfinite(sum_njobs)) {
184 f << " <closedclass population=\"" << detail::num_text(d.Nchain[c])
185 << "\" name=\"" << detail::chain_name(c + 1) << "\"/>\n";
186 } else {
187 double rate = 0.0;
188 for (std::size_t i = 0; i < M; ++i) {
189 if (!is_source_station[i]) continue;
190 for (std::size_t k : L.inchain[c]) {
191 const double r = dv(L.rates, i, k - 1);
192 if (std::isfinite(r)) rate += r;
193 }
194 }
195 f << " <openclass rate=\"" << detail::num_text(rate) << "\" name=\""
196 << detail::chain_name(c + 1) << "\"/>\n";
197 }
198 }
199 f << " </classes>\n";
200
201 // ---- stations: Queue and Delay only ----------------------------------
202 f << " <stations number=\"" << (M - nsources) << "\">\n";
203 // Whether the closed population is finite decides how far the rate vector of
204 // a load-dependent station has to run: an open chain never bounds it, so the
205 // vector stops at the server count, past which S/min(n,c) is constant.
206 bool any_open = false;
207 for (std::size_t k = 0; k < K; ++k)
208 if (!std::isfinite(L.classes[k].population)) any_open = true;
209 double total_njobs = 0.0;
210 for (std::size_t k = 0; k < K; ++k)
211 if (std::isfinite(L.classes[k].population)) total_njobs += L.classes[k].population;
212
213 for (std::size_t i = 0; i < M; ++i) {
214 const qn::NodeType nt = L.stations[i].nodetype;
215 if (nt != qn::NodeType::Queue && nt != qn::NodeType::Delay) continue;
216 const std::string name =
217 detail::xml_escape(L.nodes[L.station_to_node[i] - 1].name);
218 // Effective server count. A load-dependent scaling reaches JMVA as the c
219 // of an <ldstation>, the same encoding `save_number_of_servers` uses for
220 // JSIM: `check_model` admits only alpha(n) = min(n,c), so max(alpha) is
221 // that c. Reading `nservers` alone wrote a <listation> at nominal service
222 // time and dropped the scaling.
223 double servers = L.stations[i].nservers;
224 for (const T& s : L.stations[i].lldscaling)
225 servers = std::max(servers, num_traits<T>::to_double(s));
226 const bool is_ld = (nt == qn::NodeType::Queue) && !(servers == 1.0);
227 const char* tag = nt == qn::NodeType::Delay
228 ? "delaystation"
229 : (is_ld ? "ldstation" : "listation");
230
231 f << " <" << tag << " name=\"" << name << "\"";
232 if (nt == qn::NodeType::Queue) f << " servers=\"1\"";
233 f << ">\n";
234
235 f << " <servicetimes>\n";
236 for (std::size_t c = 0; c < C; ++c) {
237 const double st = dv(d.STchain, i, c);
238 if (is_ld) {
239 const double limit = any_open ? servers : total_njobs;
240 std::string s = detail::num_text(st);
241 for (double n = 2.0; n <= limit; n += 1.0)
242 s += ";" + detail::num_text(st / std::min(n, servers));
243 f << " <servicetimes customerclass=\"" << detail::chain_name(c + 1)
244 << "\">" << s << "</servicetimes>\n";
245 } else {
246 f << " <servicetime customerclass=\"" << detail::chain_name(c + 1)
247 << "\">" << detail::num_text(st) << "</servicetime>\n";
248 }
249 }
250 f << " </servicetimes>\n";
251
252 f << " <visits>\n";
253 for (std::size_t c = 0; c < C; ++c) {
254 const double st = dv(d.STchain, i, c);
255 const double v = st > 0.0 ? dv(d.Lchain, i, c) / st : 0.0;
256 f << " <visit customerclass=\"" << detail::chain_name(c + 1) << "\">"
257 << detail::num_text(v) << "</visit>\n";
258 }
259 f << " </visits>\n";
260 f << " </" << tag << ">\n";
261 }
262 f << " </stations>\n";
263
264 // ---- reference stations ----------------------------------------------
265 // An open chain's reference station is the Source, which is not in the
266 // document at all, so the reference substitutes the first station that is
267 // neither Source nor Sink. Naming an absent station makes qnsolver reject
268 // the whole model, so this is not cosmetic.
269 f << " <ReferenceStation number=\"" << C << "\">\n";
270 for (std::size_t c = 0; c < C; ++c) {
271 std::size_t ref = L.classes[L.inchain[c][0] - 1].refstat; // 1-based station
272 if (L.stations[ref - 1].nodetype == qn::NodeType::Source) {
273 for (std::size_t i = 0; i < M; ++i) {
274 const qn::NodeType nt = L.stations[i].nodetype;
275 if (nt != qn::NodeType::Source && nt != qn::NodeType::Sink) {
276 ref = i + 1;
277 break;
278 }
279 }
280 }
281 f << " <Class name=\"" << detail::chain_name(c + 1) << "\" refStation=\""
282 << detail::xml_escape(L.nodes[L.station_to_node[ref - 1] - 1].name) << "\"/>\n";
283 }
284 f << " </ReferenceStation>\n";
285 f << " </parameters>\n";
286
287 f << " <algParams>\n";
288 f << " <algType name=\"" << detail::xml_escape(algname)
289 << "\" tolerance=\"1.0E-7\" maxSamples=\"" << samples << "\"/>\n";
290 f << " <compareAlgs value=\"false\"/>\n";
291 f << " </algParams>\n";
292 f << "</model>\n";
293 f.close();
294 if (!f) throw InputError("writeJMVA: failed to write '" + path + "'");
295 return path;
296}
297
298} // namespace io
299} // namespace line
300
301#endif // LINE_IO_JMVA_WRITER_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
std::vector< NodeDef > nodes
every node, in creation order
std::vector< std::size_t > station_to_node
(nstations) 1-based node index
The exception types the port throws.
std::string write_jmva(const qn::NetworkStruct< T > &L, const std::string &path, const std::string &method, std::size_t samples)
Port of writeJMVA(sn, outputFileName, options).
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Chain aggregation and de-aggregation.
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
std::vector< double > Nchain
(C) population, infinite for an open chain
Definition sn_chain.h:51
Matrix< T > STchain
(M x C) mean service time
Definition sn_chain.h:48
Matrix< T > Lchain
(M x C) demand
Definition sn_chain.h:47