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