LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqns_qnsolver.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_SOLVERS_WRAPPERS_LQNS_LQNS_QNSOLVER_H
6#define LINE_SOLVERS_WRAPPERS_LQNS_LQNS_QNSOLVER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The flat-Network path of `@@SolverLQNS`: its qns methods, served by `qnsolver`
12 * of the RADS/LQNS distribution, the way SolverJMT reaches JMVA. MATLAB keeps
13 * it in `@@SolverLQNS/runAnalyzerNetwork.m` and `solver_qns.m`.
14 *
15 * `lqns::SolverLQNS<T>` remains the LqnModel solver; a flat Network reaches
16 * the same solver name through the free functions here
17 * (`solve_network_run_analyzer`, `qns_methods`, `qns_method_refusal`).
18 *
19 * WHY A WRAPPER IS PORTED AT ALL. Every other solver in `cpp/` computes its own
20 * answer; this one marshals the model to an external binary and reads the
21 * numbers back. It earns its place for the same reason it does in the other
22 * three codebases: `qnsolver` is an INDEPENDENT implementation of the multiserver
23 * AMVA lineage that `solver_mva` also implements, so it is the cross-check that
24 * catches an error common to the port and its reference. It is the only external
25 * tool in `cpp/`: LINE ships no copy of it, and every path here refuses by name
26 * when the binary is absent rather than answering natively under the LQNS label.
27 *
28 * WHAT THE PORT COVERS. The reference dispatches two ways (`runAnalyzerNetwork.m`):
29 *
30 * - product-form, or any model with open classes -> marshal to JMVA, run
31 * `qnsolver`, parse and de-aggregate. THIS IS PORTED, in full.
32 * - non-product-form closed -> convert with `QN2LQN` and delegate to
33 * `SolverLQNS`. This is the same layered path as MATLAB and the JAR; LINE
34 * still ships no LQNS binary, so its ordinary availability diagnostic is
35 * preserved when the external tool is absent.
36 *
37 * METHODS. On a Network the method names are `default` and `qns` (the default
38 * approximation, rolia) and `qns.NAME`, which selects approximation NAME; the
39 * engine below speaks the bare NAME. `conway`, `reiser`, `rolia` and `zhou` are
40 * what `qnsolver -m` accepts, and they only take effect on a model that HAS a
41 * multiserver station -- the reference passes no `-m` otherwise, and so does
42 * this. `suri` and `schmidt` reach the tool through the LQNS branch on a
43 * non-product-form closed model.
44 *
45 * REFERENCES, per method:
46 * - `conway`: Conway (1989), extending the multinomial all-servers-busy probability of de Souza e Silva and Muntz (Perform. Eval. 7(3), 1987).
47 * - `rolia`: Rolia (PhD thesis, Toronto, 1992) as used in the method of layers (Rolia and Sevcik, IEEE TSE 21(8), 1995), in the per-class Rolia-Franks form of Franks (PhD thesis, Carleton, 1999).
48 * - `zhou`: arrival-theorem binomial (AB) approximation, S. Zhou (M.A.Sc. thesis, Carleton, 2021) and Zhou and Woodside (ICPE Companion 2022).
49 * - `suri`: Suri, Sahu and Vernon (IERC 2007).
50 * - `reiser`: Reiser and Lavenberg (J. ACM 27(2), 1980) load-dependent MVA, see also Reiser (Perform. Eval. 1, 1981).
51 * - `schmidt`: Schmidt (Perform. Eval. 29(4), 1997).
52 */
53
55#include <algorithm>
56#include <cctype>
57#include <cmath>
58#include <cstdlib>
59#include <fstream>
60#include <limits>
61#include <sstream>
62#include <string>
63#include <vector>
64
65#include "line/io/jmva_writer.h"
66#include "line/io/qn2lqn.h"
72#include "line/util/error.h"
74#include "line/util/tempdir.h"
75
76namespace line {
77namespace lqns {
78
79/** `SolverLQNS.defaultOptions` on a flat Network plus the knobs the JMVA document carries. */
80struct QnsOptions {
81 /** `default`, `qns` or `qns.NAME`; see qns_methods(). */
82 std::string method = "default";
83 /**
84 * `options.config.multiserver`. Kept separate from `method` because the
85 * reference lets either name the approximation: `method` sets it, and a
86 * caller may set it directly while leaving the method at 'default'.
87 */
88 std::string multiserver;
89 std::size_t samples = 10000; ///< `options.samples`, the JMVA maxSamples
90 /** Seconds before a hung `qnsolver` is killed; not positive waits forever. */
91 int timeout = 0;
92 /** `options.keep`: leave the scratch directory behind, to inspect what was sent. */
93 bool keep = false;
94};
95
96/** Port of `SolverLQNS.listValidMethods` on a flat Network. */
97inline std::vector<std::string> qns_methods() {
98 return {"default", "qns", "qns.conway", "qns.rolia",
99 "qns.zhou", "qns.suri", "qns.reiser", "qns.schmidt"};
100}
101
102/** True for `qns` and every `qns.NAME`. */
103inline bool is_qns_method(const std::string& method) {
104 std::string m = method;
105 std::transform(m.begin(), m.end(), m.begin(),
106 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
107 return m == "qns" || m.compare(0, 4, "qns.") == 0;
108}
109
110/**
111 * The multiserver approximation a Network method selects: the part after
112 * `qns.`, or `default` for `default` and `qns`. This bare name is what the
113 * engine speaks, and what the JMVA document and the `-m` switch carry.
114 */
115inline std::string qns_multiserver(const std::string& method) {
116 std::string m = method;
117 std::transform(m.begin(), m.end(), m.begin(),
118 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
119 return m.compare(0, 4, "qns.") == 0 ? m.substr(4) : std::string("default");
120}
121
122/** The approximations `qnsolver -m` accepts; the rest reach it only via LQNS. */
123inline bool is_qnsolver_multiserver(const std::string& m) {
124 return m == "conway" || m == "reiser" || m == "rolia" || m == "zhou";
125}
126
127/**
128 * `SolverLQNS.supportsModelMethod`'s structural rules on a Network, as the REASON they refuse,
129 * empty when the pair is served.
130 *
131 * A STRING RATHER THAN A THROW, the shape `ba::method_refusal` and
132 * `ag::runner_detail::method_refusal` already carry: the AUTO report has to ASK
133 * the question without raising, so that a pair it offers is a pair the run
134 * accepts. `solve_network_run_analyzer` keeps the throws, which are the same two
135 * rules stated where the run reaches them.
136 *
137 * Mirrors matlab/src/solvers/wrappers/LQNS/qns_immfeed_refusal.m and
138 * qns_multiserver_refusal.m, and the JAR's qnsImmfeedRefusal /
139 * qnsMultiserverRefusal.
140 */
141template <class T>
142std::string qns_method_refusal(const qn::NetworkStruct<T>& L, const std::string& method) {
144 return "SolverLQNS does not support immediate feedback (sn.immfeed): neither the JMVA "
145 "document qnsolver reads nor the LQN QN2LQN writes can keep a self-looping job on "
146 "its server. Use SolverCTMC or SolverSSA, whose state space carries the self-loop.";
147
148 // THE RULE IS INSIDE THE MULTISERVER BRANCH: without a multiserver station
149 // no -m is emitted and every method name is served by the plain invocation.
150 bool has_multiserver = false;
151 for (std::size_t i = 0; i < L.nstations && !has_multiserver; ++i) {
152 const double c = L.stations[i].nservers;
153 if (std::isfinite(c) && c > 1.0) has_multiserver = true;
154 }
155 if (!has_multiserver) return std::string();
156 const std::string ms = qns_multiserver(method);
157 if (ms == "default" || is_qnsolver_multiserver(ms)) return std::string();
158 return "SolverLQNS: the multiserver approximation '" + ms +
159 "' is one LQNS offers and qnsolver does not: 'qnsolver -m' accepts conway, reiser, "
160 "rolia and zhou only; suri and schmidt are available only on the non-product-form "
161 "closed SolverLQNS branch.";
162}
163
164namespace detail {
165
166/**
167 * The argv prefix every invocation uses.
168 *
169 * LD_LIBRARY_PATH IS STRIPPED, as `solver_qns.m` does on unix: the binaries of
170 * the LQNS distribution are linked against the system libstdc++, and a
171 * LD_LIBRARY_PATH inherited from a host that ships its own (MATLAB does) makes
172 * them fail to load on a GLIBCXX version symbol. Doing it through `env -u`
173 * rather than by editing this process's environment keeps the change confined to
174 * the child.
175 */
176inline std::vector<std::string> qnsolver_argv() {
177 return {"env", "-u", "LD_LIBRARY_PATH", "qnsolver"};
178}
179
180} // namespace detail
181
182/**
183 * Port of `SolverLQNS.hasQnsolver`: a native `qnsolver` binary on the PATH.
184 *
185 * There is no container fallback, here as in the other three codebases: qnsolver
186 * ships with LQNS, whose licence forbids providing the software to another
187 * party, so no LINE code path resolves or runs an image carrying it.
188 */
190 std::vector<std::string> argv = detail::qnsolver_argv();
191 argv.push_back("--help");
192 const util::ProcResult r = util::capture(argv, 30);
193 return r.exitCode == 0;
194}
195
196/** Port of `runAnalyzerNetwork`'s method gate. */
197inline void check_qns_method(const std::string& method) {
198 const std::vector<std::string> valid = qns_methods();
199 if (std::find(valid.begin(), valid.end(), method) == valid.end())
200 throw InputError("SolverLQNS: unknown method '" + method +
201 "' on a Network; valid methods are default, qns, qns.conway, qns.rolia, "
202 "qns.zhou, qns.suri, qns.reiser, qns.schmidt");
203}
204
205/**
206 * Port of the method -> `options.config.multiserver` map of `runAnalyzerNetwork.m`.
207 * Either spelling of the method is taken: the qns name or the bare name the
208 * engine speaks.
209 *
210 * `default` resolves to `rolia`, as `runAnalyzerNetwork.m` does. THE REFERENCE HAS TWO
211 * ENTRY POINTS AND THEY DISAGREE on an explicit config: `runAnalyzerNetwork.m`
212 * overwrites `options.config.multiserver` unconditionally at method `default`,
213 * so a MATLAB caller who sets it through the solver constructor is silently
214 * ignored, while `solver_qns.m` called directly honours it and maps its own
215 * `default` to CONWAY. This resolves the pair the only way that is a superset of
216 * neither: the method wins when named, an explicit config is honoured (the
217 * low-level reference's behaviour, and the only way a caller can express the
218 * choice at all), and an unset one gives rolia (the high-level reference's).
219 * Native Python currently leaves `default` at CONWAY here, which is a genuine
220 * divergence from MATLAB and the JAR; see the report accompanying this audit.
221 */
222inline std::string resolve_multiserver(const QnsOptions& opt) {
223 const std::string bare = is_qns_method(opt.method) ? qns_multiserver(opt.method) : opt.method;
224 if (bare != "default") return bare;
225 if (!opt.multiserver.empty()) return opt.multiserver;
226 return "rolia";
227}
228
229namespace detail {
230
231/** One parsed data row of the `qnsolver` CSV output. */
232struct ParsedRow {
233 std::string station;
234 std::vector<double> Q, W, U, Tp;
235};
236
237inline double parse_double_or_zero(const std::string& s) {
238 if (s.empty()) return 0.0;
239 char* end = nullptr;
240 const double v = std::strtod(s.c_str(), &end);
241 if (end == s.c_str()) return 0.0;
242 return v;
243}
244
245/**
246 * Split a `qnsolver` result row into its four metric blocks.
247 *
248 * THE STRIDE IS DECIDED BY THE CHAIN COUNT, not the class count. `qnsolver`
249 * writes a per-chain column followed by an aggregate one for each of Q, R, U and
250 * X when there is more than one chain, and drops the aggregate entirely at one
251 * chain -- confirmed against the binary: one chain gives
252 * `Station, $Q, $R, $U, $X` and two give `$Q(Chain01), $Q(Chain02), $Q, ...`.
253 * MATLAB and the JAR both switch on `sn.nclasses == 1`, which agrees only while
254 * classes and chains are in bijection: a class-switching model with two classes
255 * folded into ONE chain takes their multi-class branch against a single-chain
256 * document, and reads the $U column as the residence time (the JAR's length
257 * guard turns the same case into an all-zero table instead).
258 */
259inline bool parse_row(const std::string& line, std::size_t nchains, ParsedRow* out) {
260 if (line.find(',') == std::string::npos) return false;
261 if (line.find('$') != std::string::npos) return false; // the header
262 std::string s;
263 for (std::size_t i = 0; i < line.size(); ++i)
264 if (line[i] != ' ' && line[i] != '\t' && line[i] != '\r') s += line[i];
265
266 std::vector<std::string> parts;
267 std::size_t b = 0;
268 while (true) {
269 const std::size_t j = s.find(',', b);
270 parts.push_back(s.substr(b, j == std::string::npos ? j : j - b));
271 if (j == std::string::npos) break;
272 b = j + 1;
273 }
274 const std::size_t stride = nchains == 1 ? 1 : nchains + 1;
275 if (parts.size() < 1 + 4 * stride) return false;
276
277 out->station = parts[0];
278 out->Q.assign(nchains, 0.0);
279 out->W.assign(nchains, 0.0);
280 out->U.assign(nchains, 0.0);
281 out->Tp.assign(nchains, 0.0);
282 std::size_t ptr = 1;
283 for (std::size_t c = 0; c < nchains; ++c) out->Q[c] = parse_double_or_zero(parts[ptr + c]);
284 ptr += stride;
285 for (std::size_t c = 0; c < nchains; ++c) out->W[c] = parse_double_or_zero(parts[ptr + c]);
286 ptr += stride;
287 for (std::size_t c = 0; c < nchains; ++c) out->U[c] = parse_double_or_zero(parts[ptr + c]);
288 ptr += stride;
289 for (std::size_t c = 0; c < nchains; ++c) out->Tp[c] = parse_double_or_zero(parts[ptr + c]);
290 return true;
291}
292
293} // namespace detail
294
295/**
296 * The gate `runAnalyzer.m` reaches through `runAnalyzerChecks`, narrowed to what
297 * the JMVA document can actually carry.
298 *
299 * The reference's feature set admits nodes -- a Cache, a Place, a Transition --
300 * that `writeJMVA` then drops on the floor, and the tool answers a SMALLER model
301 * than the caller handed it with no indication that it did. A station the
302 * document cannot express is refused by name here instead.
303 */
304template <class T>
306 for (std::size_t i = 0; i < L.nstations; ++i) {
307 const qn::NodeType nt = L.stations[i].nodetype;
308 if (nt == qn::NodeType::Queue || nt == qn::NodeType::Delay ||
309 nt == qn::NodeType::Source)
310 continue;
311 throw UnsupportedError(
312 "SolverLQNS: station '" + L.nodes[L.station_to_node[i] - 1].name +
313 "' is not a Queue, a Delay or a Source, and the JMVA document qnsolver reads "
314 "carries no other station type");
315 }
316 if (L.has_priorities())
317 throw UnsupportedError(
318 "SolverLQNS: class priorities are outside the product-form envelope qnsolver "
319 "evaluates");
320}
321
322namespace detail {
323
324/**
325 * The engine of `solve_network_run_analyzer`: `solver_qns.m` and the QN2LQN
326 * branch of `runAnalyzerNetwork.m`, speaking the BARE approximation name in
327 * `opt.method` (`default`, `conway`, ...), which is what reaches the JMVA
328 * document and the `-m` switch.
329 */
330template <class T>
331mva::AvgResult<T> qnsolver_engine(const qn::NetworkStruct<T>& L, const QnsOptions& opt) {
332 // NOTHING under the qns path reads `sn.cap` or `sn.classcap`, on EITHER of
333 // the two routes below: the JMVA document is read by `qnsolver`, whose
334 // MVA-family algorithms have no representation of a finite buffer, and the
335 // QN2LQN route hands the model to LQNS, which has none either. A capped
336 // station was therefore solved as an unbounded one and the table reported
337 // the unconstrained answer under this solver's name. There is no
338 // feature-registry name for plain capacity, hence the structural test --
339 // `SolverMVA`, `SolverNC`, `SolverAG` and `SolverFLD` gate the same way,
340 // through this same helper. It sits HERE rather than in `check_supported`,
341 // which is deliberately the narrower JMVA-document gate and is skipped on
342 // the layered route.
343 qn::check_binding_capacity("SolverLQNS", L);
344
345 // IMMEDIATE FEEDBACK keeps a self-looping job on its server instead of
346 // re-queueing it, and neither path below can state that: the JMVA document
347 // `qnsolver` reads carries a mean demand and a visit count per chain, and
348 // the LQN `qn2lqn` writes turns the routing into OR-fork precedences of
349 // pseudo-activities on the reference task, where a repeated visit is a NEW
350 // CALL. Either would answer for re-queueing under this solver's name.
351 // SolverMVA warns and solves on the same property; here it is a refusal
352 // because the two conversions cannot represent the self-loop at all.
353 // Mirrors matlab/src/solvers/wrappers/LQNS/qns_immfeed_refusal.m.
355 throw UnsupportedError(
356 "SolverLQNS does not support immediate feedback (sn.immfeed): neither the JMVA "
357 "document qnsolver reads nor the LQN QN2LQN writes can keep a self-looping job on "
358 "its server. Use SolverCTMC or SolverSSA, whose state space carries the self-loop.");
359
360 const std::size_t M = L.nstations, K = L.nclasses, C = L.nchains;
361
362 bool has_open = false;
363 for (std::size_t k = 0; k < K; ++k)
364 if (!std::isfinite(L.classes[k].population)) has_open = true;
365
366 if (!L.has_product_form() && !has_open) {
367 if (L.has_priorities())
368 throw UnsupportedError(
369 "SolverLQNS: class priorities are outside the QN2LQN conversion envelope");
370
371 const lqn::LqnModel<T> layered = io::qn2lqn(L);
372 LqnsOptions lo;
373 lo.multiserver = resolve_multiserver(opt);
374 lo.samples = static_cast<double>(opt.samples);
375 lo.keep = opt.keep;
376 lo.timeout_seconds = opt.timeout;
377 SolverLQNS<T> solver(layered, lo);
378 const LqnsSolution<T> ls = solver.get_ensemble_avg();
379 const lqn::LqnStruct<T>& lsn = solver.get_struct();
380 const T zero = num_traits<T>::from_int(0);
381 Matrix<T> Q(M, K, zero), U(M, K, zero), R(M, K, zero), Tp(M, K, zero);
382
383 const auto element = [&](const std::string& name, lqn::LqnElement kind) {
384 for (std::size_t idx = 1; idx <= lsn.nidx; ++idx)
385 if (lsn.type[idx] == kind && lsn.names[idx] == name) return idx;
386 return std::size_t(0);
387 };
388 for (std::size_t i = 0; i < M; ++i) {
389 const std::size_t node = L.station_to_node[i] - 1;
390 const qn::NodeType type = L.nodes[node].nodetype;
391 if (type != qn::NodeType::Queue && type != qn::NodeType::Delay) continue;
392 for (std::size_t r = 0; r < K; ++r) {
393 const std::string suffix = std::to_string(node + 1) + "_" +
394 std::to_string(r + 1);
395 const std::size_t a = element("Q" + suffix, lqn::LqnElement::ACTIVITY);
396 if (a == 0) continue; // the class does not visit this station
397 if (ls.defined_Q[a]) Q(i, r) = ls.QN[a];
398 if (ls.defined_U[a]) U(i, r) = ls.UN[a];
399 if (ls.defined_R[a]) R(i, r) = ls.RN[a];
400 if (ls.defined_T[a]) Tp(i, r) = ls.TN[a];
401
402 // Some LQNS releases omit activity utilization/throughput but
403 // report the bound entry phase. The JAR carries the same
404 // fallback, and it changes no value when the activity row is
405 // present.
406 const std::size_t e = element("E" + suffix, lqn::LqnElement::ENTRY);
407 if (e != 0 && !ls.defined_U[a] && ls.defined_U[e]) U(i, r) = ls.UN[e];
408 if (e != 0 && !ls.defined_T[a] && ls.defined_T[e]) Tp(i, r) = ls.TN[e];
409
410 const double servers = L.stations[i].nservers;
411 if (std::isfinite(servers) && servers > 0.0)
412 U(i, r) = T(U(i, r) / num_traits<T>::from_double(servers));
413 }
414 }
415
416 mva::AvgResult<T> out;
417 out.QN = mva::filter_metric(L, Q, mva::MetricKind::QLen, nullptr);
418 out.UN = mva::filter_metric(L, U, mva::MetricKind::Util, nullptr);
419 out.RN = mva::filter_metric(L, R, mva::MetricKind::RespT, nullptr);
420 out.TN = mva::filter_metric(L, Tp, mva::MetricKind::Tput, nullptr);
421 out.AN = mva::filter_metric(L, mva::sn_get_arvr_from_tput(L, out.TN),
422 mva::MetricKind::ArvR, nullptr);
423 // runAnalyzer.m ultimately passes an empty residence-time table to
424 // setAvgResults, which derives it from response time and visits.
426 mva::MetricKind::ResidT, nullptr);
427 out.CN.clear();
428 out.XN.clear();
429 out.method = opt.method;
430 const std::string actual = resolve_multiserver(opt);
431 out.actualmethod = opt.method == "default" ? ("default/" + actual) : actual;
432 out.iter = ls.iterations;
433 return out;
434 }
435
436 // This is the narrower capability gate of the JMVA document. The layered
437 // path above legitimately contains routing and Join nodes that JMVA cannot
438 // encode, so applying it before dispatch would reject the very models
439 // QN2LQN exists to convert.
441
442 const std::string ms = resolve_multiserver(opt);
443
444 // The reference passes -m only when a multiserver station is present, so a
445 // single-server model answers identically under every method name.
446 bool has_multiserver = false;
447 for (std::size_t i = 0; i < M; ++i) {
448 const double c = L.stations[i].nservers;
449 if (std::isfinite(c) && c > 1.0) has_multiserver = true;
450 }
451
452 // THE GATE BELONGS INSIDE THE MULTISERVER BRANCH, because that is the only
453 // branch the approximation reaches. Without a multiserver station the
454 // reference emits no -m at all and answers under the caller's method name,
455 // so refusing 'suri' there would refuse a model the reference solves; with
456 // one, `solver_qns.m` falls off its switch and leaves `cmd` unassigned,
457 // which is an undefined-variable error rather than a diagnosis.
458 if (has_multiserver && !is_qnsolver_multiserver(ms))
459 throw UnsupportedError(
460 "SolverLQNS: the multiserver approximation '" + ms +
461 "' is one LQNS offers and qnsolver does not: 'qnsolver -m' accepts conway, reiser, "
462 "rolia and zhou only; suri and schmidt are available only on the non-product-form "
463 "closed SolverLQNS branch");
464
466 throw UnsupportedError(
467 "SolverLQNS needs the external 'qnsolver' binary on the PATH. It ships with LQNS "
468 "(http://www.sce.carleton.ca/rads/lqns/); LINE distributes no copy and runs none "
469 "from a container image, because that licence forbids redistribution");
470
471 util::TempDir tmp("qns");
472 if (opt.keep) tmp.keep();
473 const std::string model_file = tmp.file("model.jmva");
474 const std::string result_file = tmp.file("result.jmva");
475 line::util::LineConsole::step("writing the JMVA model file");
476 io::write_jmva(L, model_file, opt.method, opt.samples);
477
478 std::vector<std::string> argv = detail::qnsolver_argv();
479 // `-l` IS `--linearizer`, AND IT TAKES NO ARGUMENT: `qnsolver --help` lists
480 // it beside `-e, --exact-mva` and `-s, --schweitzer` as a solver selector,
481 // and the input file is POSITIONAL. So this line does not mean "load the
482 // model" -- it selects Linearizer and lets the model file fall through as
483 // the positional argument. All four codebases emit it, so they agree, and
484 // that agreement is on LINEARIZER results rather than on the exact MVA
485 // qnsolver runs by default. Left as it is deliberately: changing it changes
486 // every qnsolver number in every codebase at once, which is the user's call.
487 argv.push_back("-l");
488 argv.push_back(model_file);
489 if (has_multiserver) argv.push_back("-m" + ms);
490 argv.push_back("-o");
491 argv.push_back(result_file);
492
493 line::util::LineConsole::step("running the qnsolver binary as a subprocess");
494 const util::ProcResult pr = util::capture(argv, opt.timeout);
495 if (pr.timedOut)
496 throw NumericError("SolverLQNS: qnsolver did not finish within " +
497 std::to_string(opt.timeout) + "s and was killed");
498 if (pr.exitCode != 0)
499 throw NumericError("SolverLQNS: qnsolver exited with code " +
500 std::to_string(pr.exitCode) +
501 (pr.out.empty() ? std::string() : ("\n" + pr.out)));
502
503 // ---- parse the chain-level table -------------------------------------
504 line::util::LineConsole::step("parsing the qnsolver output");
505 const T zero = num_traits<T>::from_int(0);
506 Matrix<T> Qchain(M, C, zero), Uchain(M, C, zero), Wchain(M, C, zero), Tchain(M, C, zero);
507 std::ifstream in(result_file.c_str());
508 if (!in)
509 // qnsolver exits 0 on a parse error, so the exit-code branch above never fires and its
510 // own message is the only evidence of what it rejected. Carry it here or a build that
511 // cannot read an <ldstation> reads as an unexplained missing file.
512 throw NumericError("SolverLQNS: qnsolver wrote no result file at '" + result_file + "'" +
513 (pr.out.empty() ? std::string() : ("\nqnsolver said: " + pr.out)));
514 std::string line;
515 std::size_t nrows = 0;
516 while (std::getline(in, line)) {
517 detail::ParsedRow row;
518 if (!detail::parse_row(line, C, &row)) continue;
519 std::size_t idx = M;
520 for (std::size_t i = 0; i < M; ++i)
521 if (L.nodes[L.station_to_node[i] - 1].name == row.station) {
522 idx = i;
523 break;
524 }
525 if (idx == M) continue; // a station qnsolver names and the model does not
526 for (std::size_t c = 0; c < C; ++c) {
527 Qchain(idx, c) = num_traits<T>::from_double(row.Q[c]);
528 Wchain(idx, c) = num_traits<T>::from_double(row.W[c]);
529 Uchain(idx, c) = num_traits<T>::from_double(row.U[c]);
530 Tchain(idx, c) = num_traits<T>::from_double(row.Tp[c]);
531 }
532 ++nrows;
533 }
534 if (nrows == 0)
535 throw NumericError(
536 "SolverLQNS: qnsolver produced no station rows this model recognises; its output "
537 "names no station of the model");
538
539 const mva::ChainDemands<T> d = mva::sn_get_demands_chain(L);
540
541 // ---- chain throughput at the reference station -----------------------
542 // THE REFERENCE STATION IS READ PER CHAIN, through the chain's first class.
543 // MATLAB and the JAR index `sn.refstat` -- a per-CLASS array -- with the
544 // chain number, which lands on an unrelated class's reference station as
545 // soon as class switching makes the two index spaces differ.
546 std::vector<T> Xchain(C, zero);
547 for (std::size_t c = 0; c < C; ++c) {
548 const std::size_t rstat = L.classes[L.inchain[c][0] - 1].refstat;
549 if (Tchain(rstat - 1, c) > zero) {
550 Xchain[c] = Tchain(rstat - 1, c);
551 continue;
552 }
553 // An open chain's reference station is the Source, which the JMVA
554 // document does not carry, so its row is zero; recover X from any
555 // station whose visit count is known.
556 for (std::size_t i = 0; i < M; ++i)
557 if (d.Vchain(i, c) > zero && Tchain(i, c) > zero) {
558 Xchain[c] = T(Tchain(i, c) / d.Vchain(i, c));
559 break;
560 }
561 }
562
563 // `Rchain = Wchain` of solver_qns.m: qnsolver's $R column is already the
564 // per-visit residence at the station, so it is not divided by the visits.
565 Matrix<T> Rchain = Wchain;
566 for (std::size_t i = 0; i < M; ++i)
567 for (std::size_t c = 0; c < C; ++c)
568 if (std::isnan(num_traits<T>::to_double(Rchain(i, c)))) Rchain(i, c) = zero;
569
570 // Utilization comes back summed over the servers of a multiserver station,
571 // which the ld encoding of writeJMVA turns it into; LINE reports it per
572 // server.
573 for (std::size_t i = 0; i < M; ++i) {
574 const double c = L.stations[i].nservers;
575 if (!std::isfinite(c)) continue;
576 for (std::size_t j = 0; j < C; ++j)
577 Uchain(i, j) = T(Uchain(i, j) / num_traits<T>::from_double(c));
578 }
579
580 const mva::ClassResults<T> cr =
581 mva::sn_deaggregate_chain_results(L, d, Qchain, Uchain, Rchain, Tchain, Xchain);
582
583 mva::AvgResult<T> out;
584 out.QN = mva::filter_metric(L, cr.Q, mva::MetricKind::QLen, nullptr);
585 out.UN = mva::filter_metric(L, cr.U, mva::MetricKind::Util, nullptr);
586 out.RN = mva::filter_metric(L, cr.R, mva::MetricKind::RespT, nullptr);
587 out.TN = mva::filter_metric(L, cr.Tp, mva::MetricKind::Tput, nullptr);
588 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(K, false));
589 for (std::size_t i = 0; i < M; ++i)
590 if (L.stations[i].nodetype == qn::NodeType::Source)
591 for (std::size_t k = 0; k < K; ++k) srcmask[i][k] = true;
593 &srcmask);
594 // MATLAB passes [] for the residence time and lets `getAvg` derive it from
595 // the response time and the visits, which is what this helper is; the JAR
596 // instead sets WN = RN, and the two agree only where the visit count is one.
598 mva::MetricKind::ResidT, nullptr);
599 out.CN = cr.C;
600 out.XN = cr.X;
601 out.method = opt.method;
602 // `default` is reported as 'default/<what ran>', the convention of
603 // runAnalyzer.m. Without a multiserver station no -m flag is passed, and the
604 // reference then leaves the label at the tool's own default.
605 const std::string ran = has_multiserver ? ms : std::string("default");
606 out.actualmethod = opt.method == "default" ? ("default/" + ran) : ran;
607 out.iter = 0;
608 return out;
609}
610
611} // namespace detail
612
613/**
614 * Port of `@@SolverLQNS/runAnalyzerNetwork.m` and `solver_qns.m`: SolverLQNS on
615 * a flat Network.
616 *
617 * The caller's qns method name is translated to the bare approximation name
618 * the engine speaks, and the engine's label is reported back under the qns
619 * names: `qns.NAME`, `default/qns.NAME` (a bare `default` stays as it is).
620 *
621 * @param L the refreshed struct
622 * @param opt the method, the multiserver rule and the JMVA sample cap
623 */
624template <class T>
626 check_qns_method(opt.method);
627 QnsOptions bare = opt;
628 bare.method = qns_multiserver(opt.method);
629 mva::AvgResult<T> out = detail::qnsolver_engine(L, bare);
630 out.method = opt.method;
631 std::string label = out.actualmethod;
632 std::string prefix;
633 if (label.compare(0, 8, "default/") == 0) {
634 prefix = "default/";
635 label = label.substr(prefix.size());
636 }
637 if (label != "default") label = "qns." + label;
638 out.actualmethod = prefix + label;
639 return out;
640}
641
642} // namespace lqns
643} // namespace line
644
645#endif // LINE_SOLVERS_WRAPPERS_LQNS_LQNS_QNSOLVER_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
bool has_immediate_feedback() const
Whether immediate feedback is EFFECTIVE anywhere in the model.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
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
static void step(const char *fmt,...)
Write one progress line.
The exception types the port throws.
The language-feature gate: what a MODEL uses against what a SOLVER declares.
Port of @@JMTIO/writeJMVA.m: the CHAIN-level product-form model in the JMVA interchange format.
Running progress log of a LINE solver run (the "solver console").
lqn::LqnModel< T > qn2lqn(const qn::NetworkStruct< T > &sn)
Port of MATLAB QN2LQN(model), over the refreshed C++ NetworkStruct.
Definition qn2lqn.h:66
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).
LqnElement
LQN element kinds, with the values of MATLAB LayeredNetworkElement.
Definition lang_types.h:466
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
mva::AvgResult< T > solve_network_run_analyzer(const qn::NetworkStruct< T > &L, const QnsOptions &opt)
Port of @@SolverLQNS/runAnalyzerNetwork.m and solver_qns.m: SolverLQNS on a flat Network.
void check_qnsolver_supported(const qn::NetworkStruct< T > &L)
The gate runAnalyzer.m reaches through runAnalyzerChecks, narrowed to what the JMVA document can actu...
std::vector< std::string > qns_methods()
Port of SolverLQNS.listValidMethods on a flat Network.
bool is_qnsolver_multiserver(const std::string &m)
The approximations qnsolver -m accepts; the rest reach it only via LQNS.
std::string qns_multiserver(const std::string &method)
The multiserver approximation a Network method selects: the part after qns.
std::string qns_method_refusal(const qn::NetworkStruct< T > &L, const std::string &method)
SolverLQNS.supportsModelMethod's structural rules on a Network, as the REASON they refuse,...
std::string resolve_multiserver(const QnsOptions &opt)
Port of the method -> options.config.multiserver map of runAnalyzerNetwork.m.
bool is_qns_method(const std::string &method)
True for qns and every qns.NAME.
void check_qns_method(const std::string &method)
Port of runAnalyzerNetwork's method gate.
bool qnsolver_is_available()
Port of SolverLQNS.hasQnsolver: a native qnsolver binary on the PATH.
Matrix< T > sn_get_residt_from_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &RN)
Port of sn_get_residt_from_respt: the per-JOB residence time.
Matrix< T > filter_metric(const qn::NetworkStruct< T > &L, const Matrix< T > &metric, MetricKind kind, const std::vector< std::vector< bool > > *zero_mask)
Port of filterMetric: what @@NetworkSolver/getAvg does between the analyzer and the caller.
ClassResults< T > sn_deaggregate_chain_results(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, const Matrix< T > &Qchain, const Matrix< T > &Uchain, const Matrix< T > &Rchain, const Matrix< T > &Tchain, const std::vector< T > &Xchain)
Port of sn_deaggregate_chain_results.
Definition sn_chain.h:214
Matrix< T > sn_get_arvr_from_tput(const qn::NetworkStruct< T > &L, const Matrix< T > &TN)
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
void check_binding_capacity(const std::string &solver, const NetworkStruct< T > &sn)
ProcResult capture(const std::vector< std::string > &argv, int timeoutSeconds, bool mergeStderr=false)
Runs a command, capturing stdout and discarding stderr.
Definition subprocess.h:82
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Convert a closed queueing network into the layered model used by SolverLQNS.
Chain aggregation and de-aggregation.
SolverLQNS: the layered model solved by the external lqns / lqsim binaries.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:409
SolverLQNS.defaultOptions on a flat Network plus the knobs the JMVA document carries.
std::size_t samples
options.samples, the JMVA maxSamples
int timeout
Seconds before a hung qnsolver is killed; not positive waits forever.
bool keep
options.keep: leave the scratch directory behind, to inspect what was sent.
std::string multiserver
options.config.multiserver.
std::string method
default, qns or qns.NAME; see qns_methods().
The metrics getAvg returns, after filtering.
std::string method
the method asked for
std::string actualmethod
the algorithm that ran
Outcome of a captured command.
Definition subprocess.h:42
int exitCode
Exit status, or -1 when the command could not run.
Definition subprocess.h:43
Running an external command and capturing its output, with a deadline.
A scratch directory for the subprocess wrappers, the port's lineTempName.