LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
network_writer.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_IO_NETWORK_WRITER_H
6#define LINE_IO_NETWORK_WRITER_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * `qn::NetworkStruct` -> model.json, the inverse of network_reader.h.
12 *
13 * The wire format is the one `linemodel_save.m`, the Python `save_model` and
14 * the JAR `LineModelIO` write, so a model saved here loads in any of the four
15 * codebases and a model saved by any of them and read back here is unchanged.
16 *
17 * WHAT IS AND IS NOT ROUND-TRIPPABLE. Two things on a model are FUNCTIONS, and
18 * a function cannot cross JSON:
19 *
20 * the rate-scaling handles beta_r(n), eta_i(n) and the OI mu(c)
21 * a reward built from a lambda
22 *
23 * The reference solves the first by MATERIALIZING the handle over the per-class
24 * box lattice, and this writer does the same, with the same cutoffs (a closed
25 * class's population, or 10 for an open one) and the same comma-joined key, so
26 * the table is byte-comparable with MATLAB's. The second it solves by writing
27 * only the DECLARATIVE rewards and warning about the rest; here a reward with
28 * no `kind` is omitted for the same reason -- emitting a guess would produce a
29 * model.json that reloads into a different reward.
30 *
31 * EVERY OTHER KEY IS EXACT. The writer emits exactly the keys the reader
32 * consumes, which is what makes the round trip a test rather than a hope: a
33 * field added to one side without the other shows up as a read-back mismatch
34 * in `test_network_roundtrip.cpp`.
35 */
36
37#include <cctype>
38#include <cmath>
39#include <fstream>
40#include <limits>
41#include <map>
42#include <string>
43#include <vector>
44
49#include "line/num/number.h"
50#include "line/util/error.h"
51
52namespace line {
53namespace io {
54
55namespace detail {
56
57/** The model.json `type` of a node kind. */
58inline const char* node_type_to_json(lang::NodeType t) {
59 switch (t) {
60 case lang::NodeType::Queue: return "Queue";
61 case lang::NodeType::Source: return "Source";
62 case lang::NodeType::Delay: return "Delay";
63 case lang::NodeType::ClassSwitch: return "ClassSwitch";
64 case lang::NodeType::Logger: return "Logger";
65 case lang::NodeType::Cache: return "Cache";
66 case lang::NodeType::Router: return "Router";
67 case lang::NodeType::Fork: return "Fork";
68 case lang::NodeType::Place: return "Place";
69 case lang::NodeType::Transition: return "Transition";
70 case lang::NodeType::Region: return "Region";
71 case lang::NodeType::Join: return "Join";
72 case lang::NodeType::Sink: return "Sink";
73 }
74 throw UnsupportedError("network_writer: unnamed node type");
75}
76
77inline const char* drop_to_json(lang::DropStrategy d) {
78 switch (d) {
79 case lang::DropStrategy::WAITQ: return "waitingQueue";
80 case lang::DropStrategy::DROP: return "drop";
81 case lang::DropStrategy::BAS: return "blockingAfterService";
82 case lang::DropStrategy::BBS: return "blockingBeforeService";
83 case lang::DropStrategy::RSRD: return "resamplingRepetitiveService";
84 case lang::DropStrategy::RETRIAL: return "retrial";
85 case lang::DropStrategy::RETRIAL_WITH_LIMIT: return "retrialWithLimit";
86 }
87 throw UnsupportedError("network_writer: unnamed drop strategy");
88}
89
90inline const char* replacement_to_json(lang::ReplacementStrategy r) {
91 switch (r) {
92 case lang::ReplacementStrategy::RR: return "RR";
93 case lang::ReplacementStrategy::FIFO: return "FIFO";
94 case lang::ReplacementStrategy::SFIFO: return "SFIFO";
95 case lang::ReplacementStrategy::LRU: return "LRU";
96 case lang::ReplacementStrategy::HLRU: return "HLRU";
97 case lang::ReplacementStrategy::CLIMB: return "CLIMB";
98 case lang::ReplacementStrategy::QLRU: return "QLRU";
99 }
100 throw UnsupportedError("network_writer: unnamed replacement strategy");
101}
102
103inline const char* polling_to_json(lang::PollingType p) {
104 switch (p) {
105 case lang::PollingType::EXHAUSTIVE: return "EXHAUSTIVE";
106 case lang::PollingType::GATED: return "GATED";
107 case lang::PollingType::KLIMITED: return "KLIMITED";
108 case lang::PollingType::DECREMENTING: return "DECREMENTING";
109 }
110 throw UnsupportedError("network_writer: unnamed polling type");
111}
112
113inline const char* impatience_to_json(lang::ImpatienceType i) {
114 switch (i) {
115 case lang::ImpatienceType::RENEGING: return "RENEGING";
116 case lang::ImpatienceType::BALKING: return "BALKING";
117 case lang::ImpatienceType::RETRIAL: return "RETRIAL";
118 case lang::ImpatienceType::NONE: break;
119 }
120 throw UnsupportedError("network_writer: a patience block with no impatience type");
121}
122
123inline const char* balking_to_json(lang::BalkingStrategy b) {
124 switch (b) {
125 case lang::BalkingStrategy::QUEUE_LENGTH: return "QUEUE_LENGTH";
126 case lang::BalkingStrategy::EXPECTED_WAIT: return "EXPECTED_WAIT";
127 case lang::BalkingStrategy::COMBINED: return "COMBINED";
128 case lang::BalkingStrategy::NONE: break;
129 }
130 throw UnsupportedError("network_writer: a balking block with no strategy");
131}
132
133/**
134 * The `routingStrategies` name of a dispatcher.
135 *
136 * NOT `lang::routing_to_text`, which is the lower-case name `sn.routing` prints
137 * in a struct dump. The wire carries the enum CONSTANT, upper case, matching
138 * the JAR's `RoutingStrategy` names one for one -- and the readers compare it
139 * exactly, so `rrobin` reloads as an unknown strategy rather than as RROBIN.
140 */
141inline const char* routing_to_json(lang::RoutingStrategy r) {
142 switch (r) {
143 case lang::RoutingStrategy::RAND: return "RAND";
144 case lang::RoutingStrategy::PROB: return "PROB";
145 case lang::RoutingStrategy::RROBIN: return "RROBIN";
146 case lang::RoutingStrategy::WRROBIN: return "WRROBIN";
147 case lang::RoutingStrategy::JSQ: return "JSQ";
148 case lang::RoutingStrategy::FIRING: return "FIRING";
149 case lang::RoutingStrategy::SQ: return "SQ";
150 case lang::RoutingStrategy::SDR: return "SDR";
151 case lang::RoutingStrategy::DISABLED: return "DISABLED";
152 }
153 throw UnsupportedError("network_writer: unnamed routing strategy");
154}
155
156inline const char* hetero_to_json(lang::HeteroSchedPolicy h) {
157 switch (h) {
158 case lang::HeteroSchedPolicy::ORDER: return "ORDER";
159 case lang::HeteroSchedPolicy::ALIS: return "ALIS";
160 case lang::HeteroSchedPolicy::ALFS: return "ALFS";
161 case lang::HeteroSchedPolicy::FAIRNESS: return "FAIRNESS";
162 case lang::HeteroSchedPolicy::FSF: return "FSF";
163 case lang::HeteroSchedPolicy::RAIS: return "RAIS";
164 }
165 throw UnsupportedError("network_writer: unnamed heterogeneous scheduling policy");
166}
167
168/**
169 * The `scheduling` name of a discipline.
170 *
171 * UPPER CASE, which is not what `lang::sched_to_text` returns: that is the
172 * lower-case spelling `sn.sched` prints in a struct dump, while the wire
173 * carries the enum constant (`upper(SchedStrategy.toText(...))` in
174 * `linemodel_save.m`, matching `jline.lang.constant.SchedStrategy`). A
175 * lower-case `fcfs` reloads as an unknown discipline.
176 */
177inline std::string sched_to_json(lang::SchedStrategy s) {
178 std::string out = lang::sched_to_text(s);
179 for (std::size_t i = 0; i < out.size(); ++i)
180 out[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(out[i])));
181 return out;
182}
183
184/** A dense matrix as the array of row arrays the readers expect. */
185template <class T>
186json mat_to_json(const Matrix<T>& M) {
187 json rows = json::array();
188 for (std::size_t i = 0; i < M.rows(); ++i) {
189 json row = json::array();
190 for (std::size_t j = 0; j < M.cols(); ++j)
191 row.push_back(num_traits<T>::to_double(M(i, j)));
192 rows.push_back(row);
193 }
194 return rows;
195}
196
197template <class T>
198json vec_to_json(const std::vector<T>& v) {
199 json a = json::array();
200 for (const T& x : v) a.push_back(num_traits<T>::to_double(x));
201 return a;
202}
203
204/**
205 * Rewrite every non-finite number into the wire's own INFINITY SPELLING.
206 *
207 * JSON has no infinity literal, so `linemodel_save` -- the reference -- writes
208 * an infinite scalar as the STRING "Infinity" / "-Infinity" and a NaN as
209 * `null`, a rule it applies to EVERY numeric field rather than a named few;
210 * `num_from_json` on the reading side decodes exactly that pair. nlohmann
211 * silently dumps an infinity as `null` instead, which the reader then takes for
212 * a NaN, so a state row that carries the sentinel -- an open class holds an
213 * infinite population, so a Source's `initialState` does the moment the model
214 * is initialized -- crossed as a hole rather than as a value. Applied once over
215 * the finished document, as the JAR and Python writers apply theirs, so no
216 * individual field has to remember the rule.
217 */
218inline void wire_nonfinite(json& j) {
219 if (j.is_object() || j.is_array()) {
220 for (json::iterator it = j.begin(); it != j.end(); ++it) wire_nonfinite(*it);
221 return;
222 }
223 if (!j.is_number_float()) return;
224 const double v = j.get<double>();
225 if (std::isnan(v))
226 j = json();
227 else if (std::isinf(v))
228 j = json(v > 0.0 ? "Infinity" : "-Infinity");
229}
230
231/**
232 * `x` of a `DiscreteSampler` whose support is the item index, i.e. 1..n.
233 *
234 * MATLAB `DiscreteSampler(p)` defaults `x = 1:n` and every writer records it;
235 * the JAR and Python readers require the key and fail on a file that omits it,
236 * so a pmf-only document is unreadable outside this port even though the value
237 * is implied.
238 */
239inline json discrete_support(std::size_t n) {
240 json a = json::array();
241 for (std::size_t i = 1; i <= n; ++i) a.push_back(static_cast<double>(i));
242 return a;
243}
244
245/**
246 * A distribution as its model.json record, the inverse of `dist_from_json`.
247 *
248 * The parameter-carrying families are written from `Distrib::params`, which
249 * holds the constructor arguments in MATLAB's getParam order -- that is what
250 * makes the record reload into the SAME object rather than into a phase-type
251 * that shares two moments with it.
252 */
253template <class T>
254json dist_to_json(const lang::Distrib<T>& d) {
255 using lang::ProcessType;
256 json j;
257 j["type"] = lang::process_to_text(d.type);
258 const std::vector<T>& p = d.params;
259 auto need = [&](std::size_t n, const char* who) {
260 if (p.size() < n)
261 throw InputError(std::string("network_writer: a ") + who + " carries " +
262 std::to_string(p.size()) + " parameters, and " + std::to_string(n) +
263 " are needed to write it back");
264 };
265 switch (d.type) {
266 case ProcessType::DISABLED:
267 case ProcessType::IMMEDIATE:
268 return j; // both are bare tags
269 case ProcessType::EXP:
270 need(1, "Exp");
271 j["params"]["lambda"] = num_traits<T>::to_double(p[0]);
272 return j;
273 case ProcessType::DET:
274 j["params"]["value"] = num_traits<T>::to_double(d.mean);
275 return j;
276 case ProcessType::ERLANG:
277 need(2, "Erlang");
278 j["params"]["lambda"] = num_traits<T>::to_double(p[0]);
279 j["params"]["k"] = static_cast<long>(std::lround(num_traits<T>::to_double(p[1])));
280 return j;
281 case ProcessType::HYPEREXP: {
282 // The two-branch form stores (p, lambda1, lambda2) and the general
283 // one (p_1..p_n, lambda_1..lambda_n); both write the same two
284 // vectors, which is the only form on the wire.
285 json pv = json::array(), lv = json::array();
286 if (p.size() == 3) {
287 pv.push_back(num_traits<T>::to_double(p[0]));
288 pv.push_back(1.0 - num_traits<T>::to_double(p[0]));
289 lv.push_back(num_traits<T>::to_double(p[1]));
290 lv.push_back(num_traits<T>::to_double(p[2]));
291 } else {
292 const std::size_t n = p.size() / 2;
293 for (std::size_t i = 0; i < n; ++i) pv.push_back(num_traits<T>::to_double(p[i]));
294 for (std::size_t i = 0; i < n; ++i) lv.push_back(num_traits<T>::to_double(p[n + i]));
295 }
296 j["params"]["p"] = pv;
297 j["params"]["lambda"] = lv;
298 return j;
299 }
300 case ProcessType::COXIAN:
301 case ProcessType::COX2: {
302 const std::size_t n = p.size() / 2;
303 json mu = json::array(), phi = json::array();
304 for (std::size_t i = 0; i < n; ++i) mu.push_back(num_traits<T>::to_double(p[i]));
305 for (std::size_t i = 0; i < n; ++i) phi.push_back(num_traits<T>::to_double(p[n + i]));
306 // Coxian is the only name the readers reconstruct from (mu, phi);
307 // Cox2 is the two-phase constructor and shares the representation.
308 j["type"] = "Coxian";
309 j["params"]["mu"] = mu;
310 j["params"]["phi"] = phi;
311 return j;
312 }
313 case ProcessType::PH:
314 case ProcessType::APH:
315 case ProcessType::ME: {
316 json alpha = json::array();
317 for (const T& v : p) alpha.push_back(num_traits<T>::to_double(v));
318 if (d.type == ProcessType::ME) {
319 j["params"]["alpha"] = alpha;
320 j["params"]["A"] = mat_to_json(d.D0);
321 } else {
322 j["ph"]["alpha"] = alpha;
323 j["ph"]["T"] = mat_to_json(d.D0);
324 }
325 return j;
326 }
327 case ProcessType::MAP:
328 j["map"]["D0"] = mat_to_json(d.D0);
329 j["map"]["D1"] = mat_to_json(d.D1);
330 return j;
331 case ProcessType::RAP:
332 j["params"]["H0"] = mat_to_json(d.D0);
333 j["params"]["H1"] = mat_to_json(d.D1);
334 return j;
335 case ProcessType::DMAP:
336 j["params"]["D0"] = mat_to_json(d.D0);
337 j["params"]["D1"] = mat_to_json(d.D1);
338 return j;
339 case ProcessType::MMPP2: {
340 // Recovered from the pair rather than from `params`, which
341 // `map_dist` does not fill: lambda_i is the arrival rate in phase i
342 // and sigma_i the modulating rate out of it.
343 if (d.D0.rows() != 2)
344 throw InputError("network_writer: an MMPP2 must be of order two");
345 j["params"]["lambda0"] = num_traits<T>::to_double(d.D1(0, 0));
346 j["params"]["lambda1"] = num_traits<T>::to_double(d.D1(1, 1));
347 j["params"]["sigma0"] = num_traits<T>::to_double(d.D0(0, 1));
348 j["params"]["sigma1"] = num_traits<T>::to_double(d.D0(1, 0));
349 return j;
350 }
351 case ProcessType::MPH: {
352 // Stored as the MMAP cell it lowers to, but the (alpha, S, {s_c}) split IS
353 // recoverable from it and is what the other codebases expect back: every row
354 // of D1_agg is (-S e)_i alpha, so alpha is any nonzero row normalised, and
355 // s_c(i) is the row sum of that mark's block. Writing the lowered cell instead
356 // would come back as a plain MMAP and lose the process type.
357 const std::size_t H = d.D0.rows();
358 std::vector<double> alpha(H, 0.0);
359 for (std::size_t i = 0; i < H; ++i) {
360 double row = 0.0;
361 for (std::size_t q = 0; q < H; ++q) row += num_traits<T>::to_double(d.D1(i, q));
362 if (row > 0.0) {
363 for (std::size_t q = 0; q < H; ++q)
364 alpha[q] = num_traits<T>::to_double(d.D1(i, q)) / row;
365 break;
366 }
367 }
368 j["mph"]["alpha"] = alpha;
369 j["mph"]["S"] = mat_to_json(d.D0);
370 json exits = json::array();
371 for (const Matrix<T>& Dk : d.Dmark) {
372 std::vector<double> sk(H, 0.0);
373 for (std::size_t i = 0; i < H; ++i)
374 for (std::size_t q = 0; q < H; ++q)
375 sk[i] += num_traits<T>::to_double(Dk(i, q));
376 exits.push_back(sk);
377 }
378 j["mph"]["exit"] = exits;
379 return j;
380 }
381 case ProcessType::MMAP: {
382 j["mmap"]["D0"] = mat_to_json(d.D0);
383 json blocks = json::array();
384 for (const Matrix<T>& Dk : d.Dmark) blocks.push_back(mat_to_json(Dk));
385 j["mmap"]["D1k"] = blocks;
386 return j;
387 }
388 case ProcessType::BMAP: {
389 json blocks = json::array();
390 blocks.push_back(mat_to_json(d.D0));
391 for (const Matrix<T>& Dk : d.Dmark) blocks.push_back(mat_to_json(Dk));
392 j["params"]["D"] = blocks;
393 return j;
394 }
395 case ProcessType::UNIFORM:
396 need(2, "Uniform");
397 j["params"]["a"] = num_traits<T>::to_double(p[0]);
398 j["params"]["b"] = num_traits<T>::to_double(p[1]);
399 return j;
400 case ProcessType::PARETO:
401 need(2, "Pareto");
402 j["params"]["alpha"] = num_traits<T>::to_double(p[0]);
403 j["params"]["scale"] = num_traits<T>::to_double(p[1]);
404 return j;
405 case ProcessType::GAMMA:
406 need(2, "Gamma");
407 j["params"]["alpha"] = num_traits<T>::to_double(p[0]);
408 j["params"]["beta"] = num_traits<T>::to_double(p[1]);
409 return j;
410 case ProcessType::WEIBULL:
411 // `alpha` is the SCALE here and the SHAPE in Pareto above; that is
412 // the reference's own key naming and the one trap in this table.
413 need(2, "Weibull");
414 j["params"]["alpha"] = num_traits<T>::to_double(p[0]);
415 j["params"]["beta"] = num_traits<T>::to_double(p[1]);
416 return j;
417 case ProcessType::LOGNORMAL:
418 need(2, "Lognormal");
419 j["params"]["mu"] = num_traits<T>::to_double(p[0]);
420 j["params"]["sigma"] = num_traits<T>::to_double(p[1]);
421 return j;
422 case ProcessType::DUNIFORM:
423 need(2, "DiscreteUniform");
424 j["params"]["min"] = num_traits<T>::to_double(p[0]);
425 j["params"]["max"] = num_traits<T>::to_double(p[1]);
426 return j;
427 case ProcessType::BERNOULLI:
428 need(1, "Bernoulli");
429 j["params"]["p"] = num_traits<T>::to_double(p[0]);
430 return j;
431 case ProcessType::BINOMIAL:
432 need(2, "Binomial");
433 j["params"]["n"] = static_cast<long>(std::lround(num_traits<T>::to_double(p[0])));
434 j["params"]["p"] = num_traits<T>::to_double(p[1]);
435 return j;
436 case ProcessType::POISSON:
437 need(1, "Poisson");
438 j["params"]["lambda"] = num_traits<T>::to_double(p[0]);
439 return j;
440 case ProcessType::GEOMETRIC:
441 need(1, "Geometric");
442 j["params"]["p"] = num_traits<T>::to_double(p[0]);
443 return j;
444 case ProcessType::ZIPF:
445 need(2, "Zipf");
446 j["params"]["s"] = num_traits<T>::to_double(p[0]);
447 j["params"]["n"] = static_cast<long>(std::lround(num_traits<T>::to_double(p[1])));
448 return j;
449 case ProcessType::DISCRETESAMPLER:
450 j["params"]["p"] = vec_to_json(p);
451 if (!d.trace.empty()) j["params"]["x"] = vec_to_json(d.trace);
452 return j;
453 case ProcessType::EMPIRICALCDF:
454 j["params"]["x"] = vec_to_json(d.trace);
455 j["params"]["F"] = vec_to_json(p);
456 return j;
457 case ProcessType::REPLAYER:
458 // The trace itself has no wire form: every writer emits the file
459 // PATH, plus the mean beside it as the documented fallback for a
460 // reader on another machine where the path does not resolve. A
461 // Replayer built from in-memory samples has no path, and then only
462 // the moments can be written -- which reloads as a distribution
463 // matching them, not as the trace, exactly as the reference warns.
464 if (!d.trace_file.empty()) j["params"]["fileName"] = d.trace_file;
465 j["params"]["mean"] = num_traits<T>::to_double(d.mean);
466 if (d.trace_file.empty())
467 j["params"]["scv"] = num_traits<T>::to_double(d.scv);
468 return j;
469 case ProcessType::MMAPT:
470 case ProcessType::MPHT: {
471 j["params"]["breakpoints"] = vec_to_json(d.sched_bp);
472 j["params"]["cyclic"] = d.sched_cyclic;
473 json A = json::array();
474 for (const Matrix<T>& M : d.sched_D0) A.push_back(mat_to_json(M));
475 json blocks = json::array();
476 for (const std::vector<Matrix<T> >& per_mark : d.sched_Dmark) {
477 json segs = json::array();
478 for (const Matrix<T>& M : per_mark) segs.push_back(mat_to_json(M));
479 blocks.push_back(segs);
480 }
481 // An MPHt is STORED lowered to MMAPt form -- see the Distrib header -- so the
482 // (alpha, S, exits) split is not recoverable here and it is written in the
483 // MMAPt spelling it reloads identically from. Exactly what a PHt does below,
484 // and for the same reason: this port keeps one representation per family.
485 j["type"] = "MMAPt";
486 j["params"]["D0"] = A;
487 j["params"]["D1k"] = blocks;
488 return j;
489 }
490 case ProcessType::BMMAPT: {
491 // Only the BLOCKS travel, nested mark, then batch size, then segment. The
492 // reader derives the batch-aggregated D1k and the aggregate D1 from them,
493 // so the wire cannot carry a level that disagrees with the blocks summing
494 // to it.
495 j["params"]["breakpoints"] = vec_to_json(d.sched_bp);
496 j["params"]["cyclic"] = d.sched_cyclic;
497 json A = json::array();
498 for (const Matrix<T>& M : d.sched_D0) A.push_back(mat_to_json(M));
499 json blocks = json::array();
500 for (const std::vector<std::vector<Matrix<T> > >& per_mark : d.sched_Dbatch) {
501 json per_batch = json::array();
502 for (const std::vector<Matrix<T> >& batch : per_mark) {
503 json segs = json::array();
504 for (const Matrix<T>& M : batch) segs.push_back(mat_to_json(M));
505 per_batch.push_back(segs);
506 }
507 blocks.push_back(per_batch);
508 }
509 j["params"]["D0"] = A;
510 j["params"]["D1kb"] = blocks;
511 return j;
512 }
513 // NHPP belongs with MAPt: it is a one-phase MAPt and is written back as its
514 // `rates` vector by the branch below. It briefly sat with the MARKED families
515 // above, where it was emitted as an `MMAPt` carrying an EMPTY `D1k` -- a
516 // document no reader accepts, since the MMAPt factory refuses a schedule with
517 // no mark block.
518 case ProcessType::NHPP:
519 case ProcessType::MAPT:
520 case ProcessType::PHT: {
521 j["params"]["breakpoints"] = vec_to_json(d.sched_bp);
522 j["params"]["cyclic"] = d.sched_cyclic;
523 if (d.type == ProcessType::NHPP) {
524 // A one-phase schedule: the rate of segment k is D1[k](0,0).
525 json rates = json::array();
526 for (const Matrix<T>& D1 : d.sched_D1)
527 rates.push_back(num_traits<T>::to_double(D1(0, 0)));
528 j["params"]["rates"] = rates;
529 return j;
530 }
531 json A = json::array(), B = json::array();
532 for (const Matrix<T>& M : d.sched_D0) A.push_back(mat_to_json(M));
533 for (const Matrix<T>& M : d.sched_D1) B.push_back(mat_to_json(M));
534 // A PHt is STORED converted to (D0, D1) -- see the Distrib header --
535 // so it is written in the MAPt spelling it reloads identically from.
536 j["type"] = "MAPt";
537 j["params"]["D0"] = A;
538 j["params"]["D1"] = B;
539 return j;
540 }
541 case ProcessType::PRIOR:
542 throw UnsupportedError(
543 "network_writer: a Prior is a set of alternative MODELS rather than one law; save "
544 "the design point SolverUQ built, not the design");
545 default:
546 break;
547 }
548 throw UnsupportedError(std::string("network_writer: distribution family '") +
549 lang::process_to_text(d.type) + "' has no model.json record");
550}
551
552/**
553 * The per-class cutoffs a materialized rate table is swept over: a closed
554 * class's population, and 10 for an open one.
555 *
556 * The same rule as `linemodel_save.m:157-163`, and it has to be, or the tables
557 * this writer emits would be keyed over a different lattice than the reference's
558 * for the same model and the two files would not compare.
559 */
560template <class T>
561std::vector<int> lattice_cutoffs(const qn::NetworkStruct<T>& sn) {
562 std::vector<int> cut(sn.classes.size(), 10);
563 for (std::size_t r = 0; r < sn.classes.size(); ++r)
564 if (sn.classes[r].type == lang::JobClassType::CLOSED &&
565 std::isfinite(sn.classes[r].population))
566 cut[r] = static_cast<int>(std::lround(sn.classes[r].population));
567 return cut;
568}
569
570/** Enumerate the box lattice 0 <= n(r) <= cut(r), calling `visit(counts, key)`. */
571template <class F>
572void for_each_lattice_point(const std::vector<int>& cut, const F& visit) {
573 std::size_t total = 1;
574 for (int c : cut) total *= static_cast<std::size_t>(c + 1);
575 const std::size_t K = cut.size();
576 for (std::size_t i = 0; i < total; ++i) {
577 std::vector<int> cnt(K, 0);
578 std::size_t li = i;
579 for (std::size_t d = 0; d < K; ++d) {
580 cnt[d] = static_cast<int>(li % static_cast<std::size_t>(cut[d] + 1));
581 li /= static_cast<std::size_t>(cut[d] + 1);
582 }
583 std::string key;
584 int tot = 0;
585 for (std::size_t d = 0; d < K; ++d) {
586 if (d) key += ',';
587 key += std::to_string(cnt[d]);
588 tot += cnt[d];
589 }
590 if (tot == 0) continue; // the empty state is omitted, as the reference omits it
591 visit(cnt, key);
592 }
593}
594
595} // namespace detail
596
597/**
598 * `qn::NetworkStruct` -> the model.json `model` object.
599 *
600 * Takes the struct rather than the `qn::Network` builder because that is what a
601 * solver holds, and because `get_struct()` is what the builder hands out.
602 */
603template <class T>
605 using detail::json;
606 typedef qn::NetworkStruct<T> SN;
607 const std::size_t K = sn.classes.size();
608 const std::vector<int> cut = detail::lattice_cutoffs(sn);
609
610 json model;
611 model["type"] = "Network";
612 model["name"] = sn.name;
613 // The directory every Logger writes into; model-level because that is where
614 // the reference keeps it and because a Logger's constructor refuses without it.
615 if (!sn.log_path.empty()) model["logPath"] = sn.log_path;
616
617 // -- classes --------------------------------------------------------------
618 json classes = json::array();
619 for (std::size_t r = 0; r < K; ++r) {
620 const qn::JobClass& c = sn.classes[r];
621 json cj;
622 cj["name"] = c.name;
623 // A G-network SIGNAL is written as its own wire type with the kind it
624 // removes under: written back as an ordinary class it becomes inert,
625 // and the reloaded model is a queueing network with no removals at all.
626 const bool sig = r < sn.issignal.size() && sn.issignal[r];
627 if (sig) {
628 cj["type"] = "Signal";
629 cj["openOrClosed"] = c.type == lang::JobClassType::CLOSED ? "Closed" : "Open";
630 const lang::SignalType st = sn.signaltype[r];
631 cj["signalType"] = st == lang::SignalType::REPLY
632 ? "reply"
633 : (st == lang::SignalType::CATASTROPHE ? "catastrophe"
634 : "negative");
635 if (sn.signaltarget[r] >= 1 && sn.signaltarget[r] <= K)
636 cj["targetClass"] = sn.classes[sn.signaltarget[r] - 1].name;
637 const lang::RemovalPolicy rp = sn.signalrempolicy[r];
639 cj["removalPolicy"] = rp == lang::RemovalPolicy::FCFS ? "FCFS" : "LCFS";
640 if (!sn.signalremdist[r].empty()) {
641 json pv = json::array();
642 for (const T& v : sn.signalremdist[r]) pv.push_back(num_traits<T>::to_double(v));
643 json xv = json::array();
644 // THE SUPPORT STARTS AT ZERO, unlike every other pmf written
645 // here: `signalremdist` is indexed BY BATCH SIZE, and a batch of
646 // zero is a real outcome. `discrete_support` numbers items from
647 // 1, so using it shifted every batch by one job on the way back
648 // through any reader that honours `x` -- MATLAB's included.
649 for (std::size_t b = 0; b < pv.size(); ++b) xv.push_back(static_cast<double>(b));
650 json rd;
651 rd["type"] = "DiscreteSampler";
652 rd["params"]["p"] = pv;
653 rd["params"]["x"] = xv;
654 cj["removalDistribution"] = rd;
655 }
656 } else {
657 cj["type"] = c.type == lang::JobClassType::CLOSED
658 ? (c.self_looping ? "SelfLooping" : "Closed")
659 : "Open";
660 }
662 // An integer, as linemodel_save.m and Python's save_model write it; a non-integral value is kept as written.
663 const double pop = c.population;
664 if (std::isfinite(pop) && pop == std::floor(pop) && std::fabs(pop) < 9.0e15)
665 cj["population"] = static_cast<long long>(pop);
666 else
667 cj["population"] = pop;
668 if (c.refstat == 0 || c.refstat > sn.station_to_node.size())
669 throw InputError("network_writer: class '" + c.name +
670 "' names no reference station, which a closed class must have");
671 cj["refNode"] = sn.nodes[sn.station_to_node[c.refstat - 1] - 1].name;
672 }
673 if (c.prio != 0) cj["priority"] = c.prio;
674 if (c.immfeed) cj["immediateFeedback"] = true;
675 if (c.is_ref_class) cj["isReferenceClass"] = true;
676 if (std::isfinite(c.deadline)) cj["deadline"] = c.deadline;
677 if (c.spawn >= 1 && c.spawn <= K) cj["spawnClass"] = sn.classes[c.spawn - 1].name;
678 if (r < sn.syncreply.size() && sn.syncreply[r] >= 1 && sn.syncreply[r] <= K)
679 cj["replySignalClass"] = sn.classes[sn.syncreply[r] - 1].name;
680 classes.push_back(cj);
681 }
682 model["classes"] = classes;
683
684 // -- nodes ----------------------------------------------------------------
685 json nodes = json::array();
686 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
687 const qn::NodeDef& nd = sn.nodes[i];
688 const std::size_t ind = i + 1, ist = nd.station;
689 json nj;
690 nj["name"] = nd.name;
691 // a Queue declared INF keeps its class on the wire, as linemodel_save writes isa(node,'Queue') nodes
692 nj["type"] = nd.queue_object ? "Queue" : detail::node_type_to_json(nd.nodetype);
693
694 if (nd.nodetype == lang::NodeType::Fork && nd.tasks_per_link > 1.0)
695 nj["tasksPerLink"] = nd.tasks_per_link;
696 // VARIABLE FORKING LEVELS, in the record shape `linemodel_save.m:509-553`
697 // writes: one {dest, class, value} per link rather than a matrix, since
698 // that is what the reader replays through the setters.
699 //
700 // Every link a class takes is listed, not only the ones the model
701 // overrode. The reader seeds an untouched link from `tasksPerLink`,
702 // which is the MEAN once an override exists, so a partial list would
703 // reload a fork that emits a count it was never given.
704 if (nd.nodetype == lang::NodeType::Fork) {
705 const qn::ForkParam<T>* fp = sn.fork_param_of(ind);
706 if (fp != 0) {
707 json by_dest = json::array(), by_prob = json::array(), by_dist = json::array();
708 for (std::size_t k = 0; k < fp->fan_out_link.rows(); ++k)
709 for (std::size_t r = 0; r < fp->fan_out_link.cols() && r < K; ++r) {
710 const double p = num_traits<T>::to_double(fp->fan_out_prob(k, r));
711 if (p == 0.0) continue; // link this class does not take
712 json rec;
713 rec["dest"] = sn.nodes[k].name;
714 rec["class"] = r + 1;
715 rec["value"] = num_traits<T>::to_double(fp->fan_out_link(k, r));
716 by_dest.push_back(rec);
717 if (p != 1.0) {
718 json pr;
719 pr["dest"] = sn.nodes[k].name;
720 pr["class"] = r + 1;
721 pr["value"] = p;
722 by_prob.push_back(pr);
723 }
724 const lang::Distrib<T>& d = fp->fan_out_dist[k][r];
725 if (!d.disabled) {
726 json dr;
727 dr["dest"] = sn.nodes[k].name;
728 dr["class"] = r + 1;
729 json pv = json::array(), xv = json::array();
730 for (std::size_t e = 0; e < d.params.size(); ++e) {
731 pv.push_back(num_traits<T>::to_double(d.params[e]));
732 xv.push_back(d.trace.empty()
733 ? static_cast<double>(e + 1)
735 }
736 dr["p"] = pv;
737 dr["x"] = xv;
738 by_dist.push_back(dr);
739 }
740 }
741 if (!by_dest.empty()) nj["fanOutByDest"] = by_dest;
742 if (!by_prob.empty()) nj["fanOutProb"] = by_prob;
743 if (!by_dist.empty()) nj["fanOutDist"] = by_dist;
744 }
745 }
746 // A Logger with no file name exports as a tunnel that logs nowhere, so
747 // the name is what makes the node mean anything on reload.
748 if (nd.nodetype == lang::NodeType::Logger && !nd.logger.file_name.empty())
749 nj["fileName"] = nd.logger.file_name;
750 if (nd.nodetype == lang::NodeType::Join) {
751 typename std::map<std::size_t, std::pair<std::size_t, std::size_t> >::const_iterator fj;
752 for (std::size_t f = 0; f < sn.fj.size(); ++f)
753 if (sn.fj[f].second == ind)
754 nj["forkNode"] = sn.nodes[sn.fj[f].first - 1].name;
755 typename std::map<std::size_t, typename SN::JoinDecl>::const_iterator jd =
756 sn.joindecl.find(ind);
757 if (jd != sn.joindecl.end()) {
758 if (jd->second.strategy == lang::JoinStrategy::PARTIAL)
759 nj["joinStrategy"] = "PARTIAL";
760 if (jd->second.quorum > 0) nj["joinQuorum"] = jd->second.quorum;
761 }
762 }
763 {
764 typename std::map<std::size_t, Matrix<T> >::const_iterator cs = sn.csmatrix.find(ind);
765 if (cs != sn.csmatrix.end()) {
766 json csm;
767 for (std::size_t r = 0; r < K; ++r) {
768 json row;
769 for (std::size_t s = 0; s < K; ++s) {
770 const double v = num_traits<T>::to_double(cs->second(r, s));
771 if (v != 0.0) row[sn.classes[s].name] = v;
772 }
773 if (!row.empty()) csm[sn.classes[r].name] = row;
774 }
775 nj["classSwitchMatrix"] = csm;
776 }
777 }
778 {
779 typename std::map<std::size_t, qn::CacheParam<T> >::const_iterator cp =
780 sn.nodeparam.find(ind);
781 if (cp != sn.nodeparam.end()) {
782 const qn::CacheParam<T>& c = cp->second;
783 nj["numItems"] = c.nitems;
784 nj["itemLevelCap"] = c.itemcap;
785 nj["replacementStrategy"] = detail::replacement_to_json(c.replacestrat);
786 if (num_traits<T>::to_double(c.qlru) != 1.0)
787 nj["admissionProb"] = num_traits<T>::to_double(c.qlru);
788 if (!c.itemsize.empty()) nj["itemSizes"] = c.itemsize;
789 if (!c.costcap.empty()) {
790 if (c.costcapglobal) nj["costCaps"] = c.costcap.empty() ? 0 : c.costcap[0];
791 else nj["costCaps"] = c.costcap;
792 }
793 json pop, hit, miss, itemcls;
794 for (std::size_t r = 0; r < K && r < c.pread.size(); ++r) {
795 if (c.pread[r].empty()) continue;
796 json pj;
797 pj["type"] = "DiscreteSampler";
798 pj["params"]["p"] = detail::vec_to_json(c.pread[r]);
799 pj["params"]["x"] = detail::discrete_support(c.pread[r].size());
800 pop[sn.classes[r].name] = pj;
801 }
802 for (std::size_t r = 0; r < K && r < c.hitclass.size(); ++r)
803 if (c.hitclass[r] != 0)
804 hit[sn.classes[r].name] = sn.classes[c.hitclass[r] - 1].name;
805 for (std::size_t r = 0; r < K && r < c.missclass.size(); ++r)
806 if (c.missclass[r] != 0)
807 miss[sn.classes[r].name] = sn.classes[c.missclass[r] - 1].name;
808 for (std::size_t r = 0; r < K && r < c.classitem.size(); ++r)
809 if (c.classitem[r] != 0)
810 itemcls[sn.classes[r].name] = static_cast<double>(c.classitem[r]);
811 if (!pop.empty()) nj["popularity"] = pop;
812 if (!hit.empty()) nj["hitClass"] = hit;
813 if (!miss.empty()) nj["missClass"] = miss;
814 if (!itemcls.empty()) nj["itemClass"] = itemcls;
815 if (!c.accost.empty()) {
816 json ap = json::array();
817 for (const std::vector<Matrix<T> >& per_class : c.accost) {
818 json row = json::array();
819 for (const Matrix<T>& g : per_class)
820 row.push_back(g.rows() == 0 ? json::array() : detail::mat_to_json(g));
821 ap.push_back(row);
822 }
823 nj["accessProb"] = ap;
824 }
825 if (!c.initstate.empty()) nj["initialState"] = detail::vec_to_json(c.initstate);
826 if (c.retrieval_capacity > 0 || !c.retrieval_queues.empty()) {
827 json rs;
828 rs["capacity"] = c.retrieval_capacity;
829 json by;
830 for (const auto& kv : c.retrieval_queues) {
831 json entry;
832 json qs = json::array();
833 for (std::size_t q : kv.second) qs.push_back(sn.nodes[q - 1].name);
834 entry["queues"] = qs;
835 json items;
836 for (std::size_t it = 0; it < c.retrieval_classes.size(); ++it)
837 if (kv.first < c.retrieval_classes[it].size() &&
838 c.retrieval_classes[it][kv.first] != 0)
839 items[std::to_string(it)] =
840 sn.classes[c.retrieval_classes[it][kv.first] - 1].name;
841 entry["items"] = items;
842 by[sn.classes[kv.first].name] = entry;
843 }
844 rs["byClass"] = by;
845 nj["retrievalSystem"] = rs;
846 }
847 }
848 }
849 {
850 typename std::map<std::size_t, std::vector<T> >::const_iterator im =
851 sn.initmarking.find(ind);
852 if (im != sn.initmarking.end()) nj["initialState"] = detail::vec_to_json(im->second);
853 typename std::map<std::size_t, std::vector<T> >::const_iterator sp =
854 sn.stateprior.find(ind);
855 typename std::map<std::size_t, Matrix<T> >::const_iterator ss = sn.statespace.find(ind);
856 if (sp != sn.stateprior.end() && ss != sn.statespace.end()) {
857 nj["stateSpace"] = detail::mat_to_json(ss->second);
858 nj["statePrior"] = detail::vec_to_json(sp->second);
859 }
860 }
861
862 if (ist != 0) {
863 const qn::Station<T>& st = sn.stations[ist - 1];
866 nj["scheduling"] = detail::sched_to_json(st.sched);
867 // The server count of a multiserver Queue, and of a QUEUEING PLACE
868 // whose embedded queue has more than one: the Place branch of the
869 // reader defaults it to one, so a c > 1 that is not written here is
870 // a c that does not survive the round trip.
872 std::isfinite(st.nservers) && st.nservers > 1.0)
873 nj["servers"] = st.nservers;
874 if (std::isfinite(st.cap) && st.cap > 0) nj["buffer"] = st.cap;
875
876 json svc, cc, dr, sp, imf;
877 for (std::size_t r = 0; r < K; ++r) {
878 if (ist - 1 < sn.service.size() && r < sn.service[ist - 1].size() &&
879 !sn.service[ist - 1][r].disabled)
880 svc[sn.classes[r].name] = detail::dist_to_json(sn.service[ist - 1][r]);
881 if (r < st.classcap.size() && std::isfinite(st.classcap[r]))
882 cc[sn.classes[r].name] = st.classcap[r];
883 // 0 is the "the model declared none" sentinel of the station's
884 // own vector, not a DropStrategy; `refresh_capacity` reads it
885 // as `user_rule` for exactly that reason. Naming it would have
886 // to invent a rule, so the entry is simply not written.
887 if (r < st.droprule.size() && st.droprule[r] != 0)
888 dr[sn.classes[r].name] =
889 detail::drop_to_json(static_cast<lang::DropStrategy>(st.droprule[r]));
890 if (r < st.immfeed.size() && st.immfeed[r]) imf[sn.classes[r].name] = true;
891 }
894 for (std::size_t r = 0; r < K && r < st.schedparam.size(); ++r)
895 sp[sn.classes[r].name] = num_traits<T>::to_double(st.schedparam[r]);
896 if (!svc.empty()) nj["service"] = svc;
897 if (!cc.empty()) nj["classCap"] = cc;
898 if (!dr.empty()) nj["dropRule"] = dr;
899 if (!sp.empty()) nj["schedParams"] = sp;
900 if (!imf.empty()) nj["immediateFeedback"] = imf;
901
902 if (!st.lldscaling.empty()) {
903 json ld;
904 ld["type"] = "loadDependent";
905 ld["scaling"] = detail::vec_to_json(st.lldscaling);
906 nj["loadDependence"] = ld;
907 }
908 // The two rate-scaling handles, materialized over the box lattice:
909 // the only way a function reaches the wire.
910 for (int kind = 0; kind < 2; ++kind) {
911 const lang::CdScaling<T>& fun = kind == 0 ? st.cdscaling : st.jdscaling;
912 if (!static_cast<bool>(fun)) continue;
913 json blk, tbl;
914 blk["type"] = kind == 0 ? "classDependent" : "jointDependent";
915 blk["cutoffs"] = cut;
916 detail::for_each_lattice_point(cut, [&](const std::vector<int>& cnt,
917 const std::string& key) {
918 std::vector<T> n(K, num_traits<T>::from_int(0));
919 for (std::size_t r = 0; r < K; ++r)
920 n[r] = num_traits<T>::from_int(static_cast<long>(cnt[r]));
921 tbl[key] = detail::vec_to_json(fun(n));
922 });
923 blk["scaling"] = tbl;
924 blk["peak"] = detail::vec_to_json(kind == 0 ? st.cdscalingpeak : st.jdscalingpeak);
925 nj[kind == 0 ? "classDependence" : "jointDependence"] = blk;
926 }
927 {
928 typename std::map<std::size_t, typename SN::PasParam>::const_iterator pas =
929 sn.pasparam.find(ist);
930 if (pas != sn.pasparam.end() && static_cast<bool>(pas->second.svc_rate_fun)) {
931 json tbl;
932 detail::for_each_lattice_point(
933 cut, [&](const std::vector<int>& cnt, const std::string& key) {
934 std::vector<std::size_t> micro;
935 for (std::size_t r = 0; r < K; ++r)
936 for (int c = 0; c < cnt[r]; ++c) micro.push_back(r + 1);
937 tbl[key] = num_traits<T>::to_double(pas->second.svc_rate_fun(micro));
938 });
939 nj["oiServiceRate"] = tbl;
940 nj["oiCutoffs"] = cut;
941 bool any_swap = false;
942 for (const std::vector<bool>& row : pas->second.swap_graph)
943 for (bool v : row) any_swap = any_swap || v;
944 if (any_swap) {
945 json sg = json::array();
946 for (const std::vector<bool>& row : pas->second.swap_graph) {
947 json rj = json::array();
948 for (bool v : row) rj.push_back(v ? 1 : 0);
949 sg.push_back(rj);
950 }
951 nj["swapGraph"] = sg;
952 }
953 }
954 }
955 if (!st.polling_type.empty()) {
956 nj["pollingType"] = detail::polling_to_json(st.polling_type[0]);
957 if (st.polling_par != 0) nj["pollingPar"] = st.polling_par;
958 }
959 {
960 // One key, two shapes, as `linemodel_save` writes them: under
961 // POLLING an entry names the buffer the server LEAVES and
962 // carries no `to`; otherwise it names the ordered class PAIR.
963 json so = json::array();
964 for (std::size_t r = 0; r < K && r < st.switchover.size(); ++r) {
965 if (st.switchover[r].disabled) continue;
966 json e;
967 e["from"] = sn.classes[r].name;
968 e["distribution"] = detail::dist_to_json(st.switchover[r]);
969 so.push_back(e);
970 }
971 for (std::size_t r = 0; r < K && r < st.switchover_pair.size(); ++r)
972 for (std::size_t c = 0; c < K && c < st.switchover_pair[r].size(); ++c) {
973 if (st.switchover_pair[r][c].disabled) continue;
974 json e;
975 e["from"] = sn.classes[r].name;
976 e["to"] = sn.classes[c].name;
977 e["distribution"] = detail::dist_to_json(st.switchover_pair[r][c]);
978 so.push_back(e);
979 }
980 if (!so.empty()) nj["switchoverTimes"] = so;
981 }
982 {
983 typename std::map<std::size_t, qn::SetupDelayOffParam<T> >::const_iterator sd =
984 sn.setupparam.find(ist);
985 if (sd != sn.setupparam.end()) {
986 json su, doff;
987 for (std::size_t r = 0; r < K && r < sd->second.setup.size(); ++r) {
988 if (sd->second.setup[r].disabled) continue;
989 su[sn.classes[r].name] = detail::dist_to_json(sd->second.setup[r]);
990 doff[sn.classes[r].name] = detail::dist_to_json(sd->second.delayoff[r]);
991 }
992 if (!su.empty()) {
993 nj["setupTime"] = su;
994 nj["delayOffTime"] = doff;
995 }
996 }
997 }
998 {
999 // Server breakdown, in `linemodel_save`'s shape: the two clocks
1000 // always, and `downService` keyed by class name only where a
1001 // degraded rate was declared. A zero rate is an ABSENT entry and
1002 // not a zero-mean distribution, which has no reading.
1003 typename std::map<std::size_t, qn::BreakdownParam<T> >::const_iterator bd =
1004 sn.breakdownparam.find(ist);
1005 if (bd != sn.breakdownparam.end()) {
1006 json bj;
1007 bj["failure"] = detail::dist_to_json(bd->second.failure);
1008 bj["repair"] = detail::dist_to_json(bd->second.repair);
1009 json ds;
1010 for (std::size_t r = 0; r < K && r < bd->second.down_service_rates.size(); ++r) {
1011 const double rate =
1012 num_traits<T>::to_double(bd->second.down_service_rates[r]);
1013 if (!(rate > 0.0)) continue;
1014 ds[sn.classes[r].name] = detail::dist_to_json(
1016 }
1017 if (!ds.empty()) bj["downService"] = ds;
1018 nj["breakdown"] = bj;
1019 }
1020 }
1021 {
1022 typename std::map<std::size_t, qn::RetrialParam<T> >::const_iterator rt =
1023 sn.retrialparam.find(ist);
1024 if (rt != sn.retrialparam.end()) {
1025 json rj;
1026 for (std::size_t r = 0; r < K && r < rt->second.retrial_proc.size(); ++r) {
1027 if (rt->second.retrial_proc[r].disabled) continue;
1028 json e;
1029 e["delay"] = detail::dist_to_json(rt->second.retrial_proc[r]);
1030 e["maxAttempts"] = rt->second.max_attempts[r];
1031 rj[sn.classes[r].name] = e;
1032 }
1033 if (!rj.empty()) nj["retrial"] = rj;
1034 }
1035 }
1036 {
1037 json pat, orb, brp, blk;
1038 for (std::size_t r = 0; r < K; ++r) {
1039 if (r < st.patience.size() && !st.patience[r].disabled) {
1040 json e;
1041 e["distribution"] = detail::dist_to_json(st.patience[r]);
1042 if (r < st.impatience.size() &&
1044 e["impatienceType"] = detail::impatience_to_json(st.impatience[r]);
1045 pat[sn.classes[r].name] = e;
1046 }
1047 if (r < st.orbit_impatience.size() && !st.orbit_impatience[r].disabled)
1048 orb[sn.classes[r].name] = detail::dist_to_json(st.orbit_impatience[r]);
1049 if (r < st.batch_reject.size() &&
1051 brp[sn.classes[r].name] = num_traits<T>::to_double(st.batch_reject[r]);
1052 if (r < st.balking.size() &&
1053 st.balking[r].strategy != lang::BalkingStrategy::NONE) {
1054 json e;
1055 e["strategy"] = detail::balking_to_json(st.balking[r].strategy);
1056 json ths = json::array();
1057 for (const typename qn::Station<T>::BalkingThreshold& th :
1058 st.balking[r].thresholds) {
1059 json tj;
1060 tj["minJobs"] = th.min_jobs;
1061 tj["maxJobs"] = th.max_jobs;
1062 tj["probability"] = num_traits<T>::to_double(th.probability);
1063 ths.push_back(tj);
1064 }
1065 e["thresholds"] = ths;
1066 blk[sn.classes[r].name] = e;
1067 }
1068 }
1069 if (!pat.empty()) nj["patience"] = pat;
1070 if (!orb.empty()) nj["orbitImpatience"] = orb;
1071 if (!brp.empty()) nj["batchRejectProb"] = brp;
1072 if (!blk.empty()) nj["balking"] = blk;
1073 }
1074 if (!st.server_types.empty()) {
1075 json sts = json::array();
1076 for (const typename qn::Station<T>::ServerType& t : st.server_types) {
1077 json tj;
1078 tj["name"] = t.name;
1079 tj["count"] = t.count;
1080 if (!t.compatible.empty()) {
1081 json cn = json::array();
1082 for (std::size_t r = 0; r < K && r < t.compatible.size(); ++r)
1083 if (t.compatible[r]) cn.push_back(sn.classes[r].name);
1084 tj["compatibleClasses"] = cn;
1085 }
1086 json sv;
1087 for (std::size_t r = 0; r < K && r < t.service.size(); ++r)
1088 if (!t.service[r].disabled)
1089 sv[sn.classes[r].name] = detail::dist_to_json(t.service[r]);
1090 if (!sv.empty()) tj["service"] = sv;
1091 sts.push_back(tj);
1092 }
1093 nj["serverTypes"] = sts;
1095 nj["heteroSchedPolicy"] = detail::hetero_to_json(st.hetero_policy);
1096 }
1097 {
1098 json par;
1099 for (std::size_t r = 0; r < K && r < st.server_parallelism.size(); ++r)
1100 if (st.server_parallelism[r] > 1)
1101 par[sn.classes[r].name] = st.server_parallelism[r];
1102 if (!par.empty()) nj["serverParallelism"] = par;
1103 }
1104 {
1105 json ab;
1106 for (std::size_t r = 0; r < K && r < st.arrival_batch.size(); ++r)
1107 if (!st.arrival_batch[r].disabled)
1108 ab[sn.classes[r].name] = detail::dist_to_json(st.arrival_batch[r]);
1109 if (!ab.empty()) nj["arrivalBatch"] = ab;
1110 if (!st.marked_classes.empty()) {
1111 json mc = json::array();
1112 for (std::size_t c : st.marked_classes) mc.push_back(sn.classes[c - 1].name);
1113 nj["markedClasses"] = mc;
1114 }
1115 json dd;
1116 for (std::size_t r = 0; r < K && r < st.departure_discipline.size(); ++r)
1118 dd[sn.classes[r].name] = "FIFO";
1119 if (!dd.empty()) nj["departureDiscipline"] = dd;
1120 }
1121 }
1122
1123 {
1124 typename std::map<std::size_t, qn::TransitionParam<T> >::const_iterator tp =
1125 sn.transparam.find(ind);
1126 if (tp != sn.transparam.end()) {
1127 json modes = json::array();
1128 for (std::size_t m = 0; m < tp->second.nmodes; ++m) {
1129 json mj;
1130 mj["name"] = tp->second.modenames[m];
1131 mj["timingStrategy"] =
1132 tp->second.timing[m] == lang::TimingStrategy::IMMEDIATE ? "IMMEDIATE"
1133 : "TIMED";
1134 if (!tp->second.firingproc[m].disabled)
1135 mj["distribution"] = detail::dist_to_json(tp->second.firingproc[m]);
1136 mj["numServers"] = tp->second.nmodeservers[m];
1137 mj["firingPriority"] = tp->second.firingprio[m];
1138 mj["firingWeight"] = num_traits<T>::to_double(tp->second.fireweight[m]);
1139 const char* kArcKey[3] = {"enablingConditions", "inhibitingConditions",
1140 "firingOutcomes"};
1141 for (int which = 0; which < 3; ++which) {
1142 const Matrix<T>& row = which == 0 ? tp->second.enabling[m]
1143 : which == 1 ? tp->second.inhibiting[m]
1144 : tp->second.firing[m];
1145 json arcs = json::array();
1146 for (std::size_t q = 0; q < row.rows(); ++q)
1147 for (std::size_t r = 0; r < row.cols(); ++r) {
1148 const double v = num_traits<T>::to_double(row(q, r));
1149 // An inhibiting threshold of Inf is "never
1150 // blocks" and an enabling or firing count of 0
1151 // is no arc.
1152 if (which == 1 ? !std::isfinite(v) : v == 0.0) continue;
1153 json a;
1154 a["node"] = sn.nodes[q].name;
1155 // REQUIRED by the reference readers: both index
1156 // the arc by (node, CLASS) -- `linemodel_load.m:
1157 // 852` reads `ec.class` unguarded and
1158 // `linemodel_io.py` does `ic["class"]` -- so an
1159 // arc written without it makes the whole
1160 // document unloadable there.
1161 a["class"] = sn.classes[r].name;
1162 a["count"] = v;
1163 arcs.push_back(a);
1164 }
1165 if (!arcs.empty()) mj[kArcKey[which]] = arcs;
1166 }
1167 // A marking-dependent firing rate is a CLOSURE, so it is
1168 // written the only way it can cross JSON: materialized over
1169 // the box lattice of the enabling slots, exactly as
1170 // `firingdep_scaling_table` in linemodel_save.m does. Dropped
1171 // instead, the net reloads at its nominal rate -- a different
1172 // marking process, reported without a diagnostic.
1173 if (m < tp->second.firingdep.size() && tp->second.firingdep[m]) {
1174 const Matrix<T>& enab = tp->second.enabling[m];
1175 json slots = json::array();
1176 std::vector<std::size_t> slot_node;
1177 std::vector<long> caps;
1178 // ONE SLOT PER PLACE, not per (place, class): the
1179 // closure's own domain is the node-indexed marking that
1180 // `state_events.h` hands it, and the reader resolves a
1181 // slot by node. The class is the first the mode reads
1182 // there, which is the only one a single-class arc set
1183 // can name and is what labels the slot for the
1184 // reference readers.
1185 for (std::size_t q = 0; q < enab.rows(); ++q) {
1186 std::size_t rq = enab.cols();
1187 for (std::size_t r = 0; r < enab.cols(); ++r)
1188 if (num_traits<T>::to_double(enab(q, r)) > 0.0) { rq = r; break; }
1189 if (rq == enab.cols()) continue;
1190 json sm;
1191 sm["node"] = sn.nodes[q].name;
1192 sm["class"] = sn.classes[rq].name;
1193 slots.push_back(sm);
1194 slot_node.push_back(q);
1195 // The open-place saturation cutoff of the three
1196 // reference writers: an unbounded place would make
1197 // the lattice infinite, so it is tabulated to 10.
1198 const std::size_t sq = sn.nodes[q].station;
1199 const double pc = sq == 0 ? std::numeric_limits<double>::infinity()
1200 : sn.stations[sq - 1].cap;
1201 caps.push_back(std::isfinite(pc) ? std::lround(pc) : 10L);
1202 }
1203 if (!slots.empty()) {
1204 json frm;
1205 frm["slots"] = slots;
1206 frm["cutoffs"] = caps;
1207 json scaling;
1208 std::size_t total = 1;
1209 for (std::size_t s = 0; s < caps.size(); ++s)
1210 total *= static_cast<std::size_t>(caps[s]) + 1;
1211 std::vector<T> mk(sn.nodes.size(), num_traits<T>::from_int(0));
1212 for (std::size_t li = 0; li < total; ++li) {
1213 std::size_t rem = li;
1214 std::string key;
1215 for (std::size_t s = 0; s < caps.size(); ++s) {
1216 const std::size_t shp = static_cast<std::size_t>(caps[s]) + 1;
1217 const std::size_t c = rem % shp;
1218 rem /= shp;
1219 mk[slot_node[s]] = num_traits<T>::from_int(
1220 static_cast<long>(c));
1221 if (s) key += ',';
1222 key += std::to_string(c);
1223 }
1224 const double v =
1225 num_traits<T>::to_double(tp->second.firingdep[m](mk));
1226 scaling[key] = std::isfinite(v) ? v : 0.0;
1227 }
1228 for (std::size_t s = 0; s < slot_node.size(); ++s)
1229 mk[slot_node[s]] = num_traits<T>::from_int(0);
1230 frm["scaling"] = scaling;
1231 mj["firingRateDependence"] = frm;
1232 }
1233 }
1234 modes.push_back(mj);
1235 }
1236 nj["modes"] = modes;
1237 }
1238 }
1239 nodes.push_back(nj);
1240 }
1241 model["nodes"] = nodes;
1242
1243 // -- routing --------------------------------------------------------------
1244 json matrix;
1245 for (const auto& kv : sn.P) {
1246 const std::size_t r = kv.first.first, s = kv.first.second;
1247 json from_to;
1248 for (std::size_t a = 0; a < kv.second.rows(); ++a) {
1249 json row;
1250 for (std::size_t b = 0; b < kv.second.cols(); ++b) {
1251 const double p = num_traits<T>::to_double(kv.second(a, b));
1252 if (p != 0.0) row[sn.nodes[b].name] = p;
1253 }
1254 if (!row.empty()) from_to[sn.nodes[a].name] = row;
1255 }
1256 if (!from_to.empty())
1257 matrix[sn.classes[r - 1].name + "," + sn.classes[s - 1].name] = from_to;
1258 }
1259 json routing;
1260 routing["type"] = "matrix";
1261 routing["matrix"] = matrix;
1262 model["routing"] = routing;
1263
1264 json strategies, weights, params;
1265 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
1266 const qn::NodeDef& nd = sn.nodes[i];
1267 json per_class, per_class_w, per_class_p;
1268 for (std::size_t r = 0; r < K && r < nd.routing.size(); ++r) {
1269 // PROB is the matrix itself; RAND is DECLARED not derived, and dropping it wrote JMT's Empirical strategy where the model asks for Random.
1271 per_class[sn.classes[r].name] = detail::routing_to_json(nd.routing[r]);
1272 if (r < nd.routing_weights.size() && !nd.routing_weights[r].empty()) {
1273 json dw;
1274 for (const auto& kv : nd.routing_weights[r]) dw[sn.nodes[kv.first - 1].name] = kv.second;
1275 per_class_w[sn.classes[r].name] = dw;
1276 }
1277 if (r < nd.routing_param.size() && nd.routing_param[r] != 0) {
1278 json pj;
1279 pj["d"] = nd.routing_param[r];
1280 per_class_p[sn.classes[r].name] = pj;
1281 }
1282 }
1283 if (!per_class.empty()) strategies[nd.name] = per_class;
1284 if (!per_class_w.empty()) weights[nd.name] = per_class_w;
1285 if (!per_class_p.empty()) params[nd.name] = per_class_p;
1286 }
1287 if (!strategies.empty()) model["routingStrategies"] = strategies;
1288 if (!weights.empty()) model["routingWeights"] = weights;
1289 if (!params.empty()) model["routingParams"] = params;
1290
1291 // Krzesinski state-dependent routing. Every centre travels by NODE NAME, so
1292 // the block is language independent and a node reordering on either side
1293 // cannot shift a centre. Branch index 1 denotes the complement M-V and is
1294 // written as an empty list, keeping the paper's own numbering.
1295 // See _kb/16-state-dependent-routing.md
1296 if (!sn.sdr_nodes.empty()) {
1297 const pfqn::SdrStruct& sd = sn.sdr_nodes; // 0-based NODE indices
1298 std::size_t cls = 0;
1299 for (std::size_t r = 0; r < K && cls == 0; ++r)
1300 if (sn.nodes[sd.entry].routing.size() > r &&
1301 sn.nodes[sd.entry].routing[r] == lang::RoutingStrategy::SDR)
1302 cls = r + 1;
1303 if (cls == 0)
1304 throw InputError("network_writer: the model declares state-dependent routing at node '" +
1305 sn.nodes[sd.entry].name + "' but no class routes SDR there");
1306 json sdr;
1307 sdr["entry"] = sn.nodes[sd.entry].name;
1308 sdr["departure"] = sn.nodes[sd.departure].name;
1309 sdr["class"] = sn.classes[cls - 1].name;
1310 json branches = json::array();
1311 for (std::size_t b = 0; b < sd.branch.size(); ++b) {
1312 json bn = json::array();
1313 for (std::size_t q = 0; q < sd.branch[b].size(); ++q)
1314 bn.push_back(sn.nodes[sd.branch[b][q]].name);
1315 branches.push_back(bn);
1316 }
1317 sdr["branches"] = branches;
1318 json level = json::array(), Cs = json::array(), dm = json::array();
1319 for (std::size_t b = 0; b < sd.level.size(); ++b)
1320 level.push_back(static_cast<double>(sd.level[b]));
1321 for (std::size_t t = 0; t < sd.C.size(); ++t) Cs.push_back(sd.C[t]);
1322 for (std::size_t t = 0; t < sd.d.rows(); ++t) {
1323 json row = json::array();
1324 for (std::size_t b = 0; b < sd.d.cols(); ++b) row.push_back(sd.d(t, b));
1325 dm.push_back(row);
1326 }
1327 sdr["level"] = level;
1328 sdr["C"] = Cs;
1329 sdr["d"] = dm;
1330 model["stateDepRouting"] = sdr;
1331 }
1332
1333 // -- finite capacity regions ---------------------------------------------
1334 if (!sn.regions.empty()) {
1335 json fcr = json::array();
1336 for (std::size_t g = 0; g < sn.regions.size(); ++g) {
1337 const typename SN::Region& rg = sn.regions[g];
1338 json rj;
1339 rj["name"] = rg.name.empty() ? "Region" + std::to_string(g + 1) : rg.name;
1340 json stations = json::array();
1341 double global = -1.0, globalmem = -1.0;
1342 json class_cap, class_size, class_weight;
1343 for (std::size_t m = 0; m < rg.members.size(); ++m) {
1344 if (!rg.members[m]) continue;
1345 json sj;
1346 sj["node"] = sn.nodes[sn.station_to_node[m] - 1].name;
1347 stations.push_back(sj);
1348 global = rg.cap[m][K];
1349 // The region's global MEMORY budget, which is enforced beside the
1350 // job cap by the CTMC, the loss-network NC arm and the JMT export
1351 // rather than folded into it. Omitted, the region reloads with
1352 // unbounded memory and every one of those three drops a
1353 // constraint the model declared.
1354 if (m < rg.maxmem.size()) globalmem = rg.maxmem[m];
1355 for (std::size_t r = 0; r < K; ++r)
1356 if (rg.cap[m][r] != -1.0) class_cap[sn.classes[r].name] = rg.cap[m][r];
1357 }
1358 rj["stations"] = stations;
1359 if (global != -1.0) rj["globalMaxJobs"] = global;
1360 if (globalmem != -1.0) rj["globalMaxMemory"] = globalmem;
1361 if (!class_cap.empty()) rj["classMaxJobs"] = class_cap;
1362 json rule;
1363 for (std::size_t r = 0; r < K && r < rg.rule.size(); ++r)
1364 rule[sn.classes[r].name] = detail::drop_to_json(rg.rule[r]);
1365 rj["dropRule"] = rule;
1366 for (std::size_t r = 0; r < K; ++r) {
1367 if (r < rg.size.size() && num_traits<T>::to_double(rg.size[r]) != 1.0)
1368 class_size[sn.classes[r].name] = num_traits<T>::to_double(rg.size[r]);
1369 if (r < rg.weight.size() && num_traits<T>::to_double(rg.weight[r]) != 1.0)
1370 class_weight[sn.classes[r].name] = num_traits<T>::to_double(rg.weight[r]);
1371 }
1372 // classSize and classWeight are per-STATION on the wire, and the
1373 // struct holds one vector per region, so they are written onto every
1374 // member station -- which is the same model the reader rebuilds.
1375 if (!class_size.empty() || !class_weight.empty())
1376 for (json& sj : rj["stations"]) {
1377 if (!class_size.empty()) sj["classSize"] = class_size;
1378 if (!class_weight.empty()) sj["classWeight"] = class_weight;
1379 }
1380 if (rg.lincon_A.rows() > 0) {
1381 rj["constraintA"] = detail::mat_to_json(rg.lincon_A);
1382 rj["constraintB"] = detail::vec_to_json(rg.lincon_b);
1383 }
1384 fcr.push_back(rj);
1385 }
1386 model["finiteCapacityRegions"] = fcr;
1387 }
1388
1389 // -- global (Whittle) dependence -----------------------------------------
1390 //
1391 // phi(n) reads the FULL population matrix, so unlike the per-station
1392 // classDependence/jointDependence blocks it is materialized over the lattice
1393 // of the WHOLE network state. Only the (station,class) SLOTS a class can
1394 // actually occupy carry a coordinate: a Source holds no jobs and a class with
1395 // zero per-class capacity at a station never appears there. That restriction
1396 // is lossless, since no DEP or PHASE event ever fires at such a slot.
1397 if (static_cast<bool>(sn.gdscaling)) {
1398 const std::size_t M = sn.nstations, K = sn.nclasses;
1399 const std::vector<double> njobs = sn.njobs();
1400 const int wcut = sn.gdscalingcutoff;
1401 std::vector<std::size_t> slot_st, slot_cl;
1402 std::vector<int> cuts;
1403 for (std::size_t i = 0; i < M; ++i) {
1404 if (sn.nodes[sn.station_to_node[i] - 1].nodetype == lang::NodeType::Source) continue;
1405 for (std::size_t r = 0; r < K; ++r) {
1406 const double cap = sn.classcap[i][r];
1407 if (!(cap > 0)) continue;
1408 int c = std::isfinite(njobs[r]) ? static_cast<int>(std::lround(njobs[r])) : wcut;
1409 if (std::isfinite(cap)) c = std::min(c, static_cast<int>(std::lround(cap)));
1410 slot_st.push_back(i);
1411 slot_cl.push_back(r);
1412 cuts.push_back(c > 0 ? c : 0);
1413 }
1414 }
1415 const std::size_t P = cuts.size();
1416 std::size_t total = 1;
1417 for (std::size_t d = 0; d < P; ++d) {
1418 total *= static_cast<std::size_t>(cuts[d] + 1);
1419 if (total > 200000u)
1420 throw InputError(
1421 "the global dependence lattice exceeds the wire limit of 200000 points; lower "
1422 "the wireCutoff argument of set_global_dependence, or solve the model "
1423 "natively");
1424 }
1425
1426 json blk, slots, tbl;
1427 blk["type"] = "globalDependent";
1428 std::vector<std::string> station_names(M), class_names(K);
1429 for (std::size_t i = 0; i < M; ++i)
1430 station_names[i] = sn.nodes[sn.station_to_node[i] - 1].name;
1431 for (std::size_t r = 0; r < K; ++r) class_names[r] = sn.classes[r].name;
1432 blk["stations"] = station_names;
1433 blk["classes"] = class_names;
1434 slots = json::array();
1435 for (std::size_t d = 0; d < P; ++d) {
1436 json sm;
1437 sm["station"] = station_names[slot_st[d]];
1438 sm["class"] = class_names[slot_cl[d]];
1439 slots.push_back(sm);
1440 }
1441 blk["slots"] = slots;
1442 blk["cutoffs"] = cuts;
1443 blk["cutoff"] = wcut;
1444
1445 for (std::size_t li = 0; li < total; ++li) {
1446 std::size_t rem = li;
1447 std::vector<int> cnt(P, 0);
1448 for (std::size_t d = 0; d < P; ++d) {
1449 cnt[d] = static_cast<int>(rem % static_cast<std::size_t>(cuts[d] + 1));
1450 rem /= static_cast<std::size_t>(cuts[d] + 1);
1451 }
1452 std::vector<T> n(M * K, num_traits<T>::from_int(0));
1453 for (std::size_t d = 0; d < P; ++d)
1454 n[slot_st[d] * K + slot_cl[d]] = num_traits<T>::from_int(cnt[d]);
1455 const std::vector<T> v = sn.gdscaling(n);
1456 std::vector<double> flat(M * K, 1.0);
1457 for (std::size_t i = 0; i < M; ++i)
1458 for (std::size_t r = 0; r < K; ++r) {
1459 double x;
1460 if (v.size() == 1) x = num_traits<T>::to_double(v[0]);
1461 else if (v.size() == M) x = num_traits<T>::to_double(v[i]);
1462 else x = num_traits<T>::to_double(v[i * K + r]);
1463 flat[i * K + r] = std::isfinite(x) ? x : 0.0;
1464 }
1465 std::string key;
1466 if (P == 0) key = "0";
1467 else
1468 for (std::size_t d = 0; d < P; ++d) {
1469 if (d) key += ',';
1470 key += std::to_string(cnt[d]);
1471 }
1472 tbl[key] = flat;
1473 }
1474 blk["scaling"] = tbl;
1475 std::vector<double> pk(M * K, 1.0);
1476 for (std::size_t j = 0; j < pk.size() && j < sn.gdscalingpeak.size(); ++j)
1477 pk[j] = num_traits<T>::to_double(sn.gdscalingpeak[j]);
1478 blk["peak"] = pk;
1479 model["globalDependence"] = blk;
1480 }
1481
1482 // -- rewards --------------------------------------------------------------
1483 //
1484 // The DECLARATIVE ones only. A reward built from a lambda carries no `kind`
1485 // and is dropped here, exactly as `linemodel_save` drops it: any record
1486 // written for it would reload as a different function.
1487 {
1488 // IN NAME ORDER, which is the cross-codebase convention and not a
1489 // cosmetic choice: `rewards2json` sorts for it explicitly, because the
1490 // JAR holds the rewards in a HashMap and has no insertion order to
1491 // preserve. Emitting them in struct order made the same model produce a
1492 // different document here than in the other three, which the JSON arm of
1493 // the parity harness reports as a difference in the model.
1494 std::map<std::string, json> by_name;
1495 for (const typename SN::Reward& rw : sn.reward) {
1496 if (rw.kind.empty() || rw.node == 0) continue;
1497 json rj;
1498 rj["name"] = rw.name;
1499 rj["type"] = rw.kind;
1500 rj["node"] = sn.nodes[rw.node - 1].name;
1501 if (rw.cls != 0) rj["class"] = sn.classes[rw.cls - 1].name;
1502 by_name[rw.name] = rj;
1503 }
1504 json rewards = json::array();
1505 for (const std::pair<const std::string, json>& kv : by_name) rewards.push_back(kv.second);
1506 if (!rewards.empty()) model["rewards"] = rewards;
1507 }
1508 return model;
1509}
1510
1511/** The complete model.json envelope: `{format, version, model}`. */
1512template <class T>
1514 detail::json root;
1515 root["format"] = "line-model";
1516 root["version"] = "1.0";
1517 root["model"] = network_to_json(sn);
1518 detail::wire_nonfinite(root);
1519 return root;
1520}
1521
1522/** Write a model.json file, indented as the reference writers indent it. */
1523template <class T>
1524void write_network_json(const qn::NetworkStruct<T>& sn, const std::string& path) {
1525 std::ofstream out(path.c_str());
1526 if (!out) throw InputError("network_writer: cannot open " + path + " for writing");
1527 out << network_json_envelope(sn).dump(2) << "\n";
1528}
1529
1530} // namespace io
1531} // namespace line
1532
1533#endif // LINE_IO_NETWORK_WRITER_H
InputError(const std::string &what)
Definition error.h:39
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
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
detail::json network_to_json(const qn::NetworkStruct< T > &sn)
qn::NetworkStruct -> the model.json model object.
detail::json network_json_envelope(const qn::NetworkStruct< T > &sn)
The complete model.json envelope: {format, version, model}.
void write_network_json(const qn::NetworkStruct< T > &sn, const std::string &path)
Write a model.json file, indented as the reference writers indent it.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:426
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:365
BalkingStrategy
Balking rules, with the values of MATLAB BalkingStrategy.
Definition lang_types.h:447
SignalType
G-network signal classes, with the values of MATLAB SignalType.
Definition lang_types.h:167
@ REPLY
completes a synchronous call, releasing a held server
Definition lang_types.h:168
@ CATASTROPHE
removes EVERY job at the station
Definition lang_types.h:170
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
@ SDR
Krzesinski (1987) product-form state-dependent routing.
Definition lang_types.h:400
RemovalPolicy
Which job a negative signal removes, with the values of MATLAB RemovalPolicy.
Definition lang_types.h:174
@ FCFS
the oldest waiting job; servers only once nobody waits
Definition lang_types.h:176
@ RANDOM
uniform over waiting AND in-service jobs
Definition lang_types.h:175
HeteroSchedPolicy
How a heterogeneous station picks among its server types, MATLAB HeteroSchedPolicy.
Definition lang_types.h:453
PollingType
Polling service disciplines, with the values of MATLAB PollingType.
Definition lang_types.h:372
@ KLIMITED
serve at most K per visit (K in pollingPar)
Definition lang_types.h:375
@ EXHAUSTIVE
serve until the queue empties
Definition lang_types.h:374
@ GATED
serve exactly the jobs present at the polling instant
Definition lang_types.h:373
@ DECREMENTING
serve until the queue is one shorter than at arrival
Definition lang_types.h:376
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:485
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:596
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:731
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:380
@ HLRU
h-LRU / LRU(m): h lists, promote i -> i+1 on a hit
Definition lang_types.h:385
@ CLIMB
move up one position on a hit (transposition rule)
Definition lang_types.h:386
@ QLRU
q-LRU: LRU with probabilistic admission on a miss
Definition lang_types.h:387
@ LRU
least recently used
Definition lang_types.h:384
@ FIFO
first in, first out
Definition lang_types.h:382
ImpatienceType
Impatience kinds, with the values of MATLAB ImpatienceType.
Definition lang_types.h:444
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Reader for the LINE model.json interchange (a Network model) into a qn::Network<T> built through the ...
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
static Distrib exp_rate(const T &r)
Definition lang_types.h:945
std::vector< T > params
Constructor arguments, in MATLAB getParam order.
Definition lang_types.h:826
std::vector< T > trace
Replayer / Trace samples; empty for every other type.
Definition lang_types.h:828
Topology and coefficients of a state-dependent routing subnetwork.
Definition pfqn_sdr.h:59
std::vector< std::size_t > level
level[b] is the unique t with B_b in V_t - V_{t+1}; level[0] is unused.
Definition pfqn_sdr.h:71
Matrix< double > d
Coefficients d_tb of eq.
Definition pfqn_sdr.h:75
std::vector< double > C
Coefficients C_t of eq.
Definition pfqn_sdr.h:73
std::size_t departure
Departure centre d of Q(V,V); may equal entry.
Definition pfqn_sdr.h:63
std::vector< std::vector< std::size_t > > branch
branch[b] holds the centres of branch b, b >= 1; branch[0] is unused.
Definition pfqn_sdr.h:65
std::size_t entry
Entry centre e of Q(V,V).
Definition pfqn_sdr.h:61
T qlru
Delayed-hit retrieval system (Cache.setRetrievalSystem).
std::vector< T > initstate
The DECLARED initial contents of the cache, as the reference dumps the node's state row: the per-clas...
std::vector< int > itemsize
Per-item storage cost (size) and per-list cap on the total cost of the resident items (ton21cache Sec...
std::vector< int > costcap
std::map< std::size_t, std::vector< std::size_t > > retrieval_queues
read class(0-based)->nodes
std::vector< std::vector< Matrix< T > > > accost
(u) x (n) of (h+1)x(h+1), or empty
std::vector< std::vector< std::size_t > > retrieval_classes
(nitems x nclasses), 1-based
std::vector< int > itemcap
std::vector< std::size_t > missclass
std::vector< std::size_t > hitclass
lang::ReplacementStrategy replacestrat
std::vector< std::size_t > classitem
Item read by each per-item class of a cache network (MATLAB Cache.setItemReadClasses,...
std::vector< std::vector< T > > pread
(u) x (n), empty row = NaN
Variable forking levels, the twin of MATLAB sn.nodeparam{f}.fanOutLink / .fanOutProb / ....
std::vector< std::vector< lang::Distrib< T > > > fan_out_dist
One job class of the network.
std::size_t refstat
1-based reference station
double deadline
sn.classdeadline(r): the soft deadline EDD and EDF order by, and the tardiness JMT reports.
double population
infinite for an open class
bool immfeed
Class-level immediate feedback, ORed with the station's own setting into sn.immfeed.
std::size_t spawn
sn.classspawn(r): the 1-based class injected at the SAME station on every completion of this class,...
bool self_looping
A SelfLoopingClass: a closed class that perpetually cycles at its reference station.
bool is_ref_class
marks the chain's reference class
std::string file_name
base name, no directory
A node of the network.
std::vector< int > routing_param
The scalar parameter of a parameterized dispatcher, per class: the d of a power-of-d (SQ) choice.
std::vector< std::map< std::size_t, double > > routing_weights
The per-destination weights of a WRROBIN dispatcher, per class: a map from 1-based destination NODE i...
bool queue_object
Declared as a Queue although its INF discipline makes nodetype Delay (Network::add_queue).
std::vector< RoutingStrategy > routing
sn.routing, per class.
double tasks_per_link
Fork.output.tasksPerLink == MATLAB sn.nodeparam{f}.fanOut: how many tasks a fork emits per outgoing l...
One balking threshold: with min_jobs <= n <= max_jobs at the station, an arriving job of the class re...
A heterogeneous server pool: count servers that serve only compatible classes, each with its own serv...
std::vector< bool > compatible
per class; empty = every class
std::vector< Distrib< T > > service
per class
One station of the network.
std::vector< Distrib< T > > orbit_impatience
Queue.setOrbitImpatience(class, dist): abandonment from the RETRIAL ORBIT, which is a different popul...
double cap
Station capacity in Kendall's K, as setCapacity sets it.
std::vector< T > jdscalingpeak
sn.jdscalingpeak for this station: the declared peak joint-dependent scaling per class.
std::vector< BalkingParam > balking
std::vector< T > batch_reject
Queue.setBatchRejectProbability: per-class rejection of a whole batch.
std::vector< Distrib< T > > patience
Queue.setPatience(class, dist): the abandonment timer of a WAITING job, with impatience[r] naming whi...
std::vector< std::size_t > server_parallelism
Queue.setServerParallelism(class, n): the servers a job seizes for the whole of its service,...
std::vector< int > droprule
Per-class blocking rule as an INT, with 0 meaning "not set".
SchedStrategy sched
std::vector< std::vector< Distrib< T > > > switchover_pair
Queue.setSwitchover(fromClass, toClass, distrib): the walk the server takes when it turns from servin...
double nservers
may be infinite (a Delay, or an inf-scheduled task)
std::vector< lang::PollingType > polling_type
Polling parameters for a POLLING station, MATLAB's pollingType, switchoverTime and pollingPar on the ...
CdScaling< T > jdscaling
sn.jdscaling for this station: MATLAB's Station.ljdScaling, the JOINT dependence map eta_i(n),...
std::vector< T > cdscalingpeak
sn.cdscalingpeak for this station: the DECLARED peak rate scaling per class, empty when the station i...
std::vector< std::size_t > marked_classes
Source.markedClasses: the 1-based class of each mark of an MMAP arrival.
std::vector< T > lldscaling
sn.lldscaling for this station: the multiplier at population 1, 2, ... Empty when the station is not ...
std::vector< lang::ImpatienceType > impatience
std::vector< T > schedparam
sn.schedparam, per class: the DPS / GPS weight, or the SEPT / LEPT rank.
lang::HeteroSchedPolicy hetero_policy
CdScaling< T > cdscaling
sn.cdscaling for this station: the class-dependence map, empty when unset.
std::vector< double > classcap
Per-class buffer from setChainCapacity; infinite where unset.
std::vector< lang::DepartureDiscipline > departure_discipline
Place.departureDiscipline, per class.
std::vector< Distrib< T > > arrival_batch
Source.setArrivalBatch(class, dist): the batch-size law released at each arrival epoch.
std::vector< Distrib< T > > switchover
std::vector< bool > immfeed
Node-level immediate feedback, per class; empty when the station sets none.
std::vector< ServerType > server_types