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 <algorithm>
39#include <cmath>
40#include <fstream>
41#include <functional>
42#include <limits>
43#include <map>
44#include <string>
45#include <vector>
46
51#include "line/num/number.h"
52#include "line/util/error.h"
53
54namespace line {
55namespace io {
56
57namespace detail {
58
59/** The model.json `type` of a node kind. */
60inline const char* node_type_to_json(lang::NodeType t) {
61 switch (t) {
62 case lang::NodeType::Queue: return "Queue";
63 case lang::NodeType::Source: return "Source";
64 case lang::NodeType::Delay: return "Delay";
65 case lang::NodeType::ClassSwitch: return "ClassSwitch";
66 case lang::NodeType::Logger: return "Logger";
67 case lang::NodeType::Cache: return "Cache";
68 case lang::NodeType::Router: return "Router";
69 case lang::NodeType::Fork: return "Fork";
70 case lang::NodeType::Place: return "Place";
71 case lang::NodeType::Transition: return "Transition";
72 case lang::NodeType::Region: return "Region";
73 case lang::NodeType::Join: return "Join";
74 case lang::NodeType::Sink: return "Sink";
75 }
76 throw UnsupportedError(ErrorKind{"Export"}, "network_writer: unnamed node type");
77}
78
79inline const char* drop_to_json(lang::DropStrategy d) {
80 switch (d) {
81 case lang::DropStrategy::WAITQ: return "waitingQueue";
82 case lang::DropStrategy::DROP: return "drop";
83 case lang::DropStrategy::BAS: return "blockingAfterService";
84 case lang::DropStrategy::BBS: return "blockingBeforeService";
85 case lang::DropStrategy::RSRD: return "resamplingRepetitiveService";
86 case lang::DropStrategy::RETRIAL: return "retrial";
87 case lang::DropStrategy::RETRIAL_WITH_LIMIT: return "retrialWithLimit";
88 }
89 throw UnsupportedError(ErrorKind{"Export"}, "network_writer: unnamed drop strategy");
90}
91
92inline const char* replacement_to_json(lang::ReplacementStrategy r) {
93 switch (r) {
94 case lang::ReplacementStrategy::RR: return "RR";
95 case lang::ReplacementStrategy::FIFO: return "FIFO";
96 case lang::ReplacementStrategy::SFIFO: return "SFIFO";
97 case lang::ReplacementStrategy::LRU: return "LRU";
98 case lang::ReplacementStrategy::HLRU: return "HLRU";
99 case lang::ReplacementStrategy::CLIMB: return "CLIMB";
100 case lang::ReplacementStrategy::QLRU: return "QLRU";
101 }
102 throw UnsupportedError(ErrorKind{"Export"}, "network_writer: unnamed replacement strategy");
103}
104
105inline const char* polling_to_json(lang::PollingType p) {
106 switch (p) {
107 case lang::PollingType::EXHAUSTIVE: return "EXHAUSTIVE";
108 case lang::PollingType::GATED: return "GATED";
109 case lang::PollingType::KLIMITED: return "KLIMITED";
110 case lang::PollingType::DECREMENTING: return "DECREMENTING";
111 }
112 throw UnsupportedError(ErrorKind{"Export"}, "network_writer: unnamed polling type");
113}
114
115inline const char* impatience_to_json(lang::ImpatienceType i) {
116 switch (i) {
117 case lang::ImpatienceType::RENEGING: return "RENEGING";
118 case lang::ImpatienceType::BALKING: return "BALKING";
119 case lang::ImpatienceType::RETRIAL: return "RETRIAL";
120 case lang::ImpatienceType::NONE: break;
121 }
122 throw UnsupportedError(ErrorKind{"Export"}, "network_writer: a patience block with no impatience type");
123}
124
125inline const char* balking_to_json(lang::BalkingStrategy b) {
126 switch (b) {
127 case lang::BalkingStrategy::QUEUE_LENGTH: return "QUEUE_LENGTH";
128 case lang::BalkingStrategy::EXPECTED_WAIT: return "EXPECTED_WAIT";
129 case lang::BalkingStrategy::COMBINED: return "COMBINED";
130 case lang::BalkingStrategy::NONE: break;
131 }
132 throw UnsupportedError(ErrorKind{"Export"}, "network_writer: a balking block with no strategy");
133}
134
135/**
136 * The `routingStrategies` name of a dispatcher.
137 *
138 * NOT `lang::routing_to_text`, which is the lower-case name `sn.routing` prints
139 * in a struct dump. The wire carries the enum CONSTANT, upper case, matching
140 * the JAR's `RoutingStrategy` names one for one -- and the readers compare it
141 * exactly, so `rrobin` reloads as an unknown strategy rather than as RROBIN.
142 */
143inline const char* routing_to_json(lang::RoutingStrategy r) {
144 switch (r) {
145 case lang::RoutingStrategy::RAND: return "RAND";
146 case lang::RoutingStrategy::PROB: return "PROB";
147 case lang::RoutingStrategy::RROBIN: return "RROBIN";
148 case lang::RoutingStrategy::WRROBIN: return "WRROBIN";
149 case lang::RoutingStrategy::JSQ: return "JSQ";
150 case lang::RoutingStrategy::FIRING: return "FIRING";
151 case lang::RoutingStrategy::SQ: return "SQ";
152 case lang::RoutingStrategy::SDR: return "SDR";
153 case lang::RoutingStrategy::DISABLED: return "DISABLED";
154 }
155 throw UnsupportedError(ErrorKind{"Export"}, "network_writer: unnamed routing strategy");
156}
157
158inline const char* hetero_to_json(lang::HeteroSchedPolicy h) {
159 switch (h) {
160 case lang::HeteroSchedPolicy::ORDER: return "ORDER";
161 case lang::HeteroSchedPolicy::ALIS: return "ALIS";
162 case lang::HeteroSchedPolicy::ALFS: return "ALFS";
163 case lang::HeteroSchedPolicy::FAIRNESS: return "FAIRNESS";
164 case lang::HeteroSchedPolicy::FSF: return "FSF";
165 case lang::HeteroSchedPolicy::RAIS: return "RAIS";
166 }
167 throw UnsupportedError(ErrorKind{"Export"}, "network_writer: unnamed heterogeneous scheduling policy");
168}
169
170/**
171 * The `scheduling` name of a discipline.
172 *
173 * UPPER CASE, which is not what `lang::sched_to_text` returns: that is the
174 * lower-case spelling `sn.sched` prints in a struct dump, while the wire
175 * carries the enum constant (`upper(SchedStrategy.toText(...))` in
176 * `linemodel_save.m`, matching `jline.lang.constant.SchedStrategy`). A
177 * lower-case `fcfs` reloads as an unknown discipline.
178 */
179inline std::string sched_to_json(lang::SchedStrategy s) {
180 std::string out = lang::sched_to_text(s);
181 for (std::size_t i = 0; i < out.size(); ++i)
182 out[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(out[i])));
183 return out;
184}
185
186/** A dense matrix as the array of row arrays the readers expect. */
187template <class T>
188json mat_to_json(const Matrix<T>& M) {
189 json rows = json::array();
190 for (std::size_t i = 0; i < M.rows(); ++i) {
191 json row = json::array();
192 for (std::size_t j = 0; j < M.cols(); ++j)
193 row.push_back(num_traits<T>::to_double(M(i, j)));
194 rows.push_back(row);
195 }
196 return rows;
197}
198
199template <class T>
200json vec_to_json(const std::vector<T>& v) {
201 json a = json::array();
202 for (const T& x : v) a.push_back(num_traits<T>::to_double(x));
203 return a;
204}
205
206/**
207 * Rewrite every non-finite number into the wire's own INFINITY SPELLING.
208 *
209 * JSON has no infinity literal, so `linemodel_save` -- the reference -- writes
210 * an infinite scalar as the STRING "Infinity" / "-Infinity" and a NaN as
211 * `null`, a rule it applies to EVERY numeric field rather than a named few;
212 * `num_from_json` on the reading side decodes exactly that pair. nlohmann
213 * silently dumps an infinity as `null` instead, which the reader then takes for
214 * a NaN, so a state row that carries the sentinel -- an open class holds an
215 * infinite population, so a Source's `initialState` does the moment the model
216 * is initialized -- crossed as a hole rather than as a value. Applied once over
217 * the finished document, as the JAR and Python writers apply theirs, so no
218 * individual field has to remember the rule.
219 */
220inline void wire_nonfinite(json& j) {
221 if (j.is_object() || j.is_array()) {
222 for (json::iterator it = j.begin(); it != j.end(); ++it) wire_nonfinite(*it);
223 return;
224 }
225 if (!j.is_number_float()) return;
226 const double v = j.get<double>();
227 if (std::isnan(v))
228 j = json();
229 else if (std::isinf(v))
230 j = json(v > 0.0 ? "Infinity" : "-Infinity");
231}
232
233/**
234 * `x` of a `DiscreteSampler` whose support is the item index, i.e. 1..n.
235 *
236 * MATLAB `DiscreteSampler(p)` defaults `x = 1:n` and every writer records it;
237 * the JAR and Python readers require the key and fail on a file that omits it,
238 * so a pmf-only document is unreadable outside this port even though the value
239 * is implied.
240 */
241inline json discrete_support(std::size_t n) {
242 json a = json::array();
243 for (std::size_t i = 1; i <= n; ++i) a.push_back(static_cast<double>(i));
244 return a;
245}
246
247/**
248 * A distribution as its model.json record, the inverse of `dist_from_json`.
249 *
250 * The parameter-carrying families are written from `Distrib::params`, which
251 * holds the constructor arguments in MATLAB's getParam order -- that is what
252 * makes the record reload into the SAME object rather than into a phase-type
253 * that shares two moments with it.
254 */
255template <class T>
256json dist_to_json(const lang::Distrib<T>& d) {
257 using lang::ProcessType;
258 json j;
259 j["type"] = lang::process_to_text(d.type);
260 const std::vector<T>& p = d.params;
261 auto need = [&](std::size_t n, const char* who) {
262 if (p.size() < n)
263 throw InputError(std::string("network_writer: a ") + who + " carries " +
264 std::to_string(p.size()) + " parameters, and " + std::to_string(n) +
265 " are needed to write it back");
266 };
267 switch (d.type) {
268 case ProcessType::DISABLED:
269 case ProcessType::IMMEDIATE:
270 return j; // both are bare tags
271 case ProcessType::EXP:
272 need(1, "Exp");
273 j["params"]["lambda"] = num_traits<T>::to_double(p[0]);
274 return j;
275 case ProcessType::DET:
276 j["params"]["value"] = num_traits<T>::to_double(d.mean);
277 return j;
278 case ProcessType::ERLANG:
279 need(2, "Erlang");
280 j["params"]["lambda"] = num_traits<T>::to_double(p[0]);
281 j["params"]["k"] = static_cast<long>(std::lround(num_traits<T>::to_double(p[1])));
282 return j;
283 case ProcessType::HYPEREXP: {
284 // The two-branch form stores (p, lambda1, lambda2) and the general
285 // one (p_1..p_n, lambda_1..lambda_n); both write the same two
286 // vectors, which is the only form on the wire.
287 json pv = json::array(), lv = json::array();
288 if (p.size() == 3) {
289 pv.push_back(num_traits<T>::to_double(p[0]));
290 pv.push_back(1.0 - num_traits<T>::to_double(p[0]));
291 lv.push_back(num_traits<T>::to_double(p[1]));
292 lv.push_back(num_traits<T>::to_double(p[2]));
293 } else {
294 const std::size_t n = p.size() / 2;
295 for (std::size_t i = 0; i < n; ++i) pv.push_back(num_traits<T>::to_double(p[i]));
296 for (std::size_t i = 0; i < n; ++i) lv.push_back(num_traits<T>::to_double(p[n + i]));
297 }
298 j["params"]["p"] = pv;
299 j["params"]["lambda"] = lv;
300 return j;
301 }
302 case ProcessType::COXIAN:
303 case ProcessType::COX2: {
304 const std::size_t n = p.size() / 2;
305 json mu = json::array(), phi = json::array();
306 for (std::size_t i = 0; i < n; ++i) mu.push_back(num_traits<T>::to_double(p[i]));
307 for (std::size_t i = 0; i < n; ++i) phi.push_back(num_traits<T>::to_double(p[n + i]));
308 // Coxian is the only name the readers reconstruct from (mu, phi);
309 // Cox2 is the two-phase constructor and shares the representation.
310 j["type"] = "Coxian";
311 j["params"]["mu"] = mu;
312 j["params"]["phi"] = phi;
313 return j;
314 }
315 case ProcessType::PH:
316 case ProcessType::APH:
317 case ProcessType::ME: {
318 json alpha = json::array();
319 for (const T& v : p) alpha.push_back(num_traits<T>::to_double(v));
320 if (d.type == ProcessType::ME) {
321 j["params"]["alpha"] = alpha;
322 j["params"]["A"] = mat_to_json(d.D0);
323 } else {
324 j["ph"]["alpha"] = alpha;
325 j["ph"]["T"] = mat_to_json(d.D0);
326 }
327 return j;
328 }
329 case ProcessType::MAP:
330 j["map"]["D0"] = mat_to_json(d.D0);
331 j["map"]["D1"] = mat_to_json(d.D1);
332 return j;
333 case ProcessType::RAP:
334 j["params"]["H0"] = mat_to_json(d.D0);
335 j["params"]["H1"] = mat_to_json(d.D1);
336 return j;
337 case ProcessType::DMAP:
338 j["params"]["D0"] = mat_to_json(d.D0);
339 j["params"]["D1"] = mat_to_json(d.D1);
340 return j;
341 case ProcessType::MMPP2: {
342 // Recovered from the pair rather than from `params`, which
343 // `map_dist` does not fill: lambda_i is the arrival rate in phase i
344 // and sigma_i the modulating rate out of it.
345 if (d.D0.rows() != 2)
346 throw InputError("network_writer: an MMPP2 must be of order two");
347 j["params"]["lambda0"] = num_traits<T>::to_double(d.D1(0, 0));
348 j["params"]["lambda1"] = num_traits<T>::to_double(d.D1(1, 1));
349 j["params"]["sigma0"] = num_traits<T>::to_double(d.D0(0, 1));
350 j["params"]["sigma1"] = num_traits<T>::to_double(d.D0(1, 0));
351 return j;
352 }
353 case ProcessType::MPH: {
354 // Stored as the MMAP cell it lowers to, but the (alpha, S, {s_c}) split IS
355 // recoverable from it and is what the other codebases expect back: every row
356 // of D1_agg is (-S e)_i alpha, so alpha is any nonzero row normalised, and
357 // s_c(i) is the row sum of that mark's block. Writing the lowered cell instead
358 // would come back as a plain MMAP and lose the process type.
359 const std::size_t H = d.D0.rows();
360 std::vector<double> alpha(H, 0.0);
361 for (std::size_t i = 0; i < H; ++i) {
362 double row = 0.0;
363 for (std::size_t q = 0; q < H; ++q) row += num_traits<T>::to_double(d.D1(i, q));
364 if (row > 0.0) {
365 for (std::size_t q = 0; q < H; ++q)
366 alpha[q] = num_traits<T>::to_double(d.D1(i, q)) / row;
367 break;
368 }
369 }
370 j["mph"]["alpha"] = alpha;
371 j["mph"]["S"] = mat_to_json(d.D0);
372 json exits = json::array();
373 for (const Matrix<T>& Dk : d.Dmark) {
374 std::vector<double> sk(H, 0.0);
375 for (std::size_t i = 0; i < H; ++i)
376 for (std::size_t q = 0; q < H; ++q)
377 sk[i] += num_traits<T>::to_double(Dk(i, q));
378 exits.push_back(sk);
379 }
380 j["mph"]["exit"] = exits;
381 return j;
382 }
383 case ProcessType::MMAP: {
384 j["mmap"]["D0"] = mat_to_json(d.D0);
385 json blocks = json::array();
386 for (const Matrix<T>& Dk : d.Dmark) blocks.push_back(mat_to_json(Dk));
387 j["mmap"]["D1k"] = blocks;
388 return j;
389 }
390 case ProcessType::BMAP: {
391 json blocks = json::array();
392 blocks.push_back(mat_to_json(d.D0));
393 for (const Matrix<T>& Dk : d.Dmark) blocks.push_back(mat_to_json(Dk));
394 j["params"]["D"] = blocks;
395 return j;
396 }
397 case ProcessType::UNIFORM:
398 need(2, "Uniform");
399 j["params"]["a"] = num_traits<T>::to_double(p[0]);
400 j["params"]["b"] = num_traits<T>::to_double(p[1]);
401 return j;
402 case ProcessType::PARETO:
403 need(2, "Pareto");
404 j["params"]["alpha"] = num_traits<T>::to_double(p[0]);
405 j["params"]["scale"] = num_traits<T>::to_double(p[1]);
406 return j;
407 case ProcessType::GAMMA:
408 need(2, "Gamma");
409 j["params"]["alpha"] = num_traits<T>::to_double(p[0]);
410 j["params"]["beta"] = num_traits<T>::to_double(p[1]);
411 return j;
412 case ProcessType::WEIBULL:
413 // `alpha` is the SCALE here and the SHAPE in Pareto above; that is
414 // the reference's own key naming and the one trap in this table.
415 need(2, "Weibull");
416 j["params"]["alpha"] = num_traits<T>::to_double(p[0]);
417 j["params"]["beta"] = num_traits<T>::to_double(p[1]);
418 return j;
419 case ProcessType::LOGNORMAL:
420 need(2, "Lognormal");
421 j["params"]["mu"] = num_traits<T>::to_double(p[0]);
422 j["params"]["sigma"] = num_traits<T>::to_double(p[1]);
423 return j;
424 case ProcessType::DUNIFORM:
425 need(2, "DiscreteUniform");
426 j["params"]["min"] = num_traits<T>::to_double(p[0]);
427 j["params"]["max"] = num_traits<T>::to_double(p[1]);
428 return j;
429 case ProcessType::BERNOULLI:
430 need(1, "Bernoulli");
431 j["params"]["p"] = num_traits<T>::to_double(p[0]);
432 return j;
433 case ProcessType::BINOMIAL:
434 need(2, "Binomial");
435 j["params"]["n"] = static_cast<long>(std::lround(num_traits<T>::to_double(p[0])));
436 j["params"]["p"] = num_traits<T>::to_double(p[1]);
437 return j;
438 case ProcessType::POISSON:
439 need(1, "Poisson");
440 j["params"]["lambda"] = num_traits<T>::to_double(p[0]);
441 return j;
442 case ProcessType::GEOMETRIC:
443 need(1, "Geometric");
444 j["params"]["p"] = num_traits<T>::to_double(p[0]);
445 return j;
446 case ProcessType::ZIPF:
447 need(2, "Zipf");
448 j["params"]["s"] = num_traits<T>::to_double(p[0]);
449 j["params"]["n"] = static_cast<long>(std::lround(num_traits<T>::to_double(p[1])));
450 return j;
451 case ProcessType::DISCRETESAMPLER:
452 j["params"]["p"] = vec_to_json(p);
453 if (!d.trace.empty()) j["params"]["x"] = vec_to_json(d.trace);
454 return j;
455 case ProcessType::EMPIRICALCDF:
456 j["params"]["x"] = vec_to_json(d.trace);
457 j["params"]["F"] = vec_to_json(p);
458 return j;
459 case ProcessType::REPLAYER:
460 // The trace itself has no wire form: every writer emits the file
461 // PATH, plus the mean beside it as the documented fallback for a
462 // reader on another machine where the path does not resolve. A
463 // Replayer built from in-memory samples has no path, and then only
464 // the moments can be written -- which reloads as a distribution
465 // matching them, not as the trace, exactly as the reference warns.
466 if (!d.trace_file.empty()) j["params"]["fileName"] = d.trace_file;
467 j["params"]["mean"] = num_traits<T>::to_double(d.mean);
468 if (d.trace_file.empty())
469 j["params"]["scv"] = num_traits<T>::to_double(d.scv);
470 return j;
471 case ProcessType::MMAPT:
472 case ProcessType::MPHT: {
473 j["params"]["breakpoints"] = vec_to_json(d.sched_bp);
474 j["params"]["cyclic"] = d.sched_cyclic;
475 json A = json::array();
476 for (const Matrix<T>& M : d.sched_D0) A.push_back(mat_to_json(M));
477 json blocks = json::array();
478 for (const std::vector<Matrix<T> >& per_mark : d.sched_Dmark) {
479 json segs = json::array();
480 for (const Matrix<T>& M : per_mark) segs.push_back(mat_to_json(M));
481 blocks.push_back(segs);
482 }
483 // An MPHt is STORED lowered to MMAPt form -- see the Distrib header -- so the
484 // (alpha, S, exits) split is not recoverable here and it is written in the
485 // MMAPt spelling it reloads identically from. Exactly what a PHt does below,
486 // and for the same reason: this port keeps one representation per family.
487 j["type"] = "MMAPt";
488 j["params"]["D0"] = A;
489 j["params"]["D1k"] = blocks;
490 return j;
491 }
492 case ProcessType::BMMAPT: {
493 // Only the BLOCKS travel, nested mark, then batch size, then segment. The
494 // reader derives the batch-aggregated D1k and the aggregate D1 from them,
495 // so the wire cannot carry a level that disagrees with the blocks summing
496 // to it.
497 j["params"]["breakpoints"] = vec_to_json(d.sched_bp);
498 j["params"]["cyclic"] = d.sched_cyclic;
499 json A = json::array();
500 for (const Matrix<T>& M : d.sched_D0) A.push_back(mat_to_json(M));
501 json blocks = json::array();
502 for (const std::vector<std::vector<Matrix<T> > >& per_mark : d.sched_Dbatch) {
503 json per_batch = json::array();
504 for (const std::vector<Matrix<T> >& batch : per_mark) {
505 json segs = json::array();
506 for (const Matrix<T>& M : batch) segs.push_back(mat_to_json(M));
507 per_batch.push_back(segs);
508 }
509 blocks.push_back(per_batch);
510 }
511 j["params"]["D0"] = A;
512 j["params"]["D1kb"] = blocks;
513 return j;
514 }
515 // NHPP belongs with MAPt: it is a one-phase MAPt and is written back as its
516 // `rates` vector by the branch below. It briefly sat with the MARKED families
517 // above, where it was emitted as an `MMAPt` carrying an EMPTY `D1k` -- a
518 // document no reader accepts, since the MMAPt factory refuses a schedule with
519 // no mark block.
520 case ProcessType::NHPP:
521 case ProcessType::MAPT:
522 case ProcessType::PHT: {
523 j["params"]["breakpoints"] = vec_to_json(d.sched_bp);
524 j["params"]["cyclic"] = d.sched_cyclic;
525 if (d.type == ProcessType::NHPP) {
526 // A one-phase schedule: the rate of segment k is D1[k](0,0).
527 json rates = json::array();
528 for (const Matrix<T>& D1 : d.sched_D1)
529 rates.push_back(num_traits<T>::to_double(D1(0, 0)));
530 j["params"]["rates"] = rates;
531 return j;
532 }
533 json A = json::array(), B = json::array();
534 for (const Matrix<T>& M : d.sched_D0) A.push_back(mat_to_json(M));
535 for (const Matrix<T>& M : d.sched_D1) B.push_back(mat_to_json(M));
536 // A PHt is STORED converted to (D0, D1) -- see the Distrib header --
537 // so it is written in the MAPt spelling it reloads identically from.
538 j["type"] = "MAPt";
539 j["params"]["D0"] = A;
540 j["params"]["D1"] = B;
541 return j;
542 }
543 case ProcessType::PRIOR:
544 throw UnsupportedError(ErrorKind{"Export"},
545 "network_writer: a Prior is a set of alternative MODELS rather than one law; save "
546 "the design point SolverUQ built, not the design");
547 default:
548 break;
549 }
550 throw UnsupportedError(ErrorKind{"Export"}, std::string("network_writer: distribution family '") +
551 lang::process_to_text(d.type) + "' has no model.json record");
552}
553
554/**
555 * The per-class cutoffs a materialized rate table is swept over: a closed
556 * class's population, and 10 for an open one.
557 *
558 * The same rule as `linemodel_save.m:157-163`, and it has to be, or the tables
559 * this writer emits would be keyed over a different lattice than the reference's
560 * for the same model and the two files would not compare.
561 */
562template <class T>
563std::vector<int> lattice_cutoffs(const qn::NetworkStruct<T>& sn) {
564 std::vector<int> cut(sn.classes.size(), 10);
565 for (std::size_t r = 0; r < sn.classes.size(); ++r)
566 if (sn.classes[r].type == lang::JobClassType::CLOSED &&
567 std::isfinite(sn.classes[r].population))
568 cut[r] = static_cast<int>(std::lround(sn.classes[r].population));
569 return cut;
570}
571
572/**
573 * Enumerate the box lattice 0 <= n(r) <= cut(r), calling `visit(counts, key)`.
574 * The empty state is skipped unless `with_empty`: an OI/PAS table omits it, a
575 * class/joint-dependence table lists it, as MATLAB, the JAR and Python do.
576 */
577template <class F>
578void for_each_lattice_point(const std::vector<int>& cut, const F& visit, bool with_empty = false) {
579 std::size_t total = 1;
580 for (int c : cut) total *= static_cast<std::size_t>(c + 1);
581 const std::size_t K = cut.size();
582 for (std::size_t i = 0; i < total; ++i) {
583 std::vector<int> cnt(K, 0);
584 std::size_t li = i;
585 for (std::size_t d = 0; d < K; ++d) {
586 cnt[d] = static_cast<int>(li % static_cast<std::size_t>(cut[d] + 1));
587 li /= static_cast<std::size_t>(cut[d] + 1);
588 }
589 std::string key;
590 int tot = 0;
591 for (std::size_t d = 0; d < K; ++d) {
592 if (d) key += ',';
593 key += std::to_string(cnt[d]);
594 tot += cnt[d];
595 }
596 if (tot == 0 && !with_empty) continue; // an OI/PAS table omits the empty state
597 visit(cnt, key);
598 }
599}
600
601/**
602 * Enumerate the simplex 1 <= sum(n) <= cmax over K classes, calling
603 * `visit(counts, key)`. The extent Python `save_model` gives a non-saturating
604 * OI/PAS rate at a finite buffer, written with no `oiCutoffs`.
605 */
606template <class F>
607void for_each_simplex_point(std::size_t K, int cmax, const F& visit) {
608 for (int tot = 1; tot <= cmax; ++tot) {
609 std::vector<int> cnt(K, 0);
610 cnt[0] = tot;
611 while (true) {
612 std::string key;
613 for (std::size_t d = 0; d < K; ++d) {
614 if (d) key += ',';
615 key += std::to_string(cnt[d]);
616 }
617 visit(cnt, key);
618 // next composition: move one unit from the first nonzero part j < K-1
619 // to part j+1 and gather the remainder in part 0
620 std::size_t j = K;
621 for (std::size_t d = 0; d + 1 < K; ++d)
622 if (cnt[d] > 0) {
623 j = d;
624 break;
625 }
626 if (j == K) break;
627 const int v = cnt[j];
628 cnt[j] = 0;
629 ++cnt[j + 1];
630 cnt[0] = v - 1;
631 }
632 }
633}
634
635} // namespace detail
636
637/** The per-class saturation cutoffs of a count-indexed rate table. */
639 std::vector<int> tau; ///< tau(r): beyond it the function no longer changes with class-r jobs
640 bool bounded = true; ///< false when some tau(r) passed the search limit or reached cmax
641};
642
643/**
644 * Per-class saturation cutoffs of a count-indexed function f(n), the search
645 * behind the OI/PAS and class/joint-dependence table extents. Twin of Python
646 * `_saturation_cutoffs` (linemodel_io.py), MATLAB `saturation_cutoffs.m` and the
647 * JAR `LineModelIO.saturationCutoffs`.
648 *
649 * `f_at(n)` returns the values the writer would tabulate at the count vector n,
650 * ALREADY SANITIZED (non-finite -> 0), so the search compares what the table
651 * holds. Starting from tau = 1, a class whose extra job still changes f anywhere
652 * on the current box has its cutoff raised, until no class moves. Only pairs
653 * (m, m + e_r) with sum(m) + 1 <= cmax are compared: a state above the buffer is
654 * never visited, so whatever a reloaded table answers there cannot move the
655 * search, which is what makes save -> load -> save a fixed point. Passing
656 * `max_cutoff`, or a cutoff reaching cmax (where the box would be no smaller
657 * than the buffer), reports the function unbounded.
658 */
660 const std::function<std::vector<double>(const std::vector<int>&)>& f_at, std::size_t K,
661 double cmax = std::numeric_limits<double>::infinity(), int max_cutoff = 256,
662 double tol = 1e-9) {
663 PasSaturation out;
664 out.tau.assign(K, 1);
665 bool changed = true;
666 while (changed) {
667 changed = false;
668 for (std::size_t r = 0; r < K; ++r) {
669 // Every count vector with n(r) = tau(r) and n(j) <= tau(j) elsewhere.
670 std::size_t total = 1;
671 for (std::size_t j = 0; j < K; ++j)
672 if (j != r) total *= static_cast<std::size_t>(out.tau[j] + 1);
673 bool sat = true;
674 for (std::size_t li = 0; li < total && sat; ++li) {
675 std::vector<int> m(K, 0);
676 std::size_t rem = li;
677 int sum = 0;
678 for (std::size_t j = 0; j < K; ++j) {
679 if (j == r) {
680 m[j] = out.tau[r];
681 } else {
682 m[j] = static_cast<int>(rem % static_cast<std::size_t>(out.tau[j] + 1));
683 rem /= static_cast<std::size_t>(out.tau[j] + 1);
684 }
685 sum += m[j];
686 }
687 if (static_cast<double>(sum) + 1.0 > cmax) continue;
688 std::vector<int> mp(m);
689 ++mp[r];
690 const std::vector<double> a = f_at(mp), b = f_at(m);
691 double d = 0.0;
692 for (std::size_t k = 0; k < a.size() && k < b.size(); ++k)
693 d = std::max(d, std::fabs(a[k] - b[k]));
694 if (d > tol) sat = false;
695 }
696 if (!sat) {
697 ++out.tau[r];
698 changed = true;
699 if (out.tau[r] > max_cutoff) {
700 out.bounded = false;
701 return out;
702 }
703 }
704 }
705 }
706 for (std::size_t r = 0; r < K; ++r)
707 if (!(static_cast<double>(out.tau[r]) < cmax)) out.bounded = false;
708 return out;
709}
710
711/**
712 * The saturation cutoffs of an order-independent rate mu(c), searched over the
713 * states a buffer of `cmax` jobs can hold (see saturation_cutoffs). Port of the
714 * search in Python `save_model`. mu receives 1-BASED class tags, the convention
715 * of `sn.pasparam`, so the canonical list holds n(r) copies of r+1; a
716 * non-finite rate compares as 0, the value the table holds.
717 */
718template <class T>
720 const std::function<T(const std::vector<std::size_t>&)>& mu, std::size_t K,
721 double cmax = std::numeric_limits<double>::infinity()) {
722 return saturation_cutoffs(
723 [&mu, K](const std::vector<int>& n) {
724 std::vector<std::size_t> order;
725 for (std::size_t r = 0; r < K; ++r)
726 for (int c = 0; c < n[r]; ++c) order.push_back(r + 1);
727 const double v = num_traits<T>::to_double(mu(order));
728 return std::vector<double>(1, std::isfinite(v) ? v : 0.0);
729 },
730 K, cmax);
731}
732
733namespace detail {
734
735/** beta(n) as the writer tabulates it: a short row padded from its first entry, non-finite -> 0. */
736template <class T>
737std::vector<double> cd_scaling_at(const lang::CdScaling<T>& fun, const std::vector<int>& cnt,
738 std::size_t K) {
739 std::vector<T> n(K, num_traits<T>::from_int(0));
740 for (std::size_t r = 0; r < K; ++r) n[r] = num_traits<T>::from_int(static_cast<long>(cnt[r]));
741 const std::vector<T> v = fun(n);
742 std::vector<double> out(K, 0.0);
743 for (std::size_t r = 0; r < K; ++r) {
744 double x = 0.0;
745 if (v.size() == 1) x = num_traits<T>::to_double(v[0]);
746 else if (r < v.size()) x = num_traits<T>::to_double(v[r]);
747 out[r] = std::isfinite(x) ? x : 0.0;
748 }
749 return out;
750}
751
752/**
753 * Materialize a class/joint-dependence handle over the extent that reproduces
754 * it on every state the station can reach. Twin of Python `_cd_scaling_extent`.
755 *
756 * bb(r) = sn.classcap(ist, r) and C = sn.cap(ist), the per-class and total
757 * counts the station can hold (chain populations, declared buffers, class caps):
758 * - every bb finite, sum(bb) <= C: the box 0..bb with `cutoffs` = bb;
759 * - every bb finite, sum(bb) > C: the box truncated at sum(n) <= C and NO
760 * `cutoffs`, so the readers look the raw counts up;
761 * - some bb infinite (an open class at a station with no buffer): the
762 * saturation box 0..tau with `cutoffs` = tau when beta saturates, else the
763 * box with 10 for each unbounded class, with cutoffs (lossy above 10).
764 */
765template <class T>
766void cd_scaling_extent(const lang::CdScaling<T>& fun, std::size_t K, const std::vector<double>& bb,
767 double C, json& blk) {
768 const auto put = [&](json& tbl, const std::vector<int>& cnt, const std::string& key) {
769 tbl[key] = cd_scaling_at<T>(fun, cnt, K);
770 };
771 bool finite = true;
772 for (std::size_t r = 0; r < K; ++r) finite = finite && r < bb.size() && std::isfinite(bb[r]);
773 json tbl = json::object();
774 if (finite) {
775 std::vector<int> maxc(K, 0);
776 long tot = 0;
777 for (std::size_t r = 0; r < K; ++r) {
778 maxc[r] = static_cast<int>(std::floor(bb[r]));
779 tot += maxc[r];
780 }
781 const double cs = std::isfinite(C) ? std::floor(C) : C;
782 if (static_cast<double>(tot) <= cs) {
783 for_each_lattice_point(maxc, [&](const std::vector<int>& cnt, const std::string& key) {
784 put(tbl, cnt, key);
785 }, true);
786 blk["cutoffs"] = maxc;
787 } else {
788 for_each_lattice_point(maxc, [&](const std::vector<int>& cnt, const std::string& key) {
789 long s = 0;
790 for (int c : cnt) s += c;
791 if (static_cast<double>(s) <= cs) put(tbl, cnt, key);
792 }, true);
793 }
794 blk["scaling"] = tbl;
795 return;
796 }
797 const PasSaturation sat = saturation_cutoffs(
798 [&fun, K](const std::vector<int>& n) { return cd_scaling_at<T>(fun, n, K); }, K);
799 std::vector<int> maxc;
800 if (sat.bounded) {
801 maxc = sat.tau;
802 } else {
803 maxc.assign(K, 10);
804 for (std::size_t r = 0; r < K; ++r)
805 if (r < bb.size() && std::isfinite(bb[r])) maxc[r] = static_cast<int>(std::floor(bb[r]));
806 }
807 for_each_lattice_point(maxc, [&](const std::vector<int>& cnt, const std::string& key) {
808 put(tbl, cnt, key);
809 }, true);
810 blk["cutoffs"] = maxc;
811 blk["scaling"] = tbl;
812}
813
814} // namespace detail
815
816/**
817 * `qn::NetworkStruct` -> the model.json `model` object.
818 *
819 * Takes the struct rather than the `qn::Network` builder because that is what a
820 * solver holds, and because `get_struct()` is what the builder hands out.
821 */
822template <class T>
824 using detail::json;
825 typedef qn::NetworkStruct<T> SN;
826 const std::size_t K = sn.classes.size();
827 const std::vector<int> cut = detail::lattice_cutoffs(sn);
828
829 json model;
830 model["type"] = "Network";
831 model["name"] = sn.name;
832 // The directory every Logger writes into; model-level because that is where
833 // the reference keeps it and because a Logger's constructor refuses without it.
834 if (!sn.log_path.empty()) model["logPath"] = sn.log_path;
835
836 // -- classes --------------------------------------------------------------
837 json classes = json::array();
838 for (std::size_t r = 0; r < K; ++r) {
839 const qn::JobClass& c = sn.classes[r];
840 json cj;
841 cj["name"] = c.name;
842 // A G-network SIGNAL is written as its own wire type with the kind it
843 // removes under: written back as an ordinary class it becomes inert,
844 // and the reloaded model is a queueing network with no removals at all.
845 const bool sig = r < sn.issignal.size() && sn.issignal[r];
846 if (sig) {
847 cj["type"] = "Signal";
848 cj["openOrClosed"] = c.type == lang::JobClassType::CLOSED ? "Closed" : "Open";
849 const lang::SignalType st = sn.signaltype[r];
850 cj["signalType"] = st == lang::SignalType::REPLY
851 ? "reply"
852 : (st == lang::SignalType::CATASTROPHE ? "catastrophe"
853 : "negative");
854 if (sn.signaltarget[r] >= 1 && sn.signaltarget[r] <= K)
855 cj["targetClass"] = sn.classes[sn.signaltarget[r] - 1].name;
856 const lang::RemovalPolicy rp = sn.signalrempolicy[r];
858 cj["removalPolicy"] = rp == lang::RemovalPolicy::FCFS ? "FCFS" : "LCFS";
859 if (!sn.signalremdist[r].empty()) {
860 json pv = json::array();
861 for (const T& v : sn.signalremdist[r]) pv.push_back(num_traits<T>::to_double(v));
862 json xv = json::array();
863 // THE SUPPORT STARTS AT ZERO, unlike every other pmf written
864 // here: `signalremdist` is indexed BY BATCH SIZE, and a batch of
865 // zero is a real outcome. `discrete_support` numbers items from
866 // 1, so using it shifted every batch by one job on the way back
867 // through any reader that honours `x` -- MATLAB's included.
868 for (std::size_t b = 0; b < pv.size(); ++b) xv.push_back(static_cast<double>(b));
869 json rd;
870 rd["type"] = "DiscreteSampler";
871 rd["params"]["p"] = pv;
872 rd["params"]["x"] = xv;
873 cj["removalDistribution"] = rd;
874 }
875 } else {
876 cj["type"] = c.type == lang::JobClassType::CLOSED
877 ? (c.self_looping ? "SelfLooping" : "Closed")
878 : "Open";
879 }
881 // An integer, as linemodel_save.m and Python's save_model write it; a non-integral value is kept as written.
882 const double pop = c.population;
883 if (std::isfinite(pop) && pop == std::floor(pop) && std::fabs(pop) < 9.0e15)
884 cj["population"] = static_cast<long long>(pop);
885 else
886 cj["population"] = pop;
887 if (c.refstat == 0 || c.refstat > sn.station_to_node.size())
888 throw InputError("network_writer: class '" + c.name +
889 "' names no reference station, which a closed class must have");
890 cj["refNode"] = sn.nodes[sn.station_to_node[c.refstat - 1] - 1].name;
891 }
892 if (c.prio != 0) cj["priority"] = c.prio;
893 if (c.immfeed) cj["immediateFeedback"] = true;
894 if (c.is_ref_class) cj["isReferenceClass"] = true;
895 if (std::isfinite(c.deadline)) cj["deadline"] = c.deadline;
896 if (c.spawn >= 1 && c.spawn <= K) cj["spawnClass"] = sn.classes[c.spawn - 1].name;
897 if (r < sn.syncreply.size() && sn.syncreply[r] >= 1 && sn.syncreply[r] <= K)
898 cj["replySignalClass"] = sn.classes[sn.syncreply[r] - 1].name;
899 classes.push_back(cj);
900 }
901 model["classes"] = classes;
902
903 // -- nodes ----------------------------------------------------------------
904 json nodes = json::array();
905 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
906 const qn::NodeDef& nd = sn.nodes[i];
907 const std::size_t ind = i + 1, ist = nd.station;
908 json nj;
909 nj["name"] = nd.name;
910 // a Queue declared INF keeps its class on the wire, as linemodel_save writes isa(node,'Queue') nodes
911 nj["type"] = nd.queue_object ? "Queue" : detail::node_type_to_json(nd.nodetype);
912
913 if (nd.nodetype == lang::NodeType::Fork && nd.tasks_per_link > 1.0)
914 nj["tasksPerLink"] = nd.tasks_per_link;
915 // VARIABLE FORKING LEVELS, in the record shape `linemodel_save.m:509-553`
916 // writes: one {dest, class, value} per link rather than a matrix, since
917 // that is what the reader replays through the setters.
918 //
919 // Every link a class takes is listed, not only the ones the model
920 // overrode. The reader seeds an untouched link from `tasksPerLink`,
921 // which is the MEAN once an override exists, so a partial list would
922 // reload a fork that emits a count it was never given.
923 if (nd.nodetype == lang::NodeType::Fork) {
924 const qn::ForkParam<T>* fp = sn.fork_param_of(ind);
925 if (fp != 0) {
926 json by_dest = json::array(), by_prob = json::array(), by_dist = json::array();
927 for (std::size_t k = 0; k < fp->fan_out_link.rows(); ++k)
928 for (std::size_t r = 0; r < fp->fan_out_link.cols() && r < K; ++r) {
929 const double p = num_traits<T>::to_double(fp->fan_out_prob(k, r));
930 if (p == 0.0) continue; // link this class does not take
931 json rec;
932 rec["dest"] = sn.nodes[k].name;
933 rec["class"] = r + 1;
934 rec["value"] = num_traits<T>::to_double(fp->fan_out_link(k, r));
935 by_dest.push_back(rec);
936 if (p != 1.0) {
937 json pr;
938 pr["dest"] = sn.nodes[k].name;
939 pr["class"] = r + 1;
940 pr["value"] = p;
941 by_prob.push_back(pr);
942 }
943 const lang::Distrib<T>& d = fp->fan_out_dist[k][r];
944 if (!d.disabled) {
945 json dr;
946 dr["dest"] = sn.nodes[k].name;
947 dr["class"] = r + 1;
948 json pv = json::array(), xv = json::array();
949 for (std::size_t e = 0; e < d.params.size(); ++e) {
950 pv.push_back(num_traits<T>::to_double(d.params[e]));
951 xv.push_back(d.trace.empty()
952 ? static_cast<double>(e + 1)
954 }
955 dr["p"] = pv;
956 dr["x"] = xv;
957 by_dist.push_back(dr);
958 }
959 }
960 if (!by_dest.empty()) nj["fanOutByDest"] = by_dest;
961 if (!by_prob.empty()) nj["fanOutProb"] = by_prob;
962 if (!by_dist.empty()) nj["fanOutDist"] = by_dist;
963 }
964 }
965 // A Logger with no file name exports as a tunnel that logs nowhere, so
966 // the name is what makes the node mean anything on reload.
967 if (nd.nodetype == lang::NodeType::Logger && !nd.logger.file_name.empty())
968 nj["fileName"] = nd.logger.file_name;
969 // The trace columns and a per-node directory, written only where they leave the
970 // defaults so the documents of existing models are unchanged; `network_reader` reads all.
972 const qn::NodeDef::LoggerParam& lg = nd.logger;
973 const qn::NodeDef::LoggerParam def;
974 if (!lg.file_path.empty()) nj["filePath"] = lg.file_path;
975 if (lg.start_time != def.start_time) nj["startTime"] = lg.start_time;
976 if (lg.logger_name != def.logger_name) nj["loggerName"] = lg.logger_name;
977 if (lg.timestamp != def.timestamp) nj["timestamp"] = lg.timestamp;
978 if (lg.job_id != def.job_id) nj["jobID"] = lg.job_id;
979 if (lg.job_class != def.job_class) nj["jobClass"] = lg.job_class;
980 if (lg.time_same_class != def.time_same_class) nj["timeSameClass"] = lg.time_same_class;
981 if (lg.time_any_class != def.time_any_class) nj["timeAnyClass"] = lg.time_any_class;
982 }
983 if (nd.nodetype == lang::NodeType::Join) {
984 typename std::map<std::size_t, std::pair<std::size_t, std::size_t> >::const_iterator fj;
985 for (std::size_t f = 0; f < sn.fj.size(); ++f)
986 if (sn.fj[f].second == ind)
987 nj["forkNode"] = sn.nodes[sn.fj[f].first - 1].name;
988 typename std::map<std::size_t, typename SN::JoinDecl>::const_iterator jd =
989 sn.joindecl.find(ind);
990 if (jd != sn.joindecl.end()) {
991 if (jd->second.strategy == lang::JoinStrategy::PARTIAL)
992 nj["joinStrategy"] = "PARTIAL";
993 if (jd->second.quorum > 0) nj["joinQuorum"] = jd->second.quorum;
994 }
995 }
996 {
997 typename std::map<std::size_t, Matrix<T> >::const_iterator cs = sn.csmatrix.find(ind);
998 if (cs != sn.csmatrix.end()) {
999 json csm;
1000 for (std::size_t r = 0; r < K; ++r) {
1001 json row;
1002 for (std::size_t s = 0; s < K; ++s) {
1003 const double v = num_traits<T>::to_double(cs->second(r, s));
1004 if (v != 0.0) row[sn.classes[s].name] = v;
1005 }
1006 if (!row.empty()) csm[sn.classes[r].name] = row;
1007 }
1008 nj["classSwitchMatrix"] = csm;
1009 }
1010 }
1011 {
1012 typename std::map<std::size_t, qn::CacheParam<T> >::const_iterator cp =
1013 sn.nodeparam.find(ind);
1014 if (cp != sn.nodeparam.end()) {
1015 const qn::CacheParam<T>& c = cp->second;
1016 nj["numItems"] = c.nitems;
1017 nj["itemLevelCap"] = c.itemcap;
1018 nj["replacementStrategy"] = detail::replacement_to_json(c.replacestrat);
1019 if (num_traits<T>::to_double(c.qlru) != 1.0)
1020 nj["admissionProb"] = num_traits<T>::to_double(c.qlru);
1021 if (!c.itemsize.empty()) nj["itemSizes"] = c.itemsize;
1022 if (!c.costcap.empty()) {
1023 if (c.costcapglobal) nj["costCaps"] = c.costcap.empty() ? 0 : c.costcap[0];
1024 else nj["costCaps"] = c.costcap;
1025 }
1026 json pop, hit, miss, itemcls;
1027 for (std::size_t r = 0; r < K && r < c.pread.size(); ++r) {
1028 if (c.pread[r].empty()) continue;
1029 json pj;
1030 pj["type"] = "DiscreteSampler";
1031 pj["params"]["p"] = detail::vec_to_json(c.pread[r]);
1032 pj["params"]["x"] = detail::discrete_support(c.pread[r].size());
1033 pop[sn.classes[r].name] = pj;
1034 }
1035 for (std::size_t r = 0; r < K && r < c.hitclass.size(); ++r)
1036 if (c.hitclass[r] != 0)
1037 hit[sn.classes[r].name] = sn.classes[c.hitclass[r] - 1].name;
1038 for (std::size_t r = 0; r < K && r < c.missclass.size(); ++r)
1039 if (c.missclass[r] != 0)
1040 miss[sn.classes[r].name] = sn.classes[c.missclass[r] - 1].name;
1041 for (std::size_t r = 0; r < K && r < c.classitem.size(); ++r)
1042 if (c.classitem[r] != 0)
1043 itemcls[sn.classes[r].name] = static_cast<double>(c.classitem[r]);
1044 if (!pop.empty()) nj["popularity"] = pop;
1045 if (!hit.empty()) nj["hitClass"] = hit;
1046 if (!miss.empty()) nj["missClass"] = miss;
1047 if (!itemcls.empty()) nj["itemClass"] = itemcls;
1048 if (!c.accost.empty()) {
1049 json ap = json::array();
1050 for (const std::vector<Matrix<T> >& per_class : c.accost) {
1051 json row = json::array();
1052 for (const Matrix<T>& g : per_class)
1053 row.push_back(g.rows() == 0 ? json::array() : detail::mat_to_json(g));
1054 ap.push_back(row);
1055 }
1056 nj["accessProb"] = ap;
1057 }
1058 if (!c.initstate.empty()) nj["initialState"] = detail::vec_to_json(c.initstate);
1059 if (c.retrieval_capacity > 0 || !c.retrieval_queues.empty()) {
1060 json rs;
1061 rs["capacity"] = c.retrieval_capacity;
1062 json by;
1063 for (const auto& kv : c.retrieval_queues) {
1064 json entry;
1065 json qs = json::array();
1066 for (std::size_t q : kv.second) qs.push_back(sn.nodes[q - 1].name);
1067 entry["queues"] = qs;
1068 json items;
1069 for (std::size_t it = 0; it < c.retrieval_classes.size(); ++it)
1070 if (kv.first < c.retrieval_classes[it].size() &&
1071 c.retrieval_classes[it][kv.first] != 0)
1072 items[std::to_string(it)] =
1073 sn.classes[c.retrieval_classes[it][kv.first] - 1].name;
1074 entry["items"] = items;
1075 by[sn.classes[kv.first].name] = entry;
1076 }
1077 rs["byClass"] = by;
1078 nj["retrievalSystem"] = rs;
1079 }
1080 }
1081 }
1082 {
1083 typename std::map<std::size_t, std::vector<T> >::const_iterator im =
1084 sn.initmarking.find(ind);
1085 if (im != sn.initmarking.end()) nj["initialState"] = detail::vec_to_json(im->second);
1086 typename std::map<std::size_t, std::vector<T> >::const_iterator sp =
1087 sn.stateprior.find(ind);
1088 typename std::map<std::size_t, Matrix<T> >::const_iterator ss = sn.statespace.find(ind);
1089 if (sp != sn.stateprior.end() && ss != sn.statespace.end()) {
1090 nj["stateSpace"] = detail::mat_to_json(ss->second);
1091 nj["statePrior"] = detail::vec_to_json(sp->second);
1092 }
1093 }
1094
1095 if (ist != 0) {
1096 const qn::Station<T>& st = sn.stations[ist - 1];
1099 nj["scheduling"] = detail::sched_to_json(st.sched);
1100 // The server count of a multiserver Queue, and of a QUEUEING PLACE
1101 // whose embedded queue has more than one: the Place branch of the
1102 // reader defaults it to one, so a c > 1 that is not written here is
1103 // a c that does not survive the round trip.
1105 std::isfinite(st.nservers) && st.nservers > 1.0)
1106 nj["servers"] = st.nservers;
1107 if (std::isfinite(st.cap) && st.cap > 0) nj["buffer"] = st.cap;
1108 // A fluid place (FSPN) holds one level instead of tokens; PlaceCodec.m writes the bound and the
1109 // initial level only when they differ from the defaults (Inf, 0).
1110 if (nd.nodetype == lang::NodeType::Place && st.fluid) {
1111 nj["fluid"] = true;
1112 if (std::isfinite(st.fluidcap)) nj["fluidCapacity"] = st.fluidcap;
1113 if (st.fluidinit > 0.0) nj["fluidLevel"] = st.fluidinit;
1114 }
1115
1116 json svc, cc, dr, sp, imf;
1117 for (std::size_t r = 0; r < K; ++r) {
1118 if (ist - 1 < sn.service.size() && r < sn.service[ist - 1].size() &&
1119 !sn.service[ist - 1][r].disabled)
1120 svc[sn.classes[r].name] = detail::dist_to_json(sn.service[ist - 1][r]);
1121 if (r < st.classcap.size() && std::isfinite(st.classcap[r]))
1122 cc[sn.classes[r].name] = st.classcap[r];
1123 // 0 is the "the model declared none" sentinel of the station's
1124 // own vector, not a DropStrategy; `refresh_capacity` reads it
1125 // as `user_rule` for exactly that reason. Naming it would have
1126 // to invent a rule, so the entry is simply not written.
1127 if (r < st.droprule.size() && st.droprule[r] != 0)
1128 dr[sn.classes[r].name] =
1129 detail::drop_to_json(static_cast<lang::DropStrategy>(st.droprule[r]));
1130 if (r < st.immfeed.size() && st.immfeed[r]) imf[sn.classes[r].name] = true;
1131 }
1134 for (std::size_t r = 0; r < K && r < st.schedparam.size(); ++r)
1135 sp[sn.classes[r].name] = num_traits<T>::to_double(st.schedparam[r]);
1136 if (!svc.empty()) nj["service"] = svc;
1137 if (!cc.empty()) nj["classCap"] = cc;
1138 if (!dr.empty()) nj["dropRule"] = dr;
1139 if (!sp.empty()) nj["schedParams"] = sp;
1140 if (!imf.empty()) nj["immediateFeedback"] = imf;
1141
1142 if (!st.lldscaling.empty()) {
1143 json ld;
1144 ld["type"] = "loadDependent";
1145 ld["scaling"] = detail::vec_to_json(st.lldscaling);
1146 nj["loadDependence"] = ld;
1147 }
1148 // The two rate-scaling handles, materialized over the extent that
1149 // reproduces them on every reachable state (detail::cd_scaling_extent):
1150 // the only way a function reaches the wire.
1151 for (int kind = 0; kind < 2; ++kind) {
1152 const lang::CdScaling<T>& fun = kind == 0 ? st.cdscaling : st.jdscaling;
1153 if (!static_cast<bool>(fun)) continue;
1154 json blk;
1155 blk["type"] = kind == 0 ? "classDependent" : "jointDependent";
1156 std::vector<double> bb(K, std::numeric_limits<double>::infinity());
1157 if (ist - 1 < sn.classcap.size())
1158 for (std::size_t r = 0; r < K && r < sn.classcap[ist - 1].size(); ++r)
1159 bb[r] = sn.classcap[ist - 1][r];
1160 const double C = ist - 1 < sn.cap.size() ? sn.cap[ist - 1]
1161 : std::numeric_limits<double>::infinity();
1162 detail::cd_scaling_extent<T>(fun, K, bb, C, blk);
1163 blk["peak"] = detail::vec_to_json(kind == 0 ? st.cdscalingpeak : st.jdscalingpeak);
1164 nj[kind == 0 ? "classDependence" : "jointDependence"] = blk;
1165 }
1166 {
1167 typename std::map<std::size_t, typename SN::PasParam>::const_iterator pas =
1168 sn.pasparam.find(ist);
1169 if (pas != sn.pasparam.end() && static_cast<bool>(pas->second.svc_rate_fun)) {
1170 // The extent of Python `save_model`: the saturation box with
1171 // `oiCutoffs` when mu saturates, else the simplex up to the
1172 // declared buffer with no cutoffs. Unbounded and uncapped saves
1173 // over the population box, exact only when every class is closed
1174 // with a finite population; an open class would be clamped at 10
1175 // and read back wrong, so it is refused with Python's message.
1176 json tbl;
1177 const auto put = [&](const std::vector<int>& cnt, const std::string& key) {
1178 std::vector<std::size_t> micro;
1179 for (std::size_t r = 0; r < K; ++r)
1180 for (int c = 0; c < cnt[r]; ++c) micro.push_back(r + 1);
1181 const double v = num_traits<T>::to_double(pas->second.svc_rate_fun(micro));
1182 tbl[key] = std::isfinite(v) ? v : 0.0; // the writers' encoding of an undefined rate
1183 };
1184 // The search reads only the states the declared buffer can hold,
1185 // so a reloaded table, whatever it answers above the buffer,
1186 // yields the same document (save -> load -> save fixed point).
1187 const double cmax = std::isfinite(st.cap)
1188 ? std::max(std::floor(st.cap), 1.0)
1189 : std::numeric_limits<double>::infinity();
1190 const PasSaturation sat =
1191 pas_saturation_cutoffs<T>(pas->second.svc_rate_fun, K, cmax);
1192 if (sat.bounded) {
1193 detail::for_each_lattice_point(sat.tau, put);
1194 nj["oiServiceRate"] = tbl;
1195 nj["oiCutoffs"] = sat.tau;
1196 } else if (std::isfinite(st.cap)) {
1197 detail::for_each_simplex_point(
1198 K, std::max(static_cast<int>(std::floor(st.cap)), 1), put);
1199 nj["oiServiceRate"] = tbl;
1200 } else {
1201 for (std::size_t r = 0; r < K; ++r)
1202 if (sn.classes[r].type != lang::JobClassType::CLOSED ||
1203 !std::isfinite(sn.classes[r].population))
1204 throw InputError("PAS station '" + st.name +
1205 "' has an unbounded order-independent service rate (mu does not "
1206 "saturate) and no finite buffer; call setCap(N) on the station.");
1207 detail::for_each_lattice_point(cut, put);
1208 nj["oiServiceRate"] = tbl;
1209 nj["oiCutoffs"] = cut;
1210 }
1211 bool any_swap = false;
1212 for (const std::vector<bool>& row : pas->second.swap_graph)
1213 for (bool v : row) any_swap = any_swap || v;
1214 if (any_swap) {
1215 json sg = json::array();
1216 for (const std::vector<bool>& row : pas->second.swap_graph) {
1217 json rj = json::array();
1218 for (bool v : row) rj.push_back(v ? 1 : 0);
1219 sg.push_back(rj);
1220 }
1221 nj["swapGraph"] = sg;
1222 }
1223 }
1224 }
1225 if (!st.polling_type.empty()) {
1226 nj["pollingType"] = detail::polling_to_json(st.polling_type[0]);
1227 if (st.polling_par != 0) nj["pollingPar"] = st.polling_par;
1228 }
1229 {
1230 // One key, two shapes, as `linemodel_save` writes them: under
1231 // POLLING an entry names the buffer the server LEAVES and
1232 // carries no `to`; otherwise it names the ordered class PAIR.
1233 json so = json::array();
1234 for (std::size_t r = 0; r < K && r < st.switchover.size(); ++r) {
1235 if (st.switchover[r].disabled) continue;
1236 json e;
1237 e["from"] = sn.classes[r].name;
1238 e["distribution"] = detail::dist_to_json(st.switchover[r]);
1239 so.push_back(e);
1240 }
1241 for (std::size_t r = 0; r < K && r < st.switchover_pair.size(); ++r)
1242 for (std::size_t c = 0; c < K && c < st.switchover_pair[r].size(); ++c) {
1243 if (st.switchover_pair[r][c].disabled) continue;
1244 json e;
1245 e["from"] = sn.classes[r].name;
1246 e["to"] = sn.classes[c].name;
1247 e["distribution"] = detail::dist_to_json(st.switchover_pair[r][c]);
1248 so.push_back(e);
1249 }
1250 if (!so.empty()) nj["switchoverTimes"] = so;
1251 }
1252 {
1253 typename std::map<std::size_t, qn::SetupDelayOffParam<T> >::const_iterator sd =
1254 sn.setupparam.find(ist);
1255 if (sd != sn.setupparam.end()) {
1256 json su, doff;
1257 for (std::size_t r = 0; r < K && r < sd->second.setup.size(); ++r) {
1258 if (sd->second.setup[r].disabled) continue;
1259 su[sn.classes[r].name] = detail::dist_to_json(sd->second.setup[r]);
1260 doff[sn.classes[r].name] = detail::dist_to_json(sd->second.delayoff[r]);
1261 }
1262 if (!su.empty()) {
1263 nj["setupTime"] = su;
1264 nj["delayOffTime"] = doff;
1265 }
1266 }
1267 }
1268 {
1269 // Server breakdown, in `linemodel_save`'s shape: the two clocks
1270 // always, and `downService` keyed by class name only where a
1271 // degraded rate was declared. A zero rate is an ABSENT entry and
1272 // not a zero-mean distribution, which has no reading.
1273 typename std::map<std::size_t, qn::BreakdownParam<T> >::const_iterator bd =
1274 sn.breakdownparam.find(ist);
1275 if (bd != sn.breakdownparam.end()) {
1276 json bj;
1277 bj["failure"] = detail::dist_to_json(bd->second.failure);
1278 bj["repair"] = detail::dist_to_json(bd->second.repair);
1279 json ds;
1280 for (std::size_t r = 0; r < K && r < bd->second.down_service_rates.size(); ++r) {
1281 const double rate =
1282 num_traits<T>::to_double(bd->second.down_service_rates[r]);
1283 if (!(rate > 0.0)) continue;
1284 ds[sn.classes[r].name] = detail::dist_to_json(
1286 }
1287 if (!ds.empty()) bj["downService"] = ds;
1288 nj["breakdown"] = bj;
1289 }
1290 }
1291 {
1292 typename std::map<std::size_t, qn::RetrialParam<T> >::const_iterator rt =
1293 sn.retrialparam.find(ist);
1294 if (rt != sn.retrialparam.end()) {
1295 json rj;
1296 for (std::size_t r = 0; r < K && r < rt->second.retrial_proc.size(); ++r) {
1297 if (rt->second.retrial_proc[r].disabled) continue;
1298 json e;
1299 e["delay"] = detail::dist_to_json(rt->second.retrial_proc[r]);
1300 e["maxAttempts"] = rt->second.max_attempts[r];
1301 if (r < rt->second.policy.size() && rt->second.policy[r] == 2)
1302 e["policy"] = "constant";
1303 if (r < rt->second.max_orbit.size() && rt->second.max_orbit[r] >= 0)
1304 e["maxOrbit"] = rt->second.max_orbit[r];
1305 rj[sn.classes[r].name] = e;
1306 }
1307 if (!rj.empty()) nj["retrial"] = rj;
1308 }
1309 }
1310 {
1311 json pat, orb, brp, blk;
1312 for (std::size_t r = 0; r < K; ++r) {
1313 if (r < st.patience.size() && !st.patience[r].disabled) {
1314 json e;
1315 e["distribution"] = detail::dist_to_json(st.patience[r]);
1316 if (r < st.impatience.size() &&
1318 e["impatienceType"] = detail::impatience_to_json(st.impatience[r]);
1319 pat[sn.classes[r].name] = e;
1320 }
1321 if (r < st.orbit_impatience.size() && !st.orbit_impatience[r].disabled)
1322 orb[sn.classes[r].name] = detail::dist_to_json(st.orbit_impatience[r]);
1323 if (r < st.batch_reject.size() &&
1325 brp[sn.classes[r].name] = num_traits<T>::to_double(st.batch_reject[r]);
1326 if (r < st.balking.size() &&
1327 st.balking[r].strategy != lang::BalkingStrategy::NONE) {
1328 json e;
1329 e["strategy"] = detail::balking_to_json(st.balking[r].strategy);
1330 json ths = json::array();
1331 for (const typename qn::Station<T>::BalkingThreshold& th :
1332 st.balking[r].thresholds) {
1333 json tj;
1334 tj["minJobs"] = th.min_jobs;
1335 tj["maxJobs"] = th.max_jobs;
1336 tj["probability"] = num_traits<T>::to_double(th.probability);
1337 ths.push_back(tj);
1338 }
1339 e["thresholds"] = ths;
1340 blk[sn.classes[r].name] = e;
1341 }
1342 }
1343 if (!pat.empty()) nj["patience"] = pat;
1344 if (!orb.empty()) nj["orbitImpatience"] = orb;
1345 if (!brp.empty()) nj["batchRejectProb"] = brp;
1346 if (!blk.empty()) nj["balking"] = blk;
1347 }
1348 if (!st.server_types.empty()) {
1349 json sts = json::array();
1350 for (const typename qn::Station<T>::ServerType& t : st.server_types) {
1351 json tj;
1352 tj["name"] = t.name;
1353 tj["count"] = t.count;
1354 if (!t.compatible.empty()) {
1355 json cn = json::array();
1356 for (std::size_t r = 0; r < K && r < t.compatible.size(); ++r)
1357 if (t.compatible[r]) cn.push_back(sn.classes[r].name);
1358 tj["compatibleClasses"] = cn;
1359 }
1360 json sv;
1361 for (std::size_t r = 0; r < K && r < t.service.size(); ++r)
1362 if (!t.service[r].disabled)
1363 sv[sn.classes[r].name] = detail::dist_to_json(t.service[r]);
1364 if (!sv.empty()) tj["service"] = sv;
1365 sts.push_back(tj);
1366 }
1367 nj["serverTypes"] = sts;
1369 nj["heteroSchedPolicy"] = detail::hetero_to_json(st.hetero_policy);
1370 }
1371 {
1372 json par;
1373 for (std::size_t r = 0; r < K && r < st.server_parallelism.size(); ++r)
1374 if (st.server_parallelism[r] > 1)
1375 par[sn.classes[r].name] = st.server_parallelism[r];
1376 if (!par.empty()) nj["serverParallelism"] = par;
1377 }
1378 {
1379 json ab;
1380 for (std::size_t r = 0; r < K && r < st.arrival_batch.size(); ++r)
1381 if (!st.arrival_batch[r].disabled)
1382 ab[sn.classes[r].name] = detail::dist_to_json(st.arrival_batch[r]);
1383 if (!ab.empty()) nj["arrivalBatch"] = ab;
1384 if (!st.marked_classes.empty()) {
1385 json mc = json::array();
1386 for (std::size_t c : st.marked_classes) mc.push_back(sn.classes[c - 1].name);
1387 nj["markedClasses"] = mc;
1388 }
1389 json dd;
1390 for (std::size_t r = 0; r < K && r < st.departure_discipline.size(); ++r)
1392 dd[sn.classes[r].name] = "FIFO";
1393 if (!dd.empty()) nj["departureDiscipline"] = dd;
1394 }
1395 }
1396
1397 {
1398 typename std::map<std::size_t, qn::TransitionParam<T> >::const_iterator tp =
1399 sn.transparam.find(ind);
1400 if (tp != sn.transparam.end()) {
1401 json modes = json::array();
1402 for (std::size_t m = 0; m < tp->second.nmodes; ++m) {
1403 json mj;
1404 mj["name"] = tp->second.modenames[m];
1405 mj["timingStrategy"] =
1406 tp->second.timing[m] == lang::TimingStrategy::IMMEDIATE ? "IMMEDIATE"
1407 : "TIMED";
1408 if (!tp->second.firingproc[m].disabled)
1409 mj["distribution"] = detail::dist_to_json(tp->second.firingproc[m]);
1410 mj["numServers"] = tp->second.nmodeservers[m];
1411 mj["firingPriority"] = tp->second.firingprio[m];
1412 mj["firingWeight"] = num_traits<T>::to_double(tp->second.fireweight[m]);
1413 const char* kArcKey[3] = {"enablingConditions", "inhibitingConditions",
1414 "firingOutcomes"};
1415 for (int which = 0; which < 3; ++which) {
1416 const Matrix<T>& row = which == 0 ? tp->second.enabling[m]
1417 : which == 1 ? tp->second.inhibiting[m]
1418 : tp->second.firing[m];
1419 json arcs = json::array();
1420 for (std::size_t q = 0; q < row.rows(); ++q)
1421 for (std::size_t r = 0; r < row.cols(); ++r) {
1422 const double v = num_traits<T>::to_double(row(q, r));
1423 // An inhibiting threshold of Inf is "never
1424 // blocks" and an enabling or firing count of 0
1425 // is no arc.
1426 if (which == 1 ? !std::isfinite(v) : v == 0.0) continue;
1427 json a;
1428 a["node"] = sn.nodes[q].name;
1429 // REQUIRED by the reference readers: both index
1430 // the arc by (node, CLASS) -- `linemodel_load.m:
1431 // 852` reads `ec.class` unguarded and
1432 // `linemodel_io.py` does `ic["class"]` -- so an
1433 // arc written without it makes the whole
1434 // document unloadable there.
1435 a["class"] = sn.classes[r].name;
1436 a["count"] = v;
1437 arcs.push_back(a);
1438 }
1439 if (!arcs.empty()) mj[kArcKey[which]] = arcs;
1440 }
1441 // A marking-dependent firing rate is a CLOSURE, so it is
1442 // written the only way it can cross JSON: materialized over
1443 // the box lattice of the enabling slots, exactly as
1444 // `firingdep_scaling_table` in linemodel_save.m does. Dropped
1445 // instead, the net reloads at its nominal rate -- a different
1446 // marking process, reported without a diagnostic.
1447 if (m < tp->second.firingdep.size() && tp->second.firingdep[m]) {
1448 const Matrix<T>& enab = tp->second.enabling[m];
1449 json slots = json::array();
1450 std::vector<std::size_t> slot_pos;
1451 std::vector<long> caps;
1452 // ONE SLOT PER (place, class) the mode reads, as linemodel_save.m writes it; the closure's
1453 // domain is the node-major flattened (node, class) marking `state_events.h` hands it.
1454 const std::size_t KW = sn.classes.size();
1455 for (std::size_t q = 0; q < enab.rows(); ++q)
1456 for (std::size_t rq = 0; rq < enab.cols(); ++rq) {
1457 if (!(num_traits<T>::to_double(enab(q, rq)) > 0.0)) continue;
1458 json sm;
1459 sm["node"] = sn.nodes[q].name;
1460 sm["class"] = sn.classes[rq].name;
1461 slots.push_back(sm);
1462 slot_pos.push_back(q * KW + rq);
1463 // The open-place saturation cutoff of the three
1464 // reference writers: an unbounded place would make
1465 // the lattice infinite, so it is tabulated to 10.
1466 const std::size_t sq = sn.nodes[q].station;
1467 const double pc = sq == 0 ? std::numeric_limits<double>::infinity()
1468 : sn.stations[sq - 1].cap;
1469 caps.push_back(std::isfinite(pc) ? std::lround(pc) : 10L);
1470 }
1471 if (!slots.empty()) {
1472 json frm;
1473 frm["slots"] = slots;
1474 frm["cutoffs"] = caps;
1475 json scaling;
1476 std::size_t total = 1;
1477 for (std::size_t s = 0; s < caps.size(); ++s)
1478 total *= static_cast<std::size_t>(caps[s]) + 1;
1479 std::vector<T> mk(sn.nodes.size() * KW, num_traits<T>::from_int(0));
1480 for (std::size_t li = 0; li < total; ++li) {
1481 std::size_t rem = li;
1482 std::string key;
1483 for (std::size_t s = 0; s < caps.size(); ++s) {
1484 const std::size_t shp = static_cast<std::size_t>(caps[s]) + 1;
1485 const std::size_t c = rem % shp;
1486 rem /= shp;
1487 mk[slot_pos[s]] = num_traits<T>::from_int(
1488 static_cast<long>(c));
1489 if (s) key += ',';
1490 key += std::to_string(c);
1491 }
1492 const double v =
1493 num_traits<T>::to_double(tp->second.firingdep[m](mk));
1494 scaling[key] = std::isfinite(v) ? v : 0.0;
1495 }
1496 for (std::size_t s = 0; s < slot_pos.size(); ++s)
1497 mk[slot_pos[s]] = num_traits<T>::from_int(0);
1498 frm["scaling"] = scaling;
1499 mj["firingRateDependence"] = frm;
1500 }
1501 }
1502 // Fluid arcs (FSPN), each naming its fluid place, as TransitionCodec.m writes them.
1503 const qn::TransitionParam<T>& ftp = tp->second;
1504 if (!ftp.fluid_rates(m).empty()) {
1505 json lst = json::array();
1506 for (const auto& a : ftp.fluid_rates(m)) {
1507 json e;
1508 e["node"] = sn.nodes[a.node - 1].name;
1509 e["rate"] = a.value;
1510 lst.push_back(e);
1511 }
1512 mj["fluidRates"] = lst;
1513 }
1514 if (!ftp.fluid_guards(m).empty()) {
1515 json lst = json::array();
1516 for (const auto& g : ftp.fluid_guards(m)) {
1517 json e;
1518 e["node"] = sn.nodes[g.node - 1].name;
1519 e["op"] = g.sgn > 0 ? ">=" : "<=";
1520 e["threshold"] = g.thr;
1521 lst.push_back(e);
1522 }
1523 mj["fluidGuards"] = lst;
1524 }
1525 if (!ftp.fluid_impulses(m).empty()) {
1526 json lst = json::array();
1527 for (const auto& a : ftp.fluid_impulses(m)) {
1528 json e;
1529 e["node"] = sn.nodes[a.node - 1].name;
1530 e["delta"] = a.value;
1531 lst.push_back(e);
1532 }
1533 mj["fluidImpulses"] = lst;
1534 }
1535 // A level-dependent multiplier is a closure, so it crosses JSON as `fluiddep_table`: 201 equally
1536 // spaced levels on [0, L] and h at each, L being `fluidTableRange` of TransitionCodec.m.
1537 if (!ftp.fluid_deps(m).empty()) {
1538 json lst = json::array();
1539 for (const auto& d : ftp.fluid_deps(m)) {
1540 const std::size_t sq = sn.nodes[d.node - 1].station;
1541 const qn::Station<T>& fst = sn.stations[sq - 1];
1542 double L = fst.fluidcap;
1543 if (!std::isfinite(L)) {
1544 L = std::max(1.0, fst.fluidinit);
1545 for (const auto& kv : sn.transparam)
1546 for (std::size_t mm = 0; mm < kv.second.nmodes; ++mm)
1547 for (const auto& g : kv.second.fluid_guards(mm))
1548 if (g.node == d.node) L = std::max(L, std::fabs(g.thr));
1549 L = std::max(10.0, 10.0 * L);
1550 }
1551 const std::size_t npts = 201;
1552 json levels = json::array(), values = json::array();
1553 for (std::size_t i = 0; i < npts; ++i) {
1554 const double lv = L * static_cast<double>(i) / static_cast<double>(npts - 1);
1555 levels.push_back(lv);
1556 values.push_back(d.fun(lv));
1557 }
1558 json e;
1559 e["node"] = sn.nodes[d.node - 1].name;
1560 e["levels"] = levels;
1561 e["values"] = values;
1562 lst.push_back(e);
1563 }
1564 mj["fluidRateDependence"] = lst;
1565 }
1566 // A marking-dependent flow multiplier is a closure too, materialized like firingRateDependence over
1567 // the box lattice of its (read place, class) slots, place-major and class-minor, slot 0 fastest.
1568 // A slot is cut off at the place capacity, at a closed class's population, and at 10 tokens for
1569 // an open class in an unbounded place, as TransitionCodec.markingSlots does.
1570 if (!ftp.fluid_flow_deps(m).empty()) {
1571 const std::vector<double> pop = sn.njobs();
1572 const std::size_t KF = sn.classes.size();
1573 json lst = json::array();
1574 for (const auto& fd : ftp.fluid_flow_deps(m)) {
1575 json slots = json::array(), readn = json::array();
1576 std::vector<std::size_t> sflat;
1577 std::vector<long> caps;
1578 for (std::size_t q : fd.read) {
1579 readn.push_back(sn.nodes[q - 1].name);
1580 const std::size_t sq = sn.nodes[q - 1].station;
1581 const double pc = sq == 0 ? std::numeric_limits<double>::infinity()
1582 : sn.stations[sq - 1].cap;
1583 for (std::size_t r = 0; r < KF; ++r) {
1584 json sm;
1585 sm["node"] = sn.nodes[q - 1].name;
1586 sm["class"] = sn.classes[r].name;
1587 slots.push_back(sm);
1588 sflat.push_back((q - 1) * KF + r);
1589 const double cc = r < pop.size() && std::isfinite(pop[r]) ? pop[r] : 10.0;
1590 caps.push_back(std::lround(std::min(pc, cc)));
1591 }
1592 }
1593 std::size_t total = 1;
1594 for (long c : caps) total *= static_cast<std::size_t>(c) + 1;
1595 std::vector<double> mk(sn.nodes.size() * KF, 0.0);
1596 json scaling = json::object();
1597 for (std::size_t li = 0; li < total; ++li) {
1598 std::size_t rem = li;
1599 std::string key;
1600 for (std::size_t s = 0; s < caps.size(); ++s) {
1601 const std::size_t shp = static_cast<std::size_t>(caps[s]) + 1;
1602 const std::size_t c = rem % shp;
1603 rem /= shp;
1604 mk[sflat[s]] = static_cast<double>(c);
1605 if (s) key += ',';
1606 key += std::to_string(c);
1607 }
1608 const double v = fd.fun(mk);
1609 scaling[key] = std::isfinite(v) ? v : 0.0;
1610 }
1611 json e;
1612 e["node"] = sn.nodes[fd.node - 1].name;
1613 e["read"] = readn;
1614 e["slots"] = slots;
1615 e["cutoffs"] = caps;
1616 e["scaling"] = scaling;
1617 lst.push_back(e);
1618 }
1619 mj["fluidFlowDependence"] = lst;
1620 }
1621 modes.push_back(mj);
1622 }
1623 nj["modes"] = modes;
1624 }
1625 }
1626 nodes.push_back(nj);
1627 }
1628 model["nodes"] = nodes;
1629
1630 // -- routing --------------------------------------------------------------
1631 json matrix;
1632 for (const auto& kv : sn.P) {
1633 const std::size_t r = kv.first.first, s = kv.first.second;
1634 json from_to;
1635 for (std::size_t a = 0; a < kv.second.rows(); ++a) {
1636 json row;
1637 for (std::size_t b = 0; b < kv.second.cols(); ++b) {
1638 const double p = num_traits<T>::to_double(kv.second(a, b));
1639 if (p != 0.0) row[sn.nodes[b].name] = p;
1640 }
1641 if (!row.empty()) from_to[sn.nodes[a].name] = row;
1642 }
1643 if (!from_to.empty())
1644 matrix[sn.classes[r - 1].name + "," + sn.classes[s - 1].name] = from_to;
1645 }
1646 json routing;
1647 routing["type"] = "matrix";
1648 routing["matrix"] = matrix;
1649 model["routing"] = routing;
1650
1651 json strategies, weights, params;
1652 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
1653 const qn::NodeDef& nd = sn.nodes[i];
1654 json per_class, per_class_w, per_class_p;
1655 for (std::size_t r = 0; r < K && r < nd.routing.size(); ++r) {
1656 // PROB is the matrix itself; RAND is DECLARED not derived, and dropping it wrote JMT's Empirical strategy where the model asks for Random.
1658 per_class[sn.classes[r].name] = detail::routing_to_json(nd.routing[r]);
1659 if (r < nd.routing_weights.size() && !nd.routing_weights[r].empty()) {
1660 json dw;
1661 for (const auto& kv : nd.routing_weights[r]) dw[sn.nodes[kv.first - 1].name] = kv.second;
1662 per_class_w[sn.classes[r].name] = dw;
1663 }
1664 if (r < nd.routing_param.size() && nd.routing_param[r] != 0) {
1665 json pj;
1666 pj["d"] = nd.routing_param[r];
1667 per_class_p[sn.classes[r].name] = pj;
1668 }
1669 }
1670 if (!per_class.empty()) strategies[nd.name] = per_class;
1671 if (!per_class_w.empty()) weights[nd.name] = per_class_w;
1672 if (!per_class_p.empty()) params[nd.name] = per_class_p;
1673 }
1674 if (!strategies.empty()) model["routingStrategies"] = strategies;
1675 if (!weights.empty()) model["routingWeights"] = weights;
1676 if (!params.empty()) model["routingParams"] = params;
1677
1678 // Krzesinski state-dependent routing. Every centre travels by NODE NAME, so
1679 // the block is language independent and a node reordering on either side
1680 // cannot shift a centre. Branch index 1 denotes the complement M-V and is
1681 // written as an empty list, keeping the paper's own numbering.
1682 // See _kb/16-state-dependent-routing.md
1683 if (!sn.sdr_nodes.empty()) {
1684 const pfqn::SdrStruct& sd = sn.sdr_nodes; // 0-based NODE indices
1685 std::size_t cls = 0;
1686 for (std::size_t r = 0; r < K && cls == 0; ++r)
1687 if (sn.nodes[sd.entry].routing.size() > r &&
1688 sn.nodes[sd.entry].routing[r] == lang::RoutingStrategy::SDR)
1689 cls = r + 1;
1690 if (cls == 0)
1691 throw InputError("network_writer: the model declares state-dependent routing at node '" +
1692 sn.nodes[sd.entry].name + "' but no class routes SDR there");
1693 json sdr;
1694 sdr["entry"] = sn.nodes[sd.entry].name;
1695 sdr["departure"] = sn.nodes[sd.departure].name;
1696 sdr["class"] = sn.classes[cls - 1].name;
1697 json branches = json::array();
1698 for (std::size_t b = 0; b < sd.branch.size(); ++b) {
1699 json bn = json::array();
1700 for (std::size_t q = 0; q < sd.branch[b].size(); ++q)
1701 bn.push_back(sn.nodes[sd.branch[b][q]].name);
1702 branches.push_back(bn);
1703 }
1704 sdr["branches"] = branches;
1705 json level = json::array(), Cs = json::array(), dm = json::array();
1706 for (std::size_t b = 0; b < sd.level.size(); ++b)
1707 level.push_back(static_cast<double>(sd.level[b]));
1708 for (std::size_t t = 0; t < sd.C.size(); ++t) Cs.push_back(sd.C[t]);
1709 for (std::size_t t = 0; t < sd.d.rows(); ++t) {
1710 json row = json::array();
1711 for (std::size_t b = 0; b < sd.d.cols(); ++b) row.push_back(sd.d(t, b));
1712 dm.push_back(row);
1713 }
1714 sdr["level"] = level;
1715 sdr["C"] = Cs;
1716 sdr["d"] = dm;
1717 model["stateDepRouting"] = sdr;
1718 }
1719
1720 // -- finite capacity regions ---------------------------------------------
1721 if (!sn.regions.empty()) {
1722 json fcr = json::array();
1723 for (std::size_t g = 0; g < sn.regions.size(); ++g) {
1724 const typename SN::Region& rg = sn.regions[g];
1725 json rj;
1726 rj["name"] = rg.name.empty() ? "Region" + std::to_string(g + 1) : rg.name;
1727 json stations = json::array();
1728 double global = -1.0, globalmem = -1.0;
1729 json class_cap, class_size, class_weight;
1730 for (std::size_t m = 0; m < rg.members.size(); ++m) {
1731 if (!rg.members[m]) continue;
1732 json sj;
1733 sj["node"] = sn.nodes[sn.station_to_node[m] - 1].name;
1734 stations.push_back(sj);
1735 global = rg.cap[m][K];
1736 // The region's global MEMORY budget, which is enforced beside the
1737 // job cap by the CTMC, the loss-network NC arm and the JMT export
1738 // rather than folded into it. Omitted, the region reloads with
1739 // unbounded memory and every one of those three drops a
1740 // constraint the model declared.
1741 if (m < rg.maxmem.size()) globalmem = rg.maxmem[m];
1742 for (std::size_t r = 0; r < K; ++r)
1743 if (rg.cap[m][r] != -1.0) class_cap[sn.classes[r].name] = rg.cap[m][r];
1744 }
1745 rj["stations"] = stations;
1746 if (global != -1.0) rj["globalMaxJobs"] = global;
1747 if (globalmem != -1.0) rj["globalMaxMemory"] = globalmem;
1748 if (!class_cap.empty()) rj["classMaxJobs"] = class_cap;
1749 json rule;
1750 for (std::size_t r = 0; r < K && r < rg.rule.size(); ++r)
1751 rule[sn.classes[r].name] = detail::drop_to_json(rg.rule[r]);
1752 rj["dropRule"] = rule;
1753 for (std::size_t r = 0; r < K; ++r) {
1754 if (r < rg.size.size() && num_traits<T>::to_double(rg.size[r]) != 1.0)
1755 class_size[sn.classes[r].name] = num_traits<T>::to_double(rg.size[r]);
1756 if (r < rg.weight.size() && num_traits<T>::to_double(rg.weight[r]) != 1.0)
1757 class_weight[sn.classes[r].name] = num_traits<T>::to_double(rg.weight[r]);
1758 }
1759 // classSize and classWeight are per-STATION on the wire, and the
1760 // struct holds one vector per region, so they are written onto every
1761 // member station -- which is the same model the reader rebuilds.
1762 if (!class_size.empty() || !class_weight.empty())
1763 for (json& sj : rj["stations"]) {
1764 if (!class_size.empty()) sj["classSize"] = class_size;
1765 if (!class_weight.empty()) sj["classWeight"] = class_weight;
1766 }
1767 if (rg.lincon_A.rows() > 0) {
1768 rj["constraintA"] = detail::mat_to_json(rg.lincon_A);
1769 rj["constraintB"] = detail::vec_to_json(rg.lincon_b);
1770 }
1771 fcr.push_back(rj);
1772 }
1773 model["finiteCapacityRegions"] = fcr;
1774 }
1775
1776 // -- global (Whittle) dependence -----------------------------------------
1777 //
1778 // phi(n) reads the FULL population matrix, so unlike the per-station
1779 // classDependence/jointDependence blocks it is materialized over the lattice
1780 // of the WHOLE network state. Only the (station,class) SLOTS a class can
1781 // actually occupy carry a coordinate: a Source holds no jobs and a class with
1782 // zero per-class capacity at a station never appears there. That restriction
1783 // is lossless, since no DEP or PHASE event ever fires at such a slot.
1784 if (static_cast<bool>(sn.gdscaling)) {
1785 const std::size_t M = sn.nstations, K = sn.nclasses;
1786 const std::vector<double> njobs = sn.njobs();
1787 const int wcut = sn.gdscalingcutoff;
1788 std::vector<std::size_t> slot_st, slot_cl;
1789 std::vector<int> cuts;
1790 for (std::size_t i = 0; i < M; ++i) {
1791 if (sn.nodes[sn.station_to_node[i] - 1].nodetype == lang::NodeType::Source) continue;
1792 for (std::size_t r = 0; r < K; ++r) {
1793 const double cap = sn.classcap[i][r];
1794 if (!(cap > 0)) continue;
1795 int c = std::isfinite(njobs[r]) ? static_cast<int>(std::lround(njobs[r])) : wcut;
1796 if (std::isfinite(cap)) c = std::min(c, static_cast<int>(std::lround(cap)));
1797 slot_st.push_back(i);
1798 slot_cl.push_back(r);
1799 cuts.push_back(c > 0 ? c : 0);
1800 }
1801 }
1802 const std::size_t P = cuts.size();
1803 std::size_t total = 1;
1804 for (std::size_t d = 0; d < P; ++d) {
1805 total *= static_cast<std::size_t>(cuts[d] + 1);
1806 if (total > 200000u)
1807 throw InputError(
1808 "the global dependence lattice exceeds the wire limit of 200000 points; lower "
1809 "the wireCutoff argument of set_global_dependence, or solve the model "
1810 "natively");
1811 }
1812
1813 json blk, slots, tbl;
1814 blk["type"] = "globalDependent";
1815 std::vector<std::string> station_names(M), class_names(K);
1816 for (std::size_t i = 0; i < M; ++i)
1817 station_names[i] = sn.nodes[sn.station_to_node[i] - 1].name;
1818 for (std::size_t r = 0; r < K; ++r) class_names[r] = sn.classes[r].name;
1819 blk["stations"] = station_names;
1820 blk["classes"] = class_names;
1821 slots = json::array();
1822 for (std::size_t d = 0; d < P; ++d) {
1823 json sm;
1824 sm["station"] = station_names[slot_st[d]];
1825 sm["class"] = class_names[slot_cl[d]];
1826 slots.push_back(sm);
1827 }
1828 blk["slots"] = slots;
1829 blk["cutoffs"] = cuts;
1830 blk["cutoff"] = wcut;
1831
1832 for (std::size_t li = 0; li < total; ++li) {
1833 std::size_t rem = li;
1834 std::vector<int> cnt(P, 0);
1835 for (std::size_t d = 0; d < P; ++d) {
1836 cnt[d] = static_cast<int>(rem % static_cast<std::size_t>(cuts[d] + 1));
1837 rem /= static_cast<std::size_t>(cuts[d] + 1);
1838 }
1839 std::vector<T> n(M * K, num_traits<T>::from_int(0));
1840 for (std::size_t d = 0; d < P; ++d)
1841 n[slot_st[d] * K + slot_cl[d]] = num_traits<T>::from_int(cnt[d]);
1842 const std::vector<T> v = sn.gdscaling(n);
1843 std::vector<double> flat(M * K, 1.0);
1844 for (std::size_t i = 0; i < M; ++i)
1845 for (std::size_t r = 0; r < K; ++r) {
1846 double x;
1847 if (v.size() == 1) x = num_traits<T>::to_double(v[0]);
1848 else if (v.size() == M) x = num_traits<T>::to_double(v[i]);
1849 else x = num_traits<T>::to_double(v[i * K + r]);
1850 flat[i * K + r] = std::isfinite(x) ? x : 0.0;
1851 }
1852 std::string key;
1853 if (P == 0) key = "0";
1854 else
1855 for (std::size_t d = 0; d < P; ++d) {
1856 if (d) key += ',';
1857 key += std::to_string(cnt[d]);
1858 }
1859 tbl[key] = flat;
1860 }
1861 blk["scaling"] = tbl;
1862 std::vector<double> pk(M * K, 1.0);
1863 for (std::size_t j = 0; j < pk.size() && j < sn.gdscalingpeak.size(); ++j)
1864 pk[j] = num_traits<T>::to_double(sn.gdscalingpeak[j]);
1865 blk["peak"] = pk;
1866 model["globalDependence"] = blk;
1867 }
1868
1869 // -- rewards --------------------------------------------------------------
1870 //
1871 // The DECLARATIVE ones only. A reward built from a lambda carries no `kind`
1872 // and is dropped here, exactly as `linemodel_save` drops it: any record
1873 // written for it would reload as a different function.
1874 {
1875 // IN NAME ORDER, which is the cross-codebase convention and not a
1876 // cosmetic choice: `rewards2json` sorts for it explicitly, because the
1877 // JAR holds the rewards in a HashMap and has no insertion order to
1878 // preserve. Emitting them in struct order made the same model produce a
1879 // different document here than in the other three, which the JSON arm of
1880 // the parity harness reports as a difference in the model.
1881 std::map<std::string, json> by_name;
1882 for (const typename SN::Reward& rw : sn.reward) {
1883 if (rw.kind.empty() || rw.node == 0) continue;
1884 json rj;
1885 rj["name"] = rw.name;
1886 rj["type"] = rw.kind;
1887 rj["node"] = sn.nodes[rw.node - 1].name;
1888 if (rw.cls != 0) rj["class"] = sn.classes[rw.cls - 1].name;
1889 by_name[rw.name] = rj;
1890 }
1891 json rewards = json::array();
1892 for (const std::pair<const std::string, json>& kv : by_name) rewards.push_back(kv.second);
1893 if (!rewards.empty()) model["rewards"] = rewards;
1894 }
1895 return model;
1896}
1897
1898/** The complete model.json envelope: `{format, version, model}`. */
1899template <class T>
1901 detail::json root;
1902 root["format"] = "line-model";
1903 root["version"] = "1.0";
1904 root["model"] = network_to_json(sn);
1905 detail::wire_nonfinite(root);
1906 return root;
1907}
1908
1909/**
1910 * Refuse an OI/PAS station with no finite buffer before it reaches LDES.
1911 *
1912 * The reference refusal is Python `save_model`'s (`linemodel_io.py`): mu(c)
1913 * crosses the wire as a table, and LDES state-space generation needs a finite
1914 * buffer whether or not the rate saturates, since saturation only keeps the
1915 * table compact and cannot bound the queue length. The writer itself refuses
1916 * only an open class with a non-saturating mu; a saturating mu, which it saves
1917 * exactly, still needs this gate to bound the state. The messages are the
1918 * reference's, word for word, naming the station.
1919 */
1920template <class T>
1922 const std::size_t K = sn.classes.size();
1923 for (typename std::map<std::size_t, typename qn::NetworkStruct<T>::PasParam>::const_iterator
1924 it = sn.pasparam.begin();
1925 it != sn.pasparam.end(); ++it) {
1926 if (!static_cast<bool>(it->second.svc_rate_fun)) continue;
1927 const std::size_t ist = it->first;
1928 if (ist < 1 || ist > sn.stations.size()) continue;
1929 const double cap = sn.stations[ist - 1].cap;
1930 if (std::isfinite(cap)) continue;
1931 const std::string& name = sn.stations[ist - 1].name;
1932 const PasSaturation s = pas_saturation_cutoffs<T>(it->second.svc_rate_fun, K);
1933 if (s.bounded) {
1934 std::string tau = "[";
1935 for (std::size_t r = 0; r < s.tau.size(); ++r)
1936 tau += (r ? ", " : "") + std::to_string(s.tau[r]);
1937 tau += "]";
1938 throw InputError("PAS station '" + name +
1939 "' has no finite buffer: LDES state-space generation requires one. "
1940 "Its order-independent service rate saturates at class counts " +
1941 tau +
1942 ", so call setCap(N) with N comfortably above the mean occupancy.");
1943 }
1944 throw InputError("PAS station '" + name +
1945 "' has an unbounded order-independent service rate (mu does not "
1946 "saturate) and no finite buffer; call setCap(N) on the station.");
1947 }
1948}
1949
1950/** Write a model.json file, indented as the reference writers indent it. */
1951template <class T>
1952void write_network_json(const qn::NetworkStruct<T>& sn, const std::string& path) {
1953 std::ofstream out(path.c_str());
1954 if (!out) throw InputError("network_writer: cannot open " + path + " for writing");
1955 out << network_json_envelope(sn).dump(2) << "\n";
1956}
1957
1958} // namespace io
1959} // namespace line
1960
1961#endif // LINE_IO_NETWORK_WRITER_H
InputError(const std::string &what)
Definition error.h:105
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:119
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.
void require_finite_pas_buffers(const qn::NetworkStruct< T > &sn)
Refuse an OI/PAS station with no finite buffer before it reaches LDES.
PasSaturation saturation_cutoffs(const std::function< std::vector< double >(const std::vector< int > &)> &f_at, std::size_t K, double cmax=std::numeric_limits< double >::infinity(), int max_cutoff=256, double tol=1e-9)
Per-class saturation cutoffs of a count-indexed function f(n), the search behind the OI/PAS and class...
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.
PasSaturation pas_saturation_cutoffs(const std::function< T(const std::vector< std::size_t > &)> &mu, std::size_t K, double cmax=std::numeric_limits< double >::infinity())
The saturation cutoffs of an order-independent rate mu(c), searched over the states a buffer of cmax ...
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:431
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:370
BalkingStrategy
Balking rules, with the values of MATLAB BalkingStrategy.
Definition lang_types.h:452
SignalType
G-network signal classes, with the values of MATLAB SignalType.
Definition lang_types.h:171
@ REPLY
completes a synchronous call, releasing a held server
Definition lang_types.h:172
@ CATASTROPHE
removes EVERY job at the station
Definition lang_types.h:174
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
@ SDR
Krzesinski (1987) product-form state-dependent routing.
Definition lang_types.h:405
RemovalPolicy
Which job a negative signal removes, with the values of MATLAB RemovalPolicy.
Definition lang_types.h:178
@ FCFS
the oldest waiting job; servers only once nobody waits
Definition lang_types.h:180
@ RANDOM
uniform over waiting AND in-service jobs
Definition lang_types.h:179
HeteroSchedPolicy
How a heterogeneous station picks among its server types, MATLAB HeteroSchedPolicy.
Definition lang_types.h:458
PollingType
Polling service disciplines, with the values of MATLAB PollingType.
Definition lang_types.h:377
@ KLIMITED
serve at most K per visit (K in pollingPar)
Definition lang_types.h:380
@ EXHAUSTIVE
serve until the queue empties
Definition lang_types.h:379
@ GATED
serve exactly the jobs present at the polling instant
Definition lang_types.h:378
@ DECREMENTING
serve until the queue is one shorter than at arrival
Definition lang_types.h:381
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:235
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:736
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:385
@ HLRU
h-LRU / LRU(m): h lists, promote i -> i+1 on a hit
Definition lang_types.h:390
@ CLIMB
move up one position on a hit (transposition rule)
Definition lang_types.h:391
@ QLRU
q-LRU: LRU with probabilistic admission on a miss
Definition lang_types.h:392
@ LRU
least recently used
Definition lang_types.h:389
@ FIFO
first in, first out
Definition lang_types.h:387
ImpatienceType
Impatience kinds, with the values of MATLAB ImpatienceType.
Definition lang_types.h:449
double tot(const std::vector< double > &pops)
void put(Matrix< double > &A, std::size_t r0, std::size_t c0, const Matrix< double > &S)
A(r0:, c0:) = S.
Definition mg1.h:927
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.
The per-class saturation cutoffs of a count-indexed rate table.
bool bounded
false when some tau(r) passed the search limit or reached cmax
std::vector< int > tau
tau(r): beyond it the function no longer changes with class-r jobs
static Distrib exp_rate(const T &r)
Definition lang_types.h:955
std::vector< T > params
Constructor arguments, in MATLAB getParam order.
Definition lang_types.h:836
std::vector< T > trace
Replayer / Trace samples; empty for every other type.
Definition lang_types.h:838
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
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
int retrieval_capacity
Delayed-hit retrieval system (Cache.setRetrievalSystem).
T qlru
q-LRU admission probability; 1 admits every miss (plain LRU).
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
The pass-and-swap / order-independent parameters of a PAS station, keyed by station index (Dorsman an...
A Logger node's trace configuration, MATLAB's Logger properties and sn.nodeparam{ind}...
std::string file_name
base name, no directory
std::string file_path
directory, MATLAB's model.getLogPath
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
bool fluid
Place.setFluid (MATLAB sn.fluid): the place holds a continuous LEVEL rather than tokens,...
std::vector< bool > immfeed
Node-level immediate feedback, per class; empty when the station sets none.
std::vector< ServerType > server_types
The parameters of a Transition node: MATLAB sn.nodeparam{ind} for an SPN transition,...
const std::vector< FluidArc > & fluid_rates(std::size_t m) const
const std::vector< FluidArc > & fluid_impulses(std::size_t m) const
const std::vector< FluidDep > & fluid_deps(std::size_t m) const
const std::vector< FluidFlowDep > & fluid_flow_deps(std::size_t m) const
const std::vector< FluidGuard > & fluid_guards(std::size_t m) const