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());
460 const std::map<std::size_t, JmtNodeTrace<T>> traces = jmt_parse_logs(sn, logged, logs.path());
461 const auto it = traces.find(node);
462 if (it == traces.end())
463 throw NumericError(
464 "SolverJMT: the simulation produced no log for node '" + sn.nodes[node - 1].name +
465 "'; the run has likely failed before any job crossed it");
466 JmtNodeTrace<T> tr = it->second;
467 if (num_events > 0 && tr.t.size() > num_events + 1) {
468 tr.t.resize(num_events + 1);
469 tr.qlen.resize(num_events + 1);
470 std::vector<JmtEvent> ev;
471 for (std::size_t i = 0; i < tr.event.size(); ++i)
472 if (tr.event[i].t <= tr.t.back()) ev.push_back(tr.event[i]);
473 tr.event = ev;
474 }
475 return tr;
476}
477
478/** The system trajectory: one per-class block per station, on a common grid. */
479template <class T>
481 std::vector<double> t;
482 /** state[ist-1] is (|t| x nclasses); a Source's block is empty. */
483 std::vector<std::vector<std::vector<double>>> state;
484 std::vector<JmtEvent> event;
485};
486
487/**
488 * Port of `sampleSysAggr`: every station's trajectory on one time grid.
489 *
490 * The per-station traces have their OWN event times, so they are resampled onto
491 * the union grid with a PREVIOUS-value hold -- a queue length is constant
492 * between its own events, so holding is exact rather than an interpolation. The
493 * grid is truncated at the earliest station's last event: past that point one
494 * station has no data, and extending its last value would report a queue that
495 * stopped changing rather than one that stopped being observed.
496 */
497template <class T>
499 const JmtOptions& opt) {
500 std::vector<bool> logged(sn.nodes.size(), false);
501 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
502 const std::size_t ind = sn.station_to_node[ist - 1];
503 if (sn.nodes[ind - 1].nodetype != lang::NodeType::Source) logged[ind - 1] = true;
504 }
505 util::TempDir logs("jmtlog");
506 if (opt.keep) logs.keep();
507 const qn::NetworkStruct<T> inst = jmt_link_and_log(sn, logged, logs.path());
508 JmtOptions o = opt;
509 o.method = "jsim"; // never `replication`, which is composed FROM this
511 const std::map<std::size_t, JmtNodeTrace<T>> traces = jmt_parse_logs(sn, logged, logs.path());
512
513 JmtSysTrace<T> out;
514 std::set<double> grid;
515 double tmax = std::numeric_limits<double>::infinity();
516 for (const auto& kv : traces) {
517 if (kv.second.t.empty()) continue;
518 tmax = std::min(tmax, kv.second.t.back());
519 for (double t : kv.second.t) grid.insert(t);
520 }
521 for (double t : grid)
522 if (t <= tmax) out.t.push_back(t);
523 if (num_events > 0 && out.t.size() > num_events) out.t.resize(num_events);
524
525 out.state.assign(sn.nstations, std::vector<std::vector<double>>());
526 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
527 const std::size_t ind = sn.station_to_node[ist - 1];
528 const auto it = traces.find(ind);
529 if (it == traces.end()) continue;
530 const JmtNodeTrace<T>& tr = it->second;
531 std::vector<std::vector<double>> block(out.t.size(),
532 std::vector<double>(sn.nclasses, 0.0));
533 std::size_t k = 0;
534 for (std::size_t g = 0; g < out.t.size(); ++g) {
535 while (k + 1 < tr.t.size() && tr.t[k + 1] <= out.t[g]) ++k;
536 if (k < tr.qlen.size()) block[g] = tr.qlen[k];
537 }
538 out.state[ist - 1] = block;
539 }
540 for (const auto& kv : traces)
541 for (std::size_t i = 0; i < kv.second.event.size(); ++i) {
542 JmtEvent e = kv.second.event[i];
543 e.node = kv.first;
544 if (e.t <= (out.t.empty() ? 0.0 : out.t.back())) out.event.push_back(e);
545 }
546 std::stable_sort(out.event.begin(), out.event.end(),
547 [](const JmtEvent& a, const JmtEvent& b) { return a.t < b.t; });
548 return out;
549}
550
551/**
552 * Port of `getCdfRespT`: the empirical response-time distribution per
553 * (station, class), as the (F, X) pairs `ecdf` returns.
554 *
555 * Every station with a service process is logged, the model is simulated, and
556 * each node's passages are turned into samples. With `seed_from_steady` (the
557 * getCdfRespT contract) the model is FIRST solved for its steady-state queue
558 * lengths and the logged run starts preloaded at their rounded values, the
559 * closed-class remainder on the bottleneck -- the reference's two-run pipeline,
560 * which shortens the warmup and is what makes the seeded curve comparable to
561 * MATLAB's for the same seed. Without it (the getTranCdfRespT contract) the
562 * logged run starts from the model's default initial state, so the samples
563 * cover the transient.
564 */
565template <class T>
566std::map<std::pair<std::size_t, std::size_t>, std::vector<std::pair<double, double>>>
568 bool seed_from_steady = true) {
569 std::vector<bool> logged(sn.nodes.size(), false);
570 std::vector<bool> cacheclass = io::jmt_cache_classes(sn);
571 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
572 const std::size_t ind = sn.station_to_node[ist - 1];
573 for (std::size_t r = 1; r <= sn.nclasses; ++r)
574 if (io::jmt_metric_enabled(sn, io::JmtMetricKind::RespT, ist, r, cacheclass))
575 logged[ind - 1] = true;
576 }
577 util::TempDir logs("jmtcdf");
578 if (opt.keep) logs.keep();
579 qn::NetworkStruct<T> inst = jmt_link_and_log(sn, logged, logs.path());
580 if (seed_from_steady) {
581 // The reference's first run: steady-state queue lengths, floored per
582 // (station, class), a closed class's remainder pushed onto its fullest
583 // station so the population balances. The rounded counts ride into the
584 // logged copy through `initmarking`, the same channel a Place's tokens
585 // take, which is what the writer's preload block reads first.
587 std::vector<std::vector<double>> n(sn.nstations,
588 std::vector<double>(sn.nclasses, 0.0));
589 for (std::size_t r = 1; r <= sn.nclasses; ++r) {
590 double tot = 0.0, vmax = -1.0;
591 std::size_t imax = 1;
592 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
593 const double q = std::floor(std::max(
594 0.0, num_traits<T>::to_double(pre.avg.QN(ist - 1, r - 1))));
595 n[ist - 1][r - 1] = q;
596 tot += q;
597 if (q > vmax) {
598 vmax = q;
599 imax = ist;
600 }
601 }
602 const double njobs = sn.classes[r - 1].population;
603 if (std::isfinite(njobs) && tot < njobs)
604 n[imax - 1][r - 1] += njobs - tot;
605 }
606 for (std::size_t ist = 1; ist <= inst.nstations && ist <= sn.nstations; ++ist) {
607 const std::size_t ind = inst.station_to_node[ist - 1];
608 const lang::NodeType ty = inst.nodes[ind - 1].nodetype;
609 if (ty == lang::NodeType::Source || ty == lang::NodeType::Join) continue;
610 std::vector<T> row(sn.nclasses);
611 for (std::size_t r = 0; r < sn.nclasses; ++r)
612 row[r] = num_traits<T>::from_double(n[ist - 1][r]);
613 inst.initmarking[ind] = row;
614 }
615 }
617
618 std::map<std::string, std::size_t> class_of;
619 for (std::size_t r = 1; r <= sn.nclasses; ++r) class_of[sn.classes[r - 1].name] = r;
620
621 std::map<std::pair<std::size_t, std::size_t>, std::vector<std::pair<double, double>>> out;
622 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
623 const std::size_t ind = sn.station_to_node[ist - 1];
624 if (!logged[ind - 1]) continue;
625 const std::string base = logs.path() + "/" + sn.nodes[ind - 1].name;
626 if (!detail::is_file(base + "-Arv.csv") || !detail::is_file(base + "-Dep.csv")) continue;
627 const JmtLogFile arv = jmt_read_log(base + "-Arv.csv");
628 const JmtLogFile dep = jmt_read_log(base + "-Dep.csv");
629 std::vector<std::size_t> ca(arv.cls.size(), 0), cd(dep.cls.size(), 0);
630 for (std::size_t i = 0; i < arv.cls.size(); ++i) {
631 const auto it = class_of.find(arv.cls[i]);
632 ca[i] = it == class_of.end() ? 0 : it->second;
633 }
634 for (std::size_t i = 0; i < dep.cls.size(); ++i) {
635 const auto it = class_of.find(dep.cls[i]);
636 cd[i] = it == class_of.end() ? 0 : it->second;
637 }
638 const std::map<std::size_t, std::vector<double>> samples =
639 jmt_parse_tran_resp_t(arv, dep, ca, cd);
640 for (const auto& kv : samples) {
641 if (kv.first == 0 || kv.second.empty()) continue;
642 std::vector<double> x = kv.second;
643 std::sort(x.begin(), x.end());
644 // `ecdf`'s convention: the first row is (0, x(1)) and the step at
645 // each distinct value carries the mass of its ties.
646 std::vector<std::pair<double, double>> fx;
647 fx.push_back(std::make_pair(0.0, x.front()));
648 std::size_t i = 0;
649 while (i < x.size()) {
650 std::size_t j = i;
651 while (j + 1 < x.size() && x[j + 1] == x[i]) ++j;
652 fx.push_back(std::make_pair(static_cast<double>(j + 1) /
653 static_cast<double>(x.size()),
654 x[i]));
655 i = j + 1;
656 }
657 out[std::make_pair(ist, kv.first)] = fx;
658 }
659 }
660 return out;
661}
662
663/**
664 * Port of `getTranProbAggr`: the transient distribution of one station's
665 * aggregate state, estimated over `replications` independent runs.
666 *
667 * Each run is a fresh seed, `seed + it`, and the runs are resampled onto the
668 * union of their event times before being counted, so a state's probability at
669 * time t is the fraction of replications occupying it at t. A FINITE HORIZON IS
670 * REQUIRED: with an unbounded one every replication ends at a different time
671 * and the union grid is not a grid the estimate is defined on -- which is
672 * exactly the error the reference raises.
673 */
674template <class T>
675std::pair<std::vector<double>, std::vector<std::vector<double>>> jmt_get_tran_prob_aggr(
676 const qn::NetworkStruct<T>& sn, std::size_t station, std::size_t replications,
677 const JmtOptions& opt, std::vector<std::vector<double>>& states_out) {
678 if (!std::isfinite(opt.max_simulated_time))
679 throw InputError(
680 "SolverJMT: getTranProbAggr requires a finite time span, e.g. "
681 "options.timespan = [0, T]");
682 if (station == 0 || station > sn.nstations)
683 throw InputError("SolverJMT: getTranProbAggr was given a station index out of range");
684 const std::size_t ind = sn.station_to_node[station - 1];
685 if (sn.nodes[ind - 1].nodetype == lang::NodeType::Source)
686 throw InputError("SolverJMT: getTranProbAggr does not apply to a Source");
687
688 std::vector<JmtSysTrace<T>> runs;
689 std::set<double> grid;
690 for (std::size_t it = 0; it < replications; ++it) {
691 JmtOptions o = opt;
692 o.seed = opt.seed + static_cast<long>(it);
694 for (double t : tr.t) grid.insert(t);
695 runs.push_back(tr);
696 }
697 std::vector<double> tu(grid.begin(), grid.end());
698
699 // The distinct aggregate states seen, in first-appearance order after the
700 // resampling; a replication that produced no data contributes none.
701 std::map<std::vector<double>, std::size_t> index;
702 std::vector<std::vector<double>> states;
703 std::vector<std::vector<std::size_t>> occupied(runs.size(),
704 std::vector<std::size_t>(tu.size(), 0));
705 for (std::size_t r = 0; r < runs.size(); ++r) {
706 const std::vector<std::vector<double>>& block = runs[r].state[station - 1];
707 if (block.empty()) continue;
708 std::size_t k = 0;
709 for (std::size_t g = 0; g < tu.size(); ++g) {
710 while (k + 1 < runs[r].t.size() && runs[r].t[k + 1] <= tu[g]) ++k;
711 if (k >= block.size()) continue;
712 const std::vector<double>& s = block[k];
713 auto it = index.find(s);
714 if (it == index.end()) {
715 index[s] = states.size();
716 states.push_back(s);
717 it = index.find(s);
718 }
719 occupied[r][g] = it->second + 1; // 1-based; 0 marks "no data"
720 }
721 }
722 std::vector<std::vector<double>> pi(tu.size(), std::vector<double>(states.size(), 0.0));
723 for (std::size_t r = 0; r < runs.size(); ++r)
724 for (std::size_t g = 0; g < tu.size(); ++g)
725 if (occupied[r][g] != 0)
726 pi[g][occupied[r][g] - 1] += 1.0 / static_cast<double>(runs.size());
727 states_out = states;
728 return std::make_pair(tu, pi);
729}
730
731/**
732 * The dwell-time weights of a trajectory sampled at `t`.
733 *
734 * The reference writes `dt = [diff(t); 0]`: a state observed at t_k is held
735 * until t_{k+1}, and the LAST sample carries no weight because the run ends
736 * there and how long it would have been held is not observed. Dropping the
737 * trailing zero instead -- weighting the last sample by the previous gap -- adds
738 * a dwell that was never measured, which is why it is kept as a zero.
739 */
740inline std::vector<double> jmt_dwell_weights(const std::vector<double>& t) {
741 std::vector<double> dt(t.size(), 0.0);
742 for (std::size_t i = 0; i + 1 < t.size(); ++i) dt[i] = t[i + 1] - t[i];
743 return dt;
744}
745
746/** What `jmt_prob_aggr` reports: the system probability and the per-station ones. */
748 double sys = 0.0; ///< P(the whole network is in the declared state)
749 std::vector<double> station; ///< P(station i holds its declared per-class counts)
750 bool sys_seen = false; ///< whether the joint state occurred at all
751 std::vector<bool> station_seen; ///< the same, per station
752};
753
754/**
755 * Port of `getProbAggr` and `getProbSysAggr`, both off ONE instrumented run.
756 *
757 * THEY ARE TIME AVERAGES, NOT SAMPLE FRACTIONS. The trajectory is event-driven,
758 * so its samples are not equally spaced; counting them would weigh a state the
759 * network leaves at once as heavily as one it sits in. Each sample is weighed by
760 * the interval it is held for, which is what makes the estimate converge to the
761 * stationary probability.
762 *
763 * ONE RUN, NOT ONE PER QUESTION. The reference instruments one node at a time
764 * (`sampleAggr` logs the node it was asked about) and therefore simulates again
765 * for every station; `jmt_sample_sys_aggr` already logs every station on a
766 * common grid, so the station marginals are read off the same sample path as the
767 * joint probability. That is a DIFFERENT estimator from the reference's -- same
768 * quantity, one sample path instead of M independent ones -- and it is the
769 * cheaper and the more consistent of the two, since the marginals it reports
770 * cannot contradict the joint they were taken from.
771 *
772 * `target` OVERRIDES THE DECLARED STATE of one station where it is given, which
773 * is `getProbAggr`'s second argument; every other station keeps the row
774 * `setState`/`initFromMarginal` left, exactly as the reference substitutes into
775 * `sn.state{isf}` and leaves the rest alone.
776 *
777 * A STATE NEVER SEEN IS 0 AND IS REPORTED AS SUCH, with `seen` false beside it.
778 * The reference warns and returns zero rather than erroring, because on a
779 * simulation a state of small probability legitimately fails to appear in a
780 * finite run: that is a statement about the run length, and the caller is the
781 * one who can lengthen it.
782 */
783template <class T>
785 std::size_t target_station = 0,
786 const std::vector<double>& target = std::vector<double>()) {
788 if (!target.empty()) {
789 if (target_station == 0 || target_station > sn.nstations)
790 throw InputError("SolverJMT: getProbAggr was given a station index out of range");
791 if (target.size() != sn.nclasses)
792 throw InputError("SolverJMT: getProbAggr takes one job count per class (" +
793 std::to_string(sn.nclasses) + "), got " +
794 std::to_string(target.size()));
795 for (std::size_t r = 0; r < sn.nclasses; ++r)
796 nirm(target_station - 1, r) = num_traits<T>::from_double(target[r]);
797 }
798 std::vector<std::vector<double>> nir(sn.nstations, std::vector<double>(sn.nclasses, 0.0));
799 for (std::size_t i = 0; i < sn.nstations; ++i)
800 for (std::size_t r = 0; r < sn.nclasses; ++r)
801 nir[i][r] = num_traits<T>::to_double(nirm(i, r));
802
804 const std::vector<double> dt = jmt_dwell_weights(tr.t);
805
806 JmtProbAggr out;
807 out.station.assign(sn.nstations, 0.0);
808 out.station_seen.assign(sn.nstations, false);
809 std::vector<double> hit(sn.nstations, 0.0);
810 double sys_hit = 0.0, total = 0.0;
811 for (std::size_t g = 0; g < dt.size(); ++g) {
812 total += dt[g];
813 bool all = true;
814 for (std::size_t ist = 1; ist <= sn.nstations; ++ist) {
815 const std::size_t ind = sn.station_to_node[ist - 1];
816 // A Source holds no jobs and the reference's own block for it is not
817 // a state; it takes no part in either comparison.
818 if (sn.nodes[ind - 1].nodetype == lang::NodeType::Source) continue;
819 const std::vector<std::vector<double>>& block = tr.state[ist - 1];
820 bool same = g < block.size();
821 for (std::size_t r = 0; r < sn.nclasses && same; ++r)
822 same = block[g][r] == nir[ist - 1][r];
823 if (same) {
824 hit[ist - 1] += dt[g];
825 if (dt[g] > 0.0) out.station_seen[ist - 1] = true;
826 } else {
827 all = false;
828 }
829 }
830 if (all) {
831 sys_hit += dt[g];
832 if (dt[g] > 0.0) out.sys_seen = true;
833 }
834 }
835 // A run that observed no dwell at all has measured nothing, and 0 would read
836 // as "the state was never visited" -- a claim the run cannot support.
837 if (!(total > 0.0))
838 throw NumericError(
839 "SolverJMT: the simulation produced no observed dwell time, so no state probability "
840 "can be estimated from it");
841 out.sys = sys_hit / total;
842 for (std::size_t i = 0; i < sn.nstations; ++i) out.station[i] = hit[i] / total;
843 return out;
844}
845
846/** Transient averages over independent replications, on one time grid. */
847template <class T>
849 std::vector<double> t;
850 /** QNt[ist-1][r] over `t`. */
851 std::vector<std::vector<std::vector<double>>> QNt;
852 std::vector<std::vector<std::vector<double>>> UNt;
853 std::vector<std::vector<std::vector<double>>> TNt;
854 /** Replications that produced a usable trajectory. */
855 std::size_t valid = 0;
856};
857
858/**
859 * Port of the `replication` method of `@@SolverJMT/runAnalyzer.m`.
860 *
861 * A SINGLE SAMPLE PATH IS NOT THE TRANSIENT MEAN. There is no time-ergodicity at
862 * a fixed t, so E[N](t) is estimated by averaging `iter_max` INDEPENDENT
863 * replications, each seeded `seed + it`, rather than by reading one trajectory.
864 *
865 * The replications have their own event grids, so they are resampled onto the
866 * union with a previous-value hold and the grid is truncated at the MINIMUM of
867 * their last events: past that point one replication has no data, and extending
868 * its last value would report a queue that stopped changing rather than one that
869 * stopped being observed -- the same rule `jmt_sample_sys_aggr` applies across
870 * stations, applied here across seeds.
871 *
872 * Utilization is `min(n, c)/c` at a finite server and the raw queue length at a
873 * delay; throughput follows it as `U*c*mu` and `U*mu`, which is how the
874 * reference reads departures off the occupancy rather than differencing the
875 * trajectory.
876 */
877template <class T>
879 // The predicate `auto_family_refusal` asks, so the gate that decides whether
880 // to OFFER 'replication' and this run cannot drift apart.
881 {
882 const std::string refusal = jmt_method_refusal(sn, "replication", opt);
883 if (!refusal.empty()) throw InputError(refusal);
884 }
885 const int reps = opt.iter_max > 0 ? opt.iter_max : 10;
886
887 std::vector<JmtSysTrace<T>> paths;
888 std::set<double> grid;
889 double tumax = std::numeric_limits<double>::infinity();
890 for (int it = 0; it < reps; ++it) {
891 JmtOptions o = opt;
892 o.method = "jsim";
893 o.seed = opt.seed + it;
895 try {
896 tr = jmt_sample_sys_aggr(sn, 0, o);
897 } catch (const std::exception&) {
898 continue; // a replication that produced no log contributes nothing
899 }
900 if (tr.t.empty()) continue;
901 tumax = std::min(tumax, tr.t.back());
902 for (double tv : tr.t) grid.insert(tv);
903 paths.push_back(tr);
904 }
905 if (paths.empty())
906 throw UnsupportedError(
907 "SolverJMT: no valid replications produced; the transient averages cannot be computed");
908
910 out.valid = paths.size();
911 for (double tv : grid)
912 if (tv <= tumax) out.t.push_back(tv);
913 const std::size_t nt = out.t.size();
914 const std::size_t M = sn.nstations, K = sn.nclasses;
915 out.QNt.assign(M, std::vector<std::vector<double>>(K, std::vector<double>(nt, 0.0)));
916 out.UNt.assign(M, std::vector<std::vector<double>>(K, std::vector<double>(nt, 0.0)));
917 out.TNt.assign(M, std::vector<std::vector<double>>(K, std::vector<double>(nt, 0.0)));
918
919 const double inv = 1.0 / static_cast<double>(paths.size());
920 for (std::size_t ist = 0; ist < M; ++ist) {
921 const double c = num_traits<T>::to_double(sn.stations[ist].nservers);
922 const bool finite_c = std::isfinite(c) && c > 0.0;
923 for (const JmtSysTrace<T>& tr : paths) {
924 if (ist >= tr.state.size() || tr.state[ist].empty()) continue;
925 const std::vector<std::vector<double>>& block = tr.state[ist];
926 std::size_t k = 0;
927 for (std::size_t g = 0; g < nt; ++g) {
928 while (k + 1 < tr.t.size() && tr.t[k + 1] <= out.t[g]) ++k;
929 if (out.t[g] < tr.t.front() || k >= block.size()) continue;
930 for (std::size_t r = 0; r < K && r < block[k].size(); ++r) {
931 const double n = block[k][r];
932 out.QNt[ist][r][g] += inv * n;
933 out.UNt[ist][r][g] += inv * (finite_c ? std::min(n, c) / c : n);
934 }
935 }
936 }
937 for (std::size_t r = 0; r < K; ++r) {
938 const double mu = num_traits<T>::to_double(sn.rates(ist, r));
939 const double scale = std::isfinite(mu) ? (finite_c ? c * mu : mu) : 0.0;
940 for (std::size_t g = 0; g < nt; ++g) out.TNt[ist][r][g] = out.UNt[ist][r][g] * scale;
941 }
942 }
943 return out;
944}
945
946} // namespace jmt
947} // namespace line
948
949#endif // LINE_SOLVERS_WRAPPERS_JMT_JMT_LOGS_H
InputError(const std::string &what)
Definition error.h:39
std::size_t size() const
Definition matrix.h:91
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
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:314
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:346
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
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:498
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
JmtResult< T > solver_jmt_run_analyzer(const qn::NetworkStruct< T > &sn, const JmtOptions &opt_in)
Port of @@SolverJMT/runAnalyzer.m, the jsim and jmva arms.
Definition solver_jmt.h:927
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:567
std::pair< std::vector< double >, std::vector< std::vector< double > > > jmt_get_tran_prob_aggr(const qn::NetworkStruct< T > &sn, std::size_t station, std::size_t replications, 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:675
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:740
JmtReplication< T > jmt_replication(const qn::NetworkStruct< T > &sn, const JmtOptions &opt)
Port of the replication method of @@SolverJMT/runAnalyzer.m.
Definition jmt_logs.h:878
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:784
std::string jmt_method_refusal(const qn::NetworkStruct< T > &sn, const std::string &method, const JmtOptions &opt)
The structural half of SolverJMT's method gate; empty when admissible.
Definition solver_jmt.h:870
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:324
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.
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:86
std::string method
default | jsim | jmva | jmva.<alg>
Definition solver_jmt.h:87
What jmt_prob_aggr reports: the system probability and the per-station ones.
Definition jmt_logs.h:747
std::vector< bool > station_seen
the same, per station
Definition jmt_logs.h:751
bool sys_seen
whether the joint state occurred at all
Definition jmt_logs.h:750
double sys
P(the whole network is in the declared state).
Definition jmt_logs.h:748
std::vector< double > station
P(station i holds its declared per-class counts).
Definition jmt_logs.h:749
Transient averages over independent replications, on one time grid.
Definition jmt_logs.h:848
std::vector< double > t
Definition jmt_logs.h:849
std::vector< std::vector< std::vector< double > > > UNt
Definition jmt_logs.h:852
std::size_t valid
Replications that produced a usable trajectory.
Definition jmt_logs.h:855
std::vector< std::vector< std::vector< double > > > TNt
Definition jmt_logs.h:853
std::vector< std::vector< std::vector< double > > > QNt
QNt[ist-1][r] over t.
Definition jmt_logs.h:851
The result of a JMT solve: the shared AvgResult plus what only JMT reports.
Definition solver_jmt.h:493
mva::AvgResult< T > avg
Definition solver_jmt.h:494
The system trajectory: one per-class block per station, on a common grid.
Definition jmt_logs.h:480
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:483
std::vector< double > t
Definition jmt_logs.h:481
std::vector< JmtEvent > event
Definition jmt_logs.h:484
A scratch directory for the subprocess wrappers, the port's lineTempName.