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