LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ldes.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_LDES_SOLVER_LDES_H
6#define LINE_SOLVERS_WRAPPERS_LDES_SOLVER_LDES_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `SolverLDES`, the discrete-event simulator, as its C++ client.
12 *
13 * WHAT IS PORTED, AND WHY IT IS A CLIENT AND NOT AN ENGINE. LDES has ONE
14 * implementation, `jar/src/main/java/jline/solvers/wrappers/ldes/` (~25 kLOC on SSJ),
15 * and every codebase that is not the JAR reaches it the same way: MATLAB
16 * (`@@SolverLDES/solveCli.m`) and native Python
17 * (`wrappers/solver_ldes/solver_ldes.py`) serialize the model to `model.json`,
18 * run `solve model.json -o result.json` as a subprocess and parse the result
19 * document back. Nothing is marshalled in process. This file is the third such
20 * client, and it is the port of SolverLDES for the same reason those two are:
21 * a second simulator would answer with different numbers under the same name,
22 * and "LDES agrees across the codebases" -- which the parity harness checks and
23 * `CLAUDE.md` states as an invariant -- is a property of there being ONE engine.
24 *
25 * THE PRIMARY ENTRY IS A DOCUMENT, not a model object, and that is a deliberate
26 * difference from the two other clients. They hold a live model and serialize
27 * it; `solver_ldes_text` / `solver_ldes_file` are given the `model.json`
28 * DOCUMENT and forward its bytes UNCHANGED. There is a writer
29 * (`io/network_writer.h`), so a round trip through `qn::NetworkStruct<T>` is
30 * possible -- `solver_ldes` below is exactly that, for a caller who built the
31 * model in C++ -- but the reader that would have to parse the document first is
32 * deliberately scoped to the subset the analytical solvers read, so making the
33 * trip MANDATORY would degrade exactly the models LDES exists for (a cache with
34 * retrieval, an SPN, a polling server, a G-network signal). Forwarding the bytes
35 * keeps the client lossless on every model the engine accepts, including those
36 * this port cannot itself represent.
37 *
38 * WHICH BINARY RUNS. `ldes_runners` reproduces `getLdesRunners` and
39 * `_build_cli_runners`: the AOT GraalVM binary `common/ldes` first, for its
40 * startup time, then `<java> -jar common/ldes.jar`, and each is tried in turn
41 * because the AOT image lacks reflective features the JVM has (the fork-join
42 * MMT transform deep-copies the model by Java serialization). THE ORDER IS
43 * REVERSED, not merely extended, when the run uses something the prebuilt image
44 * predates -- `--respt-samples`, `--initsol`, a cache cost cap, a marked class,
45 * a load/class-dependent scaling -- because on that image the flag is not
46 * refused but IGNORED, and an ignored cost cap silently simulates the uncapped
47 * cache. The two other clients inspect the live model object for those; this
48 * one inspects the document, which is the same information at the point where
49 * it actually crosses.
50 *
51 * STATE PROBABILITIES ARE READ FROM THE HISTOGRAM, not from the trajectory.
52 * `ldes_prob_aggr` / `ldes_prob_sys_aggr` take the target state as an ARGUMENT
53 * and weigh it against `stateHistogram`, the exact residence time of every
54 * integer joint state the run visited. The no-argument form the reference also
55 * offers -- "the probability of the model's CURRENT state" -- has no counterpart
56 * here because `qn::NetworkStruct` carries no current state row (only a Cache's
57 * `initstate`, a Place's `initmarking` and the `stateprior`/`statespace` pair).
58 * The DEFAULT INITIAL state is a different thing and is derivable, by the same
59 * `ctmc::analyzer_detail::default_init_state` the `-a prob` arms of CTMC, MVA
60 * and NC already answer over; the LDES arm of the CLI still refuses `-a prob`
61 * on the older premise that no state exists at all, which is a reachability gap
62 * in `cpp/src/cli/line_cli.cpp` rather than a gap in this client.
63 * The three other clients answer BOTH forms off the transient `QNt` series,
64 * which is a trajectory of interval MEANS, and so report a near-zero
65 * probability for a state the chain mostly occupies; see BUG-96 and the note on
66 * `LdesResult::QNt`.
67 *
68 * WHAT THIS CLIENT DOES NOT PORT, and why it refuses instead of approximating:
69 * LayeredNetwork MATLAB simulates an LQN through the Java ensemble
70 * backend (`self.obj`), an in-process path with no JSON
71 * interface; `LdesCLI` itself refuses a LayeredNetwork
72 * document ("Network models only"). There is nothing to
73 * forward.
74 * server breakdowns `linemodel_save` does not serialize `setBreakdown` at
75 * all, so a breakdown cannot reach this client: the
76 * refusal the Python client performs is on the OBJECT,
77 * before serialization, and has no wire counterpart.
78 *
79 * WHERE THE ENGINE IS, and whether there is one, lives in `ldes_probe.h`:
80 * SolverAUTO needs that answer -- LDES leads several of the reference's
81 * rankings, and a chooser that named it on a host without the engine would be
82 * proposing something that cannot run -- and must not take on this file's HTTP
83 * transport and result parser to get it.
84 */
85
87#include <algorithm>
88#include <cmath>
89#include <cstddef>
90#include <cstdio>
91#include <cstdlib>
92#include <cstring>
93#include <fstream>
94#include <limits>
95#include <sstream>
96#include <string>
97#include <utility>
98#include <vector>
99
100#include <sys/stat.h>
101#include <sys/utsname.h>
102#include <unistd.h>
103
104#include "json.hpp"
108#include "line/util/error.h"
109#include "line/util/http.h"
110#include "line/util/matrix.h"
111#include "line/util/subprocess.h"
112#include "line/util/tempdir.h"
113
114namespace line {
115namespace ldes {
116
117namespace detail {
118
119using Json = nlohmann::json;
120
121/**
122 * The shortest decimal that reads back as `v`, which is what Python's `repr`
123 * emits on the same flag. A fixed `%.10g`, as `solveCli.m` uses, would round a
124 * tolerance the caller set exactly, and the engine parses whatever it is given.
125 */
126inline std::string shortest(double v) {
127 char buf[64];
128 for (int prec = 15; prec <= 17; ++prec) {
129 std::snprintf(buf, sizeof(buf), "%.*g", prec, v);
130 if (std::strtod(buf, nullptr) == v) return std::string(buf);
131 }
132 return std::string(buf);
133}
134
135/** A JSON array of arrays as a matrix, with `null` read back as NaN. */
136inline Matrix<double> mat(const Json& v) {
137 if (v.is_null() || !v.is_array() || v.empty()) return Matrix<double>();
138 if (!v[0].is_array()) {
139 Matrix<double> m(1, v.size());
140 for (std::size_t j = 0; j < v.size(); ++j)
141 m(0, j) = v[j].is_null() ? std::numeric_limits<double>::quiet_NaN()
142 : v[j].get<double>();
143 return m;
144 }
145 std::size_t cols = 0;
146 for (std::size_t i = 0; i < v.size(); ++i)
147 if (v[i].is_array() && v[i].size() > cols) cols = v[i].size();
148 Matrix<double> m(v.size(), cols, std::numeric_limits<double>::quiet_NaN());
149 for (std::size_t i = 0; i < v.size(); ++i) {
150 if (!v[i].is_array()) continue;
151 for (std::size_t j = 0; j < v[i].size(); ++j)
152 if (!v[i][j].is_null()) m(i, j) = v[i][j].get<double>();
153 }
154 return m;
155}
156
157/** A JSON array as a vector of doubles, `null` read back as NaN. */
158inline std::vector<double> vec(const Json& v) {
159 std::vector<double> out;
160 if (!v.is_array()) return out;
161 out.reserve(v.size());
162 for (std::size_t i = 0; i < v.size(); ++i)
163 out.push_back(v[i].is_null() ? std::numeric_limits<double>::quiet_NaN()
164 : v[i].get<double>());
165 return out;
166}
167
168inline const Json& field(const Json& o, const char* key) {
169 static const Json null_value;
170 return o.is_object() && o.contains(key) ? o[key] : null_value;
171}
172
173/** Reads a whole file, or throws naming it. */
174inline std::string read_file(const std::string& path) {
175 std::ifstream in(path.c_str(), std::ios::binary);
176 if (!in) throw InputError("SolverLDES: cannot open the model document '" + path + "'");
177 std::ostringstream ss;
178 ss << in.rdbuf();
179 return ss.str();
180}
181
182inline void write_file(const std::string& path, const std::string& text) {
183 std::ofstream out(path.c_str(), std::ios::binary);
184 if (!out) throw InputError("SolverLDES: cannot write '" + path + "'");
185 out << text;
186}
187
188/**
189 * True when the DOCUMENT uses something the native engine would MIS-SERVE.
190 *
191 * The scan is on the bytes that actually cross rather than on a live model, and
192 * a substring test is deliberate: every key is unique enough that a false
193 * positive only reorders the runners, which costs startup time and changes no
194 * number, while a false negative would silently simulate a different model.
195 *
196 * THE BAR IS "SILENTLY IGNORED", NOT "DIFFERS". A key belongs here only while
197 * the native engine would accept it and then not honour it, because that is the
198 * failure this guard exists to prevent: an ignored cost cap simulates the
199 * uncapped cache and says nothing. A feature the native engine REFUSES by name
200 * needs no entry -- the runner list falls through to the jar on failure -- and
201 * neither does one it honours.
202 *
203 * `markedClasses` was removed on 2026-09-12, for three reasons together. The
204 * native engine has routed marks since 2026-09-11 and released batches since
205 * 2026-09-12, so it no longer ignores them. `common/ldes` is no longer the
206 * retired GraalVM image of the jar but a C++ binary built from this same tree
207 * (`add_executable(ldes src/cli/ldes_cli.cpp)`), so it cannot predate a document
208 * this tree emits. And neither other client ever carried the entry -- MATLAB's
209 * `getLdesRunners` reverses for nothing and python's `_build_cli_runners` only
210 * for `--respt-samples`, a LayeredNetwork and a cost cap -- so keeping it meant
211 * one marked model ran on TWO different engines depending on which codebase's
212 * client was asked, which is the one thing "there is one LDES" is supposed to
213 * rule out.
214 *
215 * Note that digit-for-digit agreement is NOT the bar, and could not be: the two
216 * engines part company on any model whose routing consumes extra draws, which
217 * `CLAUDE.md` states outright. `--initsol` and `--busyperiod` happened to clear
218 * that higher bar when they were pruned; marks do not have to.
219 */
220inline bool document_postdates_native(const std::string& doc) {
221 static const char* keys[] = {"\"costCaps\"", "\"classDependence\"",
222 "\"loadDependence\"", "\"jointDependence\""};
223 for (std::size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i)
224 if (doc.find(keys[i]) != std::string::npos) return true;
225 return false;
226}
227
228} // namespace detail
229
230/**
231 * The engine flags of one run, after `solve <model> -o <result>`.
232 *
233 * ONE MAPPING for the subprocess and the REST paths, as in both other clients:
234 * the server takes the same long-form flags verbatim, and deriving them twice
235 * is how the two transports start disagreeing. A knob is emitted only when it
236 * differs from the engine default, so a default run yields the minimal command
237 * line an older AOT image still parses -- except `-s` and `--seed`, which are
238 * always emitted: the CLI's seed default is -1 (random), so a silent seed makes
239 * the run irreproducible and unlike the MATLAB and Python clients.
240 */
241inline std::vector<std::string> ldes_flags(const LdesOptions& o,
242 const std::vector<std::string>& extra) {
243 std::vector<std::string> f;
244 const std::size_t budget = o.events ? o.events : o.samples;
245 f.push_back("-s");
246 f.push_back(std::to_string(budget));
247 f.push_back("--seed");
248 f.push_back(std::to_string(o.seed));
249 if (!o.method.empty() && o.method != "default") {
250 f.push_back("--method");
251 f.push_back(o.method);
252 }
253 if (o.cnvgon) {
254 f.push_back("--cnvgon");
255 if (o.cnvgtol != 0.05) {
256 f.push_back("--cnvgtol");
257 f.push_back(detail::shortest(o.cnvgtol));
258 }
259 if (o.cnvgbatch != 20) {
260 f.push_back("--cnvgbatch");
261 f.push_back(std::to_string(o.cnvgbatch));
262 }
263 if (o.cnvgchk != 0) {
264 f.push_back("--cnvgchk");
265 f.push_back(std::to_string(o.cnvgchk));
266 }
267 }
268 if (o.tranfilter != "mser5") {
269 f.push_back("--tranfilter");
270 f.push_back(o.tranfilter);
271 }
272 if (o.warmupfrac != 0.2) {
273 f.push_back("--warmupfrac");
274 f.push_back(detail::shortest(o.warmupfrac));
275 }
276 if (o.tranobs > 0) {
277 f.push_back("--tranobs");
278 f.push_back(std::to_string(o.tranobs));
279 }
280 if (o.mserbatch != 5) {
281 f.push_back("--mserbatch");
282 f.push_back(std::to_string(o.mserbatch));
283 }
284 if (o.cimethod != "obm") {
285 f.push_back("--cimethod");
286 f.push_back(o.cimethod);
287 }
288 if (o.obmoverlap != 0.5) {
289 f.push_back("--obmoverlap");
290 f.push_back(detail::shortest(o.obmoverlap));
291 }
292 if (o.ciminbatch != 10) {
293 f.push_back("--ciminbatch");
294 f.push_back(std::to_string(o.ciminbatch));
295 }
296 if (o.ciminobs != 100) {
297 f.push_back("--ciminobs");
298 f.push_back(std::to_string(o.ciminobs));
299 }
300 if (o.spectral_low_freq_frac != 0.25) {
301 f.push_back("--spectrallowfreqfrac");
302 f.push_back(detail::shortest(o.spectral_low_freq_frac));
303 }
304 // --slotlength implies --slotted on the CLI side; both are emitted when the
305 // length is non-default so the command line states the intent explicitly.
306 if (o.slotted) {
307 f.push_back("--slotted");
308 if (o.slot_length != 1.0) {
309 f.push_back("--slotlength");
310 f.push_back(detail::shortest(o.slot_length));
311 }
312 }
313 // method='parallel' (alias 'para') is a replication count, not a second engine: the caller's
314 // count when set, 8 otherwise, as solveCli.m resolves it. The native engine reads only --replications.
315 int reps = o.replications;
316 if ((o.method == "parallel" || o.method == "para") && reps <= 1) reps = 8;
317 if (reps > 1) {
318 f.push_back("--replications");
319 f.push_back(std::to_string(reps));
320 if (o.numthreads > 0) {
321 f.push_back("--numthreads");
322 f.push_back(std::to_string(o.numthreads));
323 }
324 }
325 if (o.has_timespan) {
326 f.push_back("--timespan");
327 f.push_back(detail::shortest(o.t0) + "," + detail::shortest(o.t1));
328 }
329 if (o.timeout > 0.0 && std::isfinite(o.timeout)) {
330 f.push_back("--maxtime");
331 f.push_back(detail::shortest(o.timeout));
332 }
333 if (o.busy_period_orders > 0) {
334 f.push_back("--busyperiod");
335 f.push_back(std::to_string(o.busy_period_orders));
336 for (std::size_t i = 0; i < o.busy_period_subnets.size(); ++i) {
337 // station indexes cross the wire zero-based, as the engine holds them
338 std::string v;
339 for (std::size_t t = 0; t < o.busy_period_subnets[i].size(); ++t)
340 v += (t ? "," : "") + std::to_string(o.busy_period_subnets[i][t]);
341 f.push_back("--busyperiod-subnet");
342 f.push_back(v);
343 }
344 }
345 if (!o.init_sol.empty()) {
346 std::string v;
347 for (std::size_t i = 0; i < o.init_sol.size(); ++i)
348 v += (i ? "," : "") + detail::shortest(o.init_sol[i]);
349 f.push_back("--initsol");
350 f.push_back(v);
351 }
352 for (std::size_t i = 0; i < extra.size(); ++i) f.push_back(extra[i]);
353 return f;
354}
355
356/** One runner: the argv prefix that runs the engine, and the name of its image. */
358 std::vector<std::string> argv; ///< up to and including "solve"
359 std::string engine; ///< "native" or "jar"
360};
361
362/**
363 * The runners to try, in order (see the file header for why there are two and
364 * when the order flips).
365 *
366 * @param doc the model document, scanned for features the AOT image predates
367 * @param flags the resolved flags, scanned for the same reason
368 * @throws UnsupportedError when no engine can run on this host
369 */
370inline std::vector<LdesRunner> ldes_runners(const std::string& doc,
371 const std::vector<std::string>& flags) {
372 const std::string& dir = ldes_engine_dir();
373 std::vector<LdesRunner> out;
374 const std::string native = detail::native_ldes_path(dir);
375 if (!native.empty()) {
376 LdesRunner r;
377 r.argv.push_back(native);
378 r.argv.push_back("solve");
379 r.engine = "native";
380 out.push_back(r);
381 }
382 const std::string java = detail::find_java();
383 const std::string jar = dir.empty() ? std::string() : dir + "/ldes.jar";
384 if (!java.empty() && detail::is_file(jar)) {
385 LdesRunner r;
386 r.argv.push_back(java);
387 r.argv.push_back("-jar");
388 r.argv.push_back(jar);
389 r.argv.push_back("solve");
390 r.engine = "jar";
391 out.push_back(r);
392 }
393 if (out.empty()) throw UnsupportedError(detail::no_backend_message(dir));
394 if (out.size() > 1) {
395 // --initsol and --busyperiod used to be here, because the retired
396 // GraalVM image predated them; the C++ engine honours both and agrees
397 // with the jar to the last digit, so only --respt-samples remains.
398 bool prefer_jar = detail::document_postdates_native(doc);
399 for (std::size_t i = 0; i < flags.size() && !prefer_jar; ++i)
400 prefer_jar = flags[i] == "--respt-samples";
401 if (prefer_jar) std::swap(out[0], out[1]);
402 }
403 return out;
404}
405
406/**
407 * Solves through an LDES REST server and returns its `result` document.
408 *
409 * The payload is the model text plus the same flag vector the subprocess would
410 * have been given, so a fixed seed gives the same numbers on both transports.
411 */
412inline detail::Json ldes_solve_rest(const std::string& base_url, const std::string& doc,
413 const std::vector<std::string>& flags, double timeout) {
414 std::string url = base_url;
415 while (!url.empty() && url[url.size() - 1] == '/') url.erase(url.size() - 1);
416 if (url.size() < 6 || url.compare(url.size() - 6, 6, "/solve") != 0) url += "/api/v1/solve";
417
418 detail::Json payload = detail::Json::object();
419 detail::Json model = detail::Json::object();
420 model["content"] = doc;
421 model["base64"] = false;
422 payload["model"] = model;
423 detail::Json fl = detail::Json::array();
424 for (std::size_t i = 0; i < flags.size(); ++i) fl.push_back(flags[i]);
425 payload["flags"] = fl;
426
427 const int millis = (timeout > 0.0 && std::isfinite(timeout))
428 ? static_cast<int>((timeout + 30.0) * 1000.0)
429 : 600000;
430 const http::Response resp = http::post_json(url, payload.dump(), millis);
431 if (resp.body.empty())
432 throw NumericError("SolverLDES: the REST server at " + url + " returned HTTP " +
433 std::to_string(resp.status) + " with no body");
434 detail::Json out = detail::Json::parse(resp.body, nullptr, false);
435 if (out.is_discarded() || !out.is_object())
436 throw NumericError("SolverLDES: the REST server at " + url +
437 " returned a non-JSON body: " + resp.body);
438 if (!out.contains("status") || out["status"].get<std::string>() != "ok") {
439 const std::string msg = out.contains("message") ? out["message"].get<std::string>()
440 : std::string("unspecified error");
441 const std::string err = out.contains("stderr") ? out["stderr"].get<std::string>()
442 : std::string();
443 throw NumericError("SolverLDES: the REST solve failed: " + msg +
444 (err.empty() ? "" : " (engine stderr: " + err + ")"));
445 }
446 return out["result"];
447}
448
449/**
450 * Parses one `ldes-result` document.
451 *
452 * `format` is CHECKED, not assumed: the runner falls through on failure, and a
453 * runner that wrote some other JSON to the output path would otherwise be read
454 * as a result full of absent metrics rather than as the failure it is.
455 */
456inline LdesResult parse_ldes_result(const detail::Json& d) {
457 if (d.is_object() && d.contains("error"))
458 throw NumericError("SolverLDES: the engine reported an error: " +
459 d["error"].get<std::string>());
460 if (!d.is_object() || !d.contains("format") || d["format"].get<std::string>() != "ldes-result")
461 throw NumericError(
462 "SolverLDES: the engine wrote a document that is not an ldes-result; the run did not "
463 "produce metrics");
464
465 LdesResult r;
466 r.method = d.contains("method") && !d["method"].is_null() ? d["method"].get<std::string>()
467 : std::string("default");
468 if (d.contains("runtime")) r.runtime = d["runtime"].get<double>();
469 if (d.contains("converged")) r.converged = d["converged"].get<bool>();
470 if (d.contains("stoppingReason") && !d["stoppingReason"].is_null())
471 r.stopping_reason = d["stoppingReason"].get<std::string>();
472 // The engine's console is not shown, so its warnings are re-raised here.
473 if (d.contains("warnings") && d["warnings"].is_array())
474 for (const auto& w : d["warnings"]) {
475 r.warnings.push_back(w.get<std::string>());
476 std::fprintf(stderr, "[SolverLDES] Warning: %s\n", r.warnings.back().c_str());
477 }
478 if (d.contains("convergenceBatches")) r.convergence_batches = d["convergenceBatches"].get<long>();
479 if (d.contains("totalSimulatedEvents"))
480 r.total_simulated_events = d["totalSimulatedEvents"].get<long long>();
481
482 const detail::Json& dim = detail::field(d, "dimensions");
483 if (dim.is_object()) {
484 if (dim.contains("nstations")) r.nstations = dim["nstations"].get<std::size_t>();
485 if (dim.contains("nclasses")) r.nclasses = dim["nclasses"].get<std::size_t>();
486 if (dim.contains("nchains")) r.nchains = dim["nchains"].get<std::size_t>();
487 if (dim.contains("stationNames"))
488 for (std::size_t i = 0; i < dim["stationNames"].size(); ++i)
489 r.station_names.push_back(dim["stationNames"][i].get<std::string>());
490 if (dim.contains("classNames"))
491 for (std::size_t i = 0; i < dim["classNames"].size(); ++i)
492 r.class_names.push_back(dim["classNames"][i].get<std::string>());
493 }
494
495 const detail::Json& m = detail::field(d, "metrics");
496 r.QN = detail::mat(detail::field(m, "QN"));
497 r.UN = detail::mat(detail::field(m, "UN"));
498 r.RN = detail::mat(detail::field(m, "RN"));
499 r.TN = detail::mat(detail::field(m, "TN"));
500 r.AN = detail::mat(detail::field(m, "AN"));
501 r.WN = detail::mat(detail::field(m, "WN"));
502 r.CN = detail::mat(detail::field(m, "CN"));
503 r.XN = detail::mat(detail::field(m, "XN"));
504 r.DropRateJoin = detail::mat(detail::field(m, "DropRateJoin"));
505
506 const detail::Json& ci = detail::field(d, "confidenceIntervals");
507 r.QNCI = detail::mat(detail::field(ci, "QNCI"));
508 r.UNCI = detail::mat(detail::field(ci, "UNCI"));
509 r.RNCI = detail::mat(detail::field(ci, "RNCI"));
510 r.TNCI = detail::mat(detail::field(ci, "TNCI"));
511 r.ANCI = detail::mat(detail::field(ci, "ANCI"));
512 r.WNCI = detail::mat(detail::field(ci, "WNCI"));
513
514 const detail::Json& rp = detail::field(d, "relativePrecision");
515 r.QNRelPrec = detail::mat(detail::field(rp, "QNRelPrec"));
516 r.UNRelPrec = detail::mat(detail::field(rp, "UNRelPrec"));
517 r.RNRelPrec = detail::mat(detail::field(rp, "RNRelPrec"));
518 r.TNRelPrec = detail::mat(detail::field(rp, "TNRelPrec"));
519
520 const detail::Json& sc = detail::field(d, "sampleCounts");
521 r.QNSamples = detail::mat(detail::field(sc, "QNSamples"));
522 r.UNSamples = detail::mat(detail::field(sc, "UNSamples"));
523 r.RNSamples = detail::mat(detail::field(sc, "RNSamples"));
524 r.TNSamples = detail::mat(detail::field(sc, "TNSamples"));
525
526 const detail::Json& fcr = detail::field(d, "fcr");
527 if (fcr.is_object()) {
528 if (fcr.contains("nregions")) r.nregions = fcr["nregions"].get<std::size_t>();
529 r.QNfcr = detail::mat(detail::field(fcr, "QNfcr"));
530 r.UNfcr = detail::mat(detail::field(fcr, "UNfcr"));
531 r.RNfcr = detail::mat(detail::field(fcr, "RNfcr"));
532 r.TNfcr = detail::mat(detail::field(fcr, "TNfcr"));
533 r.ANfcr = detail::mat(detail::field(fcr, "ANfcr"));
534 r.WNfcr = detail::mat(detail::field(fcr, "WNfcr"));
535 r.WeightNfcr = detail::mat(detail::field(fcr, "WeightNfcr"));
536 r.MemOccNfcr = detail::mat(detail::field(fcr, "MemOccNfcr"));
537 r.DropRateNfcr = detail::mat(detail::field(fcr, "DropRateNfcr"));
538 }
539
540 const detail::Json& imp = detail::field(d, "impatience");
541 if (imp.is_object()) {
542 r.renegedCustomers = detail::mat(detail::field(imp, "renegedCustomers"));
543 r.avgRenegingWaitTime = detail::mat(detail::field(imp, "avgRenegingWaitTime"));
544 r.renegingRate = detail::mat(detail::field(imp, "renegingRate"));
545 r.balkedCustomers = detail::mat(detail::field(imp, "balkedCustomers"));
546 r.balkingProbability = detail::mat(detail::field(imp, "balkingProbability"));
547 r.retriedCustomers = detail::mat(detail::field(imp, "retriedCustomers"));
548 r.retrialDropped = detail::mat(detail::field(imp, "retrialDropped"));
549 r.avgOrbitSize = detail::mat(detail::field(imp, "avgOrbitSize"));
550 }
551
552 const detail::Json& cm = detail::field(d, "cacheMetrics");
553 if (cm.is_object())
554 for (detail::Json::const_iterator it = cm.begin(); it != cm.end(); ++it) {
556 c.hit = detail::mat(detail::field(it.value(), "hit"));
557 c.delayed = detail::mat(detail::field(it.value(), "delayed"));
558 c.miss = detail::mat(detail::field(it.value(), "miss"));
559 c.latency = detail::mat(detail::field(it.value(), "latency"));
560 c.hitList = detail::mat(detail::field(it.value(), "hitList"));
561 c.itemProb = detail::mat(detail::field(it.value(), "itemProb"));
562 c.listCost = detail::mat(detail::field(it.value(), "listCost"));
563 r.cache_metrics[it.key()] = c;
564 }
565
566 const detail::Json& bp = detail::field(d, "busyPeriods");
567 if (bp.is_object()) {
568 const detail::Json& targets = detail::field(bp, "targets");
569 for (std::size_t i = 0; i < targets.size(); ++i) {
571 const detail::Json& tj = targets[i];
572 if (tj.contains("name") && tj["name"].is_string())
573 t.name = tj["name"].get<std::string>();
574 const detail::Json& st = detail::field(tj, "stations");
575 for (std::size_t k = 0; k < st.size(); ++k)
576 t.stations.push_back(static_cast<std::size_t>(st[k].get<double>()));
577 if (tj.contains("class") && tj["class"].is_number())
578 t.job_class = static_cast<int>(tj["class"].get<double>());
579 t.mean = detail::vec(detail::field(tj, "mean"));
580 t.count = detail::vec(detail::field(tj, "count"));
581 r.busy_periods.push_back(t);
582 }
583 }
584
585 const detail::Json& hist = detail::field(d, "stateHistogram");
586 if (hist.is_object()) {
587 r.histogram_space = detail::mat(detail::field(hist, "space"));
588 r.histogram_time = detail::mat(detail::field(hist, "time"));
589 r.traj_space = detail::mat(detail::field(hist, "trajSpace"));
590 r.traj_time = detail::mat(detail::field(hist, "trajTime"));
591 }
592
593 // `respTimeSamples` appears at TOP LEVEL under --respt-samples and inside
594 // `transient` under --trajectory; the two are the same measurement and
595 // whichever is present is read.
596 const detail::Json& tran = detail::field(d, "transient");
597 if (tran.is_object()) {
598 r.t = detail::vec(detail::field(tran, "t"));
599 const char* keys[3] = {"QNt", "UNt", "TNt"};
600 std::vector<std::vector<Matrix<double>>>* dst[3] = {&r.QNt, &r.UNt, &r.TNt};
601 for (int q = 0; q < 3; ++q) {
602 const detail::Json& blk = detail::field(tran, keys[q]);
603 if (!blk.is_array()) continue;
604 for (std::size_t i = 0; i < blk.size(); ++i) {
605 std::vector<Matrix<double>> row;
606 for (std::size_t k = 0; k < blk[i].size(); ++k)
607 row.push_back(detail::mat(blk[i][k]));
608 dst[q]->push_back(row);
609 }
610 }
611 }
612 const detail::Json& rts = tran.is_object() && tran.contains("respTimeSamples")
613 ? tran["respTimeSamples"]
614 : detail::field(d, "respTimeSamples");
615 if (rts.is_array())
616 for (std::size_t i = 0; i < rts.size(); ++i) {
617 std::vector<std::vector<double>> row;
618 for (std::size_t k = 0; k < rts[i].size(); ++k) row.push_back(detail::vec(rts[i][k]));
619 r.respTimeSamples.push_back(row);
620 }
621 return r;
622}
623
624/**
625 * Runs one LDES simulation on a `model.json` DOCUMENT and parses its result.
626 *
627 * The document is written into a private scratch directory, the engine is run
628 * there, and the directory is removed on both the success and the failure path
629 * (`TempDir`'s destructor), because a wrapper that leaks one directory per
630 * failed solve leaks silently.
631 *
632 * THE RUNNERS ARE TRIED IN ORDER and the LAST failure is reported in full,
633 * merged stdout and stderr, because the engine states why it refused there and
634 * an exit code alone is not a diagnosis.
635 *
636 * @param doc the model.json text, forwarded byte for byte
637 * @param o the run's knobs
638 * @param extra_flags flags an analysis adds (--trajectory, --export-histogram,
639 * --respt-samples)
640 * @return the parsed result, carrying which image produced it
641 */
642inline LdesResult solver_ldes_text(const std::string& doc, const LdesOptions& o,
643 const std::vector<std::string>& extra_flags =
644 std::vector<std::string>()) {
645 const std::vector<std::string> flags = ldes_flags(o, extra_flags);
646 util::TempDir tmp("ldes");
647 const std::string model_path = tmp.file("model.json");
648 const std::string result_path = tmp.file("result.json");
649 line::util::LineConsole::step("serializing the model to JSON for the LDES engine");
650 detail::write_file(model_path, doc);
651
652 if (!o.rest_url.empty()) {
654 r.engine = "rest";
655 return r;
656 }
657
658 const std::vector<LdesRunner> runners = ldes_runners(doc, flags);
659 // The cooperative --maxtime stops the event loop; this is the HARD bound and
660 // leaves the engine 30 s of grace to write its result and exit, else no
661 // result file is produced at all. An infinite budget falls back to 600 s.
662 const int hard = (o.timeout > 0.0 && std::isfinite(o.timeout))
663 ? static_cast<int>(o.timeout + 30.0)
664 : 600;
665 std::string last_err;
666 for (std::size_t i = 0; i < runners.size(); ++i) {
667 std::vector<std::string> argv = runners[i].argv;
668 argv.push_back(model_path);
669 argv.push_back("-o");
670 argv.push_back(result_path);
671 for (std::size_t j = 0; j < flags.size(); ++j) argv.push_back(flags[j]);
672 std::remove(result_path.c_str());
673 if (o.verbose) {
674 std::string line;
675 for (std::size_t j = 0; j < argv.size(); ++j) line += (j ? " " : "") + argv[j];
676 std::fprintf(stderr, "SolverLDES command: %s\n", line.c_str());
677 }
678 line::util::LineConsole::step("running the LDES engine (%s) as a subprocess",
679 runners[i].engine.c_str());
680 const util::ProcResult p = util::capture(argv, hard, true);
681 if (p.timedOut) {
682 LdesResult r;
683 r.timed_out = true;
684 r.stopping_reason = "max_time";
685 r.engine = runners[i].engine;
686 return r;
687 }
688 if (p.exitCode == 0 && detail::is_file(result_path)) {
689 line::util::LineConsole::step("parsing the LDES result document");
690 const std::string text = detail::read_file(result_path);
691 detail::Json parsed = detail::Json::parse(text, nullptr, false);
692 if (parsed.is_discarded())
693 throw NumericError("SolverLDES: the engine wrote a result that is not JSON");
694 LdesResult r = parse_ldes_result(parsed);
695 r.engine = runners[i].engine;
696 return r;
697 }
698 last_err = "exit code " + std::to_string(p.exitCode) + ": " + util::trim(p.out);
699 }
700 throw NumericError("SolverLDES: the engine failed on all " +
701 std::to_string(runners.size()) + " runner(s); " + last_err);
702}
703
704/** The same, reading the document from a file. */
705inline LdesResult solver_ldes_file(const std::string& path, const LdesOptions& o,
706 const std::vector<std::string>& extra_flags =
707 std::vector<std::string>()) {
708 return solver_ldes_text(detail::read_file(path), o, extra_flags);
709}
710
711/**
712 * One constraint of a joint-state query: a station and the per-class job counts
713 * it is required to hold.
714 *
715 * `station` is 1-BASED, matching `sn.nodeToStation`. `counts` is read up to
716 * `nclasses` entries; a shorter vector constrains only the classes it names,
717 * which is how a caller asks for "two class-1 jobs here, any number of the
718 * rest" without enumerating the remainder.
719 */
721 std::size_t station = 0;
722 std::vector<double> counts;
723};
724
725/**
726 * Residence-time probability of an aggregate joint state, from a parsed result.
727 *
728 * WHY THE HISTOGRAM AND NOT THE TRAJECTORY. `stateHistogram` is the exact
729 * residence time of every integer joint state the run visited, keyed on the
730 * state itself (`Solver_ssj.updateRewardStats` accumulates it per marking), so
731 * P(state) = t(state) / sum(t) is an unbiased estimate that is exact on the
732 * sampled path. The transient `QNt` series is a sequence of INTERVAL MEANS of
733 * the queue length; comparing one against an integer state matches only where a
734 * bucket mean happens to land on an integer, which is why the three clients
735 * that estimated a probability that way reported a near-zero number for a state
736 * the chain occupies most of the time (BUG-96).
737 *
738 * The layout is the aggregated station-major, class-minor one
739 * `ctmc_state_space_aggr` builds: column `(i-1)*nclasses + k` is the number of
740 * class-k jobs at station i. THE RESOLUTION IS PER CLASS, not per phase: the
741 * engine records the integer queue lengths, so a phase-resolved query cannot be
742 * answered from this document and the caller must aggregate first.
743 *
744 * @param r a result produced with `--export-histogram`
745 * @param nclasses the model's class count, which fixes the column stride
746 * @param query the stations to constrain, and to what
747 * @return the residence-time fraction, 0 when the state was never visited
748 * @throws NumericError when the run carried no histogram (the flag was omitted)
749 */
750inline double ldes_prob_from_histogram(const LdesResult& r, std::size_t nclasses,
751 const std::vector<LdesStateQuery>& query) {
752 if (r.histogram_space.rows() == 0 || r.histogram_time.rows() == 0)
753 throw NumericError(
754 "SolverLDES: this result carries no state histogram, so no state probability can be "
755 "read from it; the run has to pass --export-histogram");
756 if (nclasses == 0)
757 throw InputError("SolverLDES: a state probability needs the model's class count");
758 const std::size_t nstates = r.histogram_space.rows();
759 double total = 0.0;
760 for (std::size_t s = 0; s < nstates; ++s) total += r.histogram_time(s, 0);
761 if (!(total > 0.0)) return 0.0;
762
763 const std::size_t ncols = r.histogram_space.cols();
764 for (std::size_t q = 0; q < query.size(); ++q) {
765 if (query[q].station == 0)
766 throw InputError("SolverLDES: a state query names station 0; stations are 1-based");
767 const std::size_t last = (query[q].station - 1) * nclasses + nclasses;
768 if (last > ncols)
769 throw InputError(
770 "SolverLDES: station " + std::to_string(query[q].station) +
771 " is past the end of the state histogram, which holds " + std::to_string(ncols) +
772 " columns for " + std::to_string(nclasses) + " classes");
773 }
774
775 double matched = 0.0;
776 for (std::size_t s = 0; s < nstates; ++s) {
777 bool ok = true;
778 for (std::size_t q = 0; q < query.size() && ok; ++q) {
779 const std::size_t base = (query[q].station - 1) * nclasses;
780 const std::size_t n = std::min(nclasses, query[q].counts.size());
781 for (std::size_t k = 0; k < n; ++k)
782 if (std::fabs(r.histogram_space(s, base + k) - query[q].counts[k]) > 1e-9) {
783 ok = false;
784 break;
785 }
786 }
787 if (ok) matched += r.histogram_time(s, 0);
788 }
789 return matched / total;
790}
791
792/**
793 * Port of `getCdfRespT`: the EMPIRICAL response time CDF of one (station,
794 * class) pair, built from the per-job samples the engine exports under
795 * `--respt-samples`.
796 *
797 * The returned matrix is (n x 2) with columns [F(t), t], NOT [t, F(t)]. That
798 * order is the convention every `getCdfRespT` follows across the codebases
799 * (MATLAB `@SolverLDES/getCdfRespT.m`, Python `SolverJMT.getCdfRespT`), and it
800 * is the REVERSE of the transient `getTranCdfRespT`, which is a different
801 * method with a different contract. Swapping them yields a CDF that reads as a
802 * time axis and vice versa, with no error anywhere.
803 *
804 * A pair the run observed nothing at returns an EMPTY matrix rather than a
805 * fabricated law: an analytical fallback carrying the right mean says nothing
806 * about the tail, which is the whole reason to ask a simulator for a CDF.
807 * Repeated observations are collapsed, keeping the largest CDF value at each
808 * distinct time, or the ecdf is multivalued and an interpolating consumer
809 * reads a quantile off whichever duplicate it happens to hit.
810 */
811inline Matrix<double> ldes_cdf_respt(const LdesResult& r, std::size_t station,
812 std::size_t job_class) {
813 if (station >= r.respTimeSamples.size() ||
814 job_class >= r.respTimeSamples[station].size())
815 return Matrix<double>();
816 std::vector<double> x = r.respTimeSamples[station][job_class];
817 if (x.empty()) return Matrix<double>();
818 std::sort(x.begin(), x.end());
819 const std::size_t n = x.size();
820 std::vector<double> tu, fu;
821 for (std::size_t i = 0; i < n; ++i) {
822 const double f = static_cast<double>(i + 1) / static_cast<double>(n);
823 if (i + 1 < n && x[i + 1] == x[i]) continue; // keep the LAST of a tie
824 tu.push_back(x[i]);
825 fu.push_back(f);
826 }
827 Matrix<double> out(tu.size(), 2, 0.0);
828 for (std::size_t i = 0; i < tu.size(); ++i) {
829 out(i, 0) = fu[i];
830 out(i, 1) = tu[i];
831 }
832 return out;
833}
834
835/**
836 * Port of `getProbAggr`: the marginal probability of a per-class job count at
837 * one station.
838 *
839 * The run is a plain steady-state solve with `--export-histogram` added, which
840 * is what `getAvgReward` already does, so the estimate is over the same
841 * post-warmup path the mean metrics come from.
842 *
843 * @param doc the model.json document, forwarded byte for byte
844 * @param o the run's knobs
845 * @param station 1-based station index
846 * @param counts per-class job counts to test at that station
847 * @param nclasses the model's class count
848 */
849inline double ldes_prob_aggr(const std::string& doc, const LdesOptions& o, std::size_t station,
850 const std::vector<double>& counts, std::size_t nclasses) {
851 std::vector<std::string> flags;
852 flags.push_back("--export-histogram");
853 const LdesResult r = solver_ldes_text(doc, o, flags);
854 std::vector<LdesStateQuery> q(1);
855 q[0].station = station;
856 q[0].counts = counts;
857 return ldes_prob_from_histogram(r, nclasses, q);
858}
859
860/**
861 * Port of `getProbSysAggr`: the joint probability of a whole aggregate state.
862 *
863 * @param target (nstations x nclasses) job counts, EVERY station constrained
864 */
865inline double ldes_prob_sys_aggr(const std::string& doc, const LdesOptions& o,
866 const Matrix<double>& target) {
867 if (target.rows() == 0 || target.cols() == 0)
868 throw InputError("SolverLDES: getProbSysAggr needs an (nstations x nclasses) target state");
869 std::vector<std::string> flags;
870 flags.push_back("--export-histogram");
871 const LdesResult r = solver_ldes_text(doc, o, flags);
872 const std::size_t K = target.cols();
873 std::vector<LdesStateQuery> q(target.rows());
874 for (std::size_t i = 0; i < target.rows(); ++i) {
875 q[i].station = i + 1;
876 q[i].counts.resize(K);
877 for (std::size_t k = 0; k < K; ++k) q[i].counts[k] = target(i, k);
878 }
879 return ldes_prob_from_histogram(r, K, q);
880}
881
882/**
883 * The same, for a model built through the C++ API.
884 *
885 * The struct is serialized with `io::network_json_envelope`, the writer whose
886 * output the reference readers consume, and the document then takes the same
887 * path as any other. WHAT THE STRUCT CANNOT CARRY DOES NOT CROSS: a reward built
888 * from a lambda and any construct outside the writer's schema are dropped by the
889 * writer, with its own warning, which is why a caller holding a `model.json`
890 * should pass the DOCUMENT rather than a struct parsed from it -- the round trip
891 * can only lose.
892 */
893template <class T>
895 const std::vector<std::string>& extra_flags =
896 std::vector<std::string>()) {
897 return solver_ldes_text(io::network_json_envelope(sn).dump(), o, extra_flags);
898}
899
900} // namespace ldes
901} // namespace line
902
903#endif // LINE_SOLVERS_WRAPPERS_LDES_SOLVER_LDES_H
InputError(const std::string &what)
Definition error.h:39
std::size_t size() const
Definition matrix.h:91
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
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
The exception types the port throws.
Minimal HTTP/1.1 client, enough to talk to a line-*-rest service.
The option and result records of SolverLDES, the discrete-event simulator.
Where the LDES engine is, and whether this machine can run it.
Running progress log of a LINE solver run (the "solver console").
Dense matrix and non-owning view.
Response post_json(const std::string &url, const std::string &json, int timeoutMillis)
POST a JSON document.
Definition http.h:361
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Definition http.h:349
detail::json network_json_envelope(const qn::NetworkStruct< T > &sn)
The complete model.json envelope: {format, version, model}.
double ldes_prob_from_histogram(const LdesResult &r, std::size_t nclasses, const std::vector< LdesStateQuery > &query)
Residence-time probability of an aggregate joint state, from a parsed result.
LdesResult solver_ldes_text(const std::string &doc, const LdesOptions &o, const std::vector< std::string > &extra_flags=std::vector< std::string >())
Runs one LDES simulation on a model.json DOCUMENT and parses its result.
double ldes_prob_sys_aggr(const std::string &doc, const LdesOptions &o, const Matrix< double > &target)
Port of getProbSysAggr: the joint probability of a whole aggregate state.
LdesResult solver_ldes_file(const std::string &path, const LdesOptions &o, const std::vector< std::string > &extra_flags=std::vector< std::string >())
The same, reading the document from a file.
const std::string & ldes_engine_dir()
The directory holding the engine, or empty when there is none.
Definition ldes_probe.h:239
std::vector< std::string > ldes_flags(const LdesOptions &o, const std::vector< std::string > &extra)
The engine flags of one run, after solve <model> -o <result>.
Matrix< double > ldes_cdf_respt(const LdesResult &r, std::size_t station, std::size_t job_class)
Port of getCdfRespT: the EMPIRICAL response time CDF of one (station, class) pair,...
LdesResult parse_ldes_result(const detail::Json &d)
Parses one ldes-result document.
double ldes_prob_aggr(const std::string &doc, const LdesOptions &o, std::size_t station, const std::vector< double > &counts, std::size_t nclasses)
Port of getProbAggr: the marginal probability of a per-class job count at one station.
detail::Json ldes_solve_rest(const std::string &base_url, const std::string &doc, const std::vector< std::string > &flags, double timeout)
Solves through an LDES REST server and returns its result document.
LdesResult solver_ldes(const qn::NetworkStruct< T > &sn, const LdesOptions &o, const std::vector< std::string > &extra_flags=std::vector< std::string >())
The same, for a model built through the C++ API.
std::vector< LdesRunner > ldes_runners(const std::string &doc, const std::vector< std::string > &flags)
The runners to try, in order (see the file header for why there are two and when the order flips).
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 trim(const std::string &s)
Trims ASCII whitespace from both ends, as Java's String.trim() does.
Definition subprocess.h:181
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
qn::NetworkStruct -> model.json, the inverse of network_reader.h.
An HTTP response, with the body already de-chunked.
Definition http.h:60
std::string body
Response body, decoded.
Definition http.h:62
int status
HTTP status code.
Definition http.h:61
Per-cache hit/miss/latency, as the cacheMetrics block carries them.
Matrix< double > listCost
(1 x nlists) mean storage cost held per list
Matrix< double > latency
(1 x nclasses) expected retrieval latency
Matrix< double > hitList
(nclasses x nlists) hit probability by list
Matrix< double > itemProb
(nitems x nlists+1) item position law
Matrix< double > delayed
(1 x nclasses) delayed-hit probability
Matrix< double > miss
(1 x nclasses) miss probability
Matrix< double > hit
(1 x nclasses) hit probability
The knobs of one LDES run.
double warmupfrac
–warmupfrac, only for tranfilter=fixed
int ciminbatch
–ciminbatch
double spectral_low_freq_frac
–spectrallowfreqfrac
bool verbose
echo the resolved command line before running it
double cnvgtol
–cnvgtol
std::string rest_url
Base URL of an LDES REST server (the imperialqore/ldes container).
double obmoverlap
–obmoverlap; 0 reduces OBM to plain batch means
std::vector< std::vector< std::size_t > > busy_period_subnets
–busyperiod-subnet, zero-based station indexes, one flag per set.
long seed
–seed; -1 requests a random stream
bool slotted
–slotted, run on the slot lattice
int mserbatch
–mserbatch, MSER batch size
std::string cimethod
–cimethod: obm | bm | spectral | none
double slot_length
–slotlength
int tranobs
–tranobs, equally spaced transient observation points over [t0,t1]; fixes the time grid only.
int cnvgchk
–cnvgchk, events between checks; 0 = samples/50
int replications
–replications; 0 = not given (one path)
int ciminobs
–ciminobs, below which no CI is reported
std::size_t events
0 = not given; overrides samples when set
int cnvgbatch
–cnvgbatch, batches before the first check
std::string tranfilter
–tranfilter: mser5 | fixed | none
std::vector< double > init_sol
–initsol, the warm-start placement as a STATION-MAJOR vector [st0_cl0, st0_cl1, .....
int numthreads
–numthreads; 0 = not given
std::size_t samples
-s, service-completion budget
double timeout
–maxtime, a COOPERATIVE wall-clock budget the event loop polls.
bool has_timespan
true when [t0,t1] was set: a TRANSIENT run
int busy_period_orders
–busyperiod: highest busy period order measured, 0 disabling the measurement.
One measured busy period target (–busyperiod): a station, a station-class pair, or a declared station...
std::vector< std::size_t > stations
station indexes, zero-based
One ldes-result document, parsed.
Matrix< double > balkedCustomers
Matrix< double > QNSamples
std::map< std::string, LdesCacheMetrics > cache_metrics
Per-cache metrics, keyed by the Cache NODE name.
Matrix< double > avgOrbitSize
Matrix< double > QNRelPrec
Matrix< double > TNCI
Matrix< double > WNfcr
std::vector< std::vector< Matrix< double > > > QNt
[STATION][class] -> (npoints x 2), columns [value, time].
Matrix< double > renegingRate
Matrix< double > XN
(1 x nclasses), per-class visits and system tput
std::vector< std::vector< std::vector< double > > > respTimeSamples
[station][class] -> the per-job response times the engine recorded.
Matrix< double > UNfcr
Matrix< double > WeightNfcr
std::vector< std::string > warnings
ldes-result "warnings": what the engine warned on its own console, e.g.
Matrix< double > RNRelPrec
Matrix< double > avgRenegingWaitTime
Matrix< double > QNfcr
Matrix< double > traj_space
Matrix< double > UN
Matrix< double > DropRateNfcr
Matrix< double > histogram_space
The exact joint-state residence-time histogram (–export-histogram).
std::string stopping_reason
convergence | max_events | max_sim_events | max_time.
std::vector< std::vector< Matrix< double > > > UNt
std::vector< std::string > class_names
Matrix< double > TNRelPrec
Matrix< double > TN
Matrix< double > UNCI
Matrix< double > TNSamples
Matrix< double > RN
Matrix< double > UNRelPrec
std::string engine
"native" or "jar": which runner produced these numbers.
Matrix< double > QNCI
Matrix< double > traj_time
std::vector< BusyPeriodTarget > busy_periods
Matrix< double > CN
Matrix< double > RNfcr
Matrix< double > WN
Matrix< double > retriedCustomers
Matrix< double > retrialDropped
Matrix< double > AN
long long total_simulated_events
Matrix< double > MemOccNfcr
Matrix< double > TNfcr
std::vector< std::string > station_names
Matrix< double > DropRateJoin
quorum-Join sibling drops, Join rows only
Matrix< double > UNSamples
bool timed_out
True when the HARD subprocess bound fired, not the cooperative one.
Matrix< double > balkingProbability
Matrix< double > renegedCustomers
Matrix< double > histogram_time
Matrix< double > RNSamples
Matrix< double > RNCI
std::vector< double > t
the time vector, empty on a steady-state run
Matrix< double > WNCI
Matrix< double > QN
std::vector< std::vector< Matrix< double > > > TNt
Matrix< double > ANfcr
Matrix< double > ANCI
One runner: the argv prefix that runs the engine, and the name of its image.
std::vector< std::string > argv
up to and including "solve"
std::string engine
"native" or "jar"
One constraint of a joint-state query: a station and the per-class job counts it is required to hold.
std::vector< double > counts
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.