LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
jmt_logs.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_JMT_LOGS_H
6#define LINE_SOLVERS_WRAPPERS_JMT_JMT_LOGS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The log-driven half of `SolverJMT`: `linkAndLog`, `parseLogs`,
12 * `parseTranState`, `parseTranRespT`, `sampleAggr`, `sampleSysAggr`,
13 * `getProbAggr`, `getProbSysAggr`, `getCdfRespT` and `getTranProbAggr`.
14 *
15 * WHY THERE IS A SECOND MODEL. JMT reports MEANS, not trajectories; everything
16 * here recovers a trajectory instead, by rebuilding the model with a Logger on
17 * each side of every node of interest, running the simulation, and reading the
18 * arrival and departure CSV files back. `jmt_link_and_log` is that rebuild, and
19 * it is the port of `@@MNetwork/linkAndLog.m`: a job entering node i now
20 * crosses `Arv_i` first and leaves through `Dep_i`, so the two files bracket
21 * every passage through i.
22 *
23 * THE LOGGERS CHANGE THE TOPOLOGY, NOT THE MODEL. They hold no jobs and route
24 * with probability one, so the stochastic complement that removes a Router
25 * removes them too and the stationary law is unchanged; what they add is the
26 * event stream. That is why the sample path this returns is the sample path of
27 * the original model and not of an instrumented approximation of it.
28 *
29 * THE TARGET STATE NOW TRAVELS. `getProbAggr` and `getProbSysAggr` weigh the
30 * trajectory against `sn.state{isf}`, the model's CURRENT state, and this
31 * header used to refuse both because `qn::NetworkStruct` carried no such thing.
32 * It does now: every writer emits the (`stateSpace`, `statePrior`) pair for each
33 * stateful node that carries a state -- which is what `setState` and
34 * `initFromMarginal` leave -- and `sn_declared_marginal` decodes that pair back
35 * into the per-class job counts the two getters compare against, falling back
36 * per station to the default marking where nothing was declared.
37 */
38
39#include <algorithm>
40#include <cmath>
41#include <limits>
42#include <cstdlib>
43#include <fstream>
44#include <map>
45#include <set>
46#include <sstream>
47#include <string>
48#include <vector>
49
53#include "line/util/error.h"
54#include "line/util/tempdir.h"
55
56namespace line {
57namespace jmt {
58
59/** The event kinds a JMT log carries, MATLAB `EventType`. */
60enum class JmtEventType { INIT, ARV, DEP };
61
62/** One logged event: when, of which kind, in which class, for which job. */
63struct JmtEvent {
64 double t = 0.0;
66 std::size_t node = 0; ///< 1-based node index
67 std::size_t cls = 0; ///< 1-based class index, 0 at INIT
68 double job = 0.0; ///< JMT's job id, -1 at INIT
69};
70
71/**
72 * Port of `@@MNetwork/linkAndLog.m`: the model with an arrival and a departure
73 * Logger around every logged node.
74 *
75 * @param sn the model to instrument
76 * @param is_node_logged (nnodes) which nodes get the Logger pair
77 * @param log_path the directory the CSV files are written to
78 *
79 * The new node order is the reference's: the original nodes, then the arrival
80 * Loggers in node order, then the departure Loggers. A SOURCE AND A SINK ARE
81 * NEVER LOGGED -- an arrival Logger before a Source has nothing to observe and
82 * a departure Logger after a Sink is unreachable -- and the reference drops
83 * them from the request with a warning; this port drops them silently, since
84 * the caller's `is_node_logged` is derived from the metric handles and never
85 * asks for either on purpose.
86 */
87template <class T>
89 const std::vector<bool>& is_node_logged,
90 const std::string& log_path) {
91 const std::size_t N = sn.nodes.size(), K = sn.classes.size();
92 if (is_node_logged.size() != N)
93 throw InputError("linkAndLog: the isNodeLogged vector does not match the node count");
94 std::vector<bool> logged = is_node_logged;
95 for (std::size_t i = 0; i < N; ++i)
96 if (sn.nodes[i].nodetype == lang::NodeType::Source ||
97 sn.nodes[i].nodetype == lang::NodeType::Sink)
98 logged[i] = false;
99
101 out.log_path = log_path;
102 // The routing is rebuilt from scratch below, so every derived table has to
103 // go with it; `refresh_struct` at the end rederives them all.
104 out.P.clear();
105 out.Peff.clear();
106
107 std::vector<std::size_t> arv(N + 1, 0), dep(N + 1, 0);
108 for (std::size_t i = 1; i <= N; ++i) {
109 if (!logged[i - 1]) continue;
110 const std::size_t nd = out.add_node("Arv_" + sn.nodes[i - 1].name, lang::NodeType::Logger,
111 false);
112 // PROB, NOT RAND, AND IT IS THE SAMPLE PATH. A Logger has ONE
113 // destination, so the two strategies describe the same routing -- but
114 // JSIM spells them differently (an EmpiricalStrategy carrying
115 // probability 1, against a RandomStrategy) and draws from the stream
116 // differently, so a model whose loggers route RAND walks a DIFFERENT
117 // path from the reference's at the same seed. The reference re-links
118 // the logged network (`linkAndLog` calls `link`), which sets PROB
119 // everywhere, and on cdf_respt_open_twoclasses this one difference was
120 // the whole of the C++ row's 5% gap: Queue1 mean response time 2.041
121 // against MATLAB's 1.9358 at seed 23000. Same trap as building a twin
122 // from a RoutingMatrix where the reference used addLink.
123 out.nodes[nd - 1].routing.assign(K, lang::RoutingStrategy::PROB);
124 out.nodes[nd - 1].routing_weights.assign(K, std::map<std::size_t, double>());
125 out.nodes[nd - 1].routing_param.assign(K, 0);
126 out.nodes[nd - 1].logger.file_name = sn.nodes[i - 1].name + "-Arv.csv";
127 out.nodes[nd - 1].logger.file_path = log_path;
128 arv[i] = nd;
129 }
130 for (std::size_t i = 1; i <= N; ++i) {
131 if (!logged[i - 1]) continue;
132 const std::size_t nd = out.add_node("Dep_" + sn.nodes[i - 1].name, lang::NodeType::Logger,
133 false);
134 // PROB for the same reason as the arrival logger above.
135 out.nodes[nd - 1].routing.assign(K, lang::RoutingStrategy::PROB);
136 out.nodes[nd - 1].routing_weights.assign(K, std::map<std::size_t, double>());
137 out.nodes[nd - 1].routing_param.assign(K, 0);
138 out.nodes[nd - 1].logger.file_name = sn.nodes[i - 1].name + "-Dep.csv";
139 out.nodes[nd - 1].logger.file_path = log_path;
140 dep[i] = nd;
141 }
142
143 const T one = num_traits<T>::from_int(1);
144 const T zero = num_traits<T>::from_int(0);
145 for (const auto& kv : sn.P) {
146 const std::size_t r = kv.first.first, s = kv.first.second;
147 const Matrix<T>& B = kv.second;
148 for (std::size_t i = 1; i <= N && i <= B.rows(); ++i)
149 for (std::size_t j = 1; j <= N && j <= B.cols(); ++j) {
150 if (B(i - 1, j - 1) == zero) continue;
151 const std::size_t from = logged[i - 1] ? dep[i] : i;
152 const std::size_t to = logged[j - 1] ? arv[j] : j;
153 out.set_route(r, s, from, to, B(i - 1, j - 1));
154 }
155 }
156 // The two unit arcs that put the loggers on the path: Arv_i -> i -> Dep_i,
157 // both class preserving, since a Logger switches nothing.
158 for (std::size_t i = 1; i <= N; ++i) {
159 if (!logged[i - 1]) continue;
160 for (std::size_t r = 1; r <= K; ++r) {
161 out.set_route(r, r, arv[i], i, one);
162 out.set_route(r, r, i, dep[i], one);
163 }
164 }
165 out.refresh_struct();
166 return out;
167}
168
169namespace detail {
170
171/** Split a line on `;`, the JSIM header's `logDelimiter`. */
172inline std::vector<std::string> split_semi(const std::string& line) {
173 std::vector<std::string> out;
174 std::size_t b = 0;
175 while (true) {
176 const std::size_t e = line.find(';', b);
177 out.push_back(line.substr(b, e == std::string::npos ? std::string::npos : e - b));
178 if (e == std::string::npos) break;
179 b = e + 1;
180 }
181 return out;
182}
183
184} // namespace detail
185
186/** One arrival or departure CSV file, as three parallel columns. */
188 std::vector<double> ts;
189 std::vector<double> job;
190 std::vector<std::string> cls;
191};
192
193/**
194 * Read one JMT log CSV.
195 *
196 * THE COLUMN LAYOUT IS THE REFERENCE'S, not inferred from the header: the
197 * loggers this port installs carry the default flag set of `Logger.m`
198 * (timestamp, job id and job class on; wall-clock start time, logger name and
199 * the two inter-departure columns off), and `parseLogs.m` reads columns 2, 3
200 * and 4 of a six-column line after one header row. Sniffing the header instead
201 * would silently accept a file written with other flags and read the wrong
202 * column as the timestamp.
203 */
204inline JmtLogFile jmt_read_log(const std::string& path) {
205 std::ifstream f(path.c_str());
206 if (!f) throw InputError("SolverJMT: cannot read the JMT log '" + path + "'");
207 JmtLogFile out;
208 std::string line;
209 bool header = true;
210 while (std::getline(f, line)) {
211 if (header) {
212 header = false;
213 continue;
214 }
215 if (line.empty()) continue;
216 const std::vector<std::string> col = detail::split_semi(line);
217 if (col.size() < 4) continue;
218 out.ts.push_back(std::strtod(col[1].c_str(), nullptr));
219 out.job.push_back(std::strtod(col[2].c_str(), nullptr));
220 out.cls.push_back(col[3]);
221 }
222 return out;
223}
224
225/** The per-class queue-length trajectory of one node, plus its event stream. */
226template <class T>
228 std::vector<double> t; ///< event times, ascending
229 std::vector<std::vector<double>> qlen; ///< (|t| x nclasses) counts after the event
230 std::vector<JmtEvent> event;
231};
232
233/**
234 * Port of `parseTranState`: the arrival and departure logs merged into a
235 * per-class queue-length trajectory.
236 *
237 * SIMULTANEOUS EVENTS ARE REORDERED, which is the whole reason this is not a
238 * merge sort. Two events at the same timestamp involving the SAME job are a
239 * pass-through -- the job arrives and departs with no time in between -- and
240 * JMT writes them in file order, not in causal order. When the previous event
241 * of that job was of the same kind, the reference swaps the event with the next
242 * one involving the job, which restores the alternation; without it the
243 * cumulative sum below goes negative and the trajectory reports a queue with
244 * -1 jobs in it.
245 */
246template <class T>
248 const std::vector<std::size_t>& class_of_arv,
249 const std::vector<std::size_t>& class_of_dep,
250 const std::vector<double>& node_preload) {
251 const std::size_t K = node_preload.size();
252 struct Row {
253 double t;
254 std::vector<double> delta;
255 JmtEventType type;
256 std::size_t cls;
257 double job;
258 };
259 std::vector<Row> rows;
260 rows.reserve(arv.ts.size() + dep.ts.size());
261 for (std::size_t i = 0; i < arv.ts.size(); ++i) {
262 Row r;
263 r.t = arv.ts[i];
264 r.delta.assign(K, 0.0);
265 if (class_of_arv[i] >= 1 && class_of_arv[i] <= K) r.delta[class_of_arv[i] - 1] = +1.0;
266 r.type = JmtEventType::ARV;
267 r.cls = class_of_arv[i];
268 r.job = arv.job[i];
269 rows.push_back(r);
270 }
271 for (std::size_t i = 0; i < dep.ts.size(); ++i) {
272 Row r;
273 r.t = dep.ts[i];
274 r.delta.assign(K, 0.0);
275 if (class_of_dep[i] >= 1 && class_of_dep[i] <= K) r.delta[class_of_dep[i] - 1] = -1.0;
276 r.type = JmtEventType::DEP;
277 r.cls = class_of_dep[i];
278 r.job = dep.job[i];
279 rows.push_back(r);
280 }
281 // `sortrows` on the timestamp is STABLE in MATLAB, so arrivals keep their
282 // file order among themselves and ahead of the departures of the same
283 // instant; std::stable_sort reproduces that.
284 std::stable_sort(rows.begin(), rows.end(),
285 [](const Row& a, const Row& b) { return a.t < b.t; });
286
287 for (std::size_t ev = 1; ev < rows.size(); ++ev) {
288 if (rows[ev].t != rows[ev - 1].t) continue; // not an instantaneous pair
289 const double j = rows[ev].job;
290 std::size_t prev = 0;
291 bool has_prev = false;
292 for (std::size_t k = ev; k > 0; --k)
293 if (rows[k - 1].job == j) {
294 prev = k - 1;
295 has_prev = true;
296 break;
297 }
298 if (!has_prev) continue;
299 if (rows[prev].type != rows[ev].type) continue;
300 std::size_t next = 0;
301 bool has_next = false;
302 for (std::size_t k = ev + 1; k < rows.size(); ++k)
303 if (rows[k].job == j) {
304 next = k;
305 has_next = true;
306 break;
307 }
308 if (!has_next) continue;
309 std::swap(rows[ev], rows[next]);
310 }
311
312 JmtNodeTrace<T> out;
313 out.t.push_back(0.0);
314 out.qlen.push_back(node_preload);
315 JmtEvent init;
316 init.t = 0.0;
318 init.job = -1.0;
319 out.event.push_back(init);
320 std::vector<double> run = node_preload;
321 for (std::size_t i = 0; i < rows.size(); ++i) {
322 for (std::size_t r = 0; r < K; ++r) run[r] += rows[i].delta[r];
323 out.t.push_back(rows[i].t);
324 out.qlen.push_back(run);
325 JmtEvent e;
326 e.t = rows[i].t;
327 e.type = rows[i].type;
328 e.cls = rows[i].cls;
329 e.job = rows[i].job;
330 out.event.push_back(e);
331 }
332 return out;
333}
334
335/**
336 * Port of `parseTranRespT`: the per-class response-time samples of one node.
337 *
338 * A job's passages are recovered by pairing its events IN TIME ORDER: the first
339 * arrival, the matching departure, and so on. A job may pass through the same
340 * node several times -- a closed model does nothing else -- so the pairing is
341 * over the whole per-job sequence and not just its first and last event, and an
342 * unmatched trailing arrival (the job was still there when the run ended, or it
343 * was dropped) is discarded rather than paired with the run's end.
344 */
345inline std::map<std::size_t, std::vector<double>> jmt_parse_tran_resp_t(
346 const JmtLogFile& arv, const JmtLogFile& dep, const std::vector<std::size_t>& class_of_arv,
347 const std::vector<std::size_t>& class_of_dep) {
348 struct Ev {
349 double t;
350 bool is_arv;
351 std::size_t cls;
352 };
353 std::map<double, std::vector<Ev>> by_job;
354 for (std::size_t i = 0; i < arv.ts.size(); ++i)
355 by_job[arv.job[i]].push_back(Ev{arv.ts[i], true, class_of_arv[i]});
356 for (std::size_t i = 0; i < dep.ts.size(); ++i)
357 by_job[dep.job[i]].push_back(Ev{dep.ts[i], false, class_of_dep[i]});
358
359 std::map<std::size_t, std::vector<double>> out;
360 for (auto& kv : by_job) {
361 std::vector<Ev>& evs = kv.second;
362 std::stable_sort(evs.begin(), evs.end(),
363 [](const Ev& a, const Ev& b) { return a.t < b.t; });
364 std::size_t i = 0;
365 while (i + 1 < evs.size()) {
366 if (!evs[i].is_arv) {
367 ++i; // a departure with no arrival before it: the preload
368 continue;
369 }
370 if (evs[i + 1].is_arv) {
371 ++i; // two arrivals in a row: the first was dropped
372 continue;
373 }
374 // The CLASS OF THE ARRIVAL owns the sample: a job that switched
375 // class while inside the node arrived in the first one, and that is
376 // the class whose response time this passage measures.
377 out[evs[i].cls].push_back(evs[i + 1].t - evs[i].t);
378 i += 2;
379 }
380 }
381 return out;
382}
383
384/**
385 * Port of `parseLogs`: read every logged node's CSV pair.
386 *
387 * @param orig the original struct, whose node names the files are
388 * keyed by
389 * @param is_node_logged (nnodes) which nodes carry a Logger pair
390 * @param log_path where the loggers wrote
391 *
392 * The preload of a node is its `initial_marginal`, the same value the JSIM
393 * `preload` block carried, because the logs record only the CROSSINGS: a job
394 * that was already at the node when the run started never arrives, so without
395 * the preload the cumulative sum starts from zero and every count is short by
396 * the initial population.
397 */
398template <class T>
399std::map<std::size_t, JmtNodeTrace<T>> jmt_parse_logs(const qn::NetworkStruct<T>& orig,
400 const std::vector<bool>& is_node_logged,
401 const std::string& log_path) {
402 std::map<std::string, std::size_t> class_of;
403 for (std::size_t r = 1; r <= orig.nclasses; ++r) class_of[orig.classes[r - 1].name] = r;
404
405 std::map<std::size_t, JmtNodeTrace<T>> out;
406 for (std::size_t ind = 1; ind <= orig.nodes.size(); ++ind) {
407 if (!is_node_logged[ind - 1]) continue;
408 const std::string base = log_path + "/" + orig.nodes[ind - 1].name;
409 const std::string fa = base + "-Arv.csv", fd = base + "-Dep.csv";
410 if (!detail::is_file(fa) || !detail::is_file(fd)) continue;
411 const JmtLogFile arv = jmt_read_log(fa), dep = jmt_read_log(fd);
412 std::vector<std::size_t> ca(arv.cls.size(), 0), cd(dep.cls.size(), 0);
413 for (std::size_t i = 0; i < arv.cls.size(); ++i) {
414 const auto it = class_of.find(arv.cls[i]);
415 ca[i] = it == class_of.end() ? 0 : it->second;
416 }
417 for (std::size_t i = 0; i < dep.cls.size(); ++i) {
418 const auto it = class_of.find(dep.cls[i]);
419 cd[i] = it == class_of.end() ? 0 : it->second;
420 }
421 std::vector<double> preload(orig.nclasses, 0.0);
422 const std::size_t ist = orig.nodes[ind - 1].station;
423 if (ist != 0) {
424 const auto im = orig.initmarking.find(ind);
425 if (im != orig.initmarking.end()) {
426 for (std::size_t r = 0; r < orig.nclasses && r < im->second.size(); ++r)
427 preload[r] = num_traits<T>::to_double(im->second[r]);
428 } else {
429 for (std::size_t r = 0; r < orig.nclasses; ++r) {
430 const double n = orig.classes[r].population;
431 if (std::isfinite(n) && orig.classes[r].refstat == ist) preload[r] = n;
432 }
433 }
434 }
435 out[ind] = jmt_parse_tran_state<T>(arv, dep, ca, cd, preload);
436 }
437 return out;
438}
439
440/**
441 * Port of `sampleAggr`: the queue-length trajectory of one node.
442 *
443 * The model is instrumented, simulated once, and the node's CSV pair read back.
444 * `num_events` bounds how much of the trajectory is returned, as the reference
445 * does; JMT CANNOT BE ASKED FOR A NUMBER OF EVENTS AT ONE NODE -- `maxEvents`
446 * is global -- so the count is a truncation of what the run produced and not a
447 * stopping rule, which is exactly the caveat the reference warns about.
448 */
449template <class T>
451 std::size_t num_events, const JmtOptions& opt) {
452 if (node == 0 || node > sn.nodes.size())
453 throw InputError("SolverJMT: sampleAggr was given a node index out of range");
454 std::vector<bool> logged(sn.nodes.size(), false);
455 logged[node - 1] = true;
456 util::TempDir logs("jmtlog");
457 if (opt.keep) logs.keep();
458 const qn::NetworkStruct<T> inst = jmt_link_and_log(sn, logged, logs.path());
459 JmtOptions o = opt;
460 // the global budget is scaled by nodes and classes so the node sees about num_events, as in MATLAB sampleAggr
461 if (num_events > 0) o.max_events = static_cast<double>(num_events * sn.nodes.size() * sn.classes.size());
463 const std::map<std::size_t, JmtNodeTrace<T>> traces = jmt_parse_logs(sn, logged, logs.path());
464 const auto it = traces.find(node);
465 if (it == traces.end())
466 throw ExternalError(ErrorKind{"JMT"},
467 "SolverJMT: the simulation produced no log for node '" + sn.nodes[node - 1].name +
468 "'; the run has likely failed before any job crossed it");
469 JmtNodeTrace<T> tr = it->second;
470 if (num_events > 0 && tr.t.size() > num_events + 1) {
471 tr.t.resize(num_events + 1);
472 tr.qlen.resize(num_events + 1);
473 std::vector<JmtEvent> ev;
474 for (std::size_t i = 0; i < tr.event.size(); ++i)
475 if (tr.event[i].t <= tr.t.back()) ev.push_back(tr.event[i]);
476 tr.event = ev;
477 }
478 return tr;
479}
480
481/** The system trajectory: one per-class block per station, on a common grid. */
482template <class T>
484 std::vector<double> t;
485 /** state[ist-1] is (|t| x nclasses); a Source's block is empty. */
486 std::vector<std::vector<std::vector<double>>> state;
487 std::vector<JmtEvent> event;
488};
489
490/**
491 * Port of `sampleSysAggr`: every station's trajectory on one time grid.
492 *
493 * The per-station traces have their OWN event times, so they are resampled onto
494 * the union grid with a PREVIOUS-value hold -- a queue length is constant
495 * between its own events, so holding is exact rather than an interpolation. The
496 * grid is truncated at the earliest station's last event: past that point one
497 * station has no data, and extending its last value would report a queue that
498 * stopped changing rather than one that stopped being observed.
499 */
500template <class T>
502 const JmtOptions& opt) {
503 std::vector<bool> logged(sn.nodes.size(), false);
504 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
505 const std::size_t ind = sn.station_to_node[ist - 1];
506 if (sn.nodes[ind - 1].nodetype != lang::NodeType::Source) logged[ind - 1] = true;
507 }
508 util::TempDir logs("jmtlog");
509 if (opt.keep) logs.keep();
510 const qn::NetworkStruct<T> inst = jmt_link_and_log(sn, logged, logs.path());
511 JmtOptions o = opt;
512 o.method = "jsim"; // never the transient ensemble of `default`, which is composed FROM this
513 // MATLAB sampleSysAggr: 0 means options.samples, the INIT row counts as one event, and the JSIM budget is
514 // scaled by nodes and classes; every row the capped run produced is returned
515 if (num_events == 0) num_events = static_cast<std::size_t>(opt.samples);
516 o.max_events = static_cast<double>((num_events - 1) * sn.nodes.size() * sn.classes.size());
518 const std::map<std::size_t, JmtNodeTrace<T>> traces = jmt_parse_logs(sn, logged, logs.path());
519
520 JmtSysTrace<T> out;
521 std::set<double> grid;
522 double tmax = std::numeric_limits<double>::infinity();
523 for (const auto& kv : traces) {
524 if (kv.second.t.empty()) continue;
525 tmax = std::min(tmax, kv.second.t.back());
526 for (double t : kv.second.t) grid.insert(t);
527 }
528 for (double t : grid)
529 if (t <= tmax) out.t.push_back(t);
530
531 out.state.assign(sn.nstations, std::vector<std::vector<double>>());
532 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
533 const std::size_t ind = sn.station_to_node[ist - 1];
534 const auto it = traces.find(ind);
535 if (it == traces.end()) continue;
536 const JmtNodeTrace<T>& tr = it->second;
537 std::vector<std::vector<double>> block(out.t.size(),
538 std::vector<double>(sn.nclasses, 0.0));
539 std::size_t k = 0;
540 for (std::size_t g = 0; g < out.t.size(); ++g) {
541 while (k + 1 < tr.t.size() && tr.t[k + 1] <= out.t[g]) ++k;
542 if (k < tr.qlen.size()) block[g] = tr.qlen[k];
543 }
544 out.state[ist - 1] = block;
545 }
546 for (const auto& kv : traces)
547 for (std::size_t i = 0; i < kv.second.event.size(); ++i) {
548 JmtEvent e = kv.second.event[i];
549 e.node = kv.first;
550 if (e.t <= (out.t.empty() ? 0.0 : out.t.back())) out.event.push_back(e);
551 }
552 std::stable_sort(out.event.begin(), out.event.end(),
553 [](const JmtEvent& a, const JmtEvent& b) { return a.t < b.t; });
554 return out;
555}
556
557/**
558 * Port of `getCdfRespT`: the empirical response-time distribution per
559 * (station, class), as the (F, X) pairs `ecdf` returns.
560 *
561 * Every station with a service process is logged, the model is simulated, and
562 * each node's passages are turned into samples. With `seed_from_steady` (the
563 * getCdfRespT contract) the model is FIRST solved for its steady-state queue
564 * lengths and the logged run starts preloaded at their rounded values, the
565 * closed-class remainder on the bottleneck -- the reference's two-run pipeline,
566 * which shortens the warmup and is what makes the seeded curve comparable to
567 * MATLAB's for the same seed. Without it (the getTranCdfRespT contract) the
568 * logged run starts from the model's default initial state, so the samples
569 * cover the transient.
570 */
571template <class T>
572std::map<std::pair<std::size_t, std::size_t>, std::vector<std::pair<double, double>>>
574 bool seed_from_steady = true) {
575 std::vector<bool> logged(sn.nodes.size(), false);
576 std::vector<bool> cacheclass = io::jmt_cache_classes(sn);
577 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
578 const std::size_t ind = sn.station_to_node[ist - 1];
579 for (std::size_t r = 1; r <= sn.nclasses; ++r)
580 if (io::jmt_metric_enabled(sn, io::JmtMetricKind::RespT, ist, r, cacheclass))
581 logged[ind - 1] = true;
582 }
583 util::TempDir logs("jmtcdf");
584 if (opt.keep) logs.keep();
585 qn::NetworkStruct<T> inst = jmt_link_and_log(sn, logged, logs.path());
586 if (seed_from_steady) {
587 // The reference's first run: steady-state queue lengths, floored per
588 // (station, class), a closed class's remainder pushed onto its fullest
589 // station so the population balances. The rounded counts ride into the
590 // logged copy through `initmarking`, the same channel a Place's tokens
591 // take, which is what the writer's preload block reads first.
593 std::vector<std::vector<double>> n(sn.nstations,
594 std::vector<double>(sn.nclasses, 0.0));
595 for (std::size_t r = 1; r <= sn.nclasses; ++r) {
596 double tot = 0.0, vmax = -1.0;
597 std::size_t imax = 1;
598 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
599 const double q = std::floor(std::max(
600 0.0, num_traits<T>::to_double(pre.avg.QN(ist - 1, r - 1))));
601 n[ist - 1][r - 1] = q;
602 tot += q;
603 if (q > vmax) {
604 vmax = q;
605 imax = ist;
606 }
607 }
608 const double njobs = sn.classes[r - 1].population;
609 if (std::isfinite(njobs) && tot < njobs)
610 n[imax - 1][r - 1] += njobs - tot;
611 }
612 for (std::size_t ist = 1; ist <= inst.nstations && ist <= sn.nstations; ++ist) {
613 const std::size_t ind = inst.station_to_node[ist - 1];
614 const lang::NodeType ty = inst.nodes[ind - 1].nodetype;
615 if (ty == lang::NodeType::Source || ty == lang::NodeType::Join) continue;
616 std::vector<T> row(sn.nclasses);
617 for (std::size_t r = 0; r < sn.nclasses; ++r)
618 row[r] = num_traits<T>::from_double(n[ist - 1][r]);
619 inst.initmarking[ind] = row;
620 }
621 }
623
624 std::map<std::string, std::size_t> class_of;
625 for (std::size_t r = 1; r <= sn.nclasses; ++r) class_of[sn.classes[r - 1].name] = r;
626
627 std::map<std::pair<std::size_t, std::size_t>, std::vector<std::pair<double, double>>> out;
628 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
629 const std::size_t ind = sn.station_to_node[ist - 1];
630 if (!logged[ind - 1]) continue;
631 const std::string base = logs.path() + "/" + sn.nodes[ind - 1].name;
632 if (!detail::is_file(base + "-Arv.csv") || !detail::is_file(base + "-Dep.csv")) continue;
633 const JmtLogFile arv = jmt_read_log(base + "-Arv.csv");
634 const JmtLogFile dep = jmt_read_log(base + "-Dep.csv");
635 std::vector<std::size_t> ca(arv.cls.size(), 0), cd(dep.cls.size(), 0);
636 for (std::size_t i = 0; i < arv.cls.size(); ++i) {
637 const auto it = class_of.find(arv.cls[i]);
638 ca[i] = it == class_of.end() ? 0 : it->second;
639 }
640 for (std::size_t i = 0; i < dep.cls.size(); ++i) {
641 const auto it = class_of.find(dep.cls[i]);
642 cd[i] = it == class_of.end() ? 0 : it->second;
643 }
644 const std::map<std::size_t, std::vector<double>> samples =
645 jmt_parse_tran_resp_t(arv, dep, ca, cd);
646 for (const auto& kv : samples) {
647 if (kv.first == 0 || kv.second.empty()) continue;
648 std::vector<double> x = kv.second;
649 std::sort(x.begin(), x.end());
650 // `ecdf`'s convention: the first row is (0, x(1)) and the step at
651 // each distinct value carries the mass of its ties.
652 std::vector<std::pair<double, double>> fx;
653 fx.push_back(std::make_pair(0.0, x.front()));
654 std::size_t i = 0;
655 while (i < x.size()) {
656 std::size_t j = i;
657 while (j + 1 < x.size() && x[j + 1] == x[i]) ++j;
658 fx.push_back(std::make_pair(static_cast<double>(j + 1) /
659 static_cast<double>(x.size()),
660 x[i]));
661 i = j + 1;
662 }
663 out[std::make_pair(ist, kv.first)] = fx;
664 }
665 }
666 return out;
667}
668
669/**
670 * How many JSIM runs a transient estimate is over: the gate the two transient arms share.
671 *
672 * `default` is the ENSEMBLE, `options.config.replications` runs with replication k (1-based)
673 * seeded `seed + k - 1`. An explicit `jsim` stays ONE JSIM run, as in the reference, and is not
674 * silently widened into the ensemble. A JMVA method logs no trajectory, so it is refused by name
675 * rather than run as JSIM under a label naming an engine that never ran.
676 */
677inline std::size_t jmt_transient_replication_count(const JmtOptions& opt, const std::string& what) {
678 {
679 const std::string removed = jmt_removed_method_refusal(opt.method);
680 if (!removed.empty()) throw UnsupportedError(ErrorKind{"Method"}, removed);
681 }
682 if (opt.method != "default" && opt.method != "jsim")
683 throw UnsupportedError(ErrorKind{"Method"},
684 "SolverJMT: " + what + " is estimated from simulated JSIM sample paths, and method '" +
685 opt.method + "' logs no trajectory; use 'default' (the ensemble over "
686 "options.config.replications runs) or 'jsim' (one run)");
687 if (opt.method == "jsim") return 1;
688 if (opt.replications < 1)
689 throw InputError("SolverJMT: options.config.replications is a positive count of independent "
690 "runs (got " + std::to_string(opt.replications) + ")");
691 return static_cast<std::size_t>(opt.replications);
692}
693
694/**
695 * Port of `getTranProbAggr`: the transient distribution of one station's
696 * aggregate state, estimated over `jmt_transient_replication_count(opt)` independent runs.
697 *
698 * Each run is a fresh seed, `seed + it`, and the runs are resampled onto the
699 * union of their event times before being counted, so a state's probability at
700 * time t is the fraction of replications occupying it at t. A FINITE HORIZON IS
701 * REQUIRED: with an unbounded one every replication ends at a different time
702 * and the union grid is not a grid the estimate is defined on -- which is
703 * exactly the error the reference raises.
704 */
705template <class T>
706std::pair<std::vector<double>, std::vector<std::vector<double>>> jmt_get_tran_prob_aggr(
707 const qn::NetworkStruct<T>& sn, std::size_t station, const JmtOptions& opt,
708 std::vector<std::vector<double>>& states_out) {
709 const std::size_t replications = jmt_transient_replication_count(opt, "getTranProbAggr");
710 if (!std::isfinite(opt.max_simulated_time))
711 throw InputError(
712 "SolverJMT: getTranProbAggr requires a finite time span, e.g. "
713 "options.timespan = [0, T]");
714 if (station == 0 || station > sn.nstations)
715 throw InputError("SolverJMT: getTranProbAggr was given a station index out of range");
716 const std::size_t ind = sn.station_to_node[station - 1];
717 if (sn.nodes[ind - 1].nodetype == lang::NodeType::Source)
718 throw InputError("SolverJMT: getTranProbAggr does not apply to a Source");
719
720 std::vector<JmtSysTrace<T>> runs;
721 std::set<double> grid;
722 for (std::size_t it = 0; it < replications; ++it) {
723 JmtOptions o = opt;
724 o.seed = opt.seed + static_cast<long>(it); // replication k = it+1 is seeded seed + k - 1
726 for (double t : tr.t) grid.insert(t);
727 runs.push_back(tr);
728 }
729 std::vector<double> tu(grid.begin(), grid.end());
730
731 // The distinct aggregate states seen, in first-appearance order after the
732 // resampling; a replication that produced no data contributes none.
733 std::map<std::vector<double>, std::size_t> index;
734 std::vector<std::vector<double>> states;
735 std::vector<std::vector<std::size_t>> occupied(runs.size(),
736 std::vector<std::size_t>(tu.size(), 0));
737 for (std::size_t r = 0; r < runs.size(); ++r) {
738 const std::vector<std::vector<double>>& block = runs[r].state[station - 1];
739 if (block.empty()) continue;
740 std::size_t k = 0;
741 for (std::size_t g = 0; g < tu.size(); ++g) {
742 while (k + 1 < runs[r].t.size() && runs[r].t[k + 1] <= tu[g]) ++k;
743 if (k >= block.size()) continue;
744 const std::vector<double>& s = block[k];
745 auto it = index.find(s);
746 if (it == index.end()) {
747 index[s] = states.size();
748 states.push_back(s);
749 it = index.find(s);
750 }
751 occupied[r][g] = it->second + 1; // 1-based; 0 marks "no data"
752 }
753 }
754 std::vector<std::vector<double>> pi(tu.size(), std::vector<double>(states.size(), 0.0));
755 for (std::size_t r = 0; r < runs.size(); ++r)
756 for (std::size_t g = 0; g < tu.size(); ++g)
757 if (occupied[r][g] != 0)
758 pi[g][occupied[r][g] - 1] += 1.0 / static_cast<double>(runs.size());
759 states_out = states;
760 return std::make_pair(tu, pi);
761}
762
763/**
764 * The dwell-time weights of a trajectory sampled at `t`.
765 *
766 * The reference writes `dt = [diff(t); 0]`: a state observed at t_k is held
767 * until t_{k+1}, and the LAST sample carries no weight because the run ends
768 * there and how long it would have been held is not observed. Dropping the
769 * trailing zero instead -- weighting the last sample by the previous gap -- adds
770 * a dwell that was never measured, which is why it is kept as a zero.
771 */
772inline std::vector<double> jmt_dwell_weights(const std::vector<double>& t) {
773 std::vector<double> dt(t.size(), 0.0);
774 for (std::size_t i = 0; i + 1 < t.size(); ++i) dt[i] = t[i + 1] - t[i];
775 return dt;
776}
777
778/** What `jmt_prob_aggr` reports: the system probability and the per-station ones. */
780 double sys = 0.0; ///< P(the whole network is in the declared state)
781 std::vector<double> station; ///< P(station i holds its declared per-class counts)
782 bool sys_seen = false; ///< whether the joint state occurred at all
783 std::vector<bool> station_seen; ///< the same, per station
784};
785
786/**
787 * Port of `getProbAggr` and `getProbSysAggr`, both off ONE instrumented run.
788 *
789 * THEY ARE TIME AVERAGES, NOT SAMPLE FRACTIONS. The trajectory is event-driven,
790 * so its samples are not equally spaced; counting them would weigh a state the
791 * network leaves at once as heavily as one it sits in. Each sample is weighed by
792 * the interval it is held for, which is what makes the estimate converge to the
793 * stationary probability.
794 *
795 * ONE RUN, NOT ONE PER QUESTION. The reference instruments one node at a time
796 * (`sampleAggr` logs the node it was asked about) and therefore simulates again
797 * for every station; `jmt_sample_sys_aggr` already logs every station on a
798 * common grid, so the station marginals are read off the same sample path as the
799 * joint probability. That is a DIFFERENT estimator from the reference's -- same
800 * quantity, one sample path instead of M independent ones -- and it is the
801 * cheaper and the more consistent of the two, since the marginals it reports
802 * cannot contradict the joint they were taken from.
803 *
804 * `target` OVERRIDES THE DECLARED STATE of one station where it is given, which
805 * is `getProbAggr`'s second argument; every other station keeps the row
806 * `setState`/`initFromMarginal` left, exactly as the reference substitutes into
807 * `sn.state{isf}` and leaves the rest alone.
808 *
809 * A STATE NEVER SEEN IS 0 AND IS REPORTED AS SUCH, with `seen` false beside it.
810 * The reference warns and returns zero rather than erroring, because on a
811 * simulation a state of small probability legitimately fails to appear in a
812 * finite run: that is a statement about the run length, and the caller is the
813 * one who can lengthen it.
814 */
815template <class T>
817 std::size_t target_station = 0,
818 const std::vector<double>& target = std::vector<double>()) {
820 if (!target.empty()) {
821 if (target_station == 0 || target_station > sn.nstations)
822 throw InputError("SolverJMT: getProbAggr was given a station index out of range");
823 if (target.size() != sn.nclasses)
824 throw InputError("SolverJMT: getProbAggr takes one job count per class (" +
825 std::to_string(sn.nclasses) + "), got " +
826 std::to_string(target.size()));
827 for (std::size_t r = 0; r < sn.nclasses; ++r)
828 nirm(target_station - 1, r) = num_traits<T>::from_double(target[r]);
829 }
830 std::vector<std::vector<double>> nir(sn.nstations, std::vector<double>(sn.nclasses, 0.0));
831 for (std::size_t i = 0; i < sn.nstations; ++i)
832 for (std::size_t r = 0; r < sn.nclasses; ++r)
833 nir[i][r] = num_traits<T>::to_double(nirm(i, r));
834
836 const std::vector<double> dt = jmt_dwell_weights(tr.t);
837
838 JmtProbAggr out;
839 out.station.assign(sn.nstations, 0.0);
840 out.station_seen.assign(sn.nstations, false);
841 std::vector<double> hit(sn.nstations, 0.0);
842 double sys_hit = 0.0, total = 0.0;
843 for (std::size_t g = 0; g < dt.size(); ++g) {
844 total += dt[g];
845 bool all = true;
846 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
847 const std::size_t ind = sn.station_to_node[ist - 1];
848 // A Source holds no jobs and the reference's own block for it is not
849 // a state; it takes no part in either comparison.
850 if (sn.nodes[ind - 1].nodetype == lang::NodeType::Source) continue;
851 const std::vector<std::vector<double>>& block = tr.state[ist - 1];
852 bool same = g < block.size();
853 for (std::size_t r = 0; r < sn.nclasses && same; ++r)
854 same = block[g][r] == nir[ist - 1][r];
855 if (same) {
856 hit[ist - 1] += dt[g];
857 if (dt[g] > 0.0) out.station_seen[ist - 1] = true;
858 } else {
859 all = false;
860 }
861 }
862 if (all) {
863 sys_hit += dt[g];
864 if (dt[g] > 0.0) out.sys_seen = true;
865 }
866 }
867 // A run that observed no dwell at all has measured nothing, and 0 would read
868 // as "the state was never visited" -- a claim the run cannot support.
869 if (!(total > 0.0))
870 throw NumericError(ErrorKind{"Degenerate"},
871 "SolverJMT: the simulation produced no observed dwell time, so no state probability "
872 "can be estimated from it");
873 out.sys = sys_hit / total;
874 for (std::size_t i = 0; i < sn.nstations; ++i) out.station[i] = hit[i] / total;
875 return out;
876}
877
878/** Transient averages over independent replications, on one time grid. */
879template <class T>
881 std::vector<double> t;
882 /** QNt[ist-1][r] over `t`. */
883 std::vector<std::vector<std::vector<double>>> QNt;
884 std::vector<std::vector<std::vector<double>>> UNt;
885 std::vector<std::vector<std::vector<double>>> TNt;
886 /** Replications that produced a usable trajectory. */
887 std::size_t valid = 0;
888};
889
890/**
891 * Port of the transient ensemble of `@@SolverJMT/runAnalyzer.m` (`default` over a finite timespan).
892 *
893 * A SINGLE SAMPLE PATH IS NOT THE TRANSIENT MEAN. There is no time-ergodicity at
894 * a fixed t, so E[N](t) is estimated by averaging `options.config.replications`
895 * INDEPENDENT replications, replication k (1-based) seeded `seed + k - 1`,
896 * rather than by reading one trajectory. An explicit `jsim` is one such run
897 * (`jmt_transient_replication_count`).
898 *
899 * The replications have their own event grids, so they are resampled onto the
900 * union with a previous-value hold and the grid is truncated at the MINIMUM of
901 * their last events: past that point one replication has no data, and extending
902 * its last value would report a queue that stopped changing rather than one that
903 * stopped being observed -- the same rule `jmt_sample_sys_aggr` applies across
904 * stations, applied here across seeds.
905 *
906 * Utilization is `min(n, c)/c` at a finite server and the raw queue length at a
907 * delay; throughput follows it as `U*c*mu` and `U*mu`, which is how the
908 * reference reads departures off the occupancy rather than differencing the
909 * trajectory.
910 */
911template <class T>
913 const std::size_t reps = jmt_transient_replication_count(opt, "getTranAvg");
914 // THE HORIZON IS REQUIRED: without it the replications' event grids are not commensurable.
915 if (!std::isfinite(opt.max_simulated_time))
916 throw InputError("SolverJMT: the transient averages need a finite timespan; a transient mean "
917 "over an unstated horizon is not a quantity");
918 // The structural predicate `auto_family_refusal` asks, for the JSIM runs this is composed of.
919 {
920 const std::string refusal = jmt_method_refusal(sn, "jsim", opt);
921 if (!refusal.empty()) throw InputError(refusal);
922 }
923
924 std::vector<JmtSysTrace<T>> paths;
925 std::set<double> grid;
926 double tumax = std::numeric_limits<double>::infinity();
927 for (std::size_t it = 0; it < reps; ++it) {
928 JmtOptions o = opt;
929 o.method = "jsim";
930 o.seed = opt.seed + static_cast<long>(it); // replication k = it+1 is seeded seed + k - 1
932 try {
933 tr = jmt_sample_sys_aggr(sn, 0, o);
934 } catch (const std::exception&) {
935 continue; // a replication that produced no log contributes nothing
936 }
937 if (tr.t.empty()) continue;
938 tumax = std::min(tumax, tr.t.back());
939 for (double tv : tr.t) grid.insert(tv);
940 paths.push_back(tr);
941 }
942 if (paths.empty())
943 throw ExternalError(ErrorKind{"JMT"},
944 "SolverJMT: no valid replications produced; the transient averages cannot be computed");
945
947 out.valid = paths.size();
948 for (double tv : grid)
949 if (tv <= tumax) out.t.push_back(tv);
950 const std::size_t nt = out.t.size();
951 const std::size_t M = sn.nstations, K = sn.nclasses;
952 out.QNt.assign(M, std::vector<std::vector<double>>(K, std::vector<double>(nt, 0.0)));
953 out.UNt.assign(M, std::vector<std::vector<double>>(K, std::vector<double>(nt, 0.0)));
954 out.TNt.assign(M, std::vector<std::vector<double>>(K, std::vector<double>(nt, 0.0)));
955
956 const double inv = 1.0 / static_cast<double>(paths.size());
957 for (std::size_t ist = 0; ist < M; ++ist) {
958 const double c = num_traits<T>::to_double(sn.stations[ist].nservers);
959 const bool finite_c = std::isfinite(c) && c > 0.0;
960 for (const JmtSysTrace<T>& tr : paths) {
961 if (ist >= tr.state.size() || tr.state[ist].empty()) continue;
962 const std::vector<std::vector<double>>& block = tr.state[ist];
963 std::size_t k = 0;
964 for (std::size_t g = 0; g < nt; ++g) {
965 while (k + 1 < tr.t.size() && tr.t[k + 1] <= out.t[g]) ++k;
966 if (out.t[g] < tr.t.front() || k >= block.size()) continue;
967 for (std::size_t r = 0; r < K && r < block[k].size(); ++r) {
968 const double n = block[k][r];
969 out.QNt[ist][r][g] += inv * n;
970 out.UNt[ist][r][g] += inv * (finite_c ? std::min(n, c) / c : n);
971 }
972 }
973 }
974 // no queue is logged at a Source: it holds no jobs and emits at its arrival rate, as the
975 // MATLAB reference's transient arm reports it
976 const bool is_source = sn.nodes[sn.station_to_node[ist] - 1].nodetype == lang::NodeType::Source;
977 for (std::size_t r = 0; r < K; ++r) {
978 const double mu = num_traits<T>::to_double(sn.rates(ist, r));
979 if (is_source) {
980 const double lambda = (std::isfinite(mu) && mu > 0.0) ? mu : 0.0;
981 for (std::size_t g = 0; g < nt; ++g) out.TNt[ist][r][g] = lambda;
982 continue;
983 }
984 const double scale = std::isfinite(mu) ? (finite_c ? c * mu : mu) : 0.0;
985 for (std::size_t g = 0; g < nt; ++g) out.TNt[ist][r][g] = out.UNt[ist][r][g] * scale;
986 }
987 }
988 return out;
989}
990
991} // namespace jmt
992} // namespace line
993
994#endif // LINE_SOLVERS_WRAPPERS_JMT_JMT_LOGS_H
ExternalError(const std::string &what)
Definition error.h:126
InputError(const std::string &what)
Definition error.h:105
std::size_t size() const
Definition matrix.h:91
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
void set_route(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
P{r,s}(i,j) = p, with 1-based NODE and class indices.
std::map< std::pair< std::size_t, std::size_t >, Matrix< T > > P
P[(r,s)] is an (nnodes x nnodes) block; absent means all zero.
std::size_t add_node(const std::string &nm, NodeType ty, bool stateful)
Add a non-station node (a Fork, a Router).
std::vector< JobClass > classes
void refresh_struct()
The whole chain, in MATLAB's refreshStruct order.
std::map< std::pair< std::size_t, std::size_t >, Matrix< T > > Peff
The routing after refresh_routing() has expanded the non-PROB strategies and folded the class switche...
std::vector< NodeDef > nodes
every node, in creation order
std::string log_path
Network.setLogPath / getLogPath: the directory every Logger writes into, and the logPath attribute of...
std::map< std::size_t, std::vector< T > > initmarking
The DECLARED initial state of a stateful node, by 1-based node index.
std::vector< std::size_t > station_to_node
(nstations) 1-based node index
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
The exception types the port throws.
Matrix< T > sn_declared_marginal(const qn::NetworkStruct< T > &sn)
The (nstations x nclasses) per-class job counts of the model's OWN state.
Definition sn_state.h:85
bool jmt_metric_enabled(const qn::NetworkStruct< T > &sn, JmtMetricKind kind, std::size_t ist, std::size_t r, const std::vector< bool > &is_cache_class)
Port of the disabled rules in getAvgHandles, per (station, class).
Definition jmt_writer.h:315
std::vector< bool > jmt_cache_classes(const qn::NetworkStruct< T > &sn)
The classes a Cache switches jobs into; they keep their Tput/ArvR measures.
Definition jmt_writer.h:409
qn::NetworkStruct< T > jmt_link_and_log(const qn::NetworkStruct< T > &sn, const std::vector< bool > &is_node_logged, const std::string &log_path)
Port of @@MNetwork/linkAndLog.m: the model with an arrival and a departure Logger around every logged...
Definition jmt_logs.h:88
JmtLogFile jmt_read_log(const std::string &path)
Read one JMT log CSV.
Definition jmt_logs.h:204
JmtReplication< T > jmt_transient_replications(const qn::NetworkStruct< T > &sn, const JmtOptions &opt)
Port of the transient ensemble of @@SolverJMT/runAnalyzer.m (default over a finite timespan).
Definition jmt_logs.h:912
JmtSysTrace< T > jmt_sample_sys_aggr(const qn::NetworkStruct< T > &sn, std::size_t num_events, const JmtOptions &opt)
Port of sampleSysAggr: every station's trajectory on one time grid.
Definition jmt_logs.h:501
JmtNodeTrace< T > jmt_sample_aggr(const qn::NetworkStruct< T > &sn, std::size_t node, std::size_t num_events, const JmtOptions &opt)
Port of sampleAggr: the queue-length trajectory of one node.
Definition jmt_logs.h:450
std::map< std::size_t, std::vector< double > > jmt_parse_tran_resp_t(const JmtLogFile &arv, const JmtLogFile &dep, const std::vector< std::size_t > &class_of_arv, const std::vector< std::size_t > &class_of_dep)
Port of parseTranRespT: the per-class response-time samples of one node.
Definition jmt_logs.h:345
JmtNodeTrace< T > jmt_parse_tran_state(const JmtLogFile &arv, const JmtLogFile &dep, const std::vector< std::size_t > &class_of_arv, const std::vector< std::size_t > &class_of_dep, const std::vector< double > &node_preload)
Port of parseTranState: the arrival and departure logs merged into a per-class queue-length trajector...
Definition jmt_logs.h:247
JmtEventType
The event kinds a JMT log carries, MATLAB EventType.
Definition jmt_logs.h:60
std::size_t jmt_transient_replication_count(const JmtOptions &opt, const std::string &what)
How many JSIM runs a transient estimate is over: the gate the two transient arms share.
Definition jmt_logs.h:677
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::map< std::pair< std::size_t, std::size_t >, std::vector< std::pair< double, double > > > jmt_get_cdf_resp_t(const qn::NetworkStruct< T > &sn, const JmtOptions &opt, bool seed_from_steady=true)
Port of getCdfRespT: the empirical response-time distribution per (station, class),...
Definition jmt_logs.h:573
std::vector< double > jmt_dwell_weights(const std::vector< double > &t)
The dwell-time weights of a trajectory sampled at t.
Definition jmt_logs.h:772
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.
std::pair< std::vector< double >, std::vector< std::vector< double > > > jmt_get_tran_prob_aggr(const qn::NetworkStruct< T > &sn, std::size_t station, const JmtOptions &opt, std::vector< std::vector< double > > &states_out)
Port of getTranProbAggr: the transient distribution of one station's aggregate state,...
Definition jmt_logs.h:706
JmtProbAggr jmt_prob_aggr(const qn::NetworkStruct< T > &sn, const JmtOptions &opt, std::size_t target_station=0, const std::vector< double > &target=std::vector< double >())
Port of getProbAggr and getProbSysAggr, both off ONE instrumented run.
Definition jmt_logs.h:816
std::string jmt_method_refusal(const qn::NetworkStruct< T > &sn, const std::string &method, const JmtOptions &opt)
std::map< std::size_t, JmtNodeTrace< T > > jmt_parse_logs(const qn::NetworkStruct< T > &orig, const std::vector< bool > &is_node_logged, const std::string &log_path)
Port of parseLogs: read every logged node's CSV pair.
Definition jmt_logs.h:399
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Ports of matlab/src/api/sn/sn_get_state_aggr.m and sn_is_state_valid.m.
Port of SolverJMT, the Java Modelling Tools client.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
One logged event: when, of which kind, in which class, for which job.
Definition jmt_logs.h:63
std::size_t cls
1-based class index, 0 at INIT
Definition jmt_logs.h:67
std::size_t node
1-based node index
Definition jmt_logs.h:66
double job
JMT's job id, -1 at INIT.
Definition jmt_logs.h:68
JmtEventType type
Definition jmt_logs.h:65
One arrival or departure CSV file, as three parallel columns.
Definition jmt_logs.h:187
std::vector< double > job
Definition jmt_logs.h:189
std::vector< double > ts
Definition jmt_logs.h:188
std::vector< std::string > cls
Definition jmt_logs.h:190
The per-class queue-length trajectory of one node, plus its event stream.
Definition jmt_logs.h:227
std::vector< std::vector< double > > qlen
(|t| x nclasses) counts after the event
Definition jmt_logs.h:229
std::vector< JmtEvent > event
Definition jmt_logs.h:230
std::vector< double > t
event times, ascending
Definition jmt_logs.h:228
The options of one JMT solve, SolverOptions('JMT') restricted to what is read.
Definition solver_jmt.h:121
std::string method
default | jsim | jmva | jmva.<alg>
Definition solver_jmt.h:122
double max_events
JSIM event budget, -1 unlimited; set by the samplers as MATLAB's maxEvents.
Definition solver_jmt.h:128
What jmt_prob_aggr reports: the system probability and the per-station ones.
Definition jmt_logs.h:779
std::vector< bool > station_seen
the same, per station
Definition jmt_logs.h:783
bool sys_seen
whether the joint state occurred at all
Definition jmt_logs.h:782
double sys
P(the whole network is in the declared state).
Definition jmt_logs.h:780
std::vector< double > station
P(station i holds its declared per-class counts).
Definition jmt_logs.h:781
Transient averages over independent replications, on one time grid.
Definition jmt_logs.h:880
std::vector< double > t
Definition jmt_logs.h:881
std::vector< std::vector< std::vector< double > > > UNt
Definition jmt_logs.h:884
std::size_t valid
Replications that produced a usable trajectory.
Definition jmt_logs.h:887
std::vector< std::vector< std::vector< double > > > TNt
Definition jmt_logs.h:885
std::vector< std::vector< std::vector< double > > > QNt
QNt[ist-1][r] over t.
Definition jmt_logs.h:883
The result of a JMT solve: the shared AvgResult plus what only JMT reports.
Definition solver_jmt.h:759
mva::AvgResult< T > avg
Definition solver_jmt.h:760
The system trajectory: one per-class block per station, on a common grid.
Definition jmt_logs.h:483
std::vector< std::vector< std::vector< double > > > state
state[ist-1] is (|t| x nclasses); a Source's block is empty.
Definition jmt_logs.h:486
std::vector< double > t
Definition jmt_logs.h:484
std::vector< JmtEvent > event
Definition jmt_logs.h:487
A scratch directory for the subprocess wrappers, the port's lineTempName.