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