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 SolverLQNS 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 EITHER FETCH. The Docker arm is reached only when
32 * `LINE_JMT_DOCKER` opts in, and the jar is downloaded only when
33 * `LINE_JMT_DOWNLOAD` does: this port has no terminal to ask at -- the reference
34 * prompts, and refuses in a `-batch` session -- so an absent variable is a
35 * refusal, exactly as an empty answer is there. Fetching 31 MiB because a solver
36 * was called is not a decision a library may take on its own.
37 *
38 * WHAT THE FETCH ADDS OVER THE REFERENCE, once consented to: the transfer is
39 * https and stays https across redirects, the bytes are checked against a pinned
40 * SHA-256 (`JMT_JAR_SHA256`) before anything is installed, and the file is
41 * renamed into place only after it verifies. MATLAB, the JAR and python all
42 * trust the transport and stop there, so the C++ edition is the only one that
43 * authenticates the jar itself; `LINE_JMT_SHA256` takes a different digest
44 * deliberately and `LINE_JMT_URL` a mirror.
45 *
46 * WHAT IS NOT PORTED, and why it is refused rather than approximated:
47 * getProbAggr / getProbSysAggr Both weigh the simulated trajectory against
48 * `sn.state{isf}`, the model's CURRENT state,
49 * which `qn::NetworkStruct` does not carry --
50 * the same gap that stops `SolverLDES`'s
51 * `getProb` in this port.
52 * the transient ensemble `default` over a finite timespan averages
53 * `sampleSysAggr` over `config.replications`
54 * seeds, and `sampleSysAggr` reads the JMT
55 * arrival/departure LOG files, which requires
56 * a Logger on every station of the model; it is
57 * composed from `jmt_sample_sys_aggr` in
58 * `jmt_logs.h` (`jmt_transient_replications`).
59 */
60
62#include <algorithm>
63#include <cctype>
64#include <cmath>
65#include <cstdio>
66#include <cstdlib>
67#include <fstream>
68#include <limits>
69#include <map>
70#include <sstream>
71#include <string>
72#include <vector>
73
74#include <sys/stat.h>
75#include <unistd.h>
76
79#include "line/io/jmva_writer.h"
80#include "line/io/jmt_writer.h"
85#include "line/util/error.h"
86#include "line/util/http.h"
87#include "line/util/sha256.h"
89#include "line/util/tempdir.h"
90#include "line/util/xml.h"
92
93namespace line {
94namespace jmt {
95
96/** The default JMT REST/Docker image, MATLAB's `jmtDockerImage` candidate. */
97static const char* JMT_DOCKER_IMAGE = "imperialqore/jmt-rest:latest";
98
99/** Where a missing jar is fetched from; `LINE_JMT_URL` names a mirror instead. */
100static const char* JMT_JAR_URL = "https://line-solver.sourceforge.net/latest/JMT.jar";
101
102/**
103 * The digest a fetched jar must have, and the reason the fetch can be trusted.
104 *
105 * The bytes at `JMT_JAR_URL` are a ROLLING BUILD: the path says `latest`, the
106 * jar's own `META-INF/maven/jmt/jmt-light/pom.properties` says `version=latest`,
107 * and it is republished without a version number changing anywhere. So there is
108 * nothing to pin BY NAME, and this is the digest of the build the port was
109 * tested against (published 2026-07-30, 32772660 bytes).
110 *
111 * WHEN UPSTREAM REPUBLISHES, THE DOWNLOAD FAILS LOUDLY and names both digests.
112 * That is the intended failure: the alternative is a solver that silently picks
113 * up whatever a `latest` URL happens to serve. Set `LINE_JMT_SHA256` to the new
114 * digest to take the new build deliberately, and update this constant when the
115 * suite has been run against it.
116 */
117static const char* JMT_JAR_SHA256 =
118 "932cf6b34b04f9ba5b7c3ba0b07dc6c9fa80aa816ffcad7a39ac5d20a31bb9cd";
119
120/** The options of one JMT solve, `SolverOptions('JMT')` restricted to what is read. */
122 std::string method = "default"; ///< default | jsim | jmva | `jmva.<alg>`
123 double samples = 10000.0; ///< samples per measure; raised to 5000 below
124 long seed = 23000;
125 bool keep = false; ///< keep the scratch directory after the solve
126 double confint = 0.99; ///< 0 disables the confidence interval columns
127 double max_simulated_time = std::numeric_limits<double>::infinity();
128 double max_events = -1.0; ///< JSIM event budget, -1 unlimited; set by the samplers as MATLAB's maxEvents
129 std::string rest_url; ///< a JMT REST server; empty selects the local JVM
130 std::string container; ///< `options.config.container`, an image override
131 int timeout = 3600; ///< seconds, for the REST and subprocess arms
132 bool verbose = false;
133 /**
134 * `options.config.replications`: the independent JSIM runs the transient ensemble of `default`
135 * averages over a finite timespan, seeded `seed`, `seed+1`, ... No effect on steady-state JSIM.
136 */
137 int replications = 10;
138};
139
140namespace detail {
141
142inline bool is_file(const std::string& p) {
143 struct stat st;
144 return !p.empty() && ::stat(p.c_str(), &st) == 0 && S_ISREG(st.st_mode);
145}
146
147inline bool is_exec(const std::string& p) {
148 struct stat st;
149 return !p.empty() && ::stat(p.c_str(), &st) == 0 && ::access(p.c_str(), X_OK) == 0;
150}
151
152inline bool is_dir(const std::string& p) {
153 struct stat st;
154 return !p.empty() && ::stat(p.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
155}
156
157inline std::string env_or_empty(const char* name) {
158 const char* v = std::getenv(name);
159 return v != nullptr ? std::string(v) : std::string();
160}
161
162inline std::string exe_dir() {
163 char buf[4096];
164 const ssize_t n = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1);
165 if (n <= 0) return std::string();
166 std::string p(buf, static_cast<std::size_t>(n));
167 const std::size_t s = p.find_last_of('/');
168 return s == std::string::npos ? std::string() : p.substr(0, s);
169}
170
171inline std::string cwd() {
172 char buf[4096];
173 return ::getcwd(buf, sizeof(buf)) != nullptr ? std::string(buf) : std::string();
174}
175
176/**
177 * The `-Djava.io.tmpdir` switch that keeps one run's JMT scratch private.
178 *
179 * NOT A TIDINESS MEASURE: without it two JMT runs on one host CORRUPT EACH
180 * OTHER. `jmt.commandline.Jmt` hands the engine a copy of the model under
181 * `java.io.tmpdir`, and `XMLSimulationOutput.writeAllMeasures` then writes the
182 * measures to `<that dir>/res_sim_<the sim's name attribute>`, copies that file
183 * to `<model>-result.jsim` and DELETES it. Every LINE-written model carries the
184 * same name attribute -- `model.jsim`, from `jmt_write_jsim` and its
185 * MATLAB/JAR/python twins -- so with a shared /tmp every concurrent run fights
186 * over one path, `/tmp/res_sim_model.jsim`. Two outcomes, both seen in the
187 * 2026-09-11 suite: the other run deletes the file first and this one dies
188 * `FileNotFoundException ... at jmt.commandline.Jmt.copyFile`, or it copies the
189 * OTHER run's measures and reports another model's numbers as its own. The
190 * model already sits in a `util::TempDir` of its own, so pointing the JVM's
191 * temp dir at it makes every scratch file this run creates private to it.
192 */
193inline std::string private_tmpdir_flag(const std::string& model_path) {
194 const std::size_t s = model_path.find_last_of('/');
195 const std::string dir = s == std::string::npos ? cwd() : model_path.substr(0, s);
196 return "-Djava.io.tmpdir=" + (dir.empty() ? cwd() : dir);
197}
198
199/**
200 * Port of `jmtGetPath`'s SEARCH; the download is `jmt_download_jar` below.
201 *
202 * The reference fetches JMT.jar from SourceForge the moment it is missing. This
203 * function never does: it reports what is already on disk and returns empty
204 * otherwise, so a caller that only wants to know where the jar is -- the CLI's
205 * environment report, a test deciding whether to skip -- can ask without the
206 * question installing anything. `jmt_run` is what escalates from the empty
207 * answer to a fetch, and only with consent.
208 *
209 * `$LINE_JMT_DIR` overrides the search, for a jar kept outside the tree.
210 */
211inline std::string jmt_jar_path() {
212 const std::string forced = env_or_empty("LINE_JMT_DIR");
213 if (!forced.empty()) return is_file(forced + "/JMT.jar") ? forced : std::string();
214 const std::string roots[2] = {exe_dir(), cwd()};
215 for (int r = 0; r < 2; ++r) {
216 if (roots[r].empty()) continue;
217 std::string dir = roots[r];
218 for (int k = 0; k < 7; ++k) {
219 if (is_file(dir + "/JMT.jar")) return dir;
220 if (is_file(dir + "/common/JMT.jar")) return dir + "/common";
221 dir += "/..";
222 }
223 }
224 return std::string();
225}
226
227/** The first executable `name` on PATH; empty when there is none. */
228inline std::string which_on_path(const std::string& name) {
229 const std::string path = env_or_empty("PATH");
230 std::size_t b = 0;
231 while (b <= path.size()) {
232 const std::size_t e = path.find(':', b);
233 const std::string dir =
234 path.substr(b, e == std::string::npos ? std::string::npos : e - b);
235 if (!dir.empty() && is_exec(dir + "/" + name)) return dir + "/" + name;
236 if (e == std::string::npos) break;
237 b = e + 1;
238 }
239 return std::string();
240}
241
242/** `$LINE_JAVA`, then `$JAVA_HOME/bin/java`, then PATH; empty when absent. */
243inline std::string find_java() {
244 const std::string forced = env_or_empty("LINE_JAVA");
245 if (!forced.empty()) return is_exec(forced) ? forced : std::string();
246 const std::string home = env_or_empty("JAVA_HOME");
247 if (!home.empty() && is_exec(home + "/bin/java")) return home + "/bin/java";
248 return which_on_path("java");
249}
250
251/** `LINE_JMT_DOCKER` in the affirmative; anything else, including unset, is no. */
252inline bool docker_consented() {
253 const std::string v = env_or_empty("LINE_JMT_DOCKER");
254 return v == "1" || v == "true" || v == "TRUE" || v == "yes" || v == "y" || v == "Y";
255}
256
257/**
258 * `LINE_JMT_DOWNLOAD` in the affirmative; anything else, including unset, is no.
259 *
260 * SAME VARIABLE AS THE OTHER THREE EDITIONS, OPPOSITE DEFAULT. There the jar is
261 * fetched unless `LINE_JMT_DOWNLOAD=0` refuses it; here it is fetched only when
262 * something affirms it. So `=1` and `=0` mean the same thing everywhere and only
263 * the unset case differs, which is the policy difference itself: MATLAB, the JAR
264 * and python prompt at a session and this port has nobody to ask.
265 */
266inline bool download_consented() {
267 const std::string v = env_or_empty("LINE_JMT_DOWNLOAD");
268 return v == "1" || v == "true" || v == "TRUE" || v == "yes" || v == "y" || v == "Y";
269}
270
271/**
272 * Where a downloaded jar is written: `$LINE_JMT_DIR`, else the tree's `common/`,
273 * else the working directory.
274 *
275 * The walk is `jmt_jar_path`'s, looking for the DIRECTORY the jar belongs in
276 * rather than for the jar, so the download lands where the next process's search
277 * will find it. `$LINE_JMT_DIR` wins because it is where the caller has said the
278 * jar lives, and a download that ignored it would be invisible to the search
279 * that runs next.
280 */
281inline std::string jmt_download_dir() {
282 const std::string forced = env_or_empty("LINE_JMT_DIR");
283 if (!forced.empty()) return forced;
284 const std::string roots[2] = {exe_dir(), cwd()};
285 for (int r = 0; r < 2; ++r) {
286 if (roots[r].empty()) continue;
287 std::string dir = roots[r];
288 for (int k = 0; k < 7; ++k) {
289 if (is_dir(dir + "/common")) return dir + "/common";
290 dir += "/..";
291 }
292 }
293 return cwd();
294}
295
296/**
297 * Fetches `url` to `dest` with curl, or wget, whichever is on PATH.
298 *
299 * NEITHER IS A DEPENDENCY OF THE BUILD: this arm runs only when the caller has
300 * opted in, and a host with neither tool simply does not get the download. That
301 * is the whole reason the transfer is delegated rather than written here --
302 * libcurl would be a link-time obligation on every binary in the tree, and the
303 * port's own `util/http.h` has no TLS and refuses `https://` by name, which is
304 * the only scheme SourceForge serves the jar on (plain HTTP answers 301).
305 *
306 * Both are told to stay on https ACROSS REDIRECTS, not merely on the first
307 * request: the published URL is a redirect to begin with, and a downgrade to
308 * plaintext part-way through is exactly what must not pass unnoticed. Both
309 * inherit stdio, so the caller sees the transfer's own progress, as the Docker
310 * arm lets `docker pull` report its own.
311 */
312inline std::string find_fetcher() {
313 const std::string curl = which_on_path("curl");
314 return curl.empty() ? which_on_path("wget") : curl;
315}
316
317inline bool fetch_url(const std::string& tool, const std::string& url, const std::string& dest) {
318 const bool is_curl = tool.size() >= 4 && tool.compare(tool.size() - 4, 4, "curl") == 0;
319 if (is_curl) {
320 std::vector<std::string> argv;
321 argv.push_back(tool);
322 argv.push_back("-fL");
323 argv.push_back("--proto");
324 argv.push_back("=https");
325 argv.push_back("--proto-redir");
326 argv.push_back("=https");
327 argv.push_back("--tlsv1.2");
328 argv.push_back("--connect-timeout");
329 argv.push_back("30");
330 argv.push_back("--max-time");
331 argv.push_back("1800");
332 argv.push_back("-o");
333 argv.push_back(dest);
334 argv.push_back(url);
335 return util::run_inherit(argv) == 0;
336 }
337 if (tool.empty()) return false;
338 std::vector<std::string> argv;
339 argv.push_back(tool);
340 argv.push_back("--https-only");
341 argv.push_back("--timeout=30");
342 argv.push_back("--tries=2");
343 argv.push_back("-O");
344 argv.push_back(dest);
345 argv.push_back(url);
346 return util::run_inherit(argv) == 0;
347}
348
349/** The digest a fetched jar must match: `$LINE_JMT_SHA256`, else the pin. */
350inline std::string expected_jar_sha256() {
351 std::string want = util::trim(env_or_empty("LINE_JMT_SHA256"));
352 if (want.empty()) want = JMT_JAR_SHA256;
353 for (std::size_t i = 0; i < want.size(); ++i)
354 want[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(want[i])));
355 return want;
356}
357
358/**
359 * The download `jmtGetPath` does, with the consent and the verification the
360 * reference has no way to offer.
361 *
362 * @return the directory the jar now sits in, or EMPTY when nothing was fetched
363 * because nothing consented or no fetcher exists. A fetch that was
364 * ATTEMPTED and went wrong throws instead: a half-installed or
365 * unrecognised jar must not read as "JMT is simply not available here".
366 *
367 * The bytes land on a `.part-<pid>` beside the destination and are renamed in
368 * only once the digest matches, so a failed or truncated transfer never leaves
369 * a `JMT.jar` behind for the next run to find and trust, and two processes
370 * fetching at once cannot interleave into one file. The digest is the point of
371 * the exercise: curl and wget authenticate the SERVER, and only this
372 * authenticates the ARTEFACT.
373 */
374inline std::string jmt_download_jar() {
375 if (!download_consented()) return std::string();
376
377 std::string url = env_or_empty("LINE_JMT_URL");
378 if (url.empty()) url = JMT_JAR_URL;
379 if (url.compare(0, 8, "https://") != 0)
380 throw InputError("SolverJMT: refusing to fetch JMT.jar over '" + url +
381 "': the download must be https, so that the transfer is authenticated "
382 "before the digest authenticates the file");
383
384 const std::string dir = jmt_download_dir();
385 if (!is_dir(dir) || ::access(dir.c_str(), W_OK) != 0)
386 throw InputError("SolverJMT: cannot write JMT.jar to '" + dir +
387 "': the directory does not exist or is not writable. Set LINE_JMT_DIR to "
388 "a directory that is");
389
390 const std::string tool = find_fetcher();
391 if (tool.empty())
392 throw InputError("SolverJMT: LINE_JMT_DOWNLOAD is set but neither curl nor wget is on "
393 "PATH, and this port has no TLS client of its own. Install one, or place "
394 "JMT.jar in " +
395 dir + " by hand");
396
397 const std::string dest = dir + "/JMT.jar";
398 const std::string part = dest + ".part-" + std::to_string(static_cast<long>(::getpid()));
399 ::unlink(part.c_str());
400 std::printf("\nJava Modelling Tools cannot be found. Fetching %s (about 31 MiB) into %s\n",
401 url.c_str(), dir.c_str());
402 std::fflush(stdout);
403 if (!fetch_url(tool, url, part)) {
404 ::unlink(part.c_str());
405 throw InputError("SolverJMT: the download of " + url + " failed; see the transfer's own "
406 "message above");
407 }
408
409 const std::string want = expected_jar_sha256();
410 const std::string got = util::sha256_file_hex(part);
411 if (got != want) {
412 struct stat st;
413 const long long bytes = ::stat(part.c_str(), &st) == 0 ? static_cast<long long>(st.st_size)
414 : -1;
415 ::unlink(part.c_str());
416 throw InputError(
417 "SolverJMT: the jar fetched from " + url + " is not the one this port pins.\n" +
418 " expected sha256 " + want + "\n" + " received sha256 " +
419 (got.empty() ? std::string("<unreadable>") : got) + " (" + std::to_string(bytes) +
420 " bytes)\n" +
421 "The file has been discarded. That URL serves a rolling 'latest' build, so an upstream "
422 "republication looks exactly like this: check the digest against the publisher and, if "
423 "it is the build you want, set LINE_JMT_SHA256 to it");
424 }
425
426 if (::rename(part.c_str(), dest.c_str()) != 0) {
427 ::unlink(part.c_str());
428 throw InputError("SolverJMT: could not move the verified jar into place at '" + dest + "'");
429 }
430 std::printf("Verified sha256 %s and installed %s\n", got.c_str(), dest.c_str());
431 std::fflush(stdout);
432 return dir;
433}
434
435inline std::string read_file(const std::string& path) {
436 std::ifstream f(path.c_str(), std::ios::binary);
437 if (!f) throw InputError("SolverJMT: cannot read '" + path + "'");
438 std::ostringstream ss;
439 ss << f.rdbuf();
440 return ss.str();
441}
442
443inline void write_file(const std::string& path, const std::string& text) {
444 std::ofstream f(path.c_str(), std::ios::binary);
445 if (!f) throw InputError("SolverJMT: cannot write '" + path + "'");
446 f.write(text.data(), static_cast<std::streamsize>(text.size()));
447 if (!f) throw InputError("SolverJMT: write failed on '" + path + "'");
448}
449
450/** JSON string escaping, for the REST request body. */
451inline std::string json_escape(const std::string& s) {
452 std::string out;
453 out.reserve(s.size() + 16);
454 for (std::size_t i = 0; i < s.size(); ++i) {
455 const unsigned char c = static_cast<unsigned char>(s[i]);
456 switch (c) {
457 case '"': out += "\\\""; break;
458 case '\\': out += "\\\\"; break;
459 case '\n': out += "\\n"; break;
460 case '\r': out += "\\r"; break;
461 case '\t': out += "\\t"; break;
462 default:
463 if (c < 0x20) {
464 char buf[8];
465 std::snprintf(buf, sizeof(buf), "\\u%04x", c);
466 out += buf;
467 } else {
468 out.push_back(s[i]);
469 }
470 }
471 }
472 return out;
473}
474
475/**
476 * The value of a top-level JSON string field, unescaped.
477 *
478 * The REST response is a small, flat envelope -- `status`, `error`,
479 * `raw_output.result_xml`, `raw_output.stdout` -- and pulling four strings out
480 * of it does not warrant making this header depend on the JSON library that
481 * only the LDES client uses. A malformed response yields an empty string, and
482 * the caller then reports that the server sent no result document.
483 */
484inline std::string json_string_field(const std::string& body, const std::string& key) {
485 const std::string needle = "\"" + key + "\"";
486 std::size_t p = body.find(needle);
487 if (p == std::string::npos) return std::string();
488 p = body.find(':', p + needle.size());
489 if (p == std::string::npos) return std::string();
490 ++p;
491 while (p < body.size() && (body[p] == ' ' || body[p] == '\t' || body[p] == '\n')) ++p;
492 if (p >= body.size() || body[p] != '"') return std::string();
493 ++p;
494 std::string out;
495 while (p < body.size() && body[p] != '"') {
496 if (body[p] == '\\' && p + 1 < body.size()) {
497 ++p;
498 switch (body[p]) {
499 case 'n': out.push_back('\n'); break;
500 case 'r': out.push_back('\r'); break;
501 case 't': out.push_back('\t'); break;
502 case 'u': {
503 if (p + 4 < body.size()) {
504 const long cp = std::strtol(body.substr(p + 1, 4).c_str(), nullptr, 16);
505 if (cp < 0x80) out.push_back(static_cast<char>(cp));
506 p += 4;
507 }
508 break;
509 }
510 default: out.push_back(body[p]);
511 }
512 } else {
513 out.push_back(body[p]);
514 }
515 ++p;
516 }
517 return out;
518}
519
520} // namespace detail
521
522template <class T>
523struct JmtResult;
524
525inline util::ProcResult jmt_run_docker(const std::string& image, const std::string& mode,
526 const std::string& model_path, long seed,
527 const JmtOptions& opt);
528
529/** The suffix `jmt.commandline.Jmt` appends to the model path, per mode. */
530inline const char* jmt_result_ext(const std::string& mode) {
531 if (mode == "sim") return "jsim";
532 if (mode == "mva") return "jmva";
533 throw InputError("SolverJMT: unknown JMT analysis mode '" + mode + "'");
534}
535
536/** True when a local JVM and `common/JMT.jar` are both present. */
537inline bool jmt_available() {
538 return !detail::jmt_jar_path().empty() && !detail::find_java().empty();
539}
540
541/**
542 * Port of `jmtSolveRest`: POST the model document, write the result document
543 * back beside the model so the local parsers are unaffected.
544 */
545inline util::ProcResult jmt_solve_rest(const std::string& rest_url, const std::string& mode,
546 const std::string& model_path, long seed,
547 const JmtOptions& opt) {
548 std::string url = rest_url;
549 while (!url.empty() && url[url.size() - 1] == '/') url.erase(url.size() - 1);
550 // A caller may hand over the full route; anything else gets the default one.
551 if (url.size() < 8 || url.substr(url.size() - 8) != "/solve/" + mode.substr(0, 1))
552 if (url.find("/api/v") == std::string::npos) url += "/api/v1/solve/" + mode;
553
554 std::string body = "{\"model\":{\"content\":\"" +
555 detail::json_escape(detail::read_file(model_path)) +
556 "\",\"base64\":false}";
557 // JMVA takes its algorithm and tolerance from the model document, so the
558 // seed is meaningful only on the simulation route; the server's option
559 // allow-list rejects it on /solve/mva.
560 if (mode == "sim") body += ",\"options\":{\"seed\":" + std::to_string(seed) + "}";
561 body += "}";
562
563 const http::Response resp = http::post_json(url, body, opt.timeout * 1000);
564 if (resp.status != 200)
565 throw InputError("SolverJMT: the JMT REST server at " + url + " answered HTTP " +
566 std::to_string(resp.status));
567 const std::string status = detail::json_string_field(resp.body, "status");
568 if (status != "completed") {
569 const std::string err = detail::json_string_field(resp.body, "error");
570 throw InputError("SolverJMT: the JMT REST solve failed: " +
571 (err.empty() ? std::string("unspecified error") : err));
572 }
573 const std::string xml = detail::json_string_field(resp.body, "result_xml");
574 if (xml.empty())
575 throw InputError(
576 "SolverJMT: the JMT REST response carries no result document. The server was asked "
577 "to include the raw output; check that include_raw_output is not disabled");
578 detail::write_file(model_path + "-result." + jmt_result_ext(mode), xml);
580 r.exitCode = 0;
581 r.out = detail::json_string_field(resp.body, "stdout");
582 return r;
583}
584
585/**
586 * Port of `jmtRun`: one batch analysis, leaving the result where the JMT CLI
587 * itself would leave it.
588 *
589 * @param mode "sim" or "mva"
590 * @param model_path the document JMT is to read; the result lands beside it
591 */
592inline util::ProcResult jmt_run(const std::string& mode, const std::string& model_path, long seed,
593 const JmtOptions& opt) {
594 if (!opt.rest_url.empty()) return jmt_solve_rest(opt.rest_url, mode, model_path, seed, opt);
595
596 std::string jar_dir = detail::jmt_jar_path();
597 const std::string java = detail::find_java();
598 // A jar is worth fetching only when there is a JVM to run it with: the
599 // Docker arm below needs none, so downloading one for it would be 31 MiB
600 // spent on a file nothing reads. Returns empty unless LINE_JMT_DOWNLOAD
601 // consents; see the header note.
602 if (jar_dir.empty() && !java.empty()) jar_dir = detail::jmt_download_jar();
603 if (!jar_dir.empty() && !java.empty()) {
604 std::vector<std::string> argv;
605 argv.push_back(java);
606 argv.push_back(detail::private_tmpdir_flag(model_path));
607 argv.push_back("-cp");
608 argv.push_back(jar_dir + "/JMT.jar");
609 argv.push_back("jmt.commandline.Jmt");
610 argv.push_back(mode);
611 argv.push_back(model_path);
612 argv.push_back("-seed");
613 argv.push_back(std::to_string(seed));
614 argv.push_back("--illegal-access=permit");
615 const util::ProcResult r = util::capture(argv, opt.timeout, true);
616 // A KILLED RUN IS NOT A RESULT. Without this the caller falls through to
617 // "no result document", which reads as a solver that refused the model
618 // rather than one whose wall-clock budget expired -- and a jsim that is
619 // merely SLOW on a loaded host is exactly the case that hits it. Same
620 // rule the LQNS, QNS and LDES wrappers already apply to util::capture.
621 if (r.timedOut)
622 throw ExternalError(ErrorKind{"JMT"}, "SolverJMT: the JMT run did not finish within " +
623 std::to_string(opt.timeout) + "s and was killed");
624 return r;
625 }
626
627 // The Docker arm. The image is used only when it is ALREADY PRESENT or the
628 // caller has opted into the pull; see the header note on consent.
629 std::string image = opt.container.empty() ? detail::env_or_empty("LINE_JMT_IMAGE")
630 : opt.container;
631 if (image.empty()) image = JMT_DOCKER_IMAGE;
633 bool have = io::docker_has_local_image(image);
634 if (!have && detail::docker_consented() && io::docker_has_storage_for(image))
635 have = io::docker_pull(image);
636 if (have) return jmt_run_docker(image, mode, model_path, seed, opt);
637 }
638
639 // Say which half was missing: a jar outside the searched roots reads like a missing JVM otherwise.
640 const std::string found =
641 std::string(java.empty() ? "no Java runtime (LINE_JAVA, JAVA_HOME or PATH)" : "a Java runtime") +
642 (jar_dir.empty() ? " and no JMT.jar (LINE_JMT_DIR, else JMT.jar or common/JMT.jar in the "
643 "executable's or working directory or up to 6 levels above)"
644 : " and JMT.jar in " + jar_dir);
645 throw ExternalError(ErrorKind{"JMT"},
646 "SolverJMT: no way to reach JMT: found " + found + ". Install a Java runtime and put "
647 "JMT.jar in common/ (" + std::string(JMT_JAR_URL) +
648 ") or in the directory LINE_JMT_DIR names, or set LINE_JMT_DOWNLOAD=1 to let curl or wget "
649 "fetch and verify that jar for you, or "
650 "point options.rest_url at a JMT REST server, or set LINE_JMT_DOCKER=1 to allow the " +
651 image + " image to be pulled and used");
652}
653
654/**
655 * Run the analysis inside the JMT container and copy the result back beside the
656 * model, so the caller sees the layout a local JVM would have produced.
657 *
658 * The model is STAGED under a fresh directory and the container is given that
659 * directory: in `mva` mode JMT rewrites the model file itself, and a confined
660 * Docker cannot bind-mount the system temp directory the model normally lives
661 * in. The container runs as the calling user so the files it writes are not
662 * left owned by root.
663 */
664inline util::ProcResult jmt_run_docker(const std::string& image, const std::string& mode,
665 const std::string& model_path, long seed,
666 const JmtOptions& opt) {
667 util::TempDir work("jmt-docker", true);
668 const std::size_t slash = model_path.find_last_of('/');
669 const std::string base =
670 slash == std::string::npos ? model_path : model_path.substr(slash + 1);
671 const std::string staged = work.path() + "/" + base;
672 detail::write_file(staged, detail::read_file(model_path));
673
674 const std::string uid = std::to_string(static_cast<long>(::getuid()));
675 const std::string gid = std::to_string(static_cast<long>(::getgid()));
676 std::vector<std::string> argv;
677 argv.push_back("docker");
678 argv.push_back("run");
679 argv.push_back("--rm");
680 argv.push_back("--user");
681 argv.push_back(uid + ":" + gid);
682 argv.push_back("-v");
683 argv.push_back(work.path() + ":" + work.path());
684 argv.push_back("-w");
685 argv.push_back(work.path());
686 argv.push_back(image);
687 argv.push_back(mode);
688 argv.push_back(base);
689 argv.push_back("-seed");
690 argv.push_back(std::to_string(seed));
691 const util::ProcResult r = util::capture(argv, opt.timeout, true);
692 if (r.timedOut)
693 throw ExternalError(ErrorKind{"JMT"}, "SolverJMT: the JMT container run did not finish within " +
694 std::to_string(opt.timeout) + "s and was killed");
695
696 const std::string produced = staged + "-result." + jmt_result_ext(mode);
697 if (detail::is_file(produced))
698 detail::write_file(model_path + "-result." + jmt_result_ext(mode),
699 detail::read_file(produced));
700 return r;
701}
702
703/** One `<measure>` of a JMT result document, by its attributes. */
706 double mean = 0.0, lower = 0.0, upper = 0.0;
707 double analyzed_samples = 0.0;
708 bool successful = false;
709 bool has_bounds = false;
710};
711
712/**
713 * Port of `getResultsJSIM`: every `<measure>` of the result document.
714 *
715 * A MISSING FILE IS A FAILED SIMULATION, not an empty result: JMT writes the
716 * document even when every measure is unsuccessful, so its absence means the
717 * run itself did not complete, and returning zeros would report a solved model.
718 */
719inline std::vector<JmtMeasure> jmt_parse_measures(const std::string& result_path,
720 const std::string& command_output) {
721 if (!detail::is_file(result_path)) {
722 std::string msg =
723 "SolverJMT: JMT did not output a result file, the simulation has likely failed.";
724 if (!command_output.empty()) msg += " JMT output: " + command_output;
725 throw ExternalError(ErrorKind{"JMT"}, msg);
726 }
727 const std::unique_ptr<xml::Element> doc = xml::parse_file(result_path);
728 std::vector<JmtMeasure> out;
729 const std::vector<const xml::Element*> ms = doc->by_tag("measure");
730 for (std::size_t i = 0; i < ms.size(); ++i) {
731 const xml::Element* e = ms[i];
732 JmtMeasure m;
733 m.measure_type = e->attr("measureType");
734 m.station = e->attr("station");
735 m.job_class = e->attr("class");
736 m.node_type = e->attr("nodeType");
737 m.mean = std::strtod(e->attr("meanValue").c_str(), nullptr);
738 m.analyzed_samples = std::strtod(e->attr("analyzedSamples").c_str(), nullptr);
739 m.successful = e->attr("successful") == "true";
740 if (e->has_attr("lowerLimit") && e->has_attr("upperLimit")) {
741 m.lower = std::strtod(e->attr("lowerLimit").c_str(), nullptr);
742 m.upper = std::strtod(e->attr("upperLimit").c_str(), nullptr);
743 m.has_bounds = true;
744 }
745 out.push_back(m);
746 }
747 return out;
748}
749
750/**
751 * The result of a JMT solve: the shared `AvgResult` plus what only JMT reports.
752 *
753 * The FCR rows extend the metric matrices past `nstations`, which is the
754 * reference's layout (`getResults.m` allocates `nstations + nregions` rows) and
755 * is what makes a region's measures reachable through the same table as a
756 * station's.
757 */
758template <class T>
759struct JmtResult {
761 Matrix<T> QCI, UCI, RCI, TCI, ACI; ///< confidence half-widths, empty when disabled
762 Matrix<T> Weight, MemOcc; ///< FCR-only: weighted and memory occupation
763 Matrix<T> TNfcr, DropRateNfcr; ///< (nregions x nclasses) carried and lost rate
764 /** getResults.m `result.Avg.Tard` (stations x classes) and `.SysTard` (1 x classes). */
766 Matrix<T> TardCI, SysTardCI; ///< their half-widths, empty when disabled
767 /** Per Cache node (1-based node index), the per-class hit probability. */
768 std::map<std::size_t, std::vector<T>> cache_hit_prob;
769 /**
770 * `result.Prob.logNormConstAggr`, the log normalizing constant JMVA reports.
771 *
772 * NaN on the simulation path and on any JMVA algorithm that does not
773 * compute one -- an AMVA approximation has no G to report -- rather than
774 * zero, which is a legitimate value of a log constant.
775 */
776 T log_norm_const = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
777};
778
779/**
780 * Port of `getResults.m`: the measures mapped onto the metric matrices.
781 *
782 * THE RECURRENCE TEST IS WHY `analyzedSamples` IS READ. For a CLOSED class JMT
783 * may terminate a measure before it has seen more samples than there are jobs
784 * in the chain, and such a measure is not an estimate of anything; the
785 * reference zeroes it rather than reporting it, and so does this. An OPEN class
786 * has no such bound and is taken as reported.
787 */
788template <class T>
790 const std::vector<JmtMeasure>& measures, bool confint) {
791 const std::size_t M = sn.nstations, K = sn.nclasses, F = sn.regions.size();
792 const T zero = num_traits<T>::from_int(0);
793 const double nan = std::numeric_limits<double>::quiet_NaN();
794 JmtResult<T> res;
795 res.avg.QN = Matrix<T>(M + F, K, zero);
796 res.avg.UN = Matrix<T>(M + F, K, zero);
797 res.avg.RN = Matrix<T>(M + F, K, zero);
798 res.avg.TN = Matrix<T>(M + F, K, zero);
799 res.avg.AN = Matrix<T>(M + F, K, zero);
800 res.avg.WN = Matrix<T>(M + F, K, zero);
801 res.Weight = Matrix<T>(M + F, K, num_traits<T>::from_double(nan));
802 res.MemOcc = Matrix<T>(M + F, K, num_traits<T>::from_double(nan));
803 if (confint) {
804 res.QCI = Matrix<T>(M, K, zero);
805 res.UCI = Matrix<T>(M, K, zero);
806 res.RCI = Matrix<T>(M, K, zero);
807 res.TCI = Matrix<T>(M, K, zero);
808 res.ACI = Matrix<T>(M, K, zero);
809 res.TardCI = Matrix<T>(M, K, zero);
810 res.SysTardCI = Matrix<T>(1, K, zero);
811 }
812 res.Tard = Matrix<T>(M, K, zero);
813 res.SysTard = Matrix<T>(1, K, zero);
814 // JMT reports no utilization and no arrival rate for a region.
815 for (std::size_t f = 0; f < F; ++f)
816 for (std::size_t r = 0; r < K; ++r) {
817 res.avg.UN(M + f, r) = num_traits<T>::from_double(nan);
818 res.avg.AN(M + f, r) = num_traits<T>::from_double(nan);
819 }
820
821 std::map<std::string, std::size_t> station_of, class_of;
822 for (std::size_t i = 1; i <= sn.nodes.size(); ++i) station_of[sn.nodes[i - 1].name] = i;
823 for (std::size_t r = 1; r <= K; ++r) class_of[sn.classes[r - 1].name] = r;
824
825 // The chain population each closed class's recurrence test compares against.
826 std::vector<double> chainpop(K, 0.0);
827 for (std::size_t c = 0; c < sn.inchain.size(); ++c) {
828 double tot = 0.0;
829 for (std::size_t r : sn.inchain[c]) {
830 const double n = sn.classes[r - 1].population;
831 if (std::isfinite(n)) tot += n;
832 }
833 for (std::size_t r : sn.inchain[c]) chainpop[r - 1] = tot;
834 }
835
836 // Fork-join section measures, per (fork node, class), see JmtWriter::save_fj_metrics. 1-based keys.
837 std::map<std::pair<std::size_t, std::size_t>, double> fj_respt, fj_qlen;
838
839 for (std::size_t i = 0; i < measures.size(); ++i) {
840 const JmtMeasure& m = measures[i];
841 const double half = m.has_bounds ? (m.upper - m.lower) / 2.0 : 0.0;
842
843 if (m.measure_type == "Fork Join Response Time" ||
844 m.measure_type == "Fork Join Number of Customers") {
845 const auto ni = station_of.find(m.station);
846 const auto ci = class_of.find(m.job_class);
847 if (ni != station_of.end() && ci != class_of.end()) {
848 const std::pair<std::size_t, std::size_t> key(ni->second, ci->second);
849 if (m.measure_type == "Fork Join Response Time")
850 fj_respt[key] = m.mean;
851 else
852 fj_qlen[key] = m.mean;
853 }
854 continue;
855 }
856
857 // A region measure is identified by its nodeType, or by the name the
858 // writer gave it -- JMT does not always echo the nodeType back.
859 const bool is_fcr =
860 m.node_type == "region" || m.station.compare(0, 8, "FCRegion") == 0;
861 if (is_fcr && m.station.compare(0, 8, "FCRegion") == 0) {
862 const long f = std::strtol(m.station.substr(8).c_str(), nullptr, 10);
863 if (f < 1 || static_cast<std::size_t>(f) > F) continue;
864 const std::size_t row = M + static_cast<std::size_t>(f) - 1;
865 // A region measure is AGGREGATE, not per class. The reference
866 // spreads the extensive ones (customers, throughput, arrival rate,
867 // weight, memory) evenly over the classes and repeats the intensive
868 // ones (response and residence time) unchanged, so that summing a
869 // column recovers the region total in both cases.
870 const double per_class = m.mean / static_cast<double>(K);
871 for (std::size_t r = 0; r < K; ++r) {
872 if (m.measure_type == "Number of Customers")
873 res.avg.QN(row, r) = num_traits<T>::from_double(per_class);
874 else if (m.measure_type == "Response Time")
875 res.avg.RN(row, r) = num_traits<T>::from_double(m.mean);
876 else if (m.measure_type == "Residence Time")
877 res.avg.WN(row, r) = num_traits<T>::from_double(m.mean);
878 else if (m.measure_type == "Throughput")
879 res.avg.TN(row, r) = num_traits<T>::from_double(per_class);
880 else if (m.measure_type == "Arrival Rate")
881 res.avg.AN(row, r) = num_traits<T>::from_double(per_class);
882 else if (m.measure_type == "FCR Capacity")
883 res.Weight(row, r) = num_traits<T>::from_double(per_class);
884 else if (m.measure_type == "FCR Memory")
885 res.MemOcc(row, r) = num_traits<T>::from_double(per_class);
886 }
887 continue;
888 }
889
890 if (m.measure_type == "Cache Hit Rate") {
891 const auto ni = station_of.find(m.station);
892 const auto ci = class_of.find(m.job_class);
893 if (ni == station_of.end() || ci == class_of.end()) continue;
894 if (sn.nodes[ni->second - 1].nodetype != lang::NodeType::Cache) continue;
895 const auto cp = sn.nodeparam.find(ni->second);
896 if (cp == sn.nodeparam.end()) continue;
897 std::vector<T>& hit = res.cache_hit_prob[ni->second];
898 if (hit.empty()) hit.assign(K, zero);
899 // The measure names the HIT class; the probability belongs to the
900 // read class that switches into it.
901 for (std::size_t r = 0; r < cp->second.hitclass.size(); ++r)
902 if (cp->second.hitclass[r] == ci->second)
904 continue;
905 }
906
907 // A SYSTEM measure names no station (saveMetric.m blanks referenceNode), so it is
908 // matched on the class alone, with the same recurrence test as a station measure.
909 if (m.measure_type == "System Tardiness") {
910 const auto ci = class_of.find(m.job_class);
911 if (ci == class_of.end()) continue;
912 const std::size_t r = ci->second;
913 const bool open = !std::isfinite(sn.classes[r - 1].population);
914 const bool recurrent = open || m.analyzed_samples > chainpop[r - 1];
915 res.SysTard(0, r - 1) = num_traits<T>::from_double(recurrent ? m.mean : 0.0);
916 if (confint) res.SysTardCI(0, r - 1) = num_traits<T>::from_double(recurrent ? half : 0.0);
917 continue;
918 }
919
920 const auto ni = station_of.find(m.station);
921 const auto ci = class_of.find(m.job_class);
922 if (ni == station_of.end() || ci == class_of.end()) continue;
923 const std::size_t ist = sn.nodes[ni->second - 1].station;
924 if (ist == 0) continue;
925 const std::size_t r = ci->second;
926 const bool open = !std::isfinite(sn.classes[r - 1].population);
927 const bool recurrent = open || m.analyzed_samples > chainpop[r - 1];
928 const T v = num_traits<T>::from_double(recurrent ? m.mean : 0.0);
929 const T ci_v = num_traits<T>::from_double(recurrent ? half : 0.0);
930
931 if (m.measure_type == "Number of Customers") {
932 res.avg.QN(ist - 1, r - 1) = v;
933 if (confint) res.QCI(ist - 1, r - 1) = ci_v;
934 } else if (m.measure_type == "Utilization") {
935 res.avg.UN(ist - 1, r - 1) = v;
936 if (confint) res.UCI(ist - 1, r - 1) = ci_v;
937 } else if (m.measure_type == "Response Time") {
938 res.avg.RN(ist - 1, r - 1) = v;
939 if (confint) res.RCI(ist - 1, r - 1) = ci_v;
940 } else if (m.measure_type == "Throughput") {
941 res.avg.TN(ist - 1, r - 1) = v;
942 if (confint) res.TCI(ist - 1, r - 1) = ci_v;
943 } else if (m.measure_type == "Arrival Rate") {
944 res.avg.AN(ist - 1, r - 1) = v;
945 if (confint) res.ACI(ist - 1, r - 1) = ci_v;
946 } else if (m.measure_type == "Tardiness") {
947 res.Tard(ist - 1, r - 1) = v;
948 if (confint) res.TardCI(ist - 1, r - 1) = ci_v;
949 }
950 // `Residence Time` is deliberately neither requested nor read: JMT's
951 // definition disagrees with LINE's on class-switching models, and the
952 // residence time is recomputed below from the response time.
953 }
954 // JMT keeps no utilization at an ORDINARY Place (a storage section with no server). A Place is an
955 // INF station, so U == Q, the value SolverCTMC, SolverSSA and SolverNC report. A queueing place
956 // keeps the busy-server utilization JMT measures for its embedded queue.
957 for (std::size_t i = 0; i < sn.nstations; ++i) {
958 if (sn.stations[i].nodetype != lang::NodeType::Place || sn.is_queueing_place(i)) continue;
959 for (std::size_t r = 0; r < K; ++r) {
960 res.avg.UN(i, r) = res.avg.QN(i, r);
961 if (confint) res.UCI(i, r) = res.QCI(i, r);
962 }
963 }
964 // getResults.m's FJ block, carried as `avg.fj`: FJRespT and FJQLen as measured at the Fork (FJQLen is an
965 // independent measurement, so Little holds only up to noise), FJTput the Join's class throughput.
966 if (!solvers::fj_sorted_pairs(sn).empty()) {
967 solvers::FjMetrics<T>& fjm = res.avg.fj;
969 for (std::size_t p = 0; p < fjm.npairs(); ++p) {
970 const std::size_t jst = sn.nodes[fjm.joins[p] - 1].station;
971 for (std::size_t r = 1; r <= K; ++r) {
972 const std::pair<std::size_t, std::size_t> key(fjm.forks[p], r);
973 const auto rt = fj_respt.find(key);
974 if (rt == fj_respt.end() || !std::isfinite(rt->second)) continue;
975 fjm.RespT(p, r - 1) = num_traits<T>::from_double(rt->second);
976 const auto ql = fj_qlen.find(key);
977 if (ql != fj_qlen.end() && std::isfinite(ql->second))
978 fjm.QLen(p, r - 1) = num_traits<T>::from_double(ql->second);
979 if (jst != 0 && jst <= M) fjm.Tput(p, r - 1) = res.avg.TN(jst - 1, r - 1);
980 }
981 }
982 }
983 return res;
984}
985
986/**
987 * The region loss table, port of the `sn.nregions > 0` tail of `getResults.m`.
988 *
989 * JMT EXPOSES NO REGION DROP MEASURE, and its region throughput is the CARRIED
990 * (admitted) rate, so the offered rate is reconstructed by flow balance: the
991 * rate at which stations outside the region route jobs across its boundary,
992 * which the drop does not affect. The difference, clamped at zero, is the loss;
993 * a region whose rule is not DROP loses nothing by construction and is zeroed.
994 */
995template <class T>
997 const std::size_t M = sn.nstations, K = sn.nclasses, F = sn.regions.size();
998 if (F == 0) return;
999 const T zero = num_traits<T>::from_int(0);
1000 res.TNfcr = Matrix<T>(F, K, zero);
1001 res.DropRateNfcr = Matrix<T>(F, K, zero);
1002
1003 for (std::size_t f = 0; f < F; ++f) {
1004 const typename qn::NetworkStruct<T>::Region& rg = sn.regions[f];
1005 for (std::size_t r = 1; r <= K; ++r) {
1006 double acc = 0.0;
1007 // JMT's region Throughput is one value for all classes and its even split gave every class
1008 // the same carried rate; the per-class carried rate is the class-r flow LEAVING the region
1009 // (departures to the Sink are routed to the Source in sn.rt).
1010 double carried = 0.0;
1011 for (std::size_t ii = 1; ii <= M; ++ii) {
1012 if (!(ii <= rg.members.size() && rg.members[ii - 1])) continue;
1013 for (std::size_t j = 1; j <= M; ++j) {
1014 if (j <= rg.members.size() && rg.members[j - 1]) continue;
1015 for (std::size_t rp = 1; rp <= K; ++rp) {
1016 const std::size_t a = (j - 1) * K + (rp - 1);
1017 const std::size_t b = (ii - 1) * K + (r - 1);
1018 if (sn.rt.rows() <= a || sn.rt.cols() <= b) continue;
1019 const double w = num_traits<T>::to_double(sn.rt(a, b));
1020 if (w != 0.0)
1021 acc += num_traits<T>::to_double(res.avg.TN(j - 1, rp - 1)) * w;
1022 const std::size_t ao = (ii - 1) * K + (r - 1), bo = (j - 1) * K + (rp - 1);
1023 if (ao < sn.rt.rows() && bo < sn.rt.cols())
1024 carried += num_traits<T>::to_double(res.avg.TN(ii - 1, r - 1)) *
1025 num_traits<T>::to_double(sn.rt(ao, bo));
1026 }
1027 }
1028 }
1029 res.TNfcr(f, r - 1) = num_traits<T>::from_double(carried);
1030 const bool drops = r <= rg.rule.size() && rg.rule[r - 1] == lang::DropStrategy::DROP;
1031 res.DropRateNfcr(f, r - 1) =
1032 num_traits<T>::from_double(drops ? std::max(0.0, acc - carried) : 0.0);
1033 }
1034 }
1035}
1036
1037/**
1038 * Port of `getResultsJMVA.m`: the per-CHAIN answer spread back over the classes.
1039 *
1040 * JMVA answers per chain, and each measure is converted to a per-class one by
1041 * the class's share of its chain at that station: `alpha(i,k)` weighted by the
1042 * ratio of the class service time to the chain's. The conversions are the
1043 * reference's, measure by measure, and the two that are not a bare share are
1044 * the reason this cannot be a generic rescale:
1045 * Utilization is divided by the chain's visits at the REFERENCE station and,
1046 * at a multiserver station, scaled by min(N, c)/c.
1047 * Residence time is JMVA's per-CHAIN residence and is converted to LINE's
1048 * response time per visit by dividing by the class visits.
1049 *
1050 * THE STATION IS RESOLVED BY NAME, not by the position of the `stationresults`
1051 * block. The reference indexes `sn.nservers`, `ST` and `sn.visits` with the
1052 * BLOCK index, but the blocks exclude the Source while those tables do not, so
1053 * on any open model every station's demand is read one row early. Reading the
1054 * `station` attribute is the same lookup on a closed model and the correct one
1055 * on an open model.
1056 */
1057template <class T>
1058JmtResult<T> jmt_parse_jmva(const qn::NetworkStruct<T>& sn, const std::string& result_path,
1059 const std::string& command_output) {
1060 if (!detail::is_file(result_path)) {
1061 std::string msg =
1062 "SolverJMT: JMT did not output a result file, the analysis has likely failed.";
1063 if (!command_output.empty()) msg += " JMT output: " + command_output;
1064 throw ExternalError(ErrorKind{"JMT"}, msg);
1065 }
1066 const std::size_t M = sn.nstations, K = sn.nclasses, C = sn.nchains;
1067 const T zero = num_traits<T>::from_int(0);
1068 JmtResult<T> res;
1069 res.avg.QN = Matrix<T>(M, K, zero);
1070 res.avg.UN = Matrix<T>(M, K, zero);
1071 res.avg.RN = Matrix<T>(M, K, zero);
1072 res.avg.TN = Matrix<T>(M, K, zero);
1073 res.avg.AN = Matrix<T>(M, K, zero);
1074 res.avg.WN = Matrix<T>(M, K, zero);
1075
1077
1078 std::map<std::string, std::size_t> station_of;
1079 for (std::size_t i = 1; i <= M; ++i)
1080 station_of[sn.nodes[sn.station_to_node[i - 1] - 1].name] = i;
1081
1082 const std::unique_ptr<xml::Element> doc = xml::parse_file(result_path);
1083 // The normalizing constant, when the algorithm reports one.
1084 const std::vector<const xml::Element*> nc = doc->by_tag("normconst");
1085 res.log_norm_const = nc.empty()
1087 std::numeric_limits<double>::quiet_NaN())
1089 std::strtod(nc[0]->attr("logValue").c_str(), nullptr));
1090
1091 // A Source reports its own arrival rate as its throughput and holds no
1092 // jobs; JMVA does not model it, so the row is filled from the struct.
1093 if (sn.sourceIdx != 0)
1094 for (std::size_t r = 0; r < K; ++r)
1095 if (!sn.disabled[sn.sourceIdx - 1][r])
1096 res.avg.TN(sn.sourceIdx - 1, r) = sn.rates(sn.sourceIdx - 1, r);
1097
1098 const std::vector<const xml::Element*> blocks = doc->by_tag("stationresults");
1099 for (std::size_t b = 0; b < blocks.size(); ++b) {
1100 const auto si = station_of.find(blocks[b]->attr("station"));
1101 if (si == station_of.end()) continue;
1102 const std::size_t i = si->second;
1103 // The divisor is the capacity `write_jmva` exported, max(nservers, max
1104 // lldscaling): a load-dependent station carries its c in the scaling and
1105 // leaves nservers at 1, so reading nservers alone reports U = c*E[busy]/c.
1106 double ns = sn.stations[i - 1].nservers;
1107 for (const T& s : sn.stations[i - 1].lldscaling)
1108 ns = std::max(ns, num_traits<T>::to_double(s));
1109 const std::vector<const xml::Element*> crs = blocks[b]->by_tag("classresults");
1110 for (std::size_t c = 1; c <= crs.size() && c <= C; ++c) {
1111 const std::vector<const xml::Element*> ms = crs[c - 1]->by_tag("measure");
1112 // A multiserver Queue is written as <ldstation servers="1">, and one
1113 // ldstation switches JMVA to its load-dependent algorithm, whose
1114 // Utilization is 1-p_i(0) at EVERY station, delay ones included. That
1115 // is a different random variable from LINE's E[busy servers], so no
1116 // rescaling recovers it; derive U from the chain throughput instead,
1117 // which both JMVA algorithms report alike.
1118 double chain_tput = std::numeric_limits<double>::quiet_NaN();
1119 for (std::size_t m = 0; m < ms.size(); ++m) {
1120 if (ms[m]->attr("measureType") == "Throughput") {
1121 const std::string traw = ms[m]->attr("meanValue");
1122 chain_tput = traw == "NaN" ? std::numeric_limits<double>::quiet_NaN()
1123 : std::strtod(traw.c_str(), nullptr);
1124 break;
1125 }
1126 }
1127 for (std::size_t m = 0; m < ms.size(); ++m) {
1128 const std::string kind = ms[m]->attr("measureType");
1129 const std::string raw = ms[m]->attr("meanValue");
1130 const double val = raw == "NaN" ? std::numeric_limits<double>::quiet_NaN()
1131 : std::strtod(raw.c_str(), nullptr);
1132 const std::size_t refst = dem.refstatchain[c - 1];
1133 const double vchain_ref =
1134 num_traits<T>::to_double(dem.Vchain(refst - 1, c - 1));
1135 const double stchain = num_traits<T>::to_double(dem.STchain(i - 1, c - 1));
1136 for (std::size_t k : sn.inchain[c - 1]) {
1137 const double stk = num_traits<T>::to_double(dem.ST(i - 1, k - 1));
1138 const double al = num_traits<T>::to_double(dem.alpha(i - 1, k - 1));
1139 double v = val;
1140 if (kind == "Utilization") {
1141 if (vchain_ref == 0.0) continue;
1142 v = stk * chain_tput / vchain_ref * al;
1143 if (std::isfinite(ns)) v /= ns;
1144 res.avg.UN(i - 1, k - 1) = num_traits<T>::from_double(v);
1145 } else if (kind == "Throughput") {
1146 res.avg.TN(i - 1, k - 1) = num_traits<T>::from_double(val * al);
1147 } else if (kind == "Number of Customers") {
1148 if (stchain == 0.0 || vchain_ref == 0.0) continue;
1149 res.avg.QN(i - 1, k - 1) =
1150 num_traits<T>::from_double(val * stk / stchain / vchain_ref * al);
1151 } else if (kind == "Residence time" || kind == "Residence Time") {
1152 if (stchain == 0.0 || vchain_ref == 0.0) continue;
1153 double visits = 0.0;
1154 if (c - 1 < sn.visits.size()) {
1155 const std::size_t sfi = sn.stateful_of_station(i);
1156 if (sfi != 0 && sn.visits[c - 1].rows() >= sfi)
1157 visits = num_traits<T>::to_double(sn.visits[c - 1](sfi - 1, k - 1));
1158 }
1159 if (visits == 0.0) continue;
1160 v = (val / visits) * stk / stchain / vchain_ref * al;
1161 res.avg.RN(i - 1, k - 1) = num_traits<T>::from_double(v);
1162 }
1163 }
1164 }
1165 }
1166 }
1167 return res;
1168}
1169
1170/**
1171 * Port of `SolverJMT.listValidMethods`.
1172 *
1173 * THE TRANSIENT ENSEMBLE IS NOT A METHOD. A single sample path is not E[N](t),
1174 * there being no time-ergodicity at a fixed t, so `default` over a finite
1175 * timespan averages `sampleSysAggr` over `config.replications` seeds. It is
1176 * composed from `jmt_sample_sys_aggr` in `jmt_logs.h`
1177 * (`jmt_transient_replications`) rather than from `solver_jmt_run_analyzer`.
1178 * The former `replication` method was that ensemble under a method name, and
1179 * asking for it now raises `jmt_removed_method_refusal`.
1180 */
1181inline std::vector<std::string> jmt_list_valid_methods() {
1182 // the names and their order are this solver's row of the method registry (doc/line-methods.json)
1183 return ::line::method_registry::methods("JMT");
1184}
1185
1186/**
1187 * The migration sentence for a method name SolverJMT no longer has; empty for any other name.
1188 *
1189 * An ERROR and not an alias, as for the removed `'console'` in `Solver.m`: `replication` became the
1190 * `options.config.replications` knob of `default`, and the sentence says so rather than a bare "unknown".
1191 */
1192inline std::string jmt_removed_method_refusal(const std::string& method) {
1193 if (method == "replication")
1194 return "SolverJMT: the 'replication' method was removed: use a finite timespan with "
1195 "options.config.replications";
1196 return std::string();
1197}
1198
1199/**
1200 * The structural half of SolverJMT's method gate; empty when admissible.
1201 *
1202 * THE RULES A FLAT FEATURE SET CANNOT STATE, and each is named rather than
1203 * reported as a bare no:
1204 * a removed method name 'replication' is refused with its migration
1205 * sentence (`jmt_removed_method_refusal`).
1206 * a single-server station the eight closed-form JMVA algorithms are
1207 * single-server only, which is why `write_jmva`
1208 * refuses the model rather than emitting an
1209 * `<ldstation>` the algorithm cannot read. A server
1210 * count has no feature name; the load-dependent half
1211 * of the same restriction DOES, and rides in
1212 * `jmt_feature_set` as an unset LoadDependence.
1213 * the load-dependent shape JMT carries a scaling only as a server count, so
1214 * alpha(n) = min(n,c) is written exactly and nothing
1215 * else is.
1216 *
1217 * ONE PREDICATE, TWO CALLERS: `solver_jmt_run_analyzer` raises it, so a caller
1218 * naming the method by hand gets the sentence rather than a JMT stack trace, and
1219 * `autosolver::auto_family_refusal` returns it, so findSolver never offers the
1220 * pair. A second copy of any rule is how the gate and the run drift apart.
1221 */
1222/**
1223 * What the JMVA document cannot state; "" otherwise (MATLAB `jmtMethodRefusal`, isJmva arm).
1224 *
1225 * The document carries a station type, a mean demand and a visit count per chain and nothing else.
1226 * A FCFS station whose classes have different mean service times is not BCMP, and the engine answers
1227 * for the PS station with the same demands (3+2 jobs, rates 2 and 0.5: QLen 1.99/1.76 against the
1228 * exact 2.42/1.65). Every caller here names a jmt method, so all three rules key on the jmva* name.
1229 */
1230template <class T>
1231std::string jmva_document_refusal(const qn::NetworkStruct<T>& sn, const std::string& method) {
1232 if (method.compare(0, 4, "jmva") != 0 && method.compare(0, 8, "jmt.jmva") != 0) return std::string();
1234 return method + " requires a product-form model, but a FCFS station serves its classes with "
1235 "different mean service times, and the JMVA engine would answer for the "
1236 "processor-sharing station with the same demands. Use SolverJMT 'jsim', SolverMVA "
1237 "(whose FCFS correction handles it), SolverCTMC or SolverLDES.";
1239 return "The JMVA document has no fork or join element, so the analytical engine would "
1240 "solve the model without the synchronisation. Use a simulator (SolverJMT 'jsim', "
1241 "SolverLDES) or SolverMVA, which approximates fork-join.";
1242 for (std::size_t i = 0; i < sn.nstations; ++i) {
1243 const qn::SchedStrategy sc = sn.stations[i].sched;
1244 const char* sname = sc == qn::SchedStrategy::FCFS ? "FCFS"
1245 : sc == qn::SchedStrategy::SIRO ? "SIRO"
1246 : sc == qn::SchedStrategy::LCFS ? "LCFS" : nullptr;
1247 if (sname == nullptr) continue;
1248 for (std::size_t r = 0; r < sn.nclasses; ++r) {
1249 const double rate = num_traits<T>::to_double(sn.rates(i, r));
1250 const double scv = num_traits<T>::to_double(sn.scv(i, r));
1251 if (!(std::isfinite(rate) && rate > 0.0 && std::isfinite(scv))) continue;
1252 if (std::fabs(scv - 1.0) <= qn::GlobalConstants::FineTol) continue;
1253 char buf[64];
1254 std::snprintf(buf, sizeof(buf), "%.4g", scv);
1255 return "Station " + sn.stations[i].name + " serves class " + sn.classes[r].name + " under " +
1256 sname + " with a non-exponential law (SCV " + buf + "). The JMVA document carries "
1257 "the mean demand only, so the analytical engine would answer for the exponential "
1258 "law. Use a simulator (SolverJMT 'jsim', SolverLDES), SolverMVA, whose AMVA "
1259 "corrections read the SCV, or SolverCTMC or SolverSSA.";
1260 }
1261 }
1262 return std::string();
1263}
1264
1265template <class T>
1266std::string jmt_method_refusal(const qn::NetworkStruct<T>& sn, const std::string& method,
1267 const JmtOptions& opt) {
1268 (void)opt;
1269 {
1270 const std::string removed = jmt_removed_method_refusal(method);
1271 if (!removed.empty()) return removed;
1272 }
1273 {
1274 const std::string doc = jmva_document_refusal(sn, method);
1275 if (!doc.empty()) return doc;
1276 }
1277 // LINEAR ADMISSION CONSTRAINTS: no JMT region carries A*n <= b, so the writer dropped them.
1278 for (const auto& rg : sn.regions)
1279 if (rg.lincon_A.rows() > 0)
1280 return "SolverJMT does not support linear admission constraints on a finite capacity region (setLinearConstraints): no JMT region carries them, so the region would be simulated without its A*n <= b limit. Use SolverCTMC, SolverNC or SolverLDES.";
1281 // EXHAUSTIVE AND K-LIMITED POLLING. The export is JMT's own, but the engine
1282 // reports a queue length that grows without bound next to a sane response
1283 // time, and a throughput below the arrival rate (QLen 2674, Tput 0.283
1284 // against the exact 0.883 and 0.3 on two classes at 0.3/s, Exp(1.5)
1285 // service, Exp(1) switchover). GATED polling is simulated correctly.
1286 for (std::size_t i = 0; i < sn.nstations; ++i)
1287 for (const lang::PollingType pt : sn.stations[i].polling_type)
1289 return std::string("SolverJMT does not support ") +
1290 (pt == lang::PollingType::EXHAUSTIVE ? "EXHAUSTIVE" : "K-LIMITED") +
1291 " polling: JMT's polling server for it reports a queue length that grows "
1292 "without bound and a throughput below the arrival rate on a stable station. "
1293 "Use SolverMVA, SolverLDES or SolverSSA; JMT simulates GATED polling correctly.";
1294 // IMMEDIATE FEEDBACK, which no JMT engine has semantics for: a self-looping
1295 // job KEEPS the server it just used, and both the JSIM routing block and the
1296 // JMVA visit count answer for a job that leaves and re-queues. This lived in
1297 // `solver_jmt_run_analyzer` alone until 2026-09-13, which is exactly how
1298 // `--find-solver-all` came to offer `jmt.*` on a model every JMT run throws
1299 // on.
1300 if (sn.has_immediate_feedback())
1301 return "SolverJMT: JMT has no immediate-feedback semantics (a job re-entering service "
1302 "while HOLDING the server); use SolverCTMC, SolverSSA or SolverLDES";
1303 if (qn::jmva_is_closed_only(method)) {
1304 double maxFinite = 0.0;
1305 for (std::size_t i = 0; i < sn.nstations; ++i) {
1306 const double c = static_cast<double>(sn.stations[i].nservers);
1307 if (std::isfinite(c)) maxFinite = std::max(maxFinite, c);
1308 }
1309 if (maxFinite > 1.0) return "SolverJMT: " + method + " does not support multi-server stations";
1310 }
1311 // JMVA'S MULTICLASS CoMoM ANSWERS A DIFFERENT MODEL EVERY RUN: when its BTF system is singular,
1312 // SolverMultiClosedCoMoM perturbs every demand with an unseeded MersenneTwister, re-solves, and
1313 // reports the result as an exact CoMoM solution. Keyed on the chain count, as in MATLAB
1314 // jmtMethodRefusal, because the singular case cannot be predicted from the demands; a
1315 // single-chain model runs SolverSingleClosedRECAL and stays exact. An open or mixed model is
1316 // left to the OpenClass refusal of jmt_feature_set: the closed-only CoMoM never runs on it.
1317 const std::vector<double> comom_njobs = sn.njobs();
1318 const bool comom_closed = std::all_of(comom_njobs.begin(), comom_njobs.end(),
1319 [](double n) { return std::isfinite(n); });
1320 if ((method == "jmva.comom" || method == "jmt.jmva.comom") && sn.nchains > 1 && comom_closed)
1321 return method + " is refused on a model with " + std::to_string(sn.nchains) +
1322 " chains: JMT's multiclass CoMoM engine silently re-solves a randomly perturbed model "
1323 "whenever its exact linear system is singular, and reports that as an exact CoMoM "
1324 "solution, so the answer is wrong and different on every run. Use 'jmva.recal', which "
1325 "is exact and reproducible on the same document, 'jmva'/'jmva.mva' for exact MVA, or "
1326 "SolverNC for a LINE-native normalizing constant.";
1327 for (std::size_t i = 0; i < sn.nstations; ++i) {
1328 const std::vector<T>& alpha = sn.stations[i].lldscaling;
1329 if (alpha.empty()) continue;
1330 double c = 0.0;
1331 for (const T& s : alpha) c = std::max(c, num_traits<T>::to_double(s));
1332 if (c == 1.0) continue;
1333 bool ok = (c >= 1.0) && (c == std::floor(c));
1334 for (std::size_t n = 0; ok && n < alpha.size(); ++n)
1335 ok = std::abs(num_traits<T>::to_double(alpha[n]) -
1336 std::min(static_cast<double>(n + 1), c)) <= qn::GlobalConstants::Zero;
1337 if (!ok)
1338 return "SolverJMT: station '" + sn.stations[i].name +
1339 "' uses a load-dependent scaling that is not the multiserver encoding alpha(n) "
1340 "= min(n,c): JMT has no representation for it, since both the JSIM and the JMVA "
1341 "writer carry the scaling as a server count, and the model would be solved at "
1342 "the nominal service rate. Use SolverCTMC, SolverNC, SolverMVA or SolverSSA, "
1343 "which read sn.lldscaling directly";
1344 }
1345 // A BINDING FINITE BUFFER, which neither engine can carry -- JSIM because no
1346 // JMT drop strategy reproduces LINE's blocking, JMVA because its document has
1347 // no capacity element at all. The verdict is the WRITER's own
1348 // (`io::JmtWriter::buffer_capacity_refusal`), asked here without letting it
1349 // raise, so the gate binds exactly where the writer binds.
1350 //
1351 // WHICH ENGINE IS ASKED ABOUT: a method that is neither engine's gets no
1352 // verdict rather than JSIM's rule applied to a run JSIM is not going to make.
1353 const bool is_jmva = method.compare(0, 4, "jmva") == 0;
1354 const bool is_jsim = method == "default" || method == "jsim";
1355 if (is_jmva || is_jsim) {
1356 const std::string cap_reason = io::jmt_buffer_capacity_refusal(sn, is_jmva);
1357 if (!cap_reason.empty()) return cap_reason;
1358 }
1359 // A CACHE THE JSIM WRITER REFUSES (FIFO/SFIFO on a list of capacity above 1, HLRU/CLIMB/QLRU, a promotion
1360 // graph JMT cannot carry, a popularity that is neither Zipf nor uniform): the writer's own verdict
1361 // (`io::JmtWriter::cache_refusal`), so the gate stops offering jmt.jsim where the run would raise
1362 // (MATLAB jmtCachePromotion, asked by jmtMethodRefusal and SolverJMT.supports).
1363 // AN EDD OR EDF STATION WHOSE SERVED CLASSES CARRY NO DUE DATE: the JSIM run would abort on it
1364 // (jmtMethodRefusal.m asks jmtDeadlineRefusal on the jsim engine only; JMVA has no such discipline).
1365 if (is_jsim) {
1366 const std::string deadline_reason = io::jmt_deadline_refusal(sn);
1367 if (!deadline_reason.empty()) return deadline_reason;
1368 }
1369 if (is_jsim) {
1370 const std::string cache_reason = io::jmt_cache_refusal(sn);
1371 if (!cache_reason.empty()) return cache_reason;
1372 }
1373 return std::string();
1374}
1375
1376/**
1377 * Port of `@@SolverJMT/runAnalyzer.m`, the `jsim` and `jmva` arms.
1378 *
1379 * The sample count is RAISED to 5000 when the caller asks for less, as the
1380 * reference does: JMT terminates a measure on its own precision target and
1381 * needs that many samples per measure before it will report one, so a smaller
1382 * request silently yields unsuccessful measures rather than a faster run.
1383 */
1384template <class T>
1386 JmtOptions opt = opt_in;
1387 if (opt.samples < 5000.0) opt.samples = 5000.0;
1388 {
1389 const std::string removed = jmt_removed_method_refusal(opt.method);
1390 if (!removed.empty()) throw UnsupportedError(ErrorKind{"Method"}, removed);
1391 }
1392 const std::vector<std::string> valid = jmt_list_valid_methods();
1393 if (std::find(valid.begin(), valid.end(), opt.method) == valid.end())
1394 throw UnsupportedError(ErrorKind{"Method"}, "SolverJMT: unknown method '" + opt.method + "'");
1395 // runAnalyzerChecks, as MATLAB/JAR/Python ask it: without it a closed-only jmva.* algorithm on an open model
1396 // reached JMT and came back as "[LINE:external:JMT] ... UnsupportedModelException" instead of a refusal.
1397 qn::feature_gate("SolverJMT", qn::jmt_feature_set(opt.method), sn);
1398 // The structural half of the gate, which `auto_family_refusal` also asks:
1399 // the single-server restriction of the closed-form JMVA algorithms and the
1400 // load-dependent shape JMT can carry. A second copy of either rule is how
1401 // the report and the run come to disagree.
1402 {
1403 // The id follows the sentence, as in the JAR (jmtStructuralRefusalId), MATLAB and Python: a JSIM writer
1404 // cache refusal keeps the writer's own LINE:unsupported:Cache, any other structural refusal is Structure.
1405 const std::string refusal = jmt_method_refusal(sn, opt.method, opt);
1406 if (!refusal.empty())
1407 throw UnsupportedError(ErrorKind{refusal == io::jmt_cache_refusal(sn) ? "Cache" : "Structure"}, refusal);
1408 }
1409
1410 const bool is_mva = opt.method.compare(0, 4, "jmva") == 0;
1411 const std::string mode = is_mva ? "mva" : "sim";
1412 util::TempDir work(is_mva ? "jmva" : "jsim");
1413 if (opt.keep) work.keep();
1414 const std::string model_path = work.path() + "/model." + (is_mva ? "jmva" : "jsim");
1415
1416 if (is_mva) {
1417 // `io/jmva_writer.h` is the shared port of `writeJMVA`: SolverLQNS reads
1418 // the same grammar through `qnsolver`, and a second copy of the writer
1419 // here would be a second place for the chain aggregation to drift.
1420 io::write_jmva(sn, model_path, opt.method, static_cast<std::size_t>(opt.samples));
1421 } else {
1423 wopt.file_name = "model";
1424 wopt.log_path = sn.log_path;
1425 wopt.seed = opt.seed;
1426 wopt.max_samples = opt.samples;
1427 wopt.max_simulated_time = opt.max_simulated_time;
1428 wopt.max_events = opt.max_events;
1429 wopt.sim_conf_int = opt.confint > 0.0 ? opt.confint : 0.99;
1430 line::util::LineConsole::step("writing the JSIM model file");
1431 detail::write_file(model_path, io::jmt_write_jsim(sn, wopt));
1432 line::util::LineConsole::substep("model written to %s", model_path.c_str());
1433 }
1434
1435 line::util::LineConsole::step("running the JMT simulation engine as a subprocess");
1436 const util::ProcResult run = jmt_run(mode, model_path, opt.seed, opt);
1437 const std::string result_path = model_path + "-result." + jmt_result_ext(mode);
1438
1439 line::util::LineConsole::step("parsing the JMT result files");
1440 JmtResult<T> res;
1441 if (is_mva) {
1442 res = jmt_parse_jmva(sn, result_path, run.out);
1443 } else {
1444 res = jmt_map_measures(sn, jmt_parse_measures(result_path, run.out), opt.confint > 0.0);
1445 jmt_region_losses(sn, res);
1446 }
1447 // ResidT DERIVED on both arms as getAvg does (JSIM disagrees under class switching, JMVA per-chain); else column zero. Region rows keep JMT total.
1448 {
1449 const Matrix<T> WNst = mva::sn_get_residt_from_respt(sn, res.avg.RN);
1450 for (std::size_t i = 0; i < sn.nstations; ++i)
1451 for (std::size_t r = 0; r < sn.nclasses; ++r) res.avg.WN(i, r) = WNst(i, r);
1452 }
1453 // A SOURCE HAS NO ARRIVALS TO ITSELF. JMT reports an "Arrival Rate" measure
1454 // at every station the model declares, the Source included, where the
1455 // reference reports 0: `getAvg.m:197-199` builds a zeroMask over the Source
1456 // stations and applies it to the ArvR column for EVERY solver. This port's
1457 // analytical arms already leave that entry at zero, so the JMT reader was
1458 // the one path that carried JMT's own measure through -- 0.79526 against 0
1459 // on fcr_mm1kdrop, 0.30214 and 0.4979 on fcr_constraints. Region rows are
1460 // past `nstations` and are left alone, as the residence time above leaves
1461 // them.
1462 for (std::size_t i = 0; i < sn.nstations; ++i) {
1463 if (sn.stations[i].nodetype != qn::NodeType::Source) continue;
1464 for (std::size_t r = 0; r < sn.nclasses; ++r)
1465 res.avg.AN(i, r) = num_traits<T>::from_int(0);
1466 }
1467 // A CELL WITH NO RESPONSE TIME HOLDS NO JOBS AND BUSIES NO SERVER.
1468 // `@@NetworkSolver/getAvg` builds `zeroMask = RN < 10*FineTol` and applies
1469 // it to the queue length and the utilization for EVERY solver; the
1470 // analytical arms of this port take it through `mva::filter_metric`, and
1471 // the JMT reader was the one path that did not. JMT keeps sampling a
1472 // starved class's utilization long after its response time has been
1473 // discarded as non-recurrent, so the cell arrives as a small positive
1474 // number where every other codebase reports 0: 0.0010617 against 0 on
1475 // prio_hol_closed (PSQueue, Class2), on an otherwise IDENTICAL sample path.
1476 // Region rows past `nstations` are left alone, as the two rules above are.
1477 {
1478 for (std::size_t i = 0; i < sn.nstations; ++i)
1479 for (std::size_t r = 0; r < sn.nclasses; ++r)
1480 if (num_traits<T>::to_double(res.avg.RN(i, r)) <
1482 res.avg.QN(i, r) = num_traits<T>::from_int(0);
1483 res.avg.UN(i, r) = num_traits<T>::from_int(0);
1484 }
1485 }
1486 res.avg.method = opt_in.method;
1487 res.avg.actualmethod = opt.method;
1488 res.avg.iter = 1;
1489 return res;
1490}
1491
1492} // namespace jmt
1493} // namespace line
1494
1495#endif // LINE_SOLVERS_WRAPPERS_JMT_SOLVER_JMT_H
ExternalError(const std::string &what)
Definition error.h:126
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
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").
The method names of each solver, in listing order.
bool sn_has_multi_class_heter_fcfs(const qn::NetworkStruct< T > &sn)
sn_has_multi_class_heter_fcfs: an FCFS station whose per-class rates differ.
bool sn_has_fork_join(const qn::NetworkStruct< T > &sn)
any(sn.fj(:) > 0): the model has a Fork closed by a Join.
Response post_json(const std::string &url, const std::string &json, int timeoutMillis)
POST a JSON document.
Definition http.h:360
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 jmt_cache_refusal(const qn::NetworkStruct< T > &sn)
JmtWriter::cache_refusal over every Cache node, without a document: the writer's own cache verdict,...
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).
std::string jmt_deadline_refusal(const qn::NetworkStruct< T > &sn)
Port of jmtDeadlineRefusal.m: the due date an EDD or EDF station needs and the model does not carry,...
Definition jmt_writer.h:360
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:97
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:996
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:592
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.
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:664
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:545
std::vector< std::string > jmt_list_valid_methods()
Port of SolverJMT.listValidMethods.
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.
std::string jmva_document_refusal(const qn::NetworkStruct< T > &sn, const std::string &method)
The structural half of SolverJMT's method gate; empty when admissible.
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:789
bool jmt_available()
True when a local JVM and common/JMT.jar are both present.
Definition solver_jmt.h:537
std::string jmt_removed_method_refusal(const std::string &method)
The migration sentence for a method name SolverJMT no longer has; empty for any other name.
static const char * JMT_JAR_URL
Where a missing jar is fetched from; LINE_JMT_URL names a mirror instead.
Definition solver_jmt.h:100
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:719
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:530
static const char * JMT_JAR_SHA256
The digest a fetched jar must have, and the reason the fetch can be trusted.
Definition solver_jmt.h:117
std::string jmt_method_refusal(const qn::NetworkStruct< T > &sn, const std::string &method, const JmtOptions &opt)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
PollingType
Polling service disciplines, with the values of MATLAB PollingType.
Definition lang_types.h:377
@ KLIMITED
serve at most K per visit (K in pollingPar)
Definition lang_types.h:380
@ EXHAUSTIVE
serve until the queue empties
Definition lang_types.h:379
std::string name(const Element *el, const std::string &key="name")
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
FeatureSet jmt_feature_set(const std::string &method)
Port of SolverJMT.getFeatureSet (@@SolverJMT/SolverJMT.m:180-290).
void feature_gate(const std::string &solver, const FeatureSet &declared, const NetworkStruct< T > &sn, const std::string &requested_method="", const std::string &resolved_method="")
runAnalyzerChecks: refuse a model the solver does not declare, by name.
bool jmva_is_closed_only(const std::string &method)
True for the JMVA algorithms that solve a CLOSED product-form network only.
std::vector< double > stat(const Matrix< double > &A)
Stationary distribution of a stochastic matrix: the left eigenvector for eigenvalue 1,...
Definition mg1.h:221
std::vector< std::pair< std::size_t, std::size_t > > fj_sorted_pairs(const S &sn)
The Fork/Join pairs of a struct, sortrows([forks joins]) over find(sn.fj): ascending in fork node,...
Definition fj_metrics.h:62
FjMetrics< T > fj_metrics_zero(const S &sn, std::size_t nclasses)
An empty-but-shaped result: every pair of sn, all three matrices zero.
Definition fj_metrics.h:73
int run_inherit(const std::vector< std::string > &argv)
Runs a command with the parent's stdout and stderr, e.g.
Definition subprocess.h:170
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
std::string sha256_file_hex(const std::string &path)
The digest of a file's contents, streamed.
Definition sha256.h:188
std::unique_ptr< Element > parse_file(const std::string &path)
Read and parse a file.
Definition xml.h:290
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
SHA-256 (FIPS 180-4), for verifying a file the port did not build.
Chain aggregation and de-aggregation.
Ports of the sn_has_* / sn_is_* predicate family of matlab/src/api/sn.
The DECLARED side of the gate: one feature set per solver.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
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:69
std::string log_path
model.getLogPath, the logPath attribute
Definition jmt_writer.h:71
std::string file_name
base name; the header echoes it plus .jsimg
Definition jmt_writer.h:70
One <measure> of a JMT result document, by its attributes.
Definition solver_jmt.h:704
std::string measure_type
Definition solver_jmt.h:705
std::string job_class
Definition solver_jmt.h:705
std::string node_type
Definition solver_jmt.h:705
The options of one JMT solve, SolverOptions('JMT') restricted to what is read.
Definition solver_jmt.h:121
int timeout
seconds, for the REST and subprocess arms
Definition solver_jmt.h:131
int replications
options.config.replications: the independent JSIM runs the transient ensemble of default averages ove...
Definition solver_jmt.h:137
std::string method
default | jsim | jmva | jmva.<alg>
Definition solver_jmt.h:122
std::string container
options.config.container, an image override
Definition solver_jmt.h:130
std::string rest_url
a JMT REST server; empty selects the local JVM
Definition solver_jmt.h:129
bool keep
keep the scratch directory after the solve
Definition solver_jmt.h:125
double confint
0 disables the confidence interval columns
Definition solver_jmt.h:126
double max_events
JSIM event budget, -1 unlimited; set by the samplers as MATLAB's maxEvents.
Definition solver_jmt.h:128
double samples
samples per measure; raised to 5000 below
Definition solver_jmt.h:123
The result of a JMT solve: the shared AvgResult plus what only JMT reports.
Definition solver_jmt.h:759
Matrix< T > Tard
getResults.m result.Avg.Tard (stations x classes) and .SysTard (1 x classes).
Definition solver_jmt.h:765
Matrix< T > SysTardCI
their half-widths, empty when disabled
Definition solver_jmt.h:766
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:768
Matrix< T > MemOcc
FCR-only: weighted and memory occupation.
Definition solver_jmt.h:762
mva::AvgResult< T > avg
Definition solver_jmt.h:760
Matrix< T > SysTard
Definition solver_jmt.h:765
Matrix< T > ACI
confidence half-widths, empty when disabled
Definition solver_jmt.h:761
T log_norm_const
result.Prob.logNormConstAggr, the log normalizing constant JMVA reports.
Definition solver_jmt.h:776
Matrix< T > DropRateNfcr
(nregions x nclasses) carried and lost rate
Definition solver_jmt.h:763
static constexpr double FineTol
Definition lang_types.h:765
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 FineTol
Definition lang_types.h:765
static constexpr double Zero
Definition lang_types.h:767
FINITE CAPACITY REGIONS, MATLAB's refreshRegions output.
std::vector< DropStrategy > rule
per class
std::vector< bool > members
membership, independent of the caps
self.result.FJ: one row per Fork/Join pair, one column per class.
Definition fj_metrics.h:43
std::vector< std::size_t > forks
1-based fork node of each pair
Definition fj_metrics.h:44
Matrix< T > Tput
FJTput, P x R.
Definition fj_metrics.h:48
Matrix< T > QLen
FJQLen, P x R.
Definition fj_metrics.h:46
std::size_t npairs() const
Definition fj_metrics.h:53
std::vector< std::size_t > joins
1-based join node of each pair
Definition fj_metrics.h:45
Matrix< T > RespT
FJRespT, P x R.
Definition fj_metrics.h:47
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 ....