LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_jmt.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_JMT_SOLVER_JMT_H
6#define LINE_SOLVERS_WRAPPERS_JMT_SOLVER_JMT_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `SolverJMT`, the Java Modelling Tools client.
12 *
13 * WHAT A JMT SOLVE IS. The model is written out as a JMT document -- a
14 * `.jsimg` for the discrete-event engine, a `.jmva` for the analytical one --
15 * `jmt.commandline.Jmt` is run on it, and the result document JMT leaves beside
16 * the model is parsed back into the same `AvgResult` every other solver in this
17 * port returns. The translation is `io/jmt_writer.h` and the SHARED
18 * `io/jmva_writer.h`, which SolverQNS writes through as well; this file is the
19 * dispatch, the parse and the metric mapping.
20 *
21 * THREE WAYS TO REACH JMT, in the order `jmtRun.m` tries them:
22 * 1. `options.rest_url`, a JMT REST server (the imperialqore/jmt-rest image).
23 * Nothing runs locally.
24 * 2. a local JVM plus `common/JMT.jar`, the default.
25 * 3. no JVM, but a usable Docker daemon: the same image, run as a container.
26 * Every one of them leaves the result at `<model>-result.jsim` or
27 * `<model>-result.jmva`, so the parsers do not know which one ran. That
28 * contract is what makes the three interchangeable, and it is why the Docker
29 * arm copies the container's output back beside the model.
30 *
31 * CONSENT IS NOT ASSUMED FOR THE PULL. The Docker arm is reached only when
32 * `LINE_JMT_DOCKER` opts in: this port has no terminal to ask at -- the
33 * reference prompts, and refuses in a `-batch` session -- so an absent variable
34 * is a refusal, exactly as an empty answer is there. Downloading 50 MB because
35 * a solver was called is not a decision a library may take.
36 *
37 * WHAT IS NOT PORTED, and why it is refused rather than approximated:
38 * getProbAggr / getProbSysAggr Both weigh the simulated trajectory against
39 * `sn.state{isf}`, the model's CURRENT state,
40 * which `qn::NetworkStruct` does not carry --
41 * the same gap that stops `SolverLDES`'s
42 * `getProb` in this port.
43 * the `replication` method It averages `sampleSysAggr` over `iter_max`
44 * seeds, and `sampleSysAggr` reads the JMT
45 * arrival/departure LOG files, which requires
46 * a Logger on every station of the model; the
47 * transient arm is available through
48 * `jmt_sample_sys_aggr` and is composed there.
49 */
50
52#include <algorithm>
53#include <cmath>
54#include <cstdio>
55#include <cstdlib>
56#include <fstream>
57#include <limits>
58#include <map>
59#include <sstream>
60#include <string>
61#include <vector>
62
63#include <sys/stat.h>
64#include <unistd.h>
65
67#include "line/io/jmva_writer.h"
68#include "line/io/jmt_writer.h"
73#include "line/util/error.h"
74#include "line/util/http.h"
76#include "line/util/tempdir.h"
77#include "line/util/xml.h"
78
79namespace line {
80namespace jmt {
81
82/** The default JMT REST/Docker image, MATLAB's `jmtDockerImage` candidate. */
83static const char* JMT_DOCKER_IMAGE = "imperialqore/jmt-rest:latest";
84
85/** The options of one JMT solve, `SolverOptions('JMT')` restricted to what is read. */
86struct JmtOptions {
87 std::string method = "default"; ///< default | jsim | jmva | `jmva.<alg>`
88 double samples = 10000.0; ///< samples per measure; raised to 5000 below
89 long seed = 23000;
90 bool keep = false; ///< keep the scratch directory after the solve
91 double confint = 0.99; ///< 0 disables the confidence interval columns
92 double max_simulated_time = std::numeric_limits<double>::infinity();
93 std::string rest_url; ///< a JMT REST server; empty selects the local JVM
94 std::string container; ///< `options.config.container`, an image override
95 int timeout = 3600; ///< seconds, for the REST and subprocess arms
96 bool verbose = false;
97 /** `options.iter_max`: the replication count of `method = "replication"`. */
98 int iter_max = 10;
99};
100
101namespace detail {
102
103inline bool is_file(const std::string& p) {
104 struct stat st;
105 return !p.empty() && ::stat(p.c_str(), &st) == 0 && S_ISREG(st.st_mode);
106}
107
108inline bool is_exec(const std::string& p) {
109 struct stat st;
110 return !p.empty() && ::stat(p.c_str(), &st) == 0 && ::access(p.c_str(), X_OK) == 0;
111}
112
113inline std::string env_or_empty(const char* name) {
114 const char* v = std::getenv(name);
115 return v != nullptr ? std::string(v) : std::string();
116}
117
118inline std::string exe_dir() {
119 char buf[4096];
120 const ssize_t n = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1);
121 if (n <= 0) return std::string();
122 std::string p(buf, static_cast<std::size_t>(n));
123 const std::size_t s = p.find_last_of('/');
124 return s == std::string::npos ? std::string() : p.substr(0, s);
125}
126
127inline std::string cwd() {
128 char buf[4096];
129 return ::getcwd(buf, sizeof(buf)) != nullptr ? std::string(buf) : std::string();
130}
131
132/**
133 * Port of `jmtGetPath`, minus the download.
134 *
135 * The reference fetches JMT.jar from SourceForge when it is missing. THIS PORT
136 * DOES NOT: a solver call is not consent to a 50 MB download, and the reference
137 * only gets away with it because it prints and prompts. `jmt_jar_path` returns
138 * empty instead, and the dispatch then says what to install and where.
139 *
140 * `$LINE_JMT_DIR` overrides the search, for a jar kept outside the tree.
141 */
142inline std::string jmt_jar_path() {
143 const std::string forced = env_or_empty("LINE_JMT_DIR");
144 if (!forced.empty()) return is_file(forced + "/JMT.jar") ? forced : std::string();
145 const std::string roots[2] = {exe_dir(), cwd()};
146 for (int r = 0; r < 2; ++r) {
147 if (roots[r].empty()) continue;
148 std::string dir = roots[r];
149 for (int k = 0; k < 7; ++k) {
150 if (is_file(dir + "/JMT.jar")) return dir;
151 if (is_file(dir + "/common/JMT.jar")) return dir + "/common";
152 dir += "/..";
153 }
154 }
155 return std::string();
156}
157
158/** `$LINE_JAVA`, then `$JAVA_HOME/bin/java`, then PATH; empty when absent. */
159inline std::string find_java() {
160 const std::string forced = env_or_empty("LINE_JAVA");
161 if (!forced.empty()) return is_exec(forced) ? forced : std::string();
162 const std::string home = env_or_empty("JAVA_HOME");
163 if (!home.empty() && is_exec(home + "/bin/java")) return home + "/bin/java";
164 const std::string path = env_or_empty("PATH");
165 std::size_t b = 0;
166 while (b <= path.size()) {
167 const std::size_t e = path.find(':', b);
168 const std::string dir =
169 path.substr(b, e == std::string::npos ? std::string::npos : e - b);
170 if (!dir.empty() && is_exec(dir + "/java")) return dir + "/java";
171 if (e == std::string::npos) break;
172 b = e + 1;
173 }
174 return std::string();
175}
176
177/** `LINE_JMT_DOCKER` in the affirmative; anything else, including unset, is no. */
178inline bool docker_consented() {
179 const std::string v = env_or_empty("LINE_JMT_DOCKER");
180 return v == "1" || v == "true" || v == "TRUE" || v == "yes" || v == "y" || v == "Y";
181}
182
183inline std::string read_file(const std::string& path) {
184 std::ifstream f(path.c_str(), std::ios::binary);
185 if (!f) throw InputError("SolverJMT: cannot read '" + path + "'");
186 std::ostringstream ss;
187 ss << f.rdbuf();
188 return ss.str();
189}
190
191inline void write_file(const std::string& path, const std::string& text) {
192 std::ofstream f(path.c_str(), std::ios::binary);
193 if (!f) throw InputError("SolverJMT: cannot write '" + path + "'");
194 f.write(text.data(), static_cast<std::streamsize>(text.size()));
195 if (!f) throw InputError("SolverJMT: write failed on '" + path + "'");
196}
197
198/** JSON string escaping, for the REST request body. */
199inline std::string json_escape(const std::string& s) {
200 std::string out;
201 out.reserve(s.size() + 16);
202 for (std::size_t i = 0; i < s.size(); ++i) {
203 const unsigned char c = static_cast<unsigned char>(s[i]);
204 switch (c) {
205 case '"': out += "\\\""; break;
206 case '\\': out += "\\\\"; break;
207 case '\n': out += "\\n"; break;
208 case '\r': out += "\\r"; break;
209 case '\t': out += "\\t"; break;
210 default:
211 if (c < 0x20) {
212 char buf[8];
213 std::snprintf(buf, sizeof(buf), "\\u%04x", c);
214 out += buf;
215 } else {
216 out.push_back(s[i]);
217 }
218 }
219 }
220 return out;
221}
222
223/**
224 * The value of a top-level JSON string field, unescaped.
225 *
226 * The REST response is a small, flat envelope -- `status`, `error`,
227 * `raw_output.result_xml`, `raw_output.stdout` -- and pulling four strings out
228 * of it does not warrant making this header depend on the JSON library that
229 * only the LDES client uses. A malformed response yields an empty string, and
230 * the caller then reports that the server sent no result document.
231 */
232inline std::string json_string_field(const std::string& body, const std::string& key) {
233 const std::string needle = "\"" + key + "\"";
234 std::size_t p = body.find(needle);
235 if (p == std::string::npos) return std::string();
236 p = body.find(':', p + needle.size());
237 if (p == std::string::npos) return std::string();
238 ++p;
239 while (p < body.size() && (body[p] == ' ' || body[p] == '\t' || body[p] == '\n')) ++p;
240 if (p >= body.size() || body[p] != '"') return std::string();
241 ++p;
242 std::string out;
243 while (p < body.size() && body[p] != '"') {
244 if (body[p] == '\\' && p + 1 < body.size()) {
245 ++p;
246 switch (body[p]) {
247 case 'n': out.push_back('\n'); break;
248 case 'r': out.push_back('\r'); break;
249 case 't': out.push_back('\t'); break;
250 case 'u': {
251 if (p + 4 < body.size()) {
252 const long cp = std::strtol(body.substr(p + 1, 4).c_str(), nullptr, 16);
253 if (cp < 0x80) out.push_back(static_cast<char>(cp));
254 p += 4;
255 }
256 break;
257 }
258 default: out.push_back(body[p]);
259 }
260 } else {
261 out.push_back(body[p]);
262 }
263 ++p;
264 }
265 return out;
266}
267
268} // namespace detail
269
270template <class T>
271struct JmtResult;
272
273inline util::ProcResult jmt_run_docker(const std::string& image, const std::string& mode,
274 const std::string& model_path, long seed,
275 const JmtOptions& opt);
276
277/** The suffix `jmt.commandline.Jmt` appends to the model path, per mode. */
278inline const char* jmt_result_ext(const std::string& mode) {
279 if (mode == "sim") return "jsim";
280 if (mode == "mva") return "jmva";
281 throw InputError("SolverJMT: unknown JMT analysis mode '" + mode + "'");
282}
283
284/** True when a local JVM and `common/JMT.jar` are both present. */
285inline bool jmt_available() {
286 return !detail::jmt_jar_path().empty() && !detail::find_java().empty();
287}
288
289/**
290 * Port of `jmtSolveRest`: POST the model document, write the result document
291 * back beside the model so the local parsers are unaffected.
292 */
293inline util::ProcResult jmt_solve_rest(const std::string& rest_url, const std::string& mode,
294 const std::string& model_path, long seed,
295 const JmtOptions& opt) {
296 std::string url = rest_url;
297 while (!url.empty() && url[url.size() - 1] == '/') url.erase(url.size() - 1);
298 // A caller may hand over the full route; anything else gets the default one.
299 if (url.size() < 8 || url.substr(url.size() - 8) != "/solve/" + mode.substr(0, 1))
300 if (url.find("/api/v") == std::string::npos) url += "/api/v1/solve/" + mode;
301
302 std::string body = "{\"model\":{\"content\":\"" +
303 detail::json_escape(detail::read_file(model_path)) +
304 "\",\"base64\":false}";
305 // JMVA takes its algorithm and tolerance from the model document, so the
306 // seed is meaningful only on the simulation route; the server's option
307 // allow-list rejects it on /solve/mva.
308 if (mode == "sim") body += ",\"options\":{\"seed\":" + std::to_string(seed) + "}";
309 body += "}";
310
311 const http::Response resp = http::post_json(url, body, opt.timeout * 1000);
312 if (resp.status != 200)
313 throw InputError("SolverJMT: the JMT REST server at " + url + " answered HTTP " +
314 std::to_string(resp.status));
315 const std::string status = detail::json_string_field(resp.body, "status");
316 if (status != "completed") {
317 const std::string err = detail::json_string_field(resp.body, "error");
318 throw InputError("SolverJMT: the JMT REST solve failed: " +
319 (err.empty() ? std::string("unspecified error") : err));
320 }
321 const std::string xml = detail::json_string_field(resp.body, "result_xml");
322 if (xml.empty())
323 throw InputError(
324 "SolverJMT: the JMT REST response carries no result document. The server was asked "
325 "to include the raw output; check that include_raw_output is not disabled");
326 detail::write_file(model_path + "-result." + jmt_result_ext(mode), xml);
328 r.exitCode = 0;
329 r.out = detail::json_string_field(resp.body, "stdout");
330 return r;
331}
332
333/**
334 * Port of `jmtRun`: one batch analysis, leaving the result where the JMT CLI
335 * itself would leave it.
336 *
337 * @param mode "sim" or "mva"
338 * @param model_path the document JMT is to read; the result lands beside it
339 */
340inline util::ProcResult jmt_run(const std::string& mode, const std::string& model_path, long seed,
341 const JmtOptions& opt) {
342 if (!opt.rest_url.empty()) return jmt_solve_rest(opt.rest_url, mode, model_path, seed, opt);
343
344 const std::string jar_dir = detail::jmt_jar_path();
345 const std::string java = detail::find_java();
346 if (!jar_dir.empty() && !java.empty()) {
347 std::vector<std::string> argv;
348 argv.push_back(java);
349 argv.push_back("-cp");
350 argv.push_back(jar_dir + "/JMT.jar");
351 argv.push_back("jmt.commandline.Jmt");
352 argv.push_back(mode);
353 argv.push_back(model_path);
354 argv.push_back("-seed");
355 argv.push_back(std::to_string(seed));
356 argv.push_back("--illegal-access=permit");
357 const util::ProcResult r = util::capture(argv, opt.timeout, true);
358 // A KILLED RUN IS NOT A RESULT. Without this the caller falls through to
359 // "no result document", which reads as a solver that refused the model
360 // rather than one whose wall-clock budget expired -- and a jsim that is
361 // merely SLOW on a loaded host is exactly the case that hits it. Same
362 // rule the LQNS, QNS and LDES wrappers already apply to util::capture.
363 if (r.timedOut)
364 throw NumericError("SolverJMT: the JMT run did not finish within " +
365 std::to_string(opt.timeout) + "s and was killed");
366 return r;
367 }
368
369 // The Docker arm. The image is used only when it is ALREADY PRESENT or the
370 // caller has opted into the pull; see the header note on consent.
371 std::string image = opt.container.empty() ? detail::env_or_empty("LINE_JMT_IMAGE")
372 : opt.container;
373 if (image.empty()) image = JMT_DOCKER_IMAGE;
375 bool have = io::docker_has_local_image(image);
376 if (!have && detail::docker_consented() && io::docker_has_storage_for(image))
377 have = io::docker_pull(image);
378 if (have) return jmt_run_docker(image, mode, model_path, seed, opt);
379 }
380
381 throw UnsupportedError(
382 "SolverJMT: no way to reach JMT. Install a Java runtime and put JMT.jar in common/ "
383 "(https://line-solver.sourceforge.net/latest/JMT.jar), or point options.rest_url at a "
384 "JMT REST server, or set LINE_JMT_DOCKER=1 to allow the " +
385 image + " image to be pulled and used");
386}
387
388/**
389 * Run the analysis inside the JMT container and copy the result back beside the
390 * model, so the caller sees the layout a local JVM would have produced.
391 *
392 * The model is STAGED under a fresh directory and the container is given that
393 * directory: in `mva` mode JMT rewrites the model file itself, and a confined
394 * Docker cannot bind-mount the system temp directory the model normally lives
395 * in. The container runs as the calling user so the files it writes are not
396 * left owned by root.
397 */
398inline util::ProcResult jmt_run_docker(const std::string& image, const std::string& mode,
399 const std::string& model_path, long seed,
400 const JmtOptions& opt) {
401 util::TempDir work("jmt-docker", true);
402 const std::size_t slash = model_path.find_last_of('/');
403 const std::string base =
404 slash == std::string::npos ? model_path : model_path.substr(slash + 1);
405 const std::string staged = work.path() + "/" + base;
406 detail::write_file(staged, detail::read_file(model_path));
407
408 const std::string uid = std::to_string(static_cast<long>(::getuid()));
409 const std::string gid = std::to_string(static_cast<long>(::getgid()));
410 std::vector<std::string> argv;
411 argv.push_back("docker");
412 argv.push_back("run");
413 argv.push_back("--rm");
414 argv.push_back("--user");
415 argv.push_back(uid + ":" + gid);
416 argv.push_back("-v");
417 argv.push_back(work.path() + ":" + work.path());
418 argv.push_back("-w");
419 argv.push_back(work.path());
420 argv.push_back(image);
421 argv.push_back(mode);
422 argv.push_back(base);
423 argv.push_back("-seed");
424 argv.push_back(std::to_string(seed));
425 const util::ProcResult r = util::capture(argv, opt.timeout, true);
426 if (r.timedOut)
427 throw NumericError("SolverJMT: the JMT container run did not finish within " +
428 std::to_string(opt.timeout) + "s and was killed");
429
430 const std::string produced = staged + "-result." + jmt_result_ext(mode);
431 if (detail::is_file(produced))
432 detail::write_file(model_path + "-result." + jmt_result_ext(mode),
433 detail::read_file(produced));
434 return r;
435}
436
437/** One `<measure>` of a JMT result document, by its attributes. */
440 double mean = 0.0, lower = 0.0, upper = 0.0;
441 double analyzed_samples = 0.0;
442 bool successful = false;
443 bool has_bounds = false;
444};
445
446/**
447 * Port of `getResultsJSIM`: every `<measure>` of the result document.
448 *
449 * A MISSING FILE IS A FAILED SIMULATION, not an empty result: JMT writes the
450 * document even when every measure is unsuccessful, so its absence means the
451 * run itself did not complete, and returning zeros would report a solved model.
452 */
453inline std::vector<JmtMeasure> jmt_parse_measures(const std::string& result_path,
454 const std::string& command_output) {
455 if (!detail::is_file(result_path)) {
456 std::string msg =
457 "SolverJMT: JMT did not output a result file, the simulation has likely failed.";
458 if (!command_output.empty()) msg += " JMT output: " + command_output;
459 throw NumericError(msg);
460 }
461 const std::unique_ptr<xml::Element> doc = xml::parse_file(result_path);
462 std::vector<JmtMeasure> out;
463 const std::vector<const xml::Element*> ms = doc->by_tag("measure");
464 for (std::size_t i = 0; i < ms.size(); ++i) {
465 const xml::Element* e = ms[i];
466 JmtMeasure m;
467 m.measure_type = e->attr("measureType");
468 m.station = e->attr("station");
469 m.job_class = e->attr("class");
470 m.node_type = e->attr("nodeType");
471 m.mean = std::strtod(e->attr("meanValue").c_str(), nullptr);
472 m.analyzed_samples = std::strtod(e->attr("analyzedSamples").c_str(), nullptr);
473 m.successful = e->attr("successful") == "true";
474 if (e->has_attr("lowerLimit") && e->has_attr("upperLimit")) {
475 m.lower = std::strtod(e->attr("lowerLimit").c_str(), nullptr);
476 m.upper = std::strtod(e->attr("upperLimit").c_str(), nullptr);
477 m.has_bounds = true;
478 }
479 out.push_back(m);
480 }
481 return out;
482}
483
484/**
485 * The result of a JMT solve: the shared `AvgResult` plus what only JMT reports.
486 *
487 * The FCR rows extend the metric matrices past `nstations`, which is the
488 * reference's layout (`getResults.m` allocates `nstations + nregions` rows) and
489 * is what makes a region's measures reachable through the same table as a
490 * station's.
491 */
492template <class T>
493struct JmtResult {
495 Matrix<T> QCI, UCI, RCI, TCI, ACI; ///< confidence half-widths, empty when disabled
496 Matrix<T> Weight, MemOcc; ///< FCR-only: weighted and memory occupation
497 Matrix<T> TNfcr, DropRateNfcr; ///< (nregions x nclasses) carried and lost rate
498 /** Per Cache node (1-based node index), the per-class hit probability. */
499 std::map<std::size_t, std::vector<T>> cache_hit_prob;
500 /**
501 * `result.Prob.logNormConstAggr`, the log normalizing constant JMVA reports.
502 *
503 * NaN on the simulation path and on any JMVA algorithm that does not
504 * compute one -- an AMVA approximation has no G to report -- rather than
505 * zero, which is a legitimate value of a log constant.
506 */
507 T log_norm_const = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
508};
509
510/**
511 * Port of `getResults.m`: the measures mapped onto the metric matrices.
512 *
513 * THE RECURRENCE TEST IS WHY `analyzedSamples` IS READ. For a CLOSED class JMT
514 * may terminate a measure before it has seen more samples than there are jobs
515 * in the chain, and such a measure is not an estimate of anything; the
516 * reference zeroes it rather than reporting it, and so does this. An OPEN class
517 * has no such bound and is taken as reported.
518 */
519template <class T>
521 const std::vector<JmtMeasure>& measures, bool confint) {
522 const std::size_t M = sn.nstations, K = sn.nclasses, F = sn.regions.size();
523 const T zero = num_traits<T>::from_int(0);
524 const double nan = std::numeric_limits<double>::quiet_NaN();
525 JmtResult<T> res;
526 res.avg.QN = Matrix<T>(M + F, K, zero);
527 res.avg.UN = Matrix<T>(M + F, K, zero);
528 res.avg.RN = Matrix<T>(M + F, K, zero);
529 res.avg.TN = Matrix<T>(M + F, K, zero);
530 res.avg.AN = Matrix<T>(M + F, K, zero);
531 res.avg.WN = Matrix<T>(M + F, K, zero);
532 res.Weight = Matrix<T>(M + F, K, num_traits<T>::from_double(nan));
533 res.MemOcc = Matrix<T>(M + F, K, num_traits<T>::from_double(nan));
534 if (confint) {
535 res.QCI = Matrix<T>(M, K, zero);
536 res.UCI = Matrix<T>(M, K, zero);
537 res.RCI = Matrix<T>(M, K, zero);
538 res.TCI = Matrix<T>(M, K, zero);
539 res.ACI = Matrix<T>(M, K, zero);
540 }
541 // JMT reports no utilization and no arrival rate for a region.
542 for (std::size_t f = 0; f < F; ++f)
543 for (std::size_t r = 0; r < K; ++r) {
544 res.avg.UN(M + f, r) = num_traits<T>::from_double(nan);
545 res.avg.AN(M + f, r) = num_traits<T>::from_double(nan);
546 }
547
548 std::map<std::string, std::size_t> station_of, class_of;
549 for (std::size_t i = 1; i <= sn.nodes.size(); ++i) station_of[sn.nodes[i - 1].name] = i;
550 for (std::size_t r = 1; r <= K; ++r) class_of[sn.classes[r - 1].name] = r;
551
552 // The chain population each closed class's recurrence test compares against.
553 std::vector<double> chainpop(K, 0.0);
554 for (std::size_t c = 0; c < sn.inchain.size(); ++c) {
555 double tot = 0.0;
556 for (std::size_t r : sn.inchain[c]) {
557 const double n = sn.classes[r - 1].population;
558 if (std::isfinite(n)) tot += n;
559 }
560 for (std::size_t r : sn.inchain[c]) chainpop[r - 1] = tot;
561 }
562
563 for (std::size_t i = 0; i < measures.size(); ++i) {
564 const JmtMeasure& m = measures[i];
565 const double half = m.has_bounds ? (m.upper - m.lower) / 2.0 : 0.0;
566
567 // A region measure is identified by its nodeType, or by the name the
568 // writer gave it -- JMT does not always echo the nodeType back.
569 const bool is_fcr =
570 m.node_type == "region" || m.station.compare(0, 8, "FCRegion") == 0;
571 if (is_fcr && m.station.compare(0, 8, "FCRegion") == 0) {
572 const long f = std::strtol(m.station.substr(8).c_str(), nullptr, 10);
573 if (f < 1 || static_cast<std::size_t>(f) > F) continue;
574 const std::size_t row = M + static_cast<std::size_t>(f) - 1;
575 // A region measure is AGGREGATE, not per class. The reference
576 // spreads the extensive ones (customers, throughput, arrival rate,
577 // weight, memory) evenly over the classes and repeats the intensive
578 // ones (response and residence time) unchanged, so that summing a
579 // column recovers the region total in both cases.
580 const double per_class = m.mean / static_cast<double>(K);
581 for (std::size_t r = 0; r < K; ++r) {
582 if (m.measure_type == "Number of Customers")
583 res.avg.QN(row, r) = num_traits<T>::from_double(per_class);
584 else if (m.measure_type == "Response Time")
585 res.avg.RN(row, r) = num_traits<T>::from_double(m.mean);
586 else if (m.measure_type == "Residence Time")
587 res.avg.WN(row, r) = num_traits<T>::from_double(m.mean);
588 else if (m.measure_type == "Throughput")
589 res.avg.TN(row, r) = num_traits<T>::from_double(per_class);
590 else if (m.measure_type == "Arrival Rate")
591 res.avg.AN(row, r) = num_traits<T>::from_double(per_class);
592 else if (m.measure_type == "FCR Capacity")
593 res.Weight(row, r) = num_traits<T>::from_double(per_class);
594 else if (m.measure_type == "FCR Memory")
595 res.MemOcc(row, r) = num_traits<T>::from_double(per_class);
596 }
597 continue;
598 }
599
600 if (m.measure_type == "Cache Hit Rate") {
601 const auto ni = station_of.find(m.station);
602 const auto ci = class_of.find(m.job_class);
603 if (ni == station_of.end() || ci == class_of.end()) continue;
604 if (sn.nodes[ni->second - 1].nodetype != lang::NodeType::Cache) continue;
605 const auto cp = sn.nodeparam.find(ni->second);
606 if (cp == sn.nodeparam.end()) continue;
607 std::vector<T>& hit = res.cache_hit_prob[ni->second];
608 if (hit.empty()) hit.assign(K, zero);
609 // The measure names the HIT class; the probability belongs to the
610 // read class that switches into it.
611 for (std::size_t r = 0; r < cp->second.hitclass.size(); ++r)
612 if (cp->second.hitclass[r] == ci->second)
614 continue;
615 }
616
617 const auto ni = station_of.find(m.station);
618 const auto ci = class_of.find(m.job_class);
619 if (ni == station_of.end() || ci == class_of.end()) continue;
620 const std::size_t ist = sn.nodes[ni->second - 1].station;
621 if (ist == 0) continue;
622 const std::size_t r = ci->second;
623 const bool open = !std::isfinite(sn.classes[r - 1].population);
624 const bool recurrent = open || m.analyzed_samples > chainpop[r - 1];
625 const T v = num_traits<T>::from_double(recurrent ? m.mean : 0.0);
626 const T ci_v = num_traits<T>::from_double(recurrent ? half : 0.0);
627
628 if (m.measure_type == "Number of Customers") {
629 res.avg.QN(ist - 1, r - 1) = v;
630 if (confint) res.QCI(ist - 1, r - 1) = ci_v;
631 } else if (m.measure_type == "Utilization") {
632 res.avg.UN(ist - 1, r - 1) = v;
633 if (confint) res.UCI(ist - 1, r - 1) = ci_v;
634 } else if (m.measure_type == "Response Time") {
635 res.avg.RN(ist - 1, r - 1) = v;
636 if (confint) res.RCI(ist - 1, r - 1) = ci_v;
637 } else if (m.measure_type == "Throughput") {
638 res.avg.TN(ist - 1, r - 1) = v;
639 if (confint) res.TCI(ist - 1, r - 1) = ci_v;
640 } else if (m.measure_type == "Arrival Rate") {
641 res.avg.AN(ist - 1, r - 1) = v;
642 if (confint) res.ACI(ist - 1, r - 1) = ci_v;
643 }
644 // `Residence Time` is deliberately neither requested nor read: JMT's
645 // definition disagrees with LINE's on class-switching models, and the
646 // residence time is recomputed below from the response time.
647 }
648 return res;
649}
650
651/**
652 * The region loss table, port of the `sn.nregions > 0` tail of `getResults.m`.
653 *
654 * JMT EXPOSES NO REGION DROP MEASURE, and its region throughput is the CARRIED
655 * (admitted) rate, so the offered rate is reconstructed by flow balance: the
656 * rate at which stations outside the region route jobs across its boundary,
657 * which the drop does not affect. The difference, clamped at zero, is the loss;
658 * a region whose rule is not DROP loses nothing by construction and is zeroed.
659 */
660template <class T>
662 const std::size_t M = sn.nstations, K = sn.nclasses, F = sn.regions.size();
663 if (F == 0) return;
664 const T zero = num_traits<T>::from_int(0);
665 res.TNfcr = Matrix<T>(F, K, zero);
666 res.DropRateNfcr = Matrix<T>(F, K, zero);
667 for (std::size_t f = 0; f < F; ++f)
668 for (std::size_t r = 0; r < K; ++r) res.TNfcr(f, r) = res.avg.TN(M + f, r);
669
670 for (std::size_t f = 0; f < F; ++f) {
671 const typename qn::NetworkStruct<T>::Region& rg = sn.regions[f];
672 for (std::size_t r = 1; r <= K; ++r) {
673 double acc = 0.0;
674 for (std::size_t ii = 1; ii <= M; ++ii) {
675 if (!(ii <= rg.members.size() && rg.members[ii - 1])) continue;
676 for (std::size_t j = 1; j <= M; ++j) {
677 if (j <= rg.members.size() && rg.members[j - 1]) continue;
678 for (std::size_t rp = 1; rp <= K; ++rp) {
679 const std::size_t a = (j - 1) * K + (rp - 1);
680 const std::size_t b = (ii - 1) * K + (r - 1);
681 if (sn.rt.rows() <= a || sn.rt.cols() <= b) continue;
682 const double w = num_traits<T>::to_double(sn.rt(a, b));
683 if (w != 0.0)
684 acc += num_traits<T>::to_double(res.avg.TN(j - 1, rp - 1)) * w;
685 }
686 }
687 }
688 const double carried = num_traits<T>::to_double(res.TNfcr(f, r - 1));
689 const bool drops = r <= rg.rule.size() && rg.rule[r - 1] == lang::DropStrategy::DROP;
690 res.DropRateNfcr(f, r - 1) =
691 num_traits<T>::from_double(drops ? std::max(0.0, acc - carried) : 0.0);
692 }
693 }
694}
695
696/**
697 * Port of `getResultsJMVA.m`: the per-CHAIN answer spread back over the classes.
698 *
699 * JMVA answers per chain, and each measure is converted to a per-class one by
700 * the class's share of its chain at that station: `alpha(i,k)` weighted by the
701 * ratio of the class service time to the chain's. The conversions are the
702 * reference's, measure by measure, and the two that are not a bare share are
703 * the reason this cannot be a generic rescale:
704 * Utilization is divided by the chain's visits at the REFERENCE station and,
705 * at a multiserver station, scaled by min(N, c)/c.
706 * Residence time is JMVA's per-CHAIN residence and is converted to LINE's
707 * response time per visit by dividing by the class visits.
708 *
709 * THE STATION IS RESOLVED BY NAME, not by the position of the `stationresults`
710 * block. The reference indexes `sn.nservers`, `ST` and `sn.visits` with the
711 * BLOCK index, but the blocks exclude the Source while those tables do not, so
712 * on any open model every station's demand is read one row early. Reading the
713 * `station` attribute is the same lookup on a closed model and the correct one
714 * on an open model.
715 */
716template <class T>
717JmtResult<T> jmt_parse_jmva(const qn::NetworkStruct<T>& sn, const std::string& result_path,
718 const std::string& command_output) {
719 if (!detail::is_file(result_path)) {
720 std::string msg =
721 "SolverJMT: JMT did not output a result file, the analysis has likely failed.";
722 if (!command_output.empty()) msg += " JMT output: " + command_output;
723 throw NumericError(msg);
724 }
725 const std::size_t M = sn.nstations, K = sn.nclasses, C = sn.nchains;
726 const T zero = num_traits<T>::from_int(0);
727 JmtResult<T> res;
728 res.avg.QN = Matrix<T>(M, K, zero);
729 res.avg.UN = Matrix<T>(M, K, zero);
730 res.avg.RN = Matrix<T>(M, K, zero);
731 res.avg.TN = Matrix<T>(M, K, zero);
732 res.avg.AN = Matrix<T>(M, K, zero);
733 res.avg.WN = Matrix<T>(M, K, zero);
734
736
737 std::map<std::string, std::size_t> station_of;
738 for (std::size_t i = 1; i <= M; ++i)
739 station_of[sn.nodes[sn.station_to_node[i - 1] - 1].name] = i;
740
741 const std::unique_ptr<xml::Element> doc = xml::parse_file(result_path);
742 // The normalizing constant, when the algorithm reports one.
743 const std::vector<const xml::Element*> nc = doc->by_tag("normconst");
744 res.log_norm_const = nc.empty()
746 std::numeric_limits<double>::quiet_NaN())
748 std::strtod(nc[0]->attr("logValue").c_str(), nullptr));
749
750 // A Source reports its own arrival rate as its throughput and holds no
751 // jobs; JMVA does not model it, so the row is filled from the struct.
752 if (sn.sourceIdx != 0)
753 for (std::size_t r = 0; r < K; ++r)
754 if (!sn.disabled[sn.sourceIdx - 1][r])
755 res.avg.TN(sn.sourceIdx - 1, r) = sn.rates(sn.sourceIdx - 1, r);
756
757 const std::vector<const xml::Element*> blocks = doc->by_tag("stationresults");
758 for (std::size_t b = 0; b < blocks.size(); ++b) {
759 const auto si = station_of.find(blocks[b]->attr("station"));
760 if (si == station_of.end()) continue;
761 const std::size_t i = si->second;
762 // The divisor is the capacity `write_jmva` exported, max(nservers, max
763 // lldscaling): a load-dependent station carries its c in the scaling and
764 // leaves nservers at 1, so reading nservers alone reports U = c*E[busy]/c.
765 double ns = sn.stations[i - 1].nservers;
766 for (const T& s : sn.stations[i - 1].lldscaling)
767 ns = std::max(ns, num_traits<T>::to_double(s));
768 const std::vector<const xml::Element*> crs = blocks[b]->by_tag("classresults");
769 for (std::size_t c = 1; c <= crs.size() && c <= C; ++c) {
770 const std::vector<const xml::Element*> ms = crs[c - 1]->by_tag("measure");
771 // A multiserver Queue is written as <ldstation servers="1">, and one
772 // ldstation switches JMVA to its load-dependent algorithm, whose
773 // Utilization is 1-p_i(0) at EVERY station, delay ones included. That
774 // is a different random variable from LINE's E[busy servers], so no
775 // rescaling recovers it; derive U from the chain throughput instead,
776 // which both JMVA algorithms report alike.
777 double chain_tput = std::numeric_limits<double>::quiet_NaN();
778 for (std::size_t m = 0; m < ms.size(); ++m) {
779 if (ms[m]->attr("measureType") == "Throughput") {
780 const std::string traw = ms[m]->attr("meanValue");
781 chain_tput = traw == "NaN" ? std::numeric_limits<double>::quiet_NaN()
782 : std::strtod(traw.c_str(), nullptr);
783 break;
784 }
785 }
786 for (std::size_t m = 0; m < ms.size(); ++m) {
787 const std::string kind = ms[m]->attr("measureType");
788 const std::string raw = ms[m]->attr("meanValue");
789 const double val = raw == "NaN" ? std::numeric_limits<double>::quiet_NaN()
790 : std::strtod(raw.c_str(), nullptr);
791 const std::size_t refst = dem.refstatchain[c - 1];
792 const double vchain_ref =
793 num_traits<T>::to_double(dem.Vchain(refst - 1, c - 1));
794 const double stchain = num_traits<T>::to_double(dem.STchain(i - 1, c - 1));
795 for (std::size_t k : sn.inchain[c - 1]) {
796 const double stk = num_traits<T>::to_double(dem.ST(i - 1, k - 1));
797 const double al = num_traits<T>::to_double(dem.alpha(i - 1, k - 1));
798 double v = val;
799 if (kind == "Utilization") {
800 if (vchain_ref == 0.0) continue;
801 v = stk * chain_tput / vchain_ref * al;
802 if (std::isfinite(ns)) v /= ns;
803 res.avg.UN(i - 1, k - 1) = num_traits<T>::from_double(v);
804 } else if (kind == "Throughput") {
805 res.avg.TN(i - 1, k - 1) = num_traits<T>::from_double(val * al);
806 } else if (kind == "Number of Customers") {
807 if (stchain == 0.0 || vchain_ref == 0.0) continue;
808 res.avg.QN(i - 1, k - 1) =
809 num_traits<T>::from_double(val * stk / stchain / vchain_ref * al);
810 } else if (kind == "Residence time" || kind == "Residence Time") {
811 if (stchain == 0.0 || vchain_ref == 0.0) continue;
812 double visits = 0.0;
813 if (c - 1 < sn.visits.size()) {
814 const std::size_t sfi = sn.stateful_of_station(i);
815 if (sfi != 0 && sn.visits[c - 1].rows() >= sfi)
816 visits = num_traits<T>::to_double(sn.visits[c - 1](sfi - 1, k - 1));
817 }
818 if (visits == 0.0) continue;
819 v = (val / visits) * stk / stchain / vchain_ref * al;
820 res.avg.RN(i - 1, k - 1) = num_traits<T>::from_double(v);
821 }
822 }
823 }
824 }
825 }
826 return res;
827}
828
829/**
830 * Port of `SolverJMT.listValidMethods`.
831 *
832 * `replication` is the reference's own TRANSIENT route: a single sample path is
833 * not the transient mean E[N](t), there being no time-ergodicity at a fixed t,
834 * so it averages `sampleSysAggr` over `iter_max` seeds. It is composed from
835 * `jmt_sample_sys_aggr` in `jmt_logs.h` (`jmt_replication`) rather than from
836 * `solver_jmt_run_analyzer`, which is why it is not an arm of the dispatch below.
837 */
838inline std::vector<std::string> jmt_list_valid_methods() {
839 return {"default", "jsim", "replication", "jmva", "jmva.amva", "jmva.mva",
840 "jmva.recal", "jmva.comom", "jmva.chow", "jmva.bs", "jmva.aql",
841 "jmva.lin", "jmva.dmlin"};
842}
843
844/**
845 * The structural half of SolverJMT's method gate; empty when admissible.
846 *
847 * THE RULES A FLAT FEATURE SET CANNOT STATE, and each is named rather than
848 * reported as a bare no:
849 * the transient horizon 'replication' averages `iter_max` sample paths over
850 * [0,T]; a mean at an unstated horizon is not a
851 * quantity, and the horizon is an OPTION rather than a
852 * model feature.
853 * a single-server station the eight closed-form JMVA algorithms are
854 * single-server only, which is why `write_jmva`
855 * refuses the model rather than emitting an
856 * `<ldstation>` the algorithm cannot read. A server
857 * count has no feature name; the load-dependent half
858 * of the same restriction DOES, and rides in
859 * `jmt_feature_set` as an unset LoadDependence.
860 * the load-dependent shape JMT carries a scaling only as a server count, so
861 * alpha(n) = min(n,c) is written exactly and nothing
862 * else is.
863 *
864 * ONE PREDICATE, TWO CALLERS: `solver_jmt_run_analyzer` raises it, so a caller
865 * naming the method by hand gets the sentence rather than a JMT stack trace, and
866 * `autosolver::auto_family_refusal` returns it, so findSolver never offers the
867 * pair. A second copy of any rule is how the gate and the run drift apart.
868 */
869template <class T>
870std::string jmt_method_refusal(const qn::NetworkStruct<T>& sn, const std::string& method,
871 const JmtOptions& opt) {
872 if (method == "replication" && !std::isfinite(opt.max_simulated_time))
873 return "SolverJMT: the 'replication' method needs a finite timespan; a transient mean "
874 "over an unstated horizon is not a quantity";
875 if (qn::jmva_is_closed_only(method)) {
876 double maxFinite = 0.0;
877 for (std::size_t i = 0; i < sn.nstations; ++i) {
878 const double c = static_cast<double>(sn.stations[i].nservers);
879 if (std::isfinite(c)) maxFinite = std::max(maxFinite, c);
880 }
881 if (maxFinite > 1.0) return "SolverJMT: " + method + " does not support multi-server stations";
882 }
883 for (std::size_t i = 0; i < sn.nstations; ++i) {
884 const std::vector<T>& alpha = sn.stations[i].lldscaling;
885 if (alpha.empty()) continue;
886 double c = 0.0;
887 for (const T& s : alpha) c = std::max(c, num_traits<T>::to_double(s));
888 if (c == 1.0) continue;
889 bool ok = (c >= 1.0) && (c == std::floor(c));
890 for (std::size_t n = 0; ok && n < alpha.size(); ++n)
891 ok = std::abs(num_traits<T>::to_double(alpha[n]) -
892 std::min(static_cast<double>(n + 1), c)) <= qn::GlobalConstants::Zero;
893 if (!ok)
894 return "SolverJMT: station '" + sn.stations[i].name +
895 "' uses a load-dependent scaling that is not the multiserver encoding alpha(n) "
896 "= min(n,c): JMT has no representation for it, since both the JSIM and the JMVA "
897 "writer carry the scaling as a server count, and the model would be solved at "
898 "the nominal service rate. Use SolverCTMC, SolverNC, SolverMVA or SolverSSA, "
899 "which read sn.lldscaling directly";
900 }
901 // A BINDING FINITE BUFFER, which neither engine can carry -- JSIM because no
902 // JMT drop strategy reproduces LINE's blocking, JMVA because its document has
903 // no capacity element at all. The verdict is the WRITER's own
904 // (`io::JmtWriter::buffer_capacity_refusal`), asked here without letting it
905 // raise, so the gate binds exactly where the writer binds.
906 //
907 // WHICH ENGINE IS ASKED ABOUT: a method that is neither engine's gets no
908 // verdict rather than JSIM's rule applied to a run JSIM is not going to make.
909 const bool is_jmva = method.compare(0, 4, "jmva") == 0;
910 const bool is_jsim = method == "default" || method == "jsim" || method == "replication";
911 if (is_jmva || is_jsim) {
912 const std::string cap_reason = io::jmt_buffer_capacity_refusal(sn, is_jmva);
913 if (!cap_reason.empty()) return cap_reason;
914 }
915 return std::string();
916}
917
918/**
919 * Port of `@@SolverJMT/runAnalyzer.m`, the `jsim` and `jmva` arms.
920 *
921 * The sample count is RAISED to 5000 when the caller asks for less, as the
922 * reference does: JMT terminates a measure on its own precision target and
923 * needs that many samples per measure before it will report one, so a smaller
924 * request silently yields unsuccessful measures rather than a faster run.
925 */
926template <class T>
928 JmtOptions opt = opt_in;
929 if (opt.samples < 5000.0) opt.samples = 5000.0;
930 const std::vector<std::string> valid = jmt_list_valid_methods();
931 if (std::find(valid.begin(), valid.end(), opt.method) == valid.end())
932 throw UnsupportedError("SolverJMT: unknown method '" + opt.method + "'");
933 if (opt.method == "replication")
934 throw UnsupportedError(
935 "SolverJMT: 'replication' is the transient average over independent seeds and is "
936 "composed from jmt_sample_sys_aggr, not from this steady-state dispatch; call "
937 "jmt::jmt_replication (jmt_logs.h), which is what -a tran reaches");
938 if (sn.has_immediate_feedback())
939 throw UnsupportedError(
940 "SolverJMT: JMT has no immediate-feedback semantics (a job re-entering service "
941 "while HOLDING the server); use SolverCTMC, SolverSSA or SolverLDES");
942 // The structural half of the gate, which `auto_family_refusal` also asks:
943 // the single-server restriction of the closed-form JMVA algorithms and the
944 // load-dependent shape JMT can carry. A second copy of either rule is how
945 // the report and the run come to disagree.
946 {
947 const std::string refusal = jmt_method_refusal(sn, opt.method, opt);
948 if (!refusal.empty()) throw UnsupportedError(refusal);
949 }
950
951 const bool is_mva = opt.method.compare(0, 4, "jmva") == 0;
952 const std::string mode = is_mva ? "mva" : "sim";
953 util::TempDir work(is_mva ? "jmva" : "jsim");
954 if (opt.keep) work.keep();
955 const std::string model_path = work.path() + "/model." + (is_mva ? "jmva" : "jsim");
956
957 if (is_mva) {
958 // `io/jmva_writer.h` is the shared port of `writeJMVA`: SolverQNS reads
959 // the same grammar through `qnsolver`, and a second copy of the writer
960 // here would be a second place for the chain aggregation to drift.
961 io::write_jmva(sn, model_path, opt.method, static_cast<std::size_t>(opt.samples));
962 } else {
964 wopt.file_name = "model";
965 wopt.log_path = sn.log_path;
966 wopt.seed = opt.seed;
967 wopt.max_samples = opt.samples;
968 wopt.max_simulated_time = opt.max_simulated_time;
969 wopt.sim_conf_int = opt.confint > 0.0 ? opt.confint : 0.99;
970 line::util::LineConsole::step("writing the JSIM model file");
971 detail::write_file(model_path, io::jmt_write_jsim(sn, wopt));
972 line::util::LineConsole::substep("model written to %s", model_path.c_str());
973 }
974
975 line::util::LineConsole::step("running the JMT simulation engine as a subprocess");
976 const util::ProcResult run = jmt_run(mode, model_path, opt.seed, opt);
977 const std::string result_path = model_path + "-result." + jmt_result_ext(mode);
978
979 line::util::LineConsole::step("parsing the JMT result files");
980 JmtResult<T> res;
981 if (is_mva) {
982 res = jmt_parse_jmva(sn, result_path, run.out);
983 } else {
984 res = jmt_map_measures(sn, jmt_parse_measures(result_path, run.out), opt.confint > 0.0);
985 jmt_region_losses(sn, res);
986 }
987 // ResidT DERIVED on both arms as getAvg does (JSIM disagrees under class switching, JMVA per-chain); else column zero. Region rows keep JMT total.
988 {
989 const Matrix<T> WNst = mva::sn_get_residt_from_respt(sn, res.avg.RN);
990 for (std::size_t i = 0; i < sn.nstations; ++i)
991 for (std::size_t r = 0; r < sn.nclasses; ++r) res.avg.WN(i, r) = WNst(i, r);
992 }
993 // A SOURCE HAS NO ARRIVALS TO ITSELF. JMT reports an "Arrival Rate" measure
994 // at every station the model declares, the Source included, where the
995 // reference reports 0: `getAvg.m:197-199` builds a zeroMask over the Source
996 // stations and applies it to the ArvR column for EVERY solver. This port's
997 // analytical arms already leave that entry at zero, so the JMT reader was
998 // the one path that carried JMT's own measure through -- 0.79526 against 0
999 // on fcr_mm1kdrop, 0.30214 and 0.4979 on fcr_constraints. Region rows are
1000 // past `nstations` and are left alone, as the residence time above leaves
1001 // them.
1002 for (std::size_t i = 0; i < sn.nstations; ++i) {
1003 if (sn.stations[i].nodetype != qn::NodeType::Source) continue;
1004 for (std::size_t r = 0; r < sn.nclasses; ++r)
1005 res.avg.AN(i, r) = num_traits<T>::from_int(0);
1006 }
1007 // A CELL WITH NO RESPONSE TIME HOLDS NO JOBS AND BUSIES NO SERVER.
1008 // `@@NetworkSolver/getAvg` builds `zeroMask = RN < 10*FineTol` and applies
1009 // it to the queue length and the utilization for EVERY solver; the
1010 // analytical arms of this port take it through `mva::filter_metric`, and
1011 // the JMT reader was the one path that did not. JMT keeps sampling a
1012 // starved class's utilization long after its response time has been
1013 // discarded as non-recurrent, so the cell arrives as a small positive
1014 // number where every other codebase reports 0: 0.0010617 against 0 on
1015 // prio_hol_closed (PSQueue, Class2), on an otherwise IDENTICAL sample path.
1016 // Region rows past `nstations` are left alone, as the two rules above are.
1017 {
1018 for (std::size_t i = 0; i < sn.nstations; ++i)
1019 for (std::size_t r = 0; r < sn.nclasses; ++r)
1020 if (num_traits<T>::to_double(res.avg.RN(i, r)) <
1022 res.avg.QN(i, r) = num_traits<T>::from_int(0);
1023 res.avg.UN(i, r) = num_traits<T>::from_int(0);
1024 }
1025 }
1026 res.avg.method = opt_in.method;
1027 res.avg.actualmethod = opt.method;
1028 res.avg.iter = 1;
1029 return res;
1030}
1031
1032} // namespace jmt
1033} // namespace line
1034
1035#endif // LINE_SOLVERS_WRAPPERS_JMT_SOLVER_JMT_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
static void substep(const char *fmt,...)
Write one indented progress line.
static void step(const char *fmt,...)
Write one progress line.
void keep()
Leave the directory in place, for a caller that wants to inspect it.
Definition tempdir.h:130
const std::string & path() const
The directory itself, with no trailing separator.
Definition tempdir.h:124
Docker primitives for the backends that legitimately ship an image.
The exception types the port throws.
Minimal HTTP/1.1 client, enough to talk to a line-*-rest service.
Port of @@JMTIO: a refreshed NetworkStruct written out as a JMT .jsimg simulation model.
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").
Response post_json(const std::string &url, const std::string &json, int timeoutMillis)
POST a JSON document.
Definition http.h:361
bool docker_has_storage_for(const std::string &image)
std::string jmt_write_jsim(const qn::NetworkStruct< T > &sn, const JmtWriteOptions &opt)
Port of @@JMTIO/writeJSIM.m: serialize sn as a JMT .jsimg document.
std::string jmt_buffer_capacity_refusal(const qn::NetworkStruct< T > &sn, bool jmva_engine)
JmtWriter::buffer_capacity_refusal without a document: the writer's own binding-buffer verdict,...
bool docker_daemon_available()
True if the Docker daemon is reachable.
bool docker_has_local_image(const std::string &image)
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).
bool docker_pull(const std::string &image)
Pulls an image, streaming Docker's progress to stdout and stderr.
static const char * JMT_DOCKER_IMAGE
The default JMT REST/Docker image, MATLAB's jmtDockerImage candidate.
Definition solver_jmt.h:83
void jmt_region_losses(const qn::NetworkStruct< T > &sn, JmtResult< T > &res)
The region loss table, port of the sn.nregions > 0 tail of getResults.m.
Definition solver_jmt.h:661
util::ProcResult jmt_run(const std::string &mode, const std::string &model_path, long seed, const JmtOptions &opt)
Port of jmtRun: one batch analysis, leaving the result where the JMT CLI itself would leave it.
Definition solver_jmt.h:340
JmtResult< T > jmt_parse_jmva(const qn::NetworkStruct< T > &sn, const std::string &result_path, const std::string &command_output)
Port of getResultsJMVA.m: the per-CHAIN answer spread back over the classes.
Definition solver_jmt.h:717
util::ProcResult jmt_run_docker(const std::string &image, const std::string &mode, const std::string &model_path, long seed, const JmtOptions &opt)
Run the analysis inside the JMT container and copy the result back beside the model,...
Definition solver_jmt.h:398
util::ProcResult jmt_solve_rest(const std::string &rest_url, const std::string &mode, const std::string &model_path, long seed, const JmtOptions &opt)
Port of jmtSolveRest: POST the model document, write the result document back beside the model so the...
Definition solver_jmt.h:293
std::vector< std::string > jmt_list_valid_methods()
Port of SolverJMT.listValidMethods.
Definition solver_jmt.h:838
JmtResult< T > solver_jmt_run_analyzer(const qn::NetworkStruct< T > &sn, const JmtOptions &opt_in)
Port of @@SolverJMT/runAnalyzer.m, the jsim and jmva arms.
Definition solver_jmt.h:927
JmtResult< T > jmt_map_measures(const qn::NetworkStruct< T > &sn, const std::vector< JmtMeasure > &measures, bool confint)
Port of getResults.m: the measures mapped onto the metric matrices.
Definition solver_jmt.h:520
bool jmt_available()
True when a local JVM and common/JMT.jar are both present.
Definition solver_jmt.h:285
std::vector< JmtMeasure > jmt_parse_measures(const std::string &result_path, const std::string &command_output)
Port of getResultsJSIM: every <measure> of the result document.
Definition solver_jmt.h:453
const char * jmt_result_ext(const std::string &mode)
The suffix jmt.commandline.Jmt appends to the model path, per mode.
Definition solver_jmt.h:278
std::string jmt_method_refusal(const qn::NetworkStruct< T > &sn, const std::string &method, const JmtOptions &opt)
The structural half of SolverJMT's method gate; empty when admissible.
Definition solver_jmt.h:870
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.
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
bool jmva_is_closed_only(const std::string &method)
Port of SolverJMT.getFeatureSet (@@SolverJMT/SolverJMT.m:180-290).
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::unique_ptr< Element > parse_file(const std::string &path)
Read and parse a file.
Definition xml.h:290
A queueing network and its refreshed NetworkStruct.
Chain aggregation and de-aggregation.
The DECLARED side of the gate: one feature set per solver.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
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
The simulation controls the JSIM header carries, MATLAB's JMTIO properties.
Definition jmt_writer.h:68
std::string log_path
model.getLogPath, the logPath attribute
Definition jmt_writer.h:70
std::string file_name
base name; the header echoes it plus .jsimg
Definition jmt_writer.h:69
One <measure> of a JMT result document, by its attributes.
Definition solver_jmt.h:438
std::string measure_type
Definition solver_jmt.h:439
std::string job_class
Definition solver_jmt.h:439
std::string node_type
Definition solver_jmt.h:439
The options of one JMT solve, SolverOptions('JMT') restricted to what is read.
Definition solver_jmt.h:86
int timeout
seconds, for the REST and subprocess arms
Definition solver_jmt.h:95
int iter_max
options.iter_max: the replication count of method = "replication".
Definition solver_jmt.h:98
std::string method
default | jsim | jmva | jmva.<alg>
Definition solver_jmt.h:87
std::string container
options.config.container, an image override
Definition solver_jmt.h:94
std::string rest_url
a JMT REST server; empty selects the local JVM
Definition solver_jmt.h:93
bool keep
keep the scratch directory after the solve
Definition solver_jmt.h:90
double confint
0 disables the confidence interval columns
Definition solver_jmt.h:91
double samples
samples per measure; raised to 5000 below
Definition solver_jmt.h:88
The result of a JMT solve: the shared AvgResult plus what only JMT reports.
Definition solver_jmt.h:493
std::map< std::size_t, std::vector< T > > cache_hit_prob
Per Cache node (1-based node index), the per-class hit probability.
Definition solver_jmt.h:499
Matrix< T > MemOcc
FCR-only: weighted and memory occupation.
Definition solver_jmt.h:496
mva::AvgResult< T > avg
Definition solver_jmt.h:494
Matrix< T > ACI
confidence half-widths, empty when disabled
Definition solver_jmt.h:495
T log_norm_const
result.Prob.logNormConstAggr, the log normalizing constant JMVA reports.
Definition solver_jmt.h:507
Matrix< T > DropRateNfcr
(nregions x nclasses) carried and lost rate
Definition solver_jmt.h:497
static constexpr double FineTol
Definition lang_types.h:668
The metrics getAvg returns, after filtering.
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
Matrix< T > ST
(M x K) class-level mean service time, 0 where disabled
Definition sn_chain.h:54
std::vector< std::size_t > refstatchain
(C) 1-based reference station
Definition sn_chain.h:53
Matrix< T > alpha
(M x K) class share of its chain's visits at a station
Definition sn_chain.h:50
Matrix< T > STchain
(M x C) mean service time
Definition sn_chain.h:48
Matrix< T > Vchain
(M x C) visits
Definition sn_chain.h:49
static constexpr double Zero
Definition lang_types.h:670
FINITE CAPACITY REGIONS, MATLAB's refreshRegions output.
std::vector< DropStrategy > rule
per class
std::vector< bool > members
membership, independent of the caps
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
bool has_attr(const std::string &key) const
Definition xml.h:69
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 ....