LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_lqns.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_SOLVER_LQNS_H
6#define LINE_SOLVERS_WRAPPERS_LQNS_SOLVER_LQNS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverLQNS: the layered model solved by the external lqns / lqsim binaries.
12 *
13 * Port of matlab/src/solvers/wrappers/LQNS/@@SolverLQNS (SolverLQNS.m,
14 * runAnalyzer.m, parseXMLResults.m, getEnsembleAvg.m, runRemoteLQNS.m), of
15 * jline.solvers.wrappers.lqns.SolverLQNS and of the native-Python twin. It is a
16 * WRAPPER, not an engine: nothing here computes a queueing result. The model is
17 * written as .lqnx (lqn_writer.h), a binary is run over it, and the .lqxo it
18 * leaves behind is read back.
19 *
20 * LINE SHIPS NO LQNS BINARY. Its licence is an evaluation agreement that
21 * forbids redistribution, so the binary is one the user installed; a missing
22 * one is reported by name, with the two ways to obtain an answer anyway (an
23 * install, or a host already running the REST service), rather than as a failed
24 * exec.
25 *
26 * WHAT THE COLUMNS MEAN, and why they are not the ones lqns prints. The
27 * reference's getEnsembleAvg permutes them so that a layered result reads the
28 * same whichever solver produced it:
29 *
30 * QLen <- the element's UTILIZATION (a task's utilization IS its
31 * mean number in service)
32 * Util <- its PROCESSOR utilization: lqns and SolverLN both sum it over
33 * the host's servers, so no rescaling applies. Verbatim for hosts,
34 * tasks and activities; for an entry it is aggregated over the
35 * activity graph, which lqns itself reports as 0 in the
36 * activity-graph form
37 * RespT <- its PHASE 1 SERVICE TIME
38 * Tput <- its throughput
39 * ResidT, ArvR: lqns computes neither, so they stay undefined rather than
40 * being filled with a zero that would read as a computed value
41 *
42 * THE .lqxo IS THE ONLY OUTPUT READ. lqns also prints a human-readable report;
43 * parsing that instead would tie the port to a print format that changes
44 * between releases, and it carries fewer digits than the XML.
45 */
46
48#include <algorithm>
49#include <cmath>
50#include <cstdio>
51#include <cstdlib>
52#include <ctime>
53#include <fstream>
54#include <iterator>
55#include <limits>
56#include <map>
57#include <memory>
58#include <string>
59#include <utility>
60#include <vector>
61
62#include <dirent.h>
63#include <sys/stat.h>
64#include <unistd.h>
65
66#include "json.hpp"
70#include "line/util/error.h"
71#include "line/util/http.h"
73#include "line/util/tempdir.h"
74#include "line/util/xml.h"
75
76namespace line {
77namespace lqns {
78
79/** Knobs of the wrapper, the subset of SolverOptions that reaches lqns. */
81 /**
82 * default | lqns | srvn | exactmva | srvn.exactmva | sim | lqsim | lqnsdefault
83 *
84 * `sim` and `lqsim` run the SIMULATOR and are the only stochastic ones;
85 * `lqnsdefault` is lqns with no pragma at all, which is a different fixed
86 * point from `default` and not a synonym for it.
87 */
88 std::string method = "default";
89 /** conway | rolia | zhou | suri | reiser | schmidt | default (= rolia). */
90 std::string multiserver = "default";
91 /** lqsim run length, `-A`; not positive leaves lqsim's own default. */
92 double samples = 10000.0;
93 bool verbose = false;
94 /** Keep the working directory, model and result file after the run. */
95 bool keep = false;
96 /** Deadline for the child, in seconds; not positive waits indefinitely. */
98 /** Solve on a host running lqns-rest instead of on this machine. */
99 bool remote = false;
100 std::string remote_url = "http://localhost:8080";
101};
102
103/**
104 * Everything the .lqxo carries, indexed as the struct is.
105 *
106 * The element vectors are (nidx+1) with slot 0 unused and `callwaiting` is
107 * (ncalls+1); an entry lqns did not report stays NaN, which is how the
108 * reference leaves it and is what tells a caller "not reported" apart from
109 * "reported as zero".
110 */
112 std::vector<double> util, phase1util, phase2util;
113 std::vector<double> phase1svct, phase2svct;
114 std::vector<double> tput, procwaiting, procutil;
115 std::vector<double> callwaiting;
116 int iterations = 0;
117};
118
119/** The six measures, on the element index space, with a defined mask each. */
120template <class T>
122 std::vector<T> QN, UN, RN, TN, AN, WN;
124 int iterations = 0;
125};
126
127namespace detail {
128
129/** NaN as the .lqxo reader uses it: "lqns did not report this". */
130inline double nan_value() { return std::numeric_limits<double>::quiet_NaN(); }
131
132/** An attribute as a double, NaN when absent or unparsable (str2double). */
133inline double attr_num(const xml::Element* e, const char* key) {
134 const std::string s = e->attr(key);
135 if (s.empty()) return nan_value();
136 char* end = nullptr;
137 const double v = std::strtod(s.c_str(), &end);
138 if (end == s.c_str()) return nan_value();
139 return v;
140}
141
142/**
143 * Snap a value within CoarseTol of an exact tenth onto it.
144 *
145 * lqns reports a quantity that is analytically a multiple of 0.1 with a few
146 * digits of iteration noise, and the reference removes that noise in
147 * getAvgTable before tabulating. It belongs to the TABLE and not to getAvg: the
148 * raw fixed point is what a parity comparison should see.
149 */
150inline double snap_to_tenth(double v) {
151 if (!std::isfinite(v)) return v;
152 const double scaled = v * 10.0;
153 const double snapped = std::floor(scaled + 0.5);
154 if (std::fabs(scaled - snapped) < lang::GlobalConstants::CoarseTol * scaled)
155 return snapped / 10.0;
156 return v;
157}
158
159/**
160 * A private working directory, the C++ lineTempName.
161 *
162 * Delegated rather than rolled again here: LINE_WORKSPACE_ROOT has to relocate
163 * the staged .lqnx, since a containerized lqns bind-mounts that root alone and
164 * sees nothing left under TMPDIR.
165 */
166inline std::string make_temp_dir(const std::string& tag) { return util::make_temp_dir(tag); }
167
168/** Remove a directory this wrapper created, with every file it holds. */
169inline void remove_temp_dir(const std::string& dir) {
170 DIR* d = ::opendir(dir.c_str());
171 if (d != nullptr) {
172 for (struct dirent* ent = ::readdir(d); ent != nullptr; ent = ::readdir(d)) {
173 const std::string name(ent->d_name);
174 if (name == "." || name == "..") continue;
175 ::unlink((dir + "/" + name).c_str());
176 }
177 ::closedir(d);
178 }
179 ::rmdir(dir.c_str());
180}
181
182} // namespace detail
183
184/**
185 * The layered model solved by lqns or lqsim.
186 *
187 * Constructed over the INTERMEDIATE model (lqn_reader.h), not over the struct,
188 * because the document handed to the binary has to carry the precedence blocks
189 * and reply entries that getStruct flattens away.
190 */
191template <class T>
193public:
195 : model_(model), opt_(options), sn_(lqn::lqn_finalize(model)) {
196 if (!is_available() && !opt_.remote)
197 throw UnsupportedError(
198 "SolverLQNS requires the lqns and lqsim commands on the system path.\n"
199 "Obtain them from their authors at: http://www.sce.carleton.ca/rads/lqns/\n"
200 "LINE ships no LQNS binary and does not redistribute one.\n\n"
201 "Alternatively, point LINE at a host that already runs LQNS by setting\n"
202 "LqnsOptions::remote and LqnsOptions::remote_url.");
203 const std::string m = opt_.method.empty() ? std::string("default") : opt_.method;
204 const std::vector<std::string> valid = list_valid_methods();
205 if (std::find(valid.begin(), valid.end(), m) == valid.end())
206 throw InputError("SolverLQNS: '" + m +
207 "' is not a method of this solver; it takes default, lqns, srvn, "
208 "exactmva, srvn.exactmva, sim, lqsim and lqnsdefault");
209 opt_.method = m;
210 multiserver_pragma(opt_.multiserver); // refuses an unknown policy here, not mid-run
211 }
212
213 /** True when the native lqns command answers on this machine, any release. */
214 static bool has_local_binary() { return !lqns_version().empty(); }
215
216 /** True when a local binary is installed AND is 6.0 or greater. */
217 static bool is_available() { return lqns_is_available(); }
218
219 /** The version banner of the local binary, empty when there is none. */
220 static std::string version() { return lqns_version(); }
221
222 static std::vector<std::string> list_valid_methods() {
223 std::vector<std::string> m;
224 m.push_back("default");
225 m.push_back("lqns");
226 m.push_back("srvn");
227 m.push_back("exactmva");
228 m.push_back("srvn.exactmva");
229 m.push_back("sim");
230 m.push_back("lqsim");
231 m.push_back("lqnsdefault");
232 return m;
233 }
234
235 /** Only the lqsim methods draw random numbers. */
236 static bool is_stochastic_method(const std::string& method) {
237 return method == "sim" || method == "lqsim";
238 }
239
240 /**
241 * The per-layer feature set, as SolverLQNS.supports declares it.
242 *
243 * A layered model reaches lqns as a whole, so this is the set each LAYER
244 * must fall inside: the product-form station kinds and the four service
245 * distributions the LQN schema can carry.
246 */
247 static std::vector<std::string> feature_set() {
248 const char* names[] = {"Sink", "Source", "Queue",
249 "Coxian", "Erlang", "Exp",
250 "HyperExp", "Buffer", "Server",
251 "JobSink", "RandomSource", "ServiceTunnel",
252 "SchedStrategy_PS", "SchedStrategy_FCFS", "ClosedClass"};
253 return std::vector<std::string>(names, names + sizeof(names) / sizeof(names[0]));
254 }
255
256 const lqn::LqnStruct<T>& get_struct() const { return sn_; }
257 const LqnsOptions& options() const { return opt_; }
258 /** Wall-clock seconds of the last run, the reference's `runtime`. */
259 double runtime() const { return runtime_; }
260 int iterations() const { return raw_.iterations; }
261 /** Everything the .lqxo carried, before the column permutation. */
263 run_analyzer_once();
264 return raw_;
265 }
266
267 /**
268 * Run the binary and read its result back.
269 *
270 * Re-running is what the reference does on every getEnsembleAvg call, but
271 * repeating a lqsim run would silently change the answer between two reads
272 * of the same solver object, so the result is computed once and kept.
273 */
275 const std::clock_t t0 = std::clock();
276 const std::string dir = detail::make_temp_dir("lqns");
277 const std::string stem = dir + "/model";
278 const std::string modelfile = stem + ".lqnx";
279 const std::string resultfile = stem + ".lqxo";
280 try {
281 line::util::LineConsole::step("writing the LQN model to %s", modelfile.c_str());
282 const lqn::LqnWriteReport rep = lqn::write_lqnx(model_, modelfile, "LQN");
283 if (opt_.verbose)
284 for (std::size_t i = 0; i < rep.dropped.size(); ++i)
285 std::fprintf(stderr, "SolverLQNS: %s\n", rep.dropped[i].c_str());
286
287 if (opt_.remote) run_remote(modelfile, resultfile);
288 else run_local(modelfile);
289
290 line::util::LineConsole::step("parsing the lqns XML results");
291 parse_lqxo(resultfile);
292 } catch (...) {
293 if (!opt_.keep) detail::remove_temp_dir(dir);
294 throw;
295 }
296 if (!opt_.keep) detail::remove_temp_dir(dir);
297 else if (opt_.verbose) std::fprintf(stderr, "SolverLQNS: files kept in %s\n", dir.c_str());
298 runtime_ = double(std::clock() - t0) / double(CLOCKS_PER_SEC);
299 solved_ = true;
300 }
301
302 /**
303 * The six measures on the element index space.
304 *
305 * Port of getEnsembleAvg.m, including the host-multiplicity rescaling of
306 * the utilization column.
307 */
309 run_analyzer_once();
310 const std::size_t n = sn_.nidx;
312 s.iterations = raw_.iterations;
313 auto fill = [&](std::vector<T>& v, std::vector<bool>& d, const std::vector<double>& src) {
314 v.assign(n + 1, num_traits<T>::from_int(0));
315 d.assign(n + 1, false);
316 for (std::size_t i = 1; i <= n; ++i) {
317 if (std::isnan(src[i])) continue;
318 v[i] = num_traits<T>::from_double(src[i]);
319 d[i] = true;
320 }
321 };
322 // Both lqns and SolverLN report the processor utilization summed over
323 // the host's servers, so no rescaling applies; the entry rows were
324 // aggregated over the activity graph by aggregate_entry_procutil.
325 fill(s.QN, s.defined_Q, raw_.util);
326 fill(s.UN, s.defined_U, raw_.procutil);
327 fill(s.RN, s.defined_R, raw_.phase1svct);
328 fill(s.TN, s.defined_T, raw_.tput);
329 // lqns computes neither a residence time nor an arrival rate per
330 // element; leaving them undefined is the whole of the claim.
331 s.AN.assign(n + 1, num_traits<T>::from_int(0));
332 s.WN.assign(n + 1, num_traits<T>::from_int(0));
333 s.defined_A.assign(n + 1, false);
334 s.defined_W.assign(n + 1, false);
335 return s;
336 }
337
338private:
339 void run_analyzer_once() {
340 if (!solved_) run_analyzer();
341 }
342
343 /** The `-Pmultiserver=` argument of a policy name, empty when there is none. */
344 static std::string multiserver_pragma(const std::string& policy) {
345 if (policy.empty() || policy == "none") return std::string();
346 if (policy == "default") return "-Pmultiserver=rolia";
347 if (policy == "conway" || policy == "rolia" || policy == "zhou" || policy == "suri" ||
348 policy == "reiser" || policy == "schmidt")
349 return "-Pmultiserver=" + policy;
350 throw InputError("SolverLQNS: '" + policy +
351 "' is not a multiserver policy of lqns; it takes conway, rolia, zhou, "
352 "suri, reiser, schmidt and default");
353 }
354
355 /**
356 * The command line, as an argv vector.
357 *
358 * NO SHELL SEES THESE ARGUMENTS. The reference builds one string and hands
359 * it to system(); a model path holding a space would be word-split there,
360 * and this port's working directory comes from TMPDIR, which the caller
361 * controls.
362 *
363 * `env -u LD_LIBRARY_PATH`, which the MATLAB wrapper prefixes, is NOT
364 * reproduced: it exists because MATLAB injects its own libstdc++ ahead of
365 * the system one and lqns then fails to find a GLIBCXX symbol. A plain C++
366 * process has no such injection, and stripping the variable here would
367 * instead break a user who set it to reach their own lqns.
368 */
369 std::vector<std::string> build_argv(const std::string& modelfile) const {
370 const bool sim = is_stochastic_method(opt_.method);
371 std::vector<std::string> a;
372 a.push_back(sim ? "lqsim" : "lqns");
373 if (!opt_.verbose) {
374 a.push_back("-a"); // no advisories
375 a.push_back("-w"); // no warnings
376 }
377 // The simulator has no MVA to configure, so the multiserver pragma is
378 // not passed to it, exactly as the reference declines to.
379 if (!sim) {
380 const std::string ms = multiserver_pragma(opt_.multiserver);
381 if (!ms.empty()) a.push_back(ms);
382 }
383 if (opt_.method == "srvn" || opt_.method == "srvn.exactmva") a.push_back("-Playering=srvn");
384 if (opt_.method == "exactmva" || opt_.method == "srvn.exactmva") a.push_back("-Pmva=exact");
385 if (sim && opt_.samples > 0) {
386 char buf[32];
387 std::snprintf(buf, sizeof(buf), "%.0f", opt_.samples);
388 a.push_back("-A");
389 a.push_back(buf);
390 }
391 // `lqnsdefault` is lqns with NO pragma: a model that loses messages
392 // stops the run there, which is the binary's own default and a
393 // different answer from the one the other methods ask for.
394 if (opt_.method != "lqnsdefault") a.push_back("-Pstop-on-message-loss=false");
395 a.push_back("-x"); // XML result, the .lqxo this wrapper reads
396 a.push_back(modelfile);
397 return a;
398 }
399
400 void run_local(const std::string& modelfile) const {
401 const std::vector<std::string> argv = build_argv(modelfile);
402 if (opt_.verbose) {
403 std::string cmd;
404 for (std::size_t i = 0; i < argv.size(); ++i) cmd += (i ? " " : "") + argv[i];
405 std::fprintf(stderr, "SolverLQNS command: %s\n", cmd.c_str());
406 }
407 line::util::LineConsole::step("running the lqns binary as a subprocess");
408 const util::ProcResult r = util::capture(argv, opt_.timeout_seconds);
409 if (r.timedOut)
410 throw Error("SolverLQNS: " + argv[0] + " did not finish within " +
411 std::to_string(opt_.timeout_seconds) + "s and was killed");
412 if (r.exitCode < 0)
413 throw UnsupportedError("SolverLQNS: could not run '" + argv[0] +
414 "'; LINE ships no LQNS binary, so it must be installed and on "
415 "the system path");
416 if (opt_.verbose && !r.out.empty()) std::fprintf(stderr, "%s", r.out.c_str());
417 }
418
419 /**
420 * The lqns-rest protocol: one POST carrying the whole document.
421 *
422 * Twin of runRemoteLQNS.m and of the native-Python `_run_remote_lqns`. The
423 * service answers with the .lqxo text, which is written where the local run
424 * would have left it so that ONE parser serves both paths.
425 */
426 void run_remote(const std::string& modelfile, const std::string& resultfile) const {
427 std::string content;
428 {
429 std::ifstream in(modelfile.c_str());
430 if (!in) throw Error("SolverLQNS: cannot read the model it just wrote: " + modelfile);
431 content.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
432 }
433 const bool sim = is_stochastic_method(opt_.method);
434 std::string base = opt_.remote_url;
435 while (!base.empty() && base[base.size() - 1] == '/') base.erase(base.size() - 1);
436 const std::string url = base + (sim ? "/api/v1/solve/lqsim" : "/api/v1/solve/lqns");
437
438 nlohmann::json req;
439 req["model"]["content"] = content;
440 req["model"]["base64"] = false;
441 req["options"]["include_raw_output"] = true;
442 if (sim) {
443 req["options"]["blocks"] = 30;
444 if (opt_.samples > 0) req["options"]["run_time"] = opt_.samples;
445 } else {
446 std::string ms = opt_.multiserver.empty() || opt_.multiserver == "default"
447 ? std::string("rolia")
448 : opt_.multiserver;
449 req["options"]["pragmas"]["multiserver"] = ms;
450 req["options"]["pragmas"]["stop_on_message_loss"] = false;
451 if (opt_.method == "srvn" || opt_.method == "srvn.exactmva")
452 req["options"]["pragmas"]["layering"] = "srvn";
453 if (opt_.method == "exactmva" || opt_.method == "srvn.exactmva")
454 req["options"]["pragmas"]["mva"] = "exact";
455 }
456
457 const int timeout_ms =
458 (opt_.timeout_seconds > 0 ? opt_.timeout_seconds : 300) * 1000;
459 const http::Response resp = http::post_json(url, req.dump(), timeout_ms);
460 if (resp.status < 200 || resp.status >= 300)
461 throw Error("SolverLQNS: remote LQNS at " + url + " returned HTTP " +
462 std::to_string(resp.status) + ": " + resp.body);
463 nlohmann::json out;
464 try {
465 out = nlohmann::json::parse(resp.body);
466 } catch (const std::exception& e) {
467 throw Error(std::string("SolverLQNS: remote LQNS returned a body that is not JSON: ") +
468 e.what());
469 }
470 const std::string status = out.value("status", std::string());
471 if (status == "error" || status == "failed")
472 throw Error("SolverLQNS: remote solver returned an error: " +
473 out.value("error", std::string("unknown error")));
474 std::string lqxo;
475 if (out.contains("raw_output") && out["raw_output"].is_object())
476 lqxo = out["raw_output"].value("lqxo", std::string());
477 if (lqxo.empty())
478 throw Error("SolverLQNS: remote solver did not return an LQXO document");
479 std::ofstream f(resultfile.c_str());
480 if (!f) throw Error("SolverLQNS: cannot write the remote result to " + resultfile);
481 f << lqxo;
482 }
483
484 /**
485 * Element index of a name OF A GIVEN KIND, 0 when we hold no such element.
486 *
487 * THE KIND IS PART OF THE KEY, and has to be. A LINE-generated layered model
488 * routinely gives a processor, its task and that task's entry the SAME name
489 * (`c0` in the randomLQN corpus, and throughout lqn_ofbiz), and `lqn.names`
490 * holds all three. The reference looks the name up in that flat list with
491 * `findstring`, which returns EVERY match, and then assigns the result to
492 * all of them -- so a processor row ends up carrying its entry's numbers,
493 * whichever element was written last. The .lqxo says which kind it is
494 * describing, in the tag being read, so the ambiguity does not have to exist
495 * here and this port does not reproduce it. On a model whose names are
496 * unique the two agree element for element.
497 */
498 std::size_t index_of(const std::string& name, lang::LqnElement kind) const {
499 const std::map<std::pair<int, std::string>, std::size_t>::const_iterator it =
500 byname_.find(std::make_pair(static_cast<int>(kind), name));
501 return it == byname_.end() ? 0 : it->second;
502 }
503
504 std::size_t call_index_of(const std::string& name) const {
505 const std::map<std::string, std::size_t>::const_iterator it = bycallname_.find(name);
506 return it == bycallname_.end() ? 0 : it->second;
507 }
508
509 void init_name_index() {
510 if (!byname_.empty()) return;
511 // First declaration wins within a kind, as `find(strcmp(...))(1)` would.
512 for (std::size_t i = 1; i <= sn_.nidx; ++i)
513 byname_.insert(std::make_pair(
514 std::make_pair(static_cast<int>(sn_.type[i]), sn_.names[i]), i));
515 for (std::size_t c = 1; c <= sn_.ncalls; ++c)
516 bycallname_.insert(std::make_pair(sn_.callnames[c], c));
517 }
518
519 /**
520 * Read the .lqxo, a port of parseXMLResults.m.
521 *
522 * The walk is processor -> task -> entry -> phase activities, then the task
523 * activity graph. It is keyed on NAMES throughout, so an activity ordering
524 * that differs between the document and the struct cannot mis-assign a row.
525 */
526 void parse_lqxo(const std::string& resultfile) {
527 init_name_index();
528 const std::size_t n = sn_.nidx;
529 const double nan = detail::nan_value();
530 raw_ = LqnsRawAvg();
531 raw_.util.assign(n + 1, nan);
532 raw_.phase1util.assign(n + 1, nan);
533 raw_.phase2util.assign(n + 1, nan);
534 raw_.phase1svct.assign(n + 1, nan);
535 raw_.phase2svct.assign(n + 1, nan);
536 raw_.tput.assign(n + 1, nan);
537 raw_.procwaiting.assign(n + 1, nan);
538 raw_.procutil.assign(n + 1, nan);
539 raw_.callwaiting.assign(sn_.ncalls + 1, nan);
540
541 std::unique_ptr<xml::Element> doc;
542 try {
543 doc = xml::parse_file(resultfile);
544 } catch (const Error&) {
545 throw Error(
546 "SolverLQNS: no readable result at " + resultfile +
547 "; the binary ran but wrote no .lqxo, which is what it does when it rejects the "
548 "model (run with verbose to see its diagnostics)");
549 }
550
551 for (const xml::Element* sp : doc->by_tag("solver-params")) {
552 const std::vector<const xml::Element*> g = sp->by_tag("result-general");
553 if (!g.empty()) raw_.iterations = static_cast<int>(detail::attr_num(g[0], "iterations"));
554 }
555
556 for (const xml::Element* pe : doc->by_tag("processor")) {
557 const std::size_t pidx = index_of(pe->attr("name"), lang::LqnElement::HOST);
558 const std::vector<const xml::Element*> pr = pe->by_tag("result-processor");
559 if (pidx && !pr.empty()) raw_.procutil[pidx] = detail::attr_num(pr[0], "utilization");
560
561 for (const xml::Element* te : pe->by_tag("task")) {
562 const std::size_t tidx = index_of(te->attr("name"), lang::LqnElement::TASK);
563 const std::vector<const xml::Element*> tr = te->by_tag("result-task");
564 double task_tput = nan;
565 if (!tr.empty()) {
566 task_tput = detail::attr_num(tr[0], "throughput");
567 if (tidx) {
568 raw_.util[tidx] = detail::attr_num(tr[0], "utilization");
569 raw_.phase1util[tidx] = detail::attr_num(tr[0], "phase1-utilization");
570 raw_.phase2util[tidx] = detail::attr_num(tr[0], "phase2-utilization");
571 raw_.tput[tidx] = task_tput;
572 raw_.procutil[tidx] = detail::attr_num(tr[0], "proc-utilization");
573 }
574 }
575
576 for (const xml::Element* ee : te->by_tag("entry")) {
577 const std::size_t eidx = index_of(ee->attr("name"), lang::LqnElement::ENTRY);
578 const std::vector<const xml::Element*> er = ee->by_tag("result-entry");
579 double entry_tput = nan;
580 if (!er.empty()) {
581 entry_tput = detail::attr_num(er[0], "throughput");
582 if (eidx) {
583 raw_.util[eidx] = detail::attr_num(er[0], "utilization");
584 raw_.phase1util[eidx] = detail::attr_num(er[0], "phase1-utilization");
585 raw_.phase2util[eidx] = detail::attr_num(er[0], "phase2-utilization");
586 raw_.phase1svct[eidx] = detail::attr_num(er[0], "phase1-service-time");
587 raw_.phase2svct[eidx] = detail::attr_num(er[0], "phase2-service-time");
588 raw_.tput[eidx] = entry_tput;
589 raw_.procutil[eidx] = detail::attr_num(er[0], "proc-utilization");
590 }
591 }
592
593 // PH1PH2 form: the phase activities carry their own results
594 const std::vector<const xml::Element*> epa =
595 ee->by_tag("entry-phase-activities");
596 if (epa.empty()) continue;
597 for (const xml::Element* ae : epa[0]->by_tag("activity")) {
598 read_activity(ae, entry_tput, true);
599 }
600 }
601
602 const std::vector<const xml::Element*> tal = te->by_tag("task-activities");
603 if (tal.empty()) continue;
604 for (const xml::Element* ae : tal[0]->by_tag("activity")) {
605 // A synch-call of a phase activity is nested inside the same
606 // subtree in some documents; only direct children of
607 // task-activities are the graph's own activities.
608 if (ae->parent != tal[0]) continue;
609 read_activity(ae, task_tput, false);
610 }
611 }
612 }
613
614 aggregate_entry_procutil();
615 aggregate_entry_svct();
616 }
617
618 /**
619 * Phase-1 service time of an entry lqns never invoked.
620 *
621 * lqns omits `phase1-service-time` from `result-entry` exactly when the
622 * entry's throughput is zero: nothing was served, so there is no
623 * per-invocation mean to report. LINE then carried a NaN where the table
624 * says an entry HAS a response time and every other solver reports one,
625 * breaking the NaN mask -- see `_kb/06-solver-catalog.md`. The value is
626 * taken from the activity rows, and ONLY where they are unanimous: if every
627 * activity reachable from the entry reports a zero service time then every
628 * aggregation law agrees on zero -- the serial sum, the branch-weighted mean
629 * of an OrFork, the order statistic of an AndFork -- so the derivation does
630 * not depend on which one applies.
631 *
632 * It is deliberately NOT generalised the way `aggregate_entry_procutil` is.
633 * Utilizations add over an activity graph; response times do not. Measured
634 * over the example corpus, the sum over actsof reproduces
635 * `phase1-service-time` on serial chains only and misses it wherever the
636 * graph branches (`lqn_workflows` `Entry`: 12.5667 reported against 8.5667
637 * summed, `lqn_fork_open_arrival` `SE`: 0.841667 against 1.0), so a summed
638 * fallback would answer with a number lqns contradicts. An entry whose
639 * activities are unreported, absent, or not all zero keeps NaN.
640 */
641 void aggregate_entry_svct() {
642 for (std::size_t e = 1; e <= sn_.nentries; ++e) {
643 const std::size_t eidx = sn_.eshift + e;
644 if (eidx >= sn_.actsof.size() || eidx >= raw_.phase1svct.size()) continue;
645 if (!std::isnan(raw_.phase1svct[eidx])) continue;
646 const std::vector<std::size_t>& acts = sn_.actsof[eidx];
647 if (acts.empty()) continue;
648 bool all_zero = true;
649 for (std::size_t aidx : acts) {
650 if (aidx >= raw_.phase1svct.size() || std::isnan(raw_.phase1svct[aidx]) ||
651 raw_.phase1svct[aidx] != 0.0) {
652 all_zero = false;
653 break;
654 }
655 }
656 if (all_zero) raw_.phase1svct[eidx] = 0.0;
657 }
658 }
659
660 /**
661 * Processor utilization of an entry, aggregated from its activity graph.
662 *
663 * lqns credits host work to whichever level carries the host demand. In the
664 * activity-graph form an entry declares none, so lqns reports its
665 * result-entry proc-utilization as a literal 0 and the work sits on the
666 * result-activity rows; the entry's value is then the sum over the
667 * activities reachable from the entry within its own task, which is what
668 * actsof holds. In PH1PH2 form the same sum runs over the phase activities
669 * and reproduces the value lqns reports there, so no form test is needed. An
670 * entry with no activities, or any activity lqns left unreported, keeps the
671 * raw attribute rather than a partial sum.
672 */
673 void aggregate_entry_procutil() {
674 for (std::size_t e = 1; e <= sn_.nentries; ++e) {
675 const std::size_t eidx = sn_.eshift + e;
676 if (eidx >= sn_.actsof.size()) continue;
677 const std::vector<std::size_t>& acts = sn_.actsof[eidx];
678 if (acts.empty()) continue;
679 double sum = 0.0;
680 bool complete = true;
681 for (std::size_t aidx : acts) {
682 if (aidx >= raw_.procutil.size() || std::isnan(raw_.procutil[aidx])) {
683 complete = false;
684 break;
685 }
686 sum += raw_.procutil[aidx];
687 }
688 if (complete) raw_.procutil[eidx] = sum;
689 }
690 }
691
692 /**
693 * One `<activity>` and the calls it issues.
694 *
695 * @param ae the element
696 * @param owner_tput throughput of the entry (phase form) or task, used
697 * where lqns omits the activity's own
698 * @param phase_form true inside entry-phase-activities, where lqns omits
699 * both throughput and proc-utilization
700 */
701 void read_activity(const xml::Element* ae, double owner_tput, bool phase_form) {
702 const std::string aname = ae->attr("name");
703 const std::size_t aidx = index_of(aname, lang::LqnElement::ACTIVITY);
704 const std::vector<const xml::Element*> ar = ae->by_tag("result-activity");
705 if (aidx && !ar.empty()) {
706 const xml::Element* r = ar[0];
707 raw_.util[aidx] = detail::attr_num(r, "utilization");
708 raw_.phase1svct[aidx] = detail::attr_num(r, "service-time");
709 raw_.procwaiting[aidx] = detail::attr_num(r, "proc-waiting");
710 const double t = detail::attr_num(r, "throughput");
711 // Each phase executes once per entry invocation, so an omitted
712 // throughput there IS the entry's; filling it with zero would say
713 // the activity never runs.
714 raw_.tput[aidx] = std::isnan(t) && phase_form ? owner_tput : t;
715 const double pu = detail::attr_num(r, "proc-utilization");
716 if (!std::isnan(pu)) {
717 raw_.procutil[aidx] = pu;
718 } else if (phase_form) {
719 const double hd = detail::attr_num(ae, "host-demand-mean");
720 if (!std::isnan(owner_tput) && !std::isnan(hd)) raw_.procutil[aidx] = owner_tput * hd;
721 }
722 }
723 if (!aidx) return;
724 read_calls(ae, aname, "synch-call", "=>");
725 read_calls(ae, aname, "asynch-call", "->");
726 }
727
728 void read_calls(const xml::Element* ae, const std::string& aname, const char* tag,
729 const char* arrow) {
730 for (const xml::Element* ce : ae->by_tag(tag)) {
731 const std::size_t cidx = call_index_of(aname + arrow + ce->attr("dest"));
732 const std::vector<const xml::Element*> cr = ce->by_tag("result-call");
733 if (cidx && !cr.empty()) raw_.callwaiting[cidx] = detail::attr_num(cr[0], "waiting");
734 }
735 }
736
737 lqn::LqnModel<T> model_;
738 LqnsOptions opt_;
739 lqn::LqnStruct<T> sn_;
740 LqnsRawAvg raw_;
741 std::map<std::pair<int, std::string>, std::size_t> byname_;
742 std::map<std::string, std::size_t> bycallname_;
743 double runtime_ = 0.0;
744 bool solved_ = false;
745};
746
747} // namespace lqns
748} // namespace line
749
750#endif // LINE_SOLVERS_WRAPPERS_LQNS_SOLVER_LQNS_H
Error(const std::string &what)
Definition error.h:33
Malformed or inconsistent input (dimensions, negative populations, ...).
Definition error.h:37
InputError(const std::string &what)
Definition error.h:39
Requested feature or arithmetic mode is not ported yet.
Definition error.h:49
UnsupportedError(const std::string &what)
Definition error.h:51
const LqnsOptions & options() const
const lqn::LqnStruct< T > & get_struct() const
void run_analyzer()
Run the binary and read its result back.
static bool is_stochastic_method(const std::string &method)
Only the lqsim methods draw random numbers.
static bool is_available()
True when a local binary is installed AND is 6.0 or greater.
SolverLQNS(const lqn::LqnModel< T > &model, const LqnsOptions &options=LqnsOptions())
static std::string version()
The version banner of the local binary, empty when there is none.
static bool has_local_binary()
True when the native lqns command answers on this machine, any release.
static std::vector< std::string > list_valid_methods()
const LqnsRawAvg & raw_avg()
Everything the .lqxo carried, before the column permutation.
double runtime() const
Wall-clock seconds of the last run, the reference's runtime.
LqnsSolution< T > get_ensemble_avg()
The six measures on the element index space.
static std::vector< std::string > feature_set()
The per-layer feature set, as SolverLQNS.supports declares it.
static void step(const char *fmt,...)
Write one progress line.
The exception types the port throws.
Minimal HTTP/1.1 client, enough to talk to a line-*-rest service.
Running progress log of a LINE solver run (the "solver console").
.lqnx -> LqnStruct, a port of matlab/src/lang/layered/@LayeredNetwork/parseXML.m followed by ....
LqnModel -> .lqnx, a port of matlab/src/lang/layered/@LayeredNetwork/writeXML.m.
Is a usable lqns installed on this machine?
Response post_json(const std::string &url, const std::string &json, int timeoutMillis)
POST a JSON document.
Definition http.h:361
LqnElement
LQN element kinds, with the values of MATLAB LayeredNetworkElement.
Definition lang_types.h:464
LqnWriteReport write_lqnx(const LqnModel< T > &m, const std::string &path, const std::string &model_name=std::string("LQN"), bool use_abstract_names=false)
Write a layered model as a .lqnx document.
Definition lqn_writer.h:198
const std::string & lqns_version()
The version banner of the local lqns, empty when there is none.
Definition lqns_probe.h:49
bool lqns_is_available()
True when lqns is installed AND is a release this port speaks.
Definition lqns_probe.h:69
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
std::string make_temp_dir(const std::string &prefix, bool mountable=false)
Create a private scratch directory named after its caller.
Definition tempdir.h:84
std::unique_ptr< Element > parse_file(const std::string &path)
Read and parse a file.
Definition xml.h:290
static constexpr double CoarseTol
Definition lang_types.h:669
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:399
What the schema could not carry, one human-readable line per loss.
Definition lqn_writer.h:66
std::vector< std::string > dropped
Definition lqn_writer.h:67
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
std::string method
default | lqns | srvn | exactmva | srvn.exactmva | sim | lqsim | lqnsdefault
Definition solver_lqns.h:88
int timeout_seconds
Deadline for the child, in seconds; not positive waits indefinitely.
Definition solver_lqns.h:97
bool remote
Solve on a host running lqns-rest instead of on this machine.
Definition solver_lqns.h:99
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
Everything the .lqxo carries, indexed as the struct is.
std::vector< double > callwaiting
std::vector< double > procutil
std::vector< double > phase1util
std::vector< double > util
std::vector< double > phase2util
std::vector< double > phase1svct
std::vector< double > phase2svct
std::vector< double > procwaiting
std::vector< double > tput
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_W
std::vector< bool > defined_A
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
std::vector< T > AN
std::vector< T > WN
std::string attr(const std::string &key) const
Attribute value, or the empty string when absent (org.w3c.dom semantics).
Definition xml.h:63
Running an external command and capturing its output, with a deadline.
A scratch directory for the subprocess wrappers, the port's lineTempName.
A minimal XML DOM: read for the .lqnx interchange format, write for the JMT .jsimg and ....