LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
network_reader.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_READER_H
6#define LINE_IO_NETWORK_READER_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * Reader for the LINE `model.json` interchange (a Network model) into a
12 * `qn::Network<T>` built through the programmatic builder.
13 *
14 * The wire format is the one `linemodel_save.m`, the Python writer and the JAR
15 * `LineModelIO` emit: a top-level `{format, version, model}` envelope whose
16 * `model.type == "Network"` carries `nodes`, `classes` and `routing`. This
17 * reader feeds the SAME builder + finalize the C++ programmatic API uses, so a
18 * model that reaches C++ this way is indistinguishable from one authored in
19 * code -- exactly the contract `lqn_builder.h` documents for its own reader.
20 *
21 * Scope: the queueing-network subset SolverMVA analyses. Distributions are
22 * honoured on the moments the analyzers read (mean, SCV and, for QNA/polling,
23 * the family the wire names). A construct outside this subset -- a
24 * LayeredNetwork / Workflow / Environment model, an unsupported node kind, or a
25 * distribution family whose moments this reader cannot reconstruct exactly --
26 * is REFUSED BY NAME rather than silently degraded, matching the "a featset
27 * name is a claim" rule the rest of the port follows.
28 */
29
30#include <cmath>
31#include <cstdlib>
32#include <fstream>
33#include <functional>
34#include <iostream>
35#include <limits>
36#include <map>
37#include <string>
38#include <vector>
39
40#include "json.hpp"
42#include "line/lang/prior.h"
44#include "line/num/number.h"
45#include "line/util/error.h"
46
47namespace line {
48namespace io {
49
50namespace detail {
51
52using json = nlohmann::json;
53
54/**
55 * A model.json number that may arrive as the wire's INFINITY SPELLING.
56 *
57 * JSON has no infinity literal, so `linemodel_save` writes any infinite scalar
58 * as the string `"Infinity"` / `"-Infinity"` -- a rule it applies to EVERY
59 * numeric field, not to a named few. A plain `value("k", def)` therefore throws
60 * `json.exception.type_error.302 type must be number, but is string` the moment
61 * a model carries one, and the throw names no key: `spn_basic_open` (a
62 * transition mode with `"numServers": "Infinity"`) failed identically under
63 * every solver, which reads as a solver defect rather than as a reader gap.
64 * The JAR (`LineModelIO`) and Python (`_servers_from_json`) readers both accept
65 * the string form; this is the C++ twin.
66 *
67 * `nan` is also accepted, since the writer emits `null` for NaN and a reader
68 * asked for a number from `null` would throw the same way.
69 *
70 * WHICH FIELDS NEED THIS, and why it is not every numeric read. The encoder's
71 * rule is generic, but `linemodel_save` GUARDS the fields whose natural value
72 * is infinite: `servers`, `buffer` and `classCap` are each emitted only when
73 * `isfinite`, and `population` is written only for the closed and self-looping
74 * classes, which are finite by construction. So the generic spelling is only
75 * reachable on an UNGUARDED field. Transition-mode `numServers` is the proven
76 * one, and all three writers agree it is the sentinel there. The remaining uses
77 * below are the fields a non-MATLAB writer could still send infinite. Do not
78 * take this as licence to convert the other `get<double>()` reads: a
79 * distribution parameter that arrives as a string is a malformed model, and
80 * should keep throwing.
81 */
82inline double num_from_json(const json& v) {
83 if (v.is_null()) return std::numeric_limits<double>::quiet_NaN();
84 if (v.is_string()) {
85 const std::string s = v.get<std::string>();
86 if (s == "Infinity" || s == "inf" || s == "Inf")
87 return std::numeric_limits<double>::infinity();
88 if (s == "-Infinity" || s == "-inf" || s == "-Inf")
89 return -std::numeric_limits<double>::infinity();
90 if (s == "NaN" || s == "nan") return std::numeric_limits<double>::quiet_NaN();
91 // Anything else is a malformed number, not a zero: report it rather
92 // than let atof turn a typo into a silently wrong model.
93 const char* begin = s.c_str();
94 char* end = nullptr;
95 const double parsed = std::strtod(begin, &end);
96 if (end == begin || *end != '\0')
97 throw InputError("network_reader: expected a number, got the string '" + s + "'");
98 return parsed;
99 }
100 if (v.is_boolean()) return v.get<bool>() ? 1.0 : 0.0;
101 // Not a string: defer to nlohmann, so an object or an array still throws
102 // the type error it always did.
103 return v.get<double>();
104}
105
106/** `num_from_json` for an optional key, with the reader's default. */
107inline double num_value(const json& obj, const char* key, double def) {
108 return obj.contains(key) ? num_from_json(obj.at(key)) : def;
109}
110
111/**
112 * The `dest` of a fork override record: a node NAME, or the empty string for
113 * "every link this class takes", which the builder spells as node 0.
114 *
115 * An unknown name is an error and not a silent 0: dropping the destination
116 * would spread an override meant for one link over all of them, which is a
117 * different model that still solves.
118 */
119inline std::size_t fork_dest_index(const json& ov,
120 const std::map<std::string, std::size_t>& node_idx,
121 const std::string& fork_name) {
122 if (!ov.contains("dest")) return 0;
123 const std::string d = ov.at("dest").get<std::string>();
124 if (d.empty()) return 0;
125 const std::map<std::string, std::size_t>::const_iterator it = node_idx.find(d);
126 if (it == node_idx.end())
127 throw InputError("network_reader: the Fork node '" + fork_name +
128 "' declares an override towards '" + d + "', which is not a node");
129 return it->second;
130}
131
132/** The `scheduling` attribute of a model.json Queue node. */
133inline lang::SchedStrategy sched_from_json(const std::string& s) {
134 using S = lang::SchedStrategy;
135 if (s == "INF" || s == "inf") return S::INF;
136 if (s == "FCFS" || s == "fcfs") return S::FCFS;
137 if (s == "PS" || s == "ps") return S::PS;
138 if (s == "LCFS" || s == "lcfs") return S::LCFS;
139 if (s == "LCFSPR" || s == "lcfspr") return S::LCFSPR;
140 if (s == "SIRO" || s == "siro") return S::SIRO;
141 if (s == "HOL" || s == "hol") return S::HOL;
142 if (s == "DPS" || s == "dps") return S::DPS;
143 if (s == "GPS" || s == "gps") return S::GPS;
144 if (s == "SEPT" || s == "sept") return S::SEPT;
145 if (s == "LEPT" || s == "lept") return S::LEPT;
146 if (s == "SJF" || s == "sjf") return S::SJF;
147 if (s == "LJF" || s == "ljf") return S::LJF;
148 if (s == "SRPT" || s == "srpt") return S::SRPT;
149 if (s == "LPS" || s == "lps") return S::LPS;
150 if (s == "POLLING" || s == "polling") return S::POLLING;
151 // The preemptive, priority and pass-and-swap families. They were absent
152 // here while `SchedStrategy` and the state layer carried them, so a model
153 // the port can represent was refused at its own front door -- the wire
154 // spells them exactly as the enum does, and `sched_to_text` is the list to
155 // keep this one in step with.
156 if (s == "FCFSPR" || s == "fcfspr") return S::FCFSPR;
157 if (s == "FCFSPI" || s == "fcfspi") return S::FCFSPI;
158 if (s == "FCFSPRIO" || s == "fcfsprio") return S::HOL; // MATLAB's alias for HOL
159 if (s == "FCFSPRPRIO" || s == "fcfsprprio") return S::FCFSPRPRIO;
160 if (s == "FCFSPIPRIO" || s == "fcfspiprio") return S::FCFSPIPRIO;
161 if (s == "LCFSPI" || s == "lcfspi") return S::LCFSPI;
162 if (s == "LCFSPRIO" || s == "lcfsprio") return S::LCFSPRIO;
163 if (s == "LCFSPRPRIO" || s == "lcfsprprio") return S::LCFSPRPRIO;
164 if (s == "LCFSPIPRIO" || s == "lcfspiprio") return S::LCFSPIPRIO;
165 if (s == "PSPRIO" || s == "psprio") return S::PSPRIO;
166 if (s == "DPSPRIO" || s == "dpsprio") return S::DPSPRIO;
167 if (s == "GPSPRIO" || s == "gpsprio") return S::GPSPRIO;
168 if (s == "SRPT" || s == "srpt") return S::SRPT;
169 if (s == "SRPTPRIO" || s == "srptprio") return S::SRPTPRIO;
170 if (s == "PSJF" || s == "psjf") return S::PSJF;
171 if (s == "FB" || s == "fb") return S::FB;
172 if (s == "LRPT" || s == "lrpt") return S::LRPT;
173 if (s == "SETF" || s == "setf") return S::SETF;
174 if (s == "FSP" || s == "fsp") return S::FSP;
175 if (s == "EDD" || s == "edd") return S::EDD;
176 if (s == "EDF" || s == "edf") return S::EDF;
177 if (s == "PAS" || s == "pas") return S::PAS;
178 if (s == "OI" || s == "oi") return S::OI;
179 if (s == "REF" || s == "ref") return S::REF;
180 if (s == "EXT" || s == "ext") return S::EXT;
181 throw UnsupportedError("network_reader: unsupported scheduling discipline '" + s + "'");
182}
183
184/** The `dropRule` string of a Queue node, matching the linemodel_io map. An
185 * unrecognised value resolves to WAITQ, as MATLAB's str_to_droprule does. */
186inline lang::DropStrategy drop_from_json(const std::string& s) {
187 using D = lang::DropStrategy;
188 if (s == "drop") return D::DROP;
189 if (s == "waitingQueue") return D::WAITQ;
190 if (s == "blockingAfterService") return D::BAS;
191 if (s == "retrial") return D::RETRIAL;
192 if (s == "retrialWithLimit") return D::RETRIAL_WITH_LIMIT;
193 return D::WAITQ;
194}
195
196inline lang::ReplacementStrategy replacement_from_json(const std::string& s) {
198 if (s == "RR" || s == "rr") return R::RR;
199 if (s == "FIFO" || s == "fifo") return R::FIFO;
200 if (s == "SFIFO" || s == "sfifo") return R::SFIFO;
201 if (s == "LRU" || s == "lru") return R::LRU;
202 if (s == "HLRU" || s == "hlru") return R::HLRU;
203 if (s == "CLIMB" || s == "climb") return R::CLIMB;
204 if (s == "QLRU" || s == "qlru") return R::QLRU;
205 throw UnsupportedError("network_reader: unsupported cache replacement strategy '" + s + "'");
206}
207
208inline lang::PollingType polling_from_json(const std::string& s) {
209 using P = lang::PollingType;
210 if (s == "GATED" || s == "gated") return P::GATED;
211 if (s == "EXHAUSTIVE" || s == "exhaustive") return P::EXHAUSTIVE;
212 if (s == "KLIMITED" || s == "klimited" || s == "K-LIMITED") return P::KLIMITED;
213 if (s == "DECREMENTING" || s == "decrementing") return P::DECREMENTING;
214 throw UnsupportedError("network_reader: unsupported polling type '" + s + "'");
215}
216
217/**
218 * A two-phase hyper-exponential matched to a mean and SCV (SCV >= 1), by the
219 * balanced-means convention MATLAB `HyperExp.fitMeanAndSCV` and JMT use. Kept
220 * local so the reader carries the exact same moments as the reference fit
221 * rather than an approximation of them.
222 */
223template <class T>
224lang::Distrib<T> hyperexp_fit_mean_scv(double mean, double scv) {
225 if (!(scv >= 1.0))
226 throw InputError("network_reader: HyperExp fitMeanAndSCV needs SCV >= 1, got " +
227 std::to_string(scv));
228 const double p = 0.5 * (1.0 + std::sqrt((scv - 1.0) / (scv + 1.0)));
229 const double mu1 = 2.0 * p / mean;
230 const double mu2 = 2.0 * (1.0 - p) / mean;
231 return lang::Distrib<T>::hyperexp(num_traits<T>::from_double(p),
232 num_traits<T>::from_double(mu1),
233 num_traits<T>::from_double(mu2));
234}
235
236/**
237 * A Cache field, looked up in the FLAT spelling on the node first and then in
238 * the nested `cache` object.
239 *
240 * The writers emit both (`linemodel_save.m:425-435`) so that the MATLAB and the
241 * JAR readers each find what they look for; the two are mirrored from one
242 * source and therefore agree. A free function rather than a lambda because the
243 * result feeds `.get<...>()`, and a lambda declared inside a template makes
244 * that call dependent.
245 */
246inline bool has_cache_key(const json& nd, const json& cj, const char* key) {
247 return nd.contains(key) || cj.contains(key);
248}
249inline const json& cache_key(const json& nd, const json& cj, const char* key) {
250 return nd.contains(key) ? nd.at(key) : cj.at(key);
251}
252
253/** The `routingStrategies` name of a dispatcher. */
254inline lang::RoutingStrategy routing_from_json(const std::string& s) {
255 typedef lang::RoutingStrategy R;
256 if (s == "PROB") return R::PROB;
257 if (s == "RAND") return R::RAND;
258 if (s == "RROBIN") return R::RROBIN;
259 if (s == "WRROBIN") return R::WRROBIN;
260 if (s == "JSQ") return R::JSQ;
261 if (s == "SQ" || s == "KCHOICES") return R::SQ;
262 // The declaration itself rides in the `stateDepRouting` block; this only
263 // names the entry row's strategy, which the block then supersedes.
264 if (s == "SDR") return R::SDR;
265 if (s == "FIRING") return R::FIRING;
266 if (s == "DISABLED") return R::DISABLED;
267 throw UnsupportedError("network_reader: unsupported routing strategy '" + s + "'");
268}
269
270/** The `patience` block's `impatienceType`. */
271inline lang::ImpatienceType impatience_from_json(const std::string& s) {
272 typedef lang::ImpatienceType I;
273 if (s == "RENEGING") return I::RENEGING;
274 if (s == "BALKING") return I::BALKING;
275 if (s == "RETRIAL") return I::RETRIAL;
276 throw UnsupportedError("network_reader: unsupported impatience type '" + s + "'");
277}
278
279/** The `balking` block's `strategy`. */
280inline lang::BalkingStrategy balking_from_json(const std::string& s) {
281 typedef lang::BalkingStrategy B;
282 if (s == "QUEUE_LENGTH") return B::QUEUE_LENGTH;
283 if (s == "EXPECTED_WAIT") return B::EXPECTED_WAIT;
284 if (s == "COMBINED") return B::COMBINED;
285 throw UnsupportedError("network_reader: unsupported balking strategy '" + s + "'");
286}
287
288/** The `heteroSchedPolicy` of a station with several server pools. */
289inline lang::HeteroSchedPolicy hetero_from_json(const std::string& s) {
290 typedef lang::HeteroSchedPolicy H;
291 if (s == "ORDER") return H::ORDER;
292 if (s == "ALIS") return H::ALIS;
293 if (s == "ALFS") return H::ALFS;
294 if (s == "FAIRNESS") return H::FAIRNESS;
295 if (s == "FSF") return H::FSF;
296 if (s == "RAIS") return H::RAIS;
297 throw UnsupportedError("network_reader: unsupported heterogeneous scheduling policy '" + s + "'");
298}
299
300/** The `departureDiscipline` of a queueing Place. */
301inline lang::DepartureDiscipline departure_from_json(const std::string& s) {
302 if (s == "NORMAL") return lang::DepartureDiscipline::NORMAL;
303 if (s == "FIFO") return lang::DepartureDiscipline::FIFO;
304 throw UnsupportedError("network_reader: unsupported departure discipline '" + s + "'");
305}
306
307/** Read a wire field that may be a JSON array or, for length one, a bare scalar. */
308template <class T>
309std::vector<T> num_vec_from_json(const json& v) {
310 std::vector<T> out;
311 // num_from_json, not get<double>(): a state row is one of the UNGUARDED
312 // fields of the comment above it. An open model's Source holds an infinite
313 // population, so `initialState`/`stateSpace` carry "Infinity" the moment the
314 // writer emits a state for every stateful node.
315 if (v.is_array())
316 for (const json& x : v) out.push_back(num_traits<T>::from_double(num_from_json(x)));
317 else
318 out.push_back(num_traits<T>::from_double(num_from_json(v)));
319 return out;
320}
321
322/** A dense matrix written as an array of row arrays. */
323template <class T>
324Matrix<T> mat_from_json(const json& v) {
325 if (!v.is_array())
326 throw InputError("network_reader: expected an array of rows");
327 Matrix<T> M(v.size(), v.empty() ? 0 : v.at(0).size());
328 for (std::size_t i = 0; i < v.size(); ++i) {
329 const json& row = v.at(i);
330 for (std::size_t j = 0; j < row.size(); ++j)
331 M(i, j) = num_traits<T>::from_double(num_from_json(row.at(j)));
332 }
333 return M;
334}
335
336/** A list of dense matrices, the form the marked and batch families are written in. */
337template <class T>
338std::vector<Matrix<T> > mat_list_from_json(const json& v) {
339 std::vector<Matrix<T> > out;
340 for (const json& m : v) out.push_back(mat_from_json<T>(m));
341 return out;
342}
343
344/** Reconstruct a distribution from a `fit` block (moments), by declared family. */
345template <class T>
346lang::Distrib<T> dist_from_fit(const std::string& type, const json& fit) {
347 const std::string method = fit.at("method").get<std::string>();
348 if (method == "fitMean" || method == "fitMeanAndOrder") {
349 const double mean = fit.at("mean").get<double>();
350 if (type == "Det") return lang::Distrib<T>::det(num_traits<T>::from_double(mean));
351 if (type == "Erlang") {
352 const long order = fit.contains("order") ? fit.at("order").get<long>() : 1L;
353 const T rate = num_traits<T>::from_double(double(order) / mean);
354 return lang::Distrib<T>::erlang(rate, std::size_t(order));
355 }
356 // fitMean on any other family is honoured on the mean alone -> Exp.
357 return lang::Distrib<T>::exp_mean(num_traits<T>::from_double(mean));
358 }
359 if (method == "fitMeanAndSCV") {
360 const double mean = fit.at("mean").get<double>();
361 const double scv = fit.at("scv").get<double>();
362 if (type == "Erlang")
363 return lang::Distrib<T>::erlang_fit(num_traits<T>::from_double(mean),
364 num_traits<T>::from_double(scv));
365 if (type == "HyperExp") return hyperexp_fit_mean_scv<T>(mean, scv);
366 if (std::fabs(scv - 1.0) < 1e-12)
367 return lang::Distrib<T>::exp_mean(num_traits<T>::from_double(mean));
368 throw UnsupportedError("network_reader: fitMeanAndSCV for family '" + type +
369 "' is not reconstructed; use explicit params");
370 }
371 throw UnsupportedError("network_reader: unsupported fit method '" + method + "' for '" + type +
372 "'");
373}
374
375/** Reconstruct a distribution from a model.json distribution record. */
376template <class T>
377lang::Distrib<T> dist_from_json(const json& obj) {
378 const std::string type = obj.at("type").get<std::string>();
379 if (type == "Immediate") return lang::Distrib<T>::immediate();
380 if (type == "Disabled") return lang::Distrib<T>::disabled_dist();
381
382 // A Prior arrives in one of its TWO forms, tagged by `kind` and defaulting
383 // to the discrete one when the tag is absent, as every model.json written
384 // before the continuous form existed is. The discrete form is the
385 // alternative set plus the weights. The CONTINUOUS form is the parameter
386 // density plus the distribution its factory BUILDS, with the parameter left
387 // in the slots the factory fills (`linemodel_save.m:prior_factory2json`):
388 // the handle itself cannot cross JSON, but substituting theta into those
389 // slots reconstructs the same alternative at any node count, so
390 // `options.samples` still means what it means in MATLAB. See
391 // _kb/09-ldes-and-cache.md for the encoding and its one restriction.
392 if (type == "Prior") {
393 const std::string kind =
394 obj.contains("kind") ? obj.at("kind").get<std::string>() : std::string("discrete");
395 if (kind != "discrete" && kind != "continuous")
396 throw InputError("network_reader: a Prior's 'kind' is 'discrete' or 'continuous', got '" +
397 kind + "'");
398 if (kind == "continuous") {
399 if (!obj.contains("paramDist") || !obj.contains("factory"))
400 throw InputError(
401 "network_reader: a continuous Prior carries 'paramDist' and 'factory' (a "
402 "template distribution plus the parameter slots it fills)");
403 const json& fac = obj.at("factory");
404 if (!fac.contains("template") || !fac.contains("slots"))
405 throw InputError(
406 "network_reader: a continuous Prior's 'factory' carries 'template' and "
407 "'slots'");
408 const json tmpl = fac.at("template");
409 std::vector<std::string> slots;
410 for (const json& s : fac.at("slots")) slots.push_back(s.get<std::string>());
411 if (slots.empty())
412 throw InputError(
413 "network_reader: a continuous Prior's factory names no parameter slot, so the "
414 "parameter would not reach the distribution it builds");
415 if (!tmpl.contains("params"))
416 throw InputError(
417 "network_reader: a continuous Prior's factory template carries its parameters "
418 "as 'params'; a fitted or phase-type representation has no named slot");
419 for (std::size_t l = 0; l < slots.size(); ++l)
420 if (!tmpl.at("params").contains(slots[l]))
421 throw InputError("network_reader: a continuous Prior's factory names '" +
422 slots[l] + "' as a parameter slot, but its template has no "
423 "such parameter");
424 // THETA CROSSES AS A DOUBLE, which costs nothing it was not already
425 // costing: `dist_quantile` reaches it by bisection to FineTol, so the
426 // parameter is an approximation of the stratum median well above
427 // double resolution before this substitution sees it.
428 const std::function<lang::Distrib<T>(const T&)> factory =
429 [tmpl, slots](const T& theta) {
430 json j = tmpl;
431 for (std::size_t l = 0; l < slots.size(); ++l)
432 j["params"][slots[l]] = num_traits<T>::to_double(theta);
433 return dist_from_json<T>(j);
434 };
435 return lang::prior_continuous<T>(dist_from_json<T>(obj.at("paramDist")), factory);
436 }
437 if (!obj.contains("distributions") || !obj.contains("probabilities"))
438 throw InputError(
439 "network_reader: a discrete Prior carries 'distributions' and 'probabilities'");
440 std::vector<lang::Distrib<T> > alts;
441 for (const json& a : obj.at("distributions")) alts.push_back(dist_from_json<T>(a));
442 std::vector<T> probs;
443 for (const json& p : obj.at("probabilities"))
444 probs.push_back(num_traits<T>::from_double(p.get<double>()));
445 return lang::prior_discrete<T>(alts, probs);
446 }
447
448 // The time-inhomogeneous families. The wire form is the reference's
449 // (`linemodel_save.m:2275-2302`, `linemodel_io.py:64-83`): a breakpoint
450 // vector plus one matrix (or one row, or one scalar) per segment, and a
451 // `cyclic` flag. NHPP carries `rates`, MAPt `D0`/`D1`, PHt `alpha`/`S`.
452 if (type == "NHPP" || type == "MAPt" || type == "PHt") {
453 const json& p = obj.at("params");
454 std::vector<T> bp;
455 for (const json& b : p.at("breakpoints"))
456 bp.push_back(num_traits<T>::from_double(b.get<double>()));
457 const bool cyc = p.contains("cyclic") && p.at("cyclic").get<bool>();
458 auto read_mats = [&](const char* key) {
459 std::vector<Matrix<T> > out;
460 for (const json& seg : p.at(key)) {
461 const std::vector<std::vector<double> > rows =
462 seg.get<std::vector<std::vector<double> > >();
463 Matrix<T> M(rows.size(), rows.empty() ? 0 : rows[0].size());
464 for (std::size_t a = 0; a < rows.size(); ++a)
465 for (std::size_t b = 0; b < rows[a].size(); ++b)
466 M(a, b) = num_traits<T>::from_double(rows[a][b]);
467 out.push_back(M);
468 }
469 return out;
470 };
471 if (type == "NHPP") {
472 std::vector<T> rates;
473 for (const json& r : p.at("rates"))
474 rates.push_back(num_traits<T>::from_double(r.get<double>()));
475 return lang::Distrib<T>::nhpp(bp, rates, cyc);
476 }
477 if (type == "MAPt")
478 return lang::Distrib<T>::mapt(bp, read_mats("D0"), read_mats("D1"), cyc);
479 std::vector<std::vector<T> > alphas;
480 for (const json& a : p.at("alpha")) {
481 const std::vector<double> row = a.get<std::vector<double> >();
482 std::vector<T> av;
483 for (double v : row) av.push_back(num_traits<T>::from_double(v));
484 alphas.push_back(av);
485 }
486 return lang::Distrib<T>::pht(bp, alphas, read_mats("S"), cyc);
487 }
488
489 // A Replayer names a trace FILE, and the writers add the APH fit of its
490 // first three moments beside it precisely because that path may not resolve
491 // on another machine. Reading the trace back gives the same distribution
492 // MATLAB had; the fit is the documented fallback, and the stored mean the
493 // last resort. This branch precedes the `ph` one below, which would
494 // otherwise turn every Replayer into a bare PH and drop the trace.
495 if (type == "Replayer" || type == "Trace") {
496 if (obj.contains("params") && obj.at("params").contains("fileName")) {
497 const std::string path = obj.at("params").at("fileName").get<std::string>();
498 std::ifstream tr(path.c_str());
499 if (tr) {
500 std::vector<T> samples;
501 double v = 0.0;
502 while (tr >> v) samples.push_back(num_traits<T>::from_double(v));
503 if (!samples.empty()) {
504 lang::Distrib<T> d = lang::Distrib<T>::replayer(samples);
505 d.trace_file = path; // only the exporters read it back
506 return d;
507 }
508 }
509 }
510 if (obj.contains("ph")) {
511 const json& ph = obj.at("ph");
512 const std::vector<double> a = ph.at("alpha").get<std::vector<double> >();
513 const std::vector<std::vector<double> > rows =
514 ph.at("T").get<std::vector<std::vector<double> > >();
515 std::vector<T> alpha;
516 for (double x : a) alpha.push_back(num_traits<T>::from_double(x));
517 Matrix<T> A(rows.size(), rows.empty() ? 0 : rows[0].size());
518 for (std::size_t i = 0; i < rows.size(); ++i)
519 for (std::size_t j = 0; j < rows[i].size(); ++j)
520 A(i, j) = num_traits<T>::from_double(rows[i][j]);
521 return lang::Distrib<T>::phase_type(alpha, A, true);
522 }
523 if (obj.contains("params") && obj.at("params").contains("mean"))
525 num_traits<T>::from_double(obj.at("params").at("mean").get<double>()));
526 throw InputError(
527 "network_reader: a Replayer carries neither a readable trace file, nor the APH fit "
528 "the writers add beside it, nor a mean; there is nothing to reconstruct");
529 }
530
531 // Markovian arrival families carry an explicit (D0, D1). A generic MAP/MMAP
532 // arrives as a `map` object; an MMPP2 as its four rate params.
533 if (type == "MMPP2") {
534 const json& p = obj.at("params");
535 const double l0 = p.at("lambda0").get<double>(), l1 = p.at("lambda1").get<double>();
536 const double s0 = p.at("sigma0").get<double>(), s1 = p.at("sigma1").get<double>();
537 Matrix<T> D0(2, 2), D1(2, 2);
538 D1(0, 0) = num_traits<T>::from_double(l0);
539 D1(1, 1) = num_traits<T>::from_double(l1);
540 D0(0, 0) = num_traits<T>::from_double(-(l0 + s0));
541 D0(0, 1) = num_traits<T>::from_double(s0);
542 D0(1, 0) = num_traits<T>::from_double(s1);
543 D0(1, 1) = num_traits<T>::from_double(-(l1 + s1));
545 }
546 if (type == "MAP" || type == "MMPP") {
547 if (!obj.contains("map"))
548 throw InputError("network_reader: " + type + " carries no 'map' (D0, D1) object");
549 const json& mp = obj.at("map");
550 return lang::Distrib<T>::map_dist(mat_from_json<T>(mp.at("D0")),
551 mat_from_json<T>(mp.at("D1")),
553 }
554 // A MARKED MAP carries one arrival block per mark in its own `mmap` object;
555 // the aggregate D1 the unmarked consumers read is their sum, rebuilt on load
556 // exactly as the reference readers do.
557 if (type == "MMAP" || type == "MarkedMAP") {
558 if (!obj.contains("mmap"))
559 throw InputError("network_reader: " + type + " carries no 'mmap' (D0, D1k) object");
560 const json& mp = obj.at("mmap");
561 return lang::Distrib<T>::mmap(mat_from_json<T>(mp.at("D0")),
562 mat_list_from_json<T>(mp.at("D1k")));
563 }
564 // A BMAP writes the WHOLE block list D0, D1, ..., Dk, where Dj carries an
565 // arrival of batch size j; a MarkedMMPP writes the same list plus the mark
566 // count K, and lowers to the MMAP process type as MATLAB's fromText does.
567 if (type == "BMAP" || type == "MarkedMMPP") {
568 const json& p = obj.at("params");
569 const std::vector<Matrix<T> > D = mat_list_from_json<T>(p.at("D"));
570 if (type == "BMAP") return lang::Distrib<T>::bmap(D);
571 if (D.size() < 2)
572 throw InputError("network_reader: a MarkedMMPP carries D0 and at least one marked block");
573 return lang::Distrib<T>::mmap(D[0], std::vector<Matrix<T> >(D.begin() + 1, D.end()));
574 }
575 if (type == "DMAP") {
576 const json& p = obj.at("params");
577 return lang::Distrib<T>::dmap(mat_from_json<T>(p.at("D0")), mat_from_json<T>(p.at("D1")));
578 }
579 if (type == "RAP") {
580 const json& p = obj.at("params");
581 return lang::Distrib<T>::rap(mat_from_json<T>(p.at("H0")), mat_from_json<T>(p.at("H1")));
582 }
583 // ME and CME share one process type: the concentrated representation is a
584 // subclass carrying no distinct tag, exactly as MATLAB `fromText` maps it.
585 if (type == "ME" || type == "CME") {
586 const json& p = obj.at("params");
587 const std::vector<double> a = p.at("alpha").get<std::vector<double> >();
588 std::vector<T> alpha;
589 for (double v : a) alpha.push_back(num_traits<T>::from_double(v));
590 return lang::Distrib<T>::me(alpha, mat_from_json<T>(p.at("A")));
591 }
592
593 // A phase-type family carries its representation as `ph: {alpha, T}`, which
594 // is the third form the writer emits alongside `params` and `fit`. Without
595 // this branch a PH/APH/Coxian written with an explicit representation --
596 // exactly what `Coxian.fit`, `APH.fitMeanAndSCV` and the MAP-to-PH paths
597 // produce -- was refused as "has neither params nor a fit block", which is a
598 // statement about what the reader looked for and not about what the writer
599 // sent. The representation is complete on the wire; only the branch was
600 // missing.
601 if (obj.contains("ph")) {
602 const json& ph = obj.at("ph");
603 const std::vector<double> a = ph.at("alpha").get<std::vector<double> >();
604 const std::vector<std::vector<double> > rows =
605 ph.at("T").get<std::vector<std::vector<double> > >();
606 if (a.empty() || rows.size() != a.size())
607 throw InputError("network_reader: distribution '" + type +
608 "' has a 'ph' block whose alpha and T disagree in order");
609 std::vector<T> alpha;
610 alpha.reserve(a.size());
611 for (double v : a) alpha.push_back(num_traits<T>::from_double(v));
612 Matrix<T> A(rows.size(), rows.empty() ? 0 : rows[0].size());
613 for (std::size_t i = 0; i < rows.size(); ++i)
614 for (std::size_t j = 0; j < rows[i].size(); ++j)
615 A(i, j) = num_traits<T>::from_double(rows[i][j]);
616 // APH and PH share a representation and differ only in the type tag,
617 // so the wire's own name decides it rather than a structural test.
618 return lang::Distrib<T>::phase_type(alpha, A, type == "APH");
619 }
620
621 // Explicit parameters take precedence over a fit block, mirroring the
622 // reference readers.
623 if (!obj.contains("params") && obj.contains("fit"))
624 return dist_from_fit<T>(type, obj.at("fit"));
625 if (!obj.contains("params"))
626 throw InputError("network_reader: distribution '" + type +
627 "' has neither params nor a fit block");
628 const json& p = obj.at("params");
629 if (type == "Exp") {
630 const double lam = p.contains("lambda") ? p.at("lambda").get<double>()
631 : p.at("rate").get<double>();
632 return lang::Distrib<T>::exp_rate(num_traits<T>::from_double(lam));
633 }
634 if (type == "Det") return lang::Distrib<T>::det(num_traits<T>::from_double(p.at("value").get<double>()));
635 if (type == "Erlang") {
636 const double lam = p.at("lambda").get<double>();
637 const long k = p.at("k").get<long>();
638 return lang::Distrib<T>::erlang(num_traits<T>::from_double(lam), std::size_t(k));
639 }
640 if (type == "HyperExp") {
641 const std::vector<T> pv = num_vec_from_json<T>(p.at("p"));
642 const std::vector<T> lv = num_vec_from_json<T>(p.at("lambda"));
643 // The two-branch entry point carries MATLAB's getParam order, which a
644 // parameter dump compares against; wider ones take the general form.
645 if (pv.size() == 2 && lv.size() == 2)
646 return lang::Distrib<T>::hyperexp(pv[0], lv[0], lv[1]);
647 return lang::Distrib<T>::hyperexp_n(pv, lv);
648 }
649 if (type == "Coxian")
650 return lang::Distrib<T>::coxian(num_vec_from_json<T>(p.at("mu")),
651 num_vec_from_json<T>(p.at("phi")));
652 if (type == "Cox2")
653 return lang::Distrib<T>::cox2(num_traits<T>::from_double(p.at("mu1").get<double>()),
654 num_traits<T>::from_double(p.at("mu2").get<double>()),
655 num_traits<T>::from_double(p.at("phi1").get<double>()));
656 if (type == "Uniform")
657 return lang::Distrib<T>::uniform(num_traits<T>::from_double(p.at("a").get<double>()),
658 num_traits<T>::from_double(p.at("b").get<double>()));
659 // Gamma(alpha = shape, beta = SCALE), the pairing the JAR loader fixes.
660 if (type == "Gamma")
661 return lang::Distrib<T>::gamma_dist(num_traits<T>::from_double(p.at("alpha").get<double>()),
662 num_traits<T>::from_double(p.at("beta").get<double>()));
663 if (type == "Lognormal")
664 return lang::Distrib<T>::lognormal(num_traits<T>::from_double(p.at("mu").get<double>()),
665 num_traits<T>::from_double(p.at("sigma").get<double>()));
666 // `Normal` shares the (mu, sigma) key pair with `Lognormal` above, and is
667 // the one family here that is never a service process: it crosses the wire
668 // only as the parameter density of a continuous Prior. Refusing it made
669 // every such Prior a model MATLAB could write and this reader could not
670 // read, which is a UQ study that stops at the interchange.
671 if (type == "Normal")
672 return lang::Distrib<T>::normal(num_traits<T>::from_double(p.at("mu").get<double>()),
673 num_traits<T>::from_double(p.at("sigma").get<double>()));
674 // Pareto(alpha = shape, scale) and Weibull(alpha = SCALE, beta = SHAPE):
675 // the two families spell the wire key `alpha` for opposite roles, which is
676 // the reference's convention and the one trap in this table.
677 if (type == "Pareto")
678 return lang::Distrib<T>::pareto(num_traits<T>::from_double(p.at("alpha").get<double>()),
679 num_traits<T>::from_double(p.at("scale").get<double>()));
680 if (type == "Weibull")
681 return lang::Distrib<T>::weibull(num_traits<T>::from_double(p.at("alpha").get<double>()),
682 num_traits<T>::from_double(p.at("beta").get<double>()));
683 if (type == "DiscreteUniform")
685 num_traits<T>::from_double(p.at("min").get<double>()),
686 num_traits<T>::from_double(p.at("max").get<double>()));
687 if (type == "Bernoulli")
688 return lang::Distrib<T>::bernoulli(num_traits<T>::from_double(p.at("p").get<double>()));
689 if (type == "Binomial")
690 return lang::Distrib<T>::binomial(num_traits<T>::from_double(p.at("n").get<double>()),
691 num_traits<T>::from_double(p.at("p").get<double>()));
692 if (type == "Poisson")
693 return lang::Distrib<T>::poisson(num_traits<T>::from_double(p.at("lambda").get<double>()));
694 if (type == "Geometric")
695 return lang::Distrib<T>::geometric(num_traits<T>::from_double(p.at("p").get<double>()));
696 if (type == "Zipf")
697 return lang::Distrib<T>::zipf(num_traits<T>::from_double(p.at("s").get<double>()),
698 std::size_t(p.at("n").get<long>()));
699 if (type == "DiscreteSampler") {
700 const std::vector<T> pv = num_vec_from_json<T>(p.at("p"));
701 const std::vector<T> xv =
702 p.contains("x") ? num_vec_from_json<T>(p.at("x")) : std::vector<T>();
704 }
705 if (type == "EmpiricalCDF" || type == "EmpiricalCdf")
706 return lang::Distrib<T>::empirical_cdf(num_vec_from_json<T>(p.at("x")),
707 num_vec_from_json<T>(p.at("F")));
708
709 // THE MOMENT-ONLY FALLBACK the writers emit for a family with no JSON
710 // representation of its own: `{type: <name>, params: {mean, scv?}}`, written
711 // with a warning at save time. It is honoured on the moments it carries --
712 // an exponential from a mean alone, and otherwise the acyclic phase-type
713 // matching the two -- rather than refused, because refusing would make a
714 // model unreadable that the reference itself declares readable. The TYPE
715 // TAG IS NOT KEPT: the reconstruction is a different law that happens to
716 // share two moments, and claiming the original name for it would make
717 // `sn.procid` lie about what every solver is actually integrating.
718 if (p.contains("mean") && !p.contains("scv"))
719 return lang::Distrib<T>::exp_mean(num_traits<T>::from_double(p.at("mean").get<double>()));
720 if (p.contains("mean") && p.contains("scv")) {
721 const double mean = p.at("mean").get<double>();
722 const double scv = p.at("scv").get<double>();
723 if (std::fabs(scv - 1.0) < 1e-12)
724 return lang::Distrib<T>::exp_mean(num_traits<T>::from_double(mean));
725 if (scv < 1.0)
726 return lang::Distrib<T>::erlang_fit(num_traits<T>::from_double(mean),
727 num_traits<T>::from_double(scv));
728 return hyperexp_fit_mean_scv<T>(mean, scv);
729 }
730 throw UnsupportedError("network_reader: unsupported distribution family '" + type +
731 "' (params form)");
732}
733
734/**
735 * The item-popularity pmf of a cache read class, `nodeparam.pread`.
736 *
737 * A popularity is a DISCRETE distribution over the item ranks, and the writers
738 * emit whichever family the model used: `DiscreteSampler` carries the pmf
739 * itself, `Zipf` carries only (s, n) and the pmf p_i = i^-s / H(s,n) is
740 * rebuilt here. Reading only the first form left every Zipf-popularity cache
741 * with an EMPTY read vector, which is a uniform cache by default rather than a
742 * refusal.
743 */
744/**
745 * The popularity LAW, recorded beside the pmf for the exporters.
746 *
747 * JMT's Cache section takes a parametric popularity and cannot be given a pmf,
748 * so the exponent and the support size have to survive the read; every solver
749 * still reads the pmf `pmf_from_json` returns.
750 */
751template <class T>
752typename qn::CacheParam<T>::Popularity popularity_kind_from_json(const json& obj,
753 std::size_t nitems) {
754 typename qn::CacheParam<T>::Popularity k;
755 const std::string type = obj.value("type", std::string());
756 const json& p = obj.contains("params") ? obj.at("params") : obj;
757 if (type == "Zipf") {
759 k.s = p.at("s").get<double>();
760 k.n = p.contains("n") ? std::size_t(p.at("n").get<long>()) : nitems;
761 } else if (type == "DiscreteSampler" || p.contains("p")) {
763 k.n = p.contains("p") ? p.at("p").size() : nitems;
764 }
765 return k;
766}
767
768template <class T>
769std::vector<T> pmf_from_json(const json& obj, std::size_t nitems) {
770 const std::string type = obj.value("type", std::string());
771 const json& p = obj.contains("params") ? obj.at("params") : obj;
772 std::vector<T> out;
773 // THE ARRAY TEST IS THE WHOLE GUARD. `params.p` is also the SCALAR success
774 // probability of Bernoulli, Binomial and Geometric, and `p.contains("p")`
775 // alone admitted those into this branch, where iterating a JSON scalar
776 // yields one element and the family collapsed to a one-point pmf.
777 if (type == "DiscreteSampler" || (p.contains("p") && p.at("p").is_array())) {
778 for (const json& v : p.at("p")) out.push_back(num_traits<T>::from_double(v.get<double>()));
779 return out;
780 }
781 if (type == "Zipf") {
782 const double s = p.at("s").get<double>();
783 const std::size_t n = p.contains("n") ? std::size_t(p.at("n").get<long>()) : nitems;
784 double h = 0.0;
785 for (std::size_t k = 1; k <= n; ++k) h += std::pow(double(k), -s);
786 for (std::size_t k = 1; k <= n; ++k)
787 out.push_back(num_traits<T>::from_double(std::pow(double(k), -s) / h));
788 return out;
789 }
790 throw UnsupportedError(
791 "network_reader: a cache popularity is written as '" + type +
792 "', and the discrete families carrying an item pmf are DiscreteSampler and Zipf");
793}
794
795/**
796 * How far an infinitely-supported batch law is materialized.
797 *
798 * `sn.signalremdist` is a VECTOR, so a Geometric or a Poisson has to stop
799 * somewhere, and the consumer (`signal_batch_pmf`) lumps everything past the
800 * last stored entry onto "remove the whole eligible population". That lumping
801 * is the REFERENCE'S OWN rule only for mass beyond the population present, so
802 * the vector must outrun any population a station can hold; 4096 does, for a
803 * chain whose per-class cutoff is a few hundred at the very most.
804 */
805constexpr std::size_t REMOVAL_PMF_MAX_TERMS = 4096;
806
807/**
808 * The batch-size pmf of a G-network signal, INDEXED BY THE BATCH SIZE ITSELF.
809 *
810 * WHY THIS IS NOT `pmf_from_json`. That function returns a cache popularity,
811 * whose entry i is the mass of RANK i+1; here entry b is the mass of the batch
812 * size b, counting from a batch of ZERO. `sn.signalremdist` is documented on the
813 * second convention (network_struct.h) and both consumers -- `signal_batch_pmf`
814 * for the chain and the LDES draw -- read it that way, so a reader that returns
815 * the first shifts every batch by one job.
816 *
817 * MATLAB EVALUATES THE LAW, IT DOES NOT TABULATE IT: `State.signalBatchPMF`
818 * calls `dist.evalPMF(0:ntot)`, so the wire carries the FAMILY (`{type:
819 * "Geometric", params: {p: 0.5}}`) and the pmf has to be rebuilt here. Reading
820 * only `DiscreteSampler` sent every parametric family through a branch that
821 * iterated the scalar `params.p` and produced the one-point pmf
822 * P(B = 0) = p -- a signal that removes NOTHING with probability p and empties
823 * the station otherwise. On the `test_batch_removal` G-network that read
824 * utilization 0.58690 against the reference's 0.56422.
825 */
826template <class T>
827std::vector<T> removal_pmf_from_json(const json& obj) {
828 const std::string type = obj.value("type", std::string());
829 const json& p = obj.contains("params") ? obj.at("params") : obj;
830 std::vector<T> out;
831 auto put = [&out](std::size_t k, double v) {
832 if (out.size() <= k) out.resize(k + 1, num_traits<T>::from_int(0));
833 out[k] = num_traits<T>::from_double(num_traits<T>::to_double(out[k]) + v);
834 };
835 if (type == "DiscreteSampler" || (p.contains("p") && p.at("p").is_array())) {
836 // `x` IS THE SUPPORT AND NOT A LABEL: MATLAB's `DiscreteSampler(p, x)`
837 // defaults it to 1..n and `evalPMF` looks a value up in it, so dropping
838 // it would read the first mass as a batch of zero.
839 const json& pv = p.at("p");
840 const bool has_x = p.contains("x") && p.at("x").is_array();
841 for (std::size_t i = 0; i < pv.size(); ++i) {
842 const double xi =
843 has_x ? p.at("x").at(i).get<double>() : static_cast<double>(i + 1);
844 if (xi < 0) throw InputError("network_reader: a batch size cannot be negative");
845 put(static_cast<std::size_t>(xi + 0.5), pv.at(i).get<double>());
846 }
847 return out;
848 }
849 if (type == "Bernoulli") {
850 const double q = p.at("p").get<double>();
851 put(0, 1.0 - q);
852 put(1, q);
853 return out;
854 }
855 if (type == "Binomial") {
856 const double q = p.at("p").get<double>();
857 const std::size_t n = static_cast<std::size_t>(p.at("n").get<double>() + 0.5);
858 double term = std::pow(1.0 - q, static_cast<double>(n));
859 for (std::size_t k = 0; k <= n; ++k) {
860 put(k, term);
861 if (k < n && q < 1.0)
862 term *= (static_cast<double>(n - k) / static_cast<double>(k + 1)) * q / (1.0 - q);
863 }
864 return out;
865 }
866 if (type == "Poisson") {
867 const double lam = p.at("lambda").get<double>();
868 double term = std::exp(-lam), acc = 0.0;
869 for (std::size_t k = 0; k < REMOVAL_PMF_MAX_TERMS; ++k) {
870 put(k, term);
871 acc += term;
872 if (acc > 1.0 - 1e-15) break;
873 term *= lam / static_cast<double>(k + 1);
874 }
875 return out;
876 }
877 if (type == "Geometric") {
878 // MATLAB's Geometric(p) counts TRIALS TO THE FIRST SUCCESS, support
879 // {1, 2, ...}: P(B = 0) is zero, so a signal always takes at least one.
880 const double q = p.at("p").get<double>();
881 if (q <= 0.0 || q > 1.0) throw InputError("network_reader: Geometric(p) needs 0 < p <= 1");
882 put(0, 0.0);
883 double term = q, acc = 0.0;
884 for (std::size_t k = 1; k <= REMOVAL_PMF_MAX_TERMS; ++k) {
885 put(k, term);
886 acc += term;
887 if (acc > 1.0 - 1e-15) break;
888 term *= (1.0 - q);
889 }
890 return out;
891 }
892 if (type == "DiscreteUniform") {
893 const long lo = static_cast<long>(p.at("min").get<double>());
894 const long hi = static_cast<long>(p.at("max").get<double>());
895 if (hi < lo || lo < 0)
896 throw InputError("network_reader: DiscreteUniform(min,max) needs 0 <= min <= max");
897 const double w = 1.0 / static_cast<double>(hi - lo + 1);
898 for (long k = lo; k <= hi; ++k) put(static_cast<std::size_t>(k), w);
899 return out;
900 }
901 if (type == "Det") {
902 const double v = p.contains("t") ? p.at("t").get<double>() : p.at("mean").get<double>();
903 if (v < 0) throw InputError("network_reader: a batch size cannot be negative");
904 put(static_cast<std::size_t>(v + 0.5), 1.0);
905 return out;
906 }
907 throw UnsupportedError(
908 "network_reader: a signal removal law is written as '" + type +
909 "', and the discrete families a batch size can be drawn from are DiscreteSampler, "
910 "Bernoulli, Binomial, Poisson, Geometric, DiscreteUniform and Det");
911}
912
913/**
914 * Rebuild a class- or joint-dependence handle from the box-lattice table the
915 * writers emit.
916 *
917 * A function handle cannot cross JSON, so `linemodel_save.m` (`cd_scaling_table`),
918 * the JAR `LineModelIO` and the Python writer all MATERIALIZE beta(n) / eta_i(n)
919 * over 0 <= n(r) <= cutoffs(r), keyed by the comma-joined 0-based per-class
920 * counts. This rebuilds the callable from that table, clamping the population to
921 * the cutoffs so the scaling SATURATES beyond the tabulated range exactly as the
922 * table intends, and returning all-ones for a composition absent from the table
923 * so an unlisted state leaves the nominal rate unscaled. Twin of the Python
924 * `_cd_table_to_callable`.
925 */
926template <class T>
927lang::CdScaling<T> cd_scaling_from_json(const json& tbl, const std::vector<int>& cutoffs,
928 std::size_t K) {
929 std::map<std::string, std::vector<T> > table;
930 for (auto it = tbl.begin(); it != tbl.end(); ++it)
931 table[it.key()] = num_vec_from_json<T>(it.value());
932 const std::vector<int> cut = cutoffs;
933 return [table, cut, K](const std::vector<T>& n) {
934 std::string key;
935 for (std::size_t r = 0; r < K; ++r) {
936 int v = r < n.size()
937 ? static_cast<int>(std::lround(num_traits<T>::to_double(n[r])))
938 : 0;
939 if (v < 0) v = 0;
940 if (r < cut.size() && v > cut[r]) v = cut[r];
941 if (r) key += ',';
942 key += std::to_string(v);
943 }
944 std::vector<T> out(K, num_traits<T>::from_int(1));
945 typename std::map<std::string, std::vector<T> >::const_iterator it = table.find(key);
946 if (it == table.end()) return out;
947 for (std::size_t r = 0; r < K && r < it->second.size(); ++r) out[r] = it->second[r];
948 return out;
949 };
950}
951
952/**
953 * Rebuild the mu(c) of an order-independent / pass-and-swap station from the
954 * macrostate table the writers materialize (`oi_rate_table` in
955 * `linemodel_save.m`, `LineModelIO.oiServiceRate` in the JAR).
956 *
957 * The handle takes an ORDERED microstate -- the list of 1-based class indices
958 * in buffer order, which is what `set_pas` is defined over -- and the table is
959 * keyed by the per-class COUNTS, because mu is order-independent by
960 * construction. A composition beyond the tabulated cutoffs saturates at them,
961 * matching how the writer chose those cutoffs (the closed populations, or 10
962 * for an open class beyond which mu is constant); a composition the table does
963 * not list returns zero, which is the writer's own encoding of a non-finite
964 * rate and means the macrostate is unreachable.
965 */
966template <class T>
967std::function<T(const std::vector<std::size_t>&)> oi_rate_from_json(const json& tbl,
968 const std::vector<int>& cutoffs,
969 std::size_t K) {
970 std::map<std::string, T> table;
971 for (auto it = tbl.begin(); it != tbl.end(); ++it)
972 table[it.key()] = num_traits<T>::from_double(it.value().get<double>());
973 const std::vector<int> cut = cutoffs;
974 return [table, cut, K](const std::vector<std::size_t>& micro) {
975 std::vector<int> cnt(K, 0);
976 for (std::size_t j = 0; j < micro.size(); ++j)
977 if (micro[j] >= 1 && micro[j] <= K) ++cnt[micro[j] - 1];
978 std::string key;
979 for (std::size_t r = 0; r < K; ++r) {
980 int v = cnt[r];
981 if (r < cut.size() && v > cut[r]) v = cut[r];
982 if (r) key += ',';
983 key += std::to_string(v);
984 }
985 typename std::map<std::string, T>::const_iterator it = table.find(key);
986 return it == table.end() ? num_traits<T>::from_int(0) : it->second;
987 };
988}
989
990/**
991 * The declared peak the wire carries, or, for legacy JSON written before the
992 * peak became mandatory, the peak DERIVED from the table.
993 *
994 * Deriving it is not a fabrication: the table is the whole lattice the reference
995 * `cd_peak_scaling` would sweep, so the maximum over its rows and classes is
996 * exactly that routine's answer. It skips the all-zero composition (the
997 * reference's `tot > 0` guard) and any non-finite entry, which a handle may
998 * legitimately return for an unreachable composition and which would otherwise
999 * become the normalizer and zero every utilization at the station.
1000 */
1001template <class T>
1002std::vector<T> cd_peak_from_json(const json& blk, const json& tbl) {
1003 if (blk.contains("peak") && !blk.at("peak").empty())
1004 return num_vec_from_json<T>(blk.at("peak"));
1005 double bmax = 0;
1006 for (auto it = tbl.begin(); it != tbl.end(); ++it) {
1007 if (it.key().find_first_not_of("0,") == std::string::npos) continue;
1008 const std::vector<double> row = num_vec_from_json<double>(it.value());
1009 for (double x : row)
1010 if (std::isfinite(x) && x > bmax) bmax = x;
1011 }
1012 return std::vector<T>(1, num_traits<T>::from_double(bmax));
1013}
1014
1015/**
1016 * Refuse any top-level or node-level key that carries model semantics this
1017 * reader does not consume.
1018 *
1019 * The whitelists are the keys the passes below actually read, plus the purely
1020 * descriptive ones. Anything else is a construct the C++ model layer either
1021 * cannot represent or does not yet parse, and it is NAMED rather than dropped.
1022 */
1023inline void reject_unconsumed_model_keys(const json& model) {
1024 static const char* kModelKeys[] = {"name", "type", "nodes",
1025 "classes", "routing", "format",
1026 "version", "rewards", "finiteCapacityRegions",
1027 "routingStrategies", "routingWeights", "routingParams",
1028 "logPath", "globalDependence", "stateDepRouting"};
1029 // EXACTLY the keys a branch below reads, and nothing else. The first draft
1030 // of this list also carried `arrival`, `classCap`, `accessGraph`,
1031 // `joinStrategy`, `joinQuorum`, `fanOut` and `swapGraph`,
1032 // none of which this reader consumes -- whitelisting them would have
1033 // re-admitted the very silent drop the gate exists to stop. When adding a
1034 // key here, grep that the parser actually reads it.
1035 static const char* kNodeKeys[] = {
1036 "name", "type", "scheduling", "servers", "service",
1037 "buffer", "capacity", "dropRule", "schedParams", "pollingType",
1038 "pollingPar", "classSwitchMatrix", "csMatrix", "forkNode",
1039 "tasksPerLink", "fanOutByDest", "fanOutDist", "fanOutProb",
1040 "items", "numItems", "itemLevelCap", "popularity", "replacementStrategy",
1041 "itemSizes", "costCaps", "accessProb", "admissionProb", "accessGraph",
1042 "itemClass",
1043 "cache", "initialState",
1044 "retrievalSystem", "queues", "hitClass", "missClass",
1045 "immediateFeedback", "loadDependence",
1046 "classDependence", "jointDependence",
1047 "modes", "classCap", "departureDiscipline",
1048 "oiServiceRate", "oiCutoffs", "swapGraph",
1049 "arrivalBatch", "markedClasses",
1050 "stateSpace", "statePrior", "joinStrategy", "joinQuorum",
1051 "setupTime", "delayOffTime","switchoverTimes", "breakdown",
1052 "serverTypes","heteroSchedPolicy", "serverParallelism",
1053 "balking", "retrial", "patience", "orbitImpatience",
1054 "batchRejectProb",
1055 // Logger trace configuration; read into NodeDef::logger below.
1056 "fileName", "filePath", "startTime", "loggerName", "timestamp",
1057 "jobID", "jobClass", "timeSameClass","timeAnyClass"};
1058 // The class object. It had NO gate until every key below was found to be
1059 // read: `isReferenceClass`, `deadline`, `patience`, `spawnClass` and
1060 // `replySignalClass` were all being dropped in silence, each of them a
1061 // different wrong number rather than a diagnostic.
1062 static const char* kClassKeys[] = {
1063 "name", "type", "population", "refNode", "priority",
1064 "openOrClosed","signalType", "targetClass", "removalPolicy","removalDistribution",
1065 "isReferenceClass", "deadline", "patience", "impatienceType",
1066 "spawnClass", "replySignalClass", "immediateFeedback"};
1067 auto known = [](const char* const* tab, std::size_t n, const std::string& k) {
1068 for (std::size_t i = 0; i < n; ++i)
1069 if (k == tab[i]) return true;
1070 return false;
1071 };
1072 const std::string why =
1073 "', which this reader does not implement. Refusing rather than dropping it: a "
1074 "constraint silently discarded here would make every solver return a confident "
1075 "answer for a different model";
1076 for (auto it = model.begin(); it != model.end(); ++it)
1077 if (!known(kModelKeys, sizeof(kModelKeys) / sizeof(*kModelKeys), it.key()))
1078 throw UnsupportedError("network_reader: the model carries '" + it.key() + why);
1079 if (model.contains("classes"))
1080 for (const json& cl : model.at("classes"))
1081 for (auto it = cl.begin(); it != cl.end(); ++it)
1082 if (!known(kClassKeys, sizeof(kClassKeys) / sizeof(*kClassKeys), it.key()))
1083 throw UnsupportedError("network_reader: class '" +
1084 cl.value("name", std::string("?")) + "' carries '" +
1085 it.key() + why);
1086 if (!model.contains("nodes")) return;
1087 for (const json& nd : model.at("nodes"))
1088 for (auto it = nd.begin(); it != nd.end(); ++it)
1089 if (!known(kNodeKeys, sizeof(kNodeKeys) / sizeof(*kNodeKeys), it.key()))
1090 throw UnsupportedError("network_reader: node '" +
1091 nd.value("name", std::string("?")) + "' carries '" +
1092 it.key() + why);
1093}
1094
1095} // namespace detail
1096
1097/**
1098 * Build a `qn::Network<T>` from a parsed model.json envelope.
1099 *
1100 * Two passes: classes and nodes are declared first (a class references its
1101 * reference node by name, a Join references its Fork by name, so every node
1102 * index must exist before the cross-references are wired), then service,
1103 * arrival and routing are applied.
1104 */
1105template <class T>
1106qn::Network<T> build_network_from_json(const detail::json& root) {
1107 using detail::json;
1108 const json& model = root.contains("model") ? root.at("model") : root;
1109 const std::string mtype = model.value("type", std::string("Network"));
1110 if (mtype != "Network") {
1111 // An Environment IS solved by this port, just not by this reader, so
1112 // the refusal names the arm that reads it rather than leaving the
1113 // caller to conclude the model is unsupported.
1114 if (mtype == "Environment")
1115 throw UnsupportedError(
1116 "network_reader: this is an Environment model (a network per stage plus the "
1117 "stage transitions); solve it with -s env, which reads it through "
1118 "environment_reader.h");
1119 throw UnsupportedError("network_reader: model type '" + mtype +
1120 "' is not a Network; only Network models are solved by this path");
1121 }
1122
1123 // WHAT THIS READER DOES NOT UNDERSTAND, IT REFUSES. A key carrying model
1124 // semantics that no branch below consumes would otherwise be SILENTLY
1125 // DROPPED, and every solver would then return an answer correct for the
1126 // model received and wrong for the model intended, with no diagnostic
1127 // anywhere. Not hypothetical: `fcr_mm1kdrop` exports a
1128 // `finiteCapacityRegions` block with globalMaxJobs 3 and a drop rule, and
1129 // without this gate the C++ MVA reported QLen 4 -- exactly rho/(1-rho) for
1130 // the UNBOUNDED M/M/1 -- against the exact M/M/1/K value 1.224932. Same
1131 // rule, and same reason, as the unknown-argument refusal on the --api
1132 // boundary: a key that silently takes its default is a wrong answer.
1133 detail::reject_unconsumed_model_keys(model);
1134
1135 qn::Network<T> net(model.value("name", std::string("model")));
1136 net.set_log_path(model.value("logPath", std::string()));
1137
1138 // A ClassSwitch node needs the class count at construction and a Join needs
1139 // its Fork to exist, while a closed class needs its reference node. That
1140 // cycle is broken by ordering the declarations, not by post-hoc setters:
1141 // every node a class or a Join can depend on is created first (pass 1a),
1142 // then the classes (1b), then the dependent nodes (1c). Routing is wired by
1143 // node NAME (pass 3), so this reordering never perturbs the parity table.
1144 const json& nodes = model.at("nodes");
1145 std::map<std::string, std::size_t> node_idx;
1146 std::vector<std::string> node_type(nodes.size());
1147 for (std::size_t i = 0; i < nodes.size(); ++i)
1148 node_type[i] = nodes[i].at("type").get<std::string>();
1149
1150 // -- Pass 1a: nodes that carry no class/fork dependency. -----------------
1151 for (std::size_t i = 0; i < nodes.size(); ++i) {
1152 const json& nd = nodes[i];
1153 const std::string& type = node_type[i];
1154 // A Join is a STATION, so deferring its creation would shift every
1155 // station index after it and rotate the station rows of the result
1156 // document; it is declared here, in model order, and bound to its Fork
1157 // in pass 1c. ClassSwitch, Cache and Transition are plain nodes, so
1158 // their late creation perturbs no station index.
1159 if (type == "ClassSwitch" || type == "Cache" || type == "Transition") continue;
1160 const std::string name = nd.at("name").get<std::string>();
1161 std::size_t idx = 0;
1162 if (type == "Source") {
1163 idx = net.add_source(name);
1164 } else if (type == "Sink") {
1165 idx = net.add_sink(name);
1166 } else if (type == "Delay") {
1167 idx = net.add_delay(name);
1168 } else if (type == "Queue") {
1169 idx = net.add_queue(name,
1170 detail::sched_from_json(nd.value("scheduling", std::string("FCFS"))));
1171 } else if (type == "Router") {
1172 idx = net.add_router(name);
1173 } else if (type == "Logger" || type == "LogTunnel") {
1174 idx = net.add_logger(name, nd.value("fileName", std::string()));
1175 qn::NodeDef::LoggerParam& lg = net.raw_struct().nodes[idx - 1].logger;
1176 if (nd.contains("filePath")) lg.file_path = nd.at("filePath").get<std::string>();
1177 lg.start_time = nd.value("startTime", lg.start_time);
1178 lg.logger_name = nd.value("loggerName", lg.logger_name);
1179 lg.timestamp = nd.value("timestamp", lg.timestamp);
1180 lg.job_id = nd.value("jobID", lg.job_id);
1181 lg.job_class = nd.value("jobClass", lg.job_class);
1182 lg.time_same_class = nd.value("timeSameClass", lg.time_same_class);
1183 lg.time_any_class = nd.value("timeAnyClass", lg.time_any_class);
1184 } else if (type == "Place") {
1185 idx = net.add_place(name);
1186 } else if (type == "Join") {
1187 idx = net.add_join_unbound(name);
1188 } else if (type == "Fork") {
1189 idx = net.add_fork(name, detail::num_value(nd, "tasksPerLink", 1.0));
1190 } else {
1191 throw UnsupportedError("network_reader: unsupported node type '" + type + "' at node '" +
1192 name + "'");
1193 }
1194 node_idx[name] = idx;
1195 }
1196
1197 // -- Pass 1b: classes. ---------------------------------------------------
1198 const json& classes = model.at("classes");
1199 std::map<std::string, std::size_t> class_idx;
1200 // The signal classes, resolved after the whole class list exists: a
1201 // signal's TARGET is another class by name, which may follow it.
1202 std::vector<std::size_t> signal_classes;
1203 for (std::size_t r = 0; r < classes.size(); ++r) {
1204 const json& cl = classes[r];
1205 const std::string name = cl.at("name").get<std::string>();
1206 const std::string type = cl.at("type").get<std::string>();
1207 std::size_t idx = 0;
1208 if (type == "Open") {
1209 idx = net.add_open_class(name, cl.value("priority", 0));
1210 } else if (type == "Closed" || type == "SelfLooping") {
1211 // A `SelfLoopingClass` IS a closed class -- its jobs cycle at the
1212 // reference station, which is a property of the routing -- so it is
1213 // built as one and only tagged, exactly as `linemodel_load.m:125`
1214 // handles the two labels in one branch.
1215 const double pop = cl.at("population").get<double>();
1216 const std::string ref = cl.at("refNode").get<std::string>();
1217 auto it = node_idx.find(ref);
1218 if (it == node_idx.end())
1219 throw InputError("network_reader: class '" + name + "' references unknown node '" +
1220 ref + "'");
1221 idx = type == "SelfLooping"
1222 ? net.add_self_looping_class(name, pop, it->second, cl.value("priority", 0))
1223 : net.add_closed_class(name, pop, it->second, cl.value("priority", 0));
1224 } else if (type == "Signal") {
1225 // A G-network SIGNAL is an ordinary open or closed class that
1226 // REMOVES jobs instead of joining a queue, so it is created as its
1227 // underlying kind first and marked afterwards. `openOrClosed`
1228 // carries that kind; without it the signal would be built as a
1229 // closed class with no population.
1230 const std::string kind = cl.value("openOrClosed", std::string("Open"));
1231 if (kind == "Closed") {
1232 const std::string ref = cl.at("refNode").get<std::string>();
1233 auto it = node_idx.find(ref);
1234 if (it == node_idx.end())
1235 throw InputError("network_reader: signal class '" + name +
1236 "' references unknown node '" + ref + "'");
1237 // A CLOSED SIGNAL HAS NO POPULATION OF ITS OWN, and the writers
1238 // therefore emit no `population` for it: `ClosedSignal.m:69`
1239 // passes 0 up to ClosedClass, its jobs arriving only by class
1240 // switch from the caller. Requiring the key made every REPLY
1241 // model unreadable here with a raw json out_of_range.
1242 idx = net.add_closed_class(name, cl.value("population", 0.0), it->second,
1243 cl.value("priority", 0));
1244 } else {
1245 idx = net.add_open_class(name, cl.value("priority", 0));
1246 }
1247 signal_classes.push_back(r);
1248 } else {
1249 throw UnsupportedError("network_reader: unsupported class type '" + type +
1250 "' for class '" + name + "'");
1251 }
1252 // The class-wide spelling of immediate feedback; the node-level map is
1253 // read in pass 2 and `sn.immfeed` is the OR of the two.
1254 if (cl.value("immediateFeedback", false)) net.set_class_immediate_feedback(idx);
1255 // `setReferenceClass`: which class of a chain `sn.refclass` names. It is
1256 // the denominator of every chain visit ratio in `sn_get_demands_chain`
1257 // and `sn_get_product_form_params`, so dropping it silently rescales
1258 // the demands of every multi-class chain that declares one.
1259 if (cl.value("isReferenceClass", false)) net.set_reference_class(idx);
1260 // `JobClass.deadline`, `sn.classdeadline`: EDD and EDF order by it.
1261 if (cl.contains("deadline")) {
1262 const double due = cl.at("deadline").get<double>();
1263 if (std::isfinite(due)) net.set_class_deadline(idx, due);
1264 }
1265 // The CLASS-WIDE patience, `Queue.getPatience`'s fallback. A node-scoped
1266 // `patience` read in pass 2 overrides it at the station that names it.
1267 if (cl.contains("patience")) {
1268 const json& pt = cl.at("patience");
1269 const lang::Distrib<T> pd = detail::dist_from_json<T>(pt);
1270 if (!pd.disabled)
1272 idx, pd,
1273 cl.contains("impatienceType")
1274 ? detail::impatience_from_json(cl.at("impatienceType").get<std::string>())
1276 }
1277 class_idx[name] = idx;
1278 }
1279 // The class-to-class bindings, resolved once every class exists: either
1280 // side may be declared after the class that names it.
1281 for (std::size_t r = 0; r < classes.size(); ++r) {
1282 const json& cl = classes[r];
1283 const std::size_t idx = class_idx.at(cl.at("name").get<std::string>());
1284 if (cl.contains("spawnClass")) {
1285 const std::string sp = cl.at("spawnClass").get<std::string>();
1286 auto sit = class_idx.find(sp);
1287 if (sit == class_idx.end())
1288 throw InputError("network_reader: class '" + cl.at("name").get<std::string>() +
1289 "' spawns class '" + sp + "', which the model does not declare");
1290 net.set_class_spawn(idx, sit->second);
1291 }
1292 // Without this a REPLY signal class is INERT after a round trip:
1293 // nothing unblocks the servers waiting on it (`linemodel_save.m:797`).
1294 if (cl.contains("replySignalClass")) {
1295 const std::string rp = cl.at("replySignalClass").get<std::string>();
1296 auto rit = class_idx.find(rp);
1297 if (rit == class_idx.end())
1298 throw InputError("network_reader: class '" + cl.at("name").get<std::string>() +
1299 "' replies with class '" + rp +
1300 "', which the model does not declare");
1301 net.set_reply_signal_class(idx, rit->second);
1302 }
1303 }
1304 // Signals, now that every class the target may name exists.
1305 for (std::size_t si = 0; si < signal_classes.size(); ++si) {
1306 const json& cl = classes[signal_classes[si]];
1307 const std::string name = cl.at("name").get<std::string>();
1308 const std::string st = cl.value("signalType", std::string("negative"));
1310 if (st == "reply" || st == "REPLY") kind = lang::SignalType::REPLY;
1311 else if (st == "catastrophe" || st == "CATASTROPHE") kind = lang::SignalType::CATASTROPHE;
1312 else if (st != "negative" && st != "NEGATIVE")
1313 throw UnsupportedError("network_reader: signal class '" + name + "' is of type '" + st +
1314 "', and the kinds on the wire are negative, catastrophe and "
1315 "reply");
1317 const std::string rp = cl.value("removalPolicy", std::string("RANDOM"));
1318 if (rp == "FCFS" || rp == "fcfs") pol = lang::RemovalPolicy::FCFS;
1319 else if (rp == "LCFS" || rp == "lcfs") pol = lang::RemovalPolicy::LCFS;
1320 std::size_t target = 0;
1321 if (cl.contains("targetClass")) {
1322 auto tit = class_idx.find(cl.at("targetClass").get<std::string>());
1323 if (tit == class_idx.end())
1324 throw InputError("network_reader: signal class '" + name + "' targets class '" +
1325 cl.at("targetClass").get<std::string>() +
1326 "', which the model does not declare");
1327 target = tit->second;
1328 }
1329 std::vector<T> remdist;
1330 if (cl.contains("removalDistribution"))
1331 remdist = detail::removal_pmf_from_json<T>(cl.at("removalDistribution"));
1332 net.set_signal(class_idx.at(name), kind, pol, target, remdist);
1333 }
1334 const std::size_t K = class_idx.size();
1335
1336 // -- Pass 1c: ClassSwitch (matrix now sizeable), Join (fork exists) and
1337 // Cache (its hit/miss classes and popularity are class-indexed). --------
1338 for (std::size_t i = 0; i < nodes.size(); ++i) {
1339 const json& nd = nodes[i];
1340 const std::string& type = node_type[i];
1341 if (type != "ClassSwitch" && type != "Join" && type != "Cache" && type != "Fork") continue;
1342 const std::string name = nd.at("name").get<std::string>();
1343 std::size_t idx = 0;
1344 if (type == "Fork") {
1345 // VARIABLE FORKING LEVELS. Each list is an array of
1346 // {dest, class, ...} records, exactly as `linemodel_save.m:509-553`
1347 // writes them; `dest` is a node NAME and `class` a 1-based index.
1348 // The overrides are recorded here and replayed by `link()` in Pass 3,
1349 // because a per-destination override reads the routing.
1350 if (!nd.contains("fanOutByDest") && !nd.contains("fanOutDist") &&
1351 !nd.contains("fanOutProb"))
1352 continue;
1353 idx = node_idx.at(name);
1354 if (nd.contains("fanOutByDest")) {
1355 const json& ovs = nd.at("fanOutByDest");
1356 for (std::size_t e = 0; e < ovs.size(); ++e)
1357 net.set_fork_tasks_per_link(idx, ovs[e].at("class").get<std::size_t>(),
1358 ovs[e].at("value").get<double>(),
1359 detail::fork_dest_index(ovs[e], node_idx, name));
1360 }
1361 if (nd.contains("fanOutDist")) {
1362 const json& ovs = nd.at("fanOutDist");
1363 for (std::size_t e = 0; e < ovs.size(); ++e) {
1364 const std::vector<double> pv = ovs[e].at("p").get<std::vector<double> >();
1365 const std::vector<double> xv = ovs[e].at("x").get<std::vector<double> >();
1366 std::vector<T> p, x;
1367 for (std::size_t q = 0; q < pv.size(); ++q)
1368 p.push_back(num_traits<T>::from_double(pv[q]));
1369 for (std::size_t q = 0; q < xv.size(); ++q)
1370 x.push_back(num_traits<T>::from_double(xv[q]));
1371 net.set_fork_tasks_per_link_dist(idx, ovs[e].at("class").get<std::size_t>(),
1373 detail::fork_dest_index(ovs[e], node_idx, name));
1374 }
1375 }
1376 if (nd.contains("fanOutProb")) {
1377 const json& ovs = nd.at("fanOutProb");
1378 for (std::size_t e = 0; e < ovs.size(); ++e)
1380 idx, ovs[e].at("class").get<std::size_t>(),
1381 detail::fork_dest_index(ovs[e], node_idx, name),
1382 ovs[e].at("value").get<double>());
1383 }
1384 continue;
1385 }
1386 if (type == "Cache") {
1387 // THE WRITERS EMIT THE CACHE TWICE, by design: a nested `cache`
1388 // object and the same fields flattened onto the node, so that both
1389 // the MATLAB and the JAR readers find what they look for
1390 // (`linemodel_save.m:425-435`). The flat spelling wins where both
1391 // are present -- they are mirrored from the nested one, so they
1392 // agree -- and the nested one is consulted for anything the flat
1393 // mirror does not carry.
1394 const json empty_obj = json::object();
1395 const json& cj = nd.contains("cache") ? nd.at("cache") : empty_obj;
1397 cp.nitems = detail::has_cache_key(nd, cj, "numItems")
1398 ? detail::cache_key(nd, cj, "numItems").get<std::size_t>()
1399 : cj.at("items").get<std::size_t>();
1400 cp.itemcap = detail::has_cache_key(nd, cj, "itemLevelCap")
1401 ? detail::cache_key(nd, cj, "itemLevelCap").get<std::vector<int> >()
1402 : cj.at("capacity").get<std::vector<int> >();
1403 // Per-item storage costs and per-list cost caps (ton21cache Sec. IX).
1404 // A scalar costCaps is the single cache-wide cap, replicated per list.
1405 if (detail::has_cache_key(nd, cj, "itemSizes"))
1406 cp.itemsize = detail::cache_key(nd, cj, "itemSizes").get<std::vector<int> >();
1407 if (detail::has_cache_key(nd, cj, "costCaps")) {
1408 const json& cc = detail::cache_key(nd, cj, "costCaps");
1409 if (cc.is_array()) {
1410 cp.costcap = cc.get<std::vector<int> >();
1411 } else {
1412 cp.costcapglobal = true;
1413 cp.costcap.assign(cp.itemcap.size(), cc.get<int>());
1414 }
1415 }
1416 cp.replacestrat = detail::replacement_from_json(
1417 detail::has_cache_key(nd, cj, "replacementStrategy")
1418 ? detail::cache_key(nd, cj, "replacementStrategy").get<std::string>()
1419 : cj.value("replacement", std::string("RR")));
1420 cp.pread.assign(K, std::vector<T>());
1421 cp.hitclass.assign(K, 0);
1422 cp.missclass.assign(K, 0);
1423 cp.preadkind.assign(K, typename qn::CacheParam<T>::Popularity());
1424 cp.classitem.assign(K, 0);
1425 if (detail::has_cache_key(nd, cj, "popularity")) {
1426 const json& pop = detail::cache_key(nd, cj, "popularity");
1427 for (auto it = pop.begin(); it != pop.end(); ++it) {
1428 // A class that does not read the cache is written with a
1429 // `Disabled` popularity, and leaves `pread` empty
1430 // (`linemodel_load.m:602`).
1431 if (it.value().value("type", std::string()) == "Disabled") continue;
1432 cp.pread[class_idx.at(it.key()) - 1] =
1433 detail::pmf_from_json<T>(it.value(), cp.nitems);
1434 cp.preadkind[class_idx.at(it.key()) - 1] =
1435 detail::popularity_kind_from_json<T>(it.value(), cp.nitems);
1436 }
1437 }
1438 auto fill_switch = [&](const char* key, std::vector<std::size_t>& dst) {
1439 if (!detail::has_cache_key(nd, cj, key)) return;
1440 const json& blk = detail::cache_key(nd, cj, key);
1441 for (auto it = blk.begin(); it != blk.end(); ++it)
1442 dst[class_idx.at(it.key()) - 1] = class_idx.at(it.value().get<std::string>());
1443 };
1444 fill_switch("hitClass", cp.hitclass);
1445 fill_switch("missClass", cp.missclass);
1446 // `itemClass`: for a cache network, the item each per-item class reads
1447 // (`Cache.setItemReadClasses`). Carried so a reader need not infer it
1448 // from a one-hot pread, which a genuine single-item popularity also has.
1449 if (detail::has_cache_key(nd, cj, "itemClass")) {
1450 const json& blk = detail::cache_key(nd, cj, "itemClass");
1451 for (auto it = blk.begin(); it != blk.end(); ++it)
1452 cp.classitem[class_idx.at(it.key()) - 1] =
1453 static_cast<std::size_t>(it.value().get<double>());
1454 }
1455 // `accessProb`: the per-(class, item) access graph, `sn.nodeparam{i}.accost`.
1456 // The wire form is a class-major array of item-major arrays of
1457 // (h+1)x(h+1) matrices, with an empty entry where the pair declares
1458 // none (`linemodel_save.m:402-416`). It is READ HERE and nowhere else,
1459 // and dropping it would solve a cache whose admission and promotion
1460 // are item-dependent as if they were the linear chain.
1461 // `admissionProb`: the q-LRU admission probability, `nodeparam.qlru`.
1462 // Every Cache the reference writes carries it, defaulting to 1, so
1463 // refusing it made every MATLAB-written cache model unreadable here.
1464 if (detail::has_cache_key(nd, cj, "admissionProb"))
1465 cp.qlru = num_traits<T>::from_double(detail::cache_key(nd, cj, "admissionProb").get<double>());
1466 if (detail::has_cache_key(nd, cj, "accessProb")) {
1467 const json& ap = detail::cache_key(nd, cj, "accessProb");
1468 cp.accost.clear();
1469 for (const json& per_class : ap) {
1470 std::vector<Matrix<T> > row;
1471 for (const json& g : per_class) {
1472 if (g.is_null() || g.empty()) {
1473 row.push_back(Matrix<T>());
1474 continue;
1475 }
1476 row.push_back(detail::mat_from_json<T>(g));
1477 }
1478 cp.accost.push_back(row);
1479 }
1480 } else if (detail::has_cache_key(nd, cj, "accessGraph")) {
1481 // `accessGraph` is the SAME access cost shared by every class:
1482 // one (h+1)x(h+1) matrix per item, written when the model set
1483 // `node.graph` rather than the full per-class `accessProb`
1484 // (`linemodel_save.m:395-401`). Replicating it across the classes
1485 // here is what makes the two spellings the same model.
1486 const std::vector<Matrix<T> > shared =
1487 detail::mat_list_from_json<T>(detail::cache_key(nd, cj, "accessGraph"));
1488 cp.accost.assign(K, shared);
1489 }
1490 // The initial cache contents: the state row `[class counts | list
1491 // contents | retrieval bitmap]` the reference dumps from the node.
1492 if (detail::has_cache_key(nd, cj, "initialState"))
1493 cp.initstate = detail::num_vec_from_json<T>(detail::cache_key(nd, cj, "initialState"));
1494 // Delayed-hit retrieval system: the retrieval classes are already in
1495 // the class list (created in pass 1b) with their service and routing,
1496 // so only the cache-side maps are recovered here -- no re-creation.
1497 // Read through the same nested-or-flat helper every other cache key
1498 // uses. All three writers put this one flat today, so a direct
1499 // `nd.contains` happens to work -- but it is the shape python got
1500 // wrong (it chose ONE source per node and lost whatever the other
1501 // held), and a reader that treats one key differently from its
1502 // siblings is the trap that made that possible.
1503 if (detail::has_cache_key(nd, cj, "retrievalSystem")) {
1504 const json& rs = detail::cache_key(nd, cj, "retrievalSystem");
1505 cp.retrieval_capacity = rs.value("capacity", 0);
1506 cp.retrieval_classes.assign(cp.nitems, std::vector<std::size_t>(K, 0));
1507 if (rs.contains("byClass")) {
1508 for (auto rc = rs.at("byClass").begin(); rc != rs.at("byClass").end(); ++rc) {
1509 const std::size_t rdcls = class_idx.at(rc.key()); // 1-based read class
1510 std::vector<std::size_t> qnodes;
1511 for (const auto& qn : rc.value().at("queues"))
1512 qnodes.push_back(node_idx.at(qn.get<std::string>()));
1513 cp.retrieval_queues[rdcls - 1] = qnodes;
1514 for (auto it2 = rc.value().at("items").begin();
1515 it2 != rc.value().at("items").end(); ++it2) {
1516 const std::size_t item = std::stoul(it2.key());
1517 if (item < cp.nitems)
1518 cp.retrieval_classes[item][rdcls - 1] =
1519 class_idx.at(it2.value().get<std::string>());
1520 }
1521 }
1522 }
1523 }
1524 idx = net.add_cache(name, cp);
1525 node_idx[name] = idx;
1526 continue;
1527 }
1528 if (type == "Join") {
1529 std::size_t fork = 0;
1530 if (nd.contains("forkNode")) {
1531 auto it = node_idx.find(nd.at("forkNode").get<std::string>());
1532 if (it == node_idx.end())
1533 throw InputError("network_reader: Join '" + name +
1534 "' references unknown fork '" +
1535 nd.at("forkNode").get<std::string>() + "'");
1536 fork = it->second;
1537 }
1538 // The station itself was declared in pass 1a, in model order, so
1539 // only the Fork it closes is recorded here.
1540 idx = node_idx.at(name);
1541 net.bind_join(idx, fork);
1542 // The declared join rule. PARTIAL fires on a quorum of siblings
1543 // rather than on all of them, so dropping it would make a partial
1544 // join wait for arrivals that never come.
1545 if (nd.contains("joinStrategy") || nd.contains("joinQuorum")) {
1546 const std::string js = nd.value("joinStrategy", std::string("STD"));
1547 if (js != "STD" && js != "PARTIAL")
1548 throw UnsupportedError("network_reader: Join '" + name + "' declares strategy '" +
1549 js + "', and the rules on the wire are STD and PARTIAL");
1550 net.set_join_strategy(idx,
1551 js == "PARTIAL" ? lang::JoinStrategy::PARTIAL
1553 nd.value("joinQuorum", 0.0));
1554 }
1555 } else {
1556 // Rows a class-switch matrix omits leave that class unchanged, so the
1557 // matrix defaults to the identity before the listed rows overwrite it.
1558 // The builder's class indices are 1-based; the matrix is 0-indexed.
1559 // `csMatrix` is the JAR writer's spelling of the same block.
1560 Matrix<T> C(K, K);
1561 for (std::size_t d = 0; d < K; ++d) C(d, d) = num_traits<T>::from_int(1);
1562 if (nd.contains("classSwitchMatrix") || nd.contains("csMatrix")) {
1563 const json& csm =
1564 nd.contains("classSwitchMatrix") ? nd.at("classSwitchMatrix") : nd.at("csMatrix");
1565 for (auto ri = csm.begin(); ri != csm.end(); ++ri) {
1566 const std::size_t rr = class_idx.at(ri.key()) - 1;
1567 for (std::size_t d = 0; d < K; ++d) C(rr, d) = num_traits<T>::from_int(0);
1568 for (auto ci = ri.value().begin(); ci != ri.value().end(); ++ci)
1569 C(rr, class_idx.at(ci.key()) - 1) =
1570 num_traits<T>::from_double(ci.value().get<double>());
1571 }
1572 }
1573 idx = net.add_class_switch(name, C);
1574 }
1575 node_idx[name] = idx;
1576 }
1577
1578 // -- Pass 1d: Transitions, whose arcs name other nodes. -------------------
1579 // A mode's enabling/inhibiting/firing arcs are written as (node, class,
1580 // count) triples, so every node must exist before they can be resolved, and
1581 // the arc matrices are sized against the FULL node count rather than the
1582 // count so far. THE CLASS IS KEPT: `TransitionParam` stores an
1583 // (nnodes x nclasses) matrix per mode, as MATLAB's `enablingConditions{m}`
1584 // does, so a mode requiring two Class1 tokens at a place is not satisfied by
1585 // Class2 tokens sitting there. An arc naming a class the model does not
1586 // declare is an error rather than a silent drop.
1587 for (std::size_t i = 0; i < nodes.size(); ++i) {
1588 if (node_type[i] != "Transition") continue;
1589 const json& nd = nodes[i];
1590 const std::string name = nd.at("name").get<std::string>();
1591 if (!nd.contains("modes") || nd.at("modes").empty())
1592 throw InputError("network_reader: transition '" + name + "' declares no mode");
1593 const json& modes = nd.at("modes");
1595 tp.nmodes = modes.size();
1596 const std::size_t nn = nodes.size();
1597 const double inf = std::numeric_limits<double>::infinity();
1598 for (std::size_t m = 0; m < modes.size(); ++m) {
1599 const json& mj = modes[m];
1600 tp.modenames.push_back(mj.value("name", std::string("Mode") + std::to_string(m + 1)));
1601 // A marking-dependent firing rate g_m(marking) crosses the wire as
1602 // the MATERIALIZED lattice `{slots, cutoffs, scaling}`, because a
1603 // handle has no JSON form: `slots` names the enabling (place,class)
1604 // pairs that g reads, `cutoffs` caps each of them, and `scaling` is
1605 // keyed by the comma-joined 0-based counts in slot order. Rebuilt
1606 // here into the same closure MATLAB's `firingdep_table_to_handle`
1607 // and the Python `_firingdep_table_to_handle` build, over the
1608 // node-indexed marking `state_events.h` passes in.
1609 tp.firingdep.push_back(std::function<T(const std::vector<T>&)>());
1610 if (mj.contains("firingRateDependence")) {
1611 const json& frm = mj.at("firingRateDependence");
1612 if (!frm.contains("slots") || !frm.contains("scaling"))
1613 throw InputError("network_reader: the marking-dependent firing rate of mode " +
1614 std::to_string(m + 1) + " of transition '" + name +
1615 "' carries no 'slots'/'scaling' lattice");
1616 std::vector<std::size_t> slot_node;
1617 for (const json& sm : frm.at("slots")) {
1618 const std::string on = sm.at("node").get<std::string>();
1619 const std::map<std::string, std::size_t>::const_iterator ni = node_idx.find(on);
1620 if (ni == node_idx.end())
1621 throw InputError("network_reader: the marking-dependent firing rate of "
1622 "transition '" +
1623 name + "' reads node '" + on +
1624 "', which the model does not declare");
1625 slot_node.push_back(ni->second - 1);
1626 }
1627 std::vector<long> cutoffs;
1628 if (frm.contains("cutoffs"))
1629 for (const json& c : frm.at("cutoffs")) cutoffs.push_back(c.get<long>());
1630 std::map<std::string, double> table;
1631 const json& sc = frm.at("scaling");
1632 for (json::const_iterator it = sc.begin(); it != sc.end(); ++it)
1633 table[it.key()] = it.value().get<double>();
1634 tp.firingdep.back() = [slot_node, cutoffs,
1635 table](const std::vector<T>& mk) -> T {
1636 std::string key;
1637 for (std::size_t s = 0; s < slot_node.size(); ++s) {
1638 long c = slot_node[s] < mk.size()
1639 ? static_cast<long>(std::llround(
1640 num_traits<T>::to_double(mk[slot_node[s]])))
1641 : 0L;
1642 if (c < 0) c = 0;
1643 if (s < cutoffs.size() && c > cutoffs[s]) c = cutoffs[s];
1644 if (s) key += ',';
1645 key += std::to_string(c);
1646 }
1647 const std::map<std::string, double>::const_iterator hit = table.find(key);
1648 // A marking outside the tabulated box is NEUTRAL, not zero:
1649 // the same default the three reference readers apply.
1650 return hit == table.end() ? num_traits<T>::from_int(1)
1651 : num_traits<T>::from_double(hit->second);
1652 };
1653 }
1654 const bool immediate =
1655 mj.value("timingStrategy", std::string("TIMED")) == "IMMEDIATE";
1656 tp.timing.push_back(immediate ? lang::TimingStrategy::IMMEDIATE
1659 if (mj.contains("distribution")) proc = detail::dist_from_json<T>(mj.at("distribution"));
1660 tp.firingproc.push_back(proc);
1661 tp.firingphases.push_back(immediate || proc.disabled ? 0
1662 : lang::dist_to_map(proc).order());
1663 tp.nmodeservers.push_back(detail::num_value(mj, "numServers", 1.0));
1664 // ONE, not zero: `Transition.addMode` gives a new mode priority 1 in
1665 // all four codebases (MATLAB Transition.m:88, python nodes.py:2660,
1666 // JAR Transition.java:114), and the three readers leave that default
1667 // in place when the key is absent. Defaulting to 0 here made an
1668 // omitted key mean a DIFFERENT mode than it means everywhere else,
1669 // and firing priority selects which immediate mode fires, so the
1670 // marking process itself changed rather than a reported decimal.
1671 tp.firingprio.push_back(mj.value("firingPriority", 1.0));
1672 tp.fireweight.push_back(num_traits<T>::from_double(mj.value("firingWeight", 1.0)));
1673 const std::size_t K = classes.size();
1674 Matrix<T> enab(nn, K, num_traits<T>::from_int(0));
1675 Matrix<T> inhib(nn, K, num_traits<T>::from_double(inf));
1676 Matrix<T> fire(nn, K, num_traits<T>::from_int(0));
1677 const char* kArcKey[3] = {"enablingConditions", "inhibitingConditions",
1678 "firingOutcomes"};
1679 for (int which = 0; which < 3; ++which) {
1680 if (!mj.contains(kArcKey[which])) continue;
1681 for (const json& arc : mj.at(kArcKey[which])) {
1682 const std::string on = arc.at("node").get<std::string>();
1683 const std::map<std::string, std::size_t>::const_iterator ni = node_idx.find(on);
1684 if (ni == node_idx.end())
1685 throw InputError("network_reader: transition '" + name + "' names node '" +
1686 on + "', which the model does not declare");
1687 // AN ARC WITHOUT A CLASS IS THE FIRST CLASS'S, which is what
1688 // a single-class document means by omitting it; a name the
1689 // model never declared is an error, because dropping the arc
1690 // would build a net with one fewer precondition.
1691 std::size_t rr = 1;
1692 if (arc.contains("class")) {
1693 const std::string cn = arc.at("class").get<std::string>();
1694 const std::map<std::string, std::size_t>::const_iterator ci =
1695 class_idx.find(cn);
1696 if (ci == class_idx.end())
1697 throw InputError("network_reader: transition '" + name +
1698 "' names class '" + cn +
1699 "', which the model does not declare");
1700 rr = ci->second;
1701 }
1702 const T cnt = num_traits<T>::from_double(arc.value("count", 1.0));
1703 const std::size_t q = ni->second - 1;
1704 if (which == 0) enab(q, rr - 1) = enab(q, rr - 1) + cnt;
1705 else if (which == 1) inhib(q, rr - 1) = cnt; // a threshold, not a count
1706 else fire(q, rr - 1) = fire(q, rr - 1) + cnt;
1707 }
1708 }
1709 tp.enabling.push_back(enab);
1710 tp.inhibiting.push_back(inhib);
1711 tp.firing.push_back(fire);
1712 }
1713 node_idx[name] = net.add_transition(name, tp);
1714 }
1715
1716 // -- Pass 2: per-node parameters (service/arrival, servers, CS matrix). ---
1717 for (std::size_t i = 0; i < nodes.size(); ++i) {
1718 const json& nd = nodes[i];
1719 const std::string& type = node_type[i];
1720 const std::size_t idx = node_idx.at(nd.at("name").get<std::string>());
1721
1722 if (nd.contains("servers") && (type == "Queue"))
1723 net.set_number_of_servers(idx, detail::num_from_json(nd.at("servers")));
1724
1725 if (nd.contains("service")) {
1726 const json& svc = nd.at("service");
1727 for (auto it = svc.begin(); it != svc.end(); ++it) {
1728 auto cit = class_idx.find(it.key());
1729 if (cit == class_idx.end())
1730 throw InputError("network_reader: service names unknown class '" + it.key() +
1731 "' at node '" + nd.at("name").get<std::string>() + "'");
1732 const lang::Distrib<T> d = detail::dist_from_json<T>(it.value());
1733 if (type == "Source") net.set_arrival(idx, cit->second, d);
1734 else net.set_service(idx, cit->second, d);
1735 }
1736 }
1737
1738 if (type == "Queue" && nd.contains("scheduling") &&
1739 nd.at("scheduling").get<std::string>() == "POLLING") {
1740 if (nd.contains("pollingType"))
1741 net.set_polling_type(idx, detail::polling_from_json(nd.at("pollingType").get<std::string>()),
1742 nd.value("pollingPar", 0));
1743 }
1744
1745 // Per-class scheduling weights (DPS / GPS). Without these the AMVA DPS
1746 // path either errors (no weights) or degenerates to plain PS.
1747 if (nd.contains("schedParams") && (type == "Queue" || type == "Delay")) {
1748 const json& sp = nd.at("schedParams");
1749 for (auto it = sp.begin(); it != sp.end(); ++it) {
1750 auto cit = class_idx.find(it.key());
1751 if (cit != class_idx.end())
1752 net.set_sched_param(idx, cit->second,
1753 num_traits<T>::from_double(it.value().get<double>()));
1754 }
1755 }
1756
1757 // Finite-buffer blocking. `dropRule` per class (blockingAfterService ->
1758 // BAS makes mva_is_bas_model true, routing the model to solver_sqd) and
1759 // `buffer` the total station capacity.
1760 // The writers emit these for `isa(node,'Station')`, which is wider than
1761 // Queue and Delay: a Join and a queueing Place are stations too, and a
1762 // rule read only at a Queue is a rule silently dropped at those.
1763 const bool station_node =
1764 type == "Queue" || type == "Delay" || type == "Join" || type == "Place";
1765 if (nd.contains("dropRule") && station_node) {
1766 const json& dr = nd.at("dropRule");
1767 for (auto it = dr.begin(); it != dr.end(); ++it) {
1768 auto cit = class_idx.find(it.key());
1769 if (cit != class_idx.end())
1770 net.set_drop_rule(idx, cit->second,
1771 detail::drop_from_json(it.value().get<std::string>()));
1772 }
1773 }
1774 if (nd.contains("buffer") && station_node)
1775 net.set_capacity(idx, detail::num_from_json(nd.at("buffer")));
1776 // Per-class buffer capacity, the refinement of `buffer`.
1777 if (nd.contains("classCap")) {
1778 const json& cc = nd.at("classCap");
1779 for (auto it = cc.begin(); it != cc.end(); ++it) {
1780 auto cit = class_idx.find(it.key());
1781 if (cit != class_idx.end())
1782 net.set_class_capacity(idx, cit->second, detail::num_from_json(it.value()));
1783 }
1784 }
1785
1786 // -- Impatience, balking and the retrial orbit -----------------------
1787 //
1788 // Four DIFFERENT populations, and the reference keeps them apart: a
1789 // patience timer abandons a job that has JOINED the queue, balking
1790 // refuses to join at all, an orbit impatience abandons a retrial orbit
1791 // (never a buffer slot), and a batch rejection drops a whole arriving
1792 // batch. Folding any two together changes which jobs are counted.
1793 if (nd.contains("patience")) {
1794 const json& pt = nd.at("patience");
1795 for (auto it = pt.begin(); it != pt.end(); ++it) {
1796 auto cit = class_idx.find(it.key());
1797 if (cit == class_idx.end()) continue;
1798 const json& pj = it.value();
1799 const lang::ImpatienceType kind =
1800 pj.contains("impatienceType")
1801 ? detail::impatience_from_json(pj.at("impatienceType").get<std::string>())
1803 net.set_patience(idx, cit->second, detail::dist_from_json<T>(pj.at("distribution")),
1804 kind);
1805 }
1806 }
1807 if (nd.contains("orbitImpatience")) {
1808 const json& oi = nd.at("orbitImpatience");
1809 for (auto it = oi.begin(); it != oi.end(); ++it) {
1810 auto cit = class_idx.find(it.key());
1811 if (cit != class_idx.end())
1812 net.set_orbit_impatience(idx, cit->second, detail::dist_from_json<T>(it.value()));
1813 }
1814 }
1815 if (nd.contains("batchRejectProb")) {
1816 const json& br = nd.at("batchRejectProb");
1817 for (auto it = br.begin(); it != br.end(); ++it) {
1818 auto cit = class_idx.find(it.key());
1819 if (cit != class_idx.end())
1820 net.set_batch_reject(idx, cit->second,
1821 num_traits<T>::from_double(it.value().get<double>()));
1822 }
1823 }
1824 if (nd.contains("balking")) {
1825 const json& bk = nd.at("balking");
1826 for (auto it = bk.begin(); it != bk.end(); ++it) {
1827 auto cit = class_idx.find(it.key());
1828 if (cit == class_idx.end()) continue;
1829 const json& bj = it.value();
1830 std::vector<typename qn::Station<T>::BalkingThreshold> ths;
1831 if (bj.contains("thresholds"))
1832 for (const json& tj : bj.at("thresholds")) {
1834 th.min_jobs = tj.value("minJobs", 0.0);
1835 // -1 IS THE WIRE'S INFINITY, not a count: the writer maps
1836 // an unbounded upper end to it rather than emitting Inf,
1837 // which JSON has no literal for.
1838 th.max_jobs = tj.value("maxJobs", -1.0);
1839 th.probability = num_traits<T>::from_double(tj.value("probability", 1.0));
1840 ths.push_back(th);
1841 }
1842 net.set_balking(idx, cit->second,
1843 detail::balking_from_json(bj.at("strategy").get<std::string>()), ths);
1844 }
1845 }
1846 if (nd.contains("retrial")) {
1847 const json& rt = nd.at("retrial");
1848 for (auto it = rt.begin(); it != rt.end(); ++it) {
1849 auto cit = class_idx.find(it.key());
1850 if (cit == class_idx.end()) continue;
1851 const lang::Distrib<T> delay = detail::dist_from_json<T>(it.value().at("delay"));
1852 // `sn.retrialMu` is the RATE of that delay, which is what the
1853 // state layer multiplies the orbit population by.
1854 const T rate = T(num_traits<T>::from_int(1) / delay.mean);
1855 net.set_retrial(idx, cit->second, delay, rate,
1856 int(detail::num_value(it.value(), "maxAttempts", 0.0)));
1857 }
1858 }
1859
1860 // -- Setup / delay-off, switchover and heterogeneous servers ---------
1861 if (nd.contains("setupTime")) {
1862 if (!nd.contains("delayOffTime"))
1863 throw InputError(
1864 "network_reader: node '" + nd.at("name").get<std::string>() +
1865 "' declares a setup time with no delay-off time; a server that never powers "
1866 "down never pays the setup, so the pair is meaningless alone");
1867 const json& su = nd.at("setupTime");
1868 const json& doff = nd.at("delayOffTime");
1869 for (auto it = su.begin(); it != su.end(); ++it) {
1870 auto cit = class_idx.find(it.key());
1871 if (cit == class_idx.end() || !doff.contains(it.key())) continue;
1872 net.set_setup_delayoff(idx, cit->second, detail::dist_from_json<T>(it.value()),
1873 detail::dist_from_json<T>(doff.at(it.key())));
1874 }
1875 }
1876 // -- Server breakdown / repair ---------------------------------------
1877 // The wire form is `linemodel_save`'s: {failure, repair, downService?},
1878 // with `downService` keyed by CLASS NAME. MATLAB writes it per class
1879 // even when `setBreakdown` was given one distribution for every class,
1880 // flattening the class-independent form, so there is only one shape to
1881 // read here.
1882 if (nd.contains("breakdown")) {
1883 const json& bd = nd.at("breakdown");
1884 if (!bd.contains("failure") || !bd.contains("repair"))
1885 throw InputError(
1886 "network_reader: node '" + nd.at("name").get<std::string>() +
1887 "' declares a breakdown without both a failure and a repair time; a server "
1888 "that never recovers is an absorbing model, not a breakdown");
1889 std::vector<lang::Distrib<T> > down(class_idx.size(),
1891 if (bd.contains("downService")) {
1892 const json& ds = bd.at("downService");
1893 for (auto it = ds.begin(); it != ds.end(); ++it) {
1894 auto cit = class_idx.find(it.key());
1895 if (cit == class_idx.end()) continue;
1896 if (cit->second >= 1 && cit->second <= down.size())
1897 down[cit->second - 1] = detail::dist_from_json<T>(it.value());
1898 }
1899 }
1900 net.set_breakdown(idx, detail::dist_from_json<T>(bd.at("failure")),
1901 detail::dist_from_json<T>(bd.at("repair")), down);
1902 }
1903 if (nd.contains("switchoverTimes")) {
1904 // A SWITCHOVER IS A POLLING SERVER'S WALK, AND NOTHING ELSE READS
1905 // ONE. Every consumer in this port -- the state machinery, the SSA
1906 // events, the JMT writer -- reaches it through the polling buffers,
1907 // and so does the reference: `State.afterEventStation` handles
1908 // SWITCH only inside `pollingInfo`, and `writeJSIM` warns and drops
1909 // the times on an ordinary Server. Declared away from a POLLING
1910 // queue the block is therefore inert in both codebases, and it is
1911 // dropped here with the reference's own warning rather than
1912 // refused: refusing made switchover_basic, which the reference
1913 // solves and reports, unsolvable in C++.
1914 const bool polling = type == "Queue" &&
1915 nd.value("scheduling", std::string()) == "POLLING";
1916 if (!polling) {
1917 std::cerr << "[LINE] Warning: node '" << nd.at("name").get<std::string>()
1918 << "' declares switchover times but is not POLLING-scheduled; a "
1919 << "switchover is the walk between a polling server's buffers, so "
1920 << "the times are ignored." << std::endl;
1921 } else {
1922 // Indexed by the class the server LEAVES. The non-polling form
1923 // also names the class it moves TO, which this port's per-class
1924 // vector cannot express, so a genuinely (from, to)-dependent
1925 // walk is refused rather than collapsed onto its `from` alone.
1926 std::map<std::size_t, lang::Distrib<T> > walk;
1927 std::map<std::size_t, std::string> first_to;
1928 for (const json& so : nd.at("switchoverTimes")) {
1929 auto cit = class_idx.find(so.at("from").get<std::string>());
1930 if (cit == class_idx.end()) continue;
1931 const std::string to = so.value("to", std::string());
1932 if (walk.count(cit->second) && first_to[cit->second] != to)
1933 throw UnsupportedError(
1934 "network_reader: node '" + nd.at("name").get<std::string>() +
1935 "' declares a switchover that depends on the class moved TO as well "
1936 "as the one moved FROM; this port carries one walk per departing "
1937 "class");
1938 walk[cit->second] = detail::dist_from_json<T>(so.at("distribution"));
1939 first_to[cit->second] = to;
1940 }
1941 for (const auto& kv : walk) net.set_switchover(idx, kv.first, kv.second);
1942 }
1943 }
1944 if (nd.contains("serverTypes")) {
1945 for (const json& st : nd.at("serverTypes")) {
1946 typename qn::Station<T>::ServerType stype;
1947 stype.name = st.value("name", std::string());
1948 stype.count = st.value("count", 1.0);
1949 if (st.contains("compatibleClasses")) {
1950 stype.compatible.assign(classes.size(), false);
1951 for (const json& cn : st.at("compatibleClasses")) {
1952 auto cit = class_idx.find(cn.get<std::string>());
1953 if (cit != class_idx.end()) stype.compatible[cit->second - 1] = true;
1954 }
1955 }
1956 if (st.contains("service")) {
1957 stype.service.assign(classes.size(), lang::Distrib<T>::disabled_dist());
1958 const json& sv = st.at("service");
1959 for (auto it = sv.begin(); it != sv.end(); ++it) {
1960 auto cit = class_idx.find(it.key());
1961 if (cit != class_idx.end())
1962 stype.service[cit->second - 1] = detail::dist_from_json<T>(it.value());
1963 }
1964 }
1965 net.add_server_type(idx, stype);
1966 }
1967 if (nd.contains("heteroSchedPolicy"))
1969 idx, detail::hetero_from_json(nd.at("heteroSchedPolicy").get<std::string>()));
1970 }
1971 // Job parallelism: servers seized at once by a job, per class. Read after
1972 // the pools, which size the station and so bound the admissible values.
1973 if (nd.contains("serverParallelism")) {
1974 const json& sp = nd.at("serverParallelism");
1975 for (auto it = sp.begin(); it != sp.end(); ++it) {
1976 auto cit = class_idx.find(it.key());
1977 if (cit != class_idx.end())
1978 net.set_server_parallelism(idx, cit->second, it.value().get<std::size_t>());
1979 }
1980 }
1981
1982 // -- The Source refinements: batch size and the MMAP mark binding ----
1983 if (nd.contains("arrivalBatch")) {
1984 const json& ab = nd.at("arrivalBatch");
1985 for (auto it = ab.begin(); it != ab.end(); ++it) {
1986 auto cit = class_idx.find(it.key());
1987 if (cit != class_idx.end())
1988 net.set_arrival_batch(idx, cit->second, detail::dist_from_json<T>(it.value()));
1989 }
1990 }
1991 if (nd.contains("markedClasses")) {
1992 std::vector<std::size_t> marks;
1993 for (const json& cn : nd.at("markedClasses")) {
1994 auto cit = class_idx.find(cn.get<std::string>());
1995 if (cit == class_idx.end())
1996 throw InputError("network_reader: node '" + nd.at("name").get<std::string>() +
1997 "' binds mark " + std::to_string(marks.size() + 1) +
1998 " to class '" + cn.get<std::string>() +
1999 "', which the model does not declare");
2000 marks.push_back(cit->second);
2001 }
2002 net.set_marked_classes(idx, marks);
2003 }
2004
2005 // -- The order-independent / pass-and-swap station -------------------
2006 //
2007 // mu(c) crosses as a macrostate TABLE, so `oiCutoffs` is not decoration:
2008 // it is the lattice the table was materialized over and the point beyond
2009 // which the rate saturates.
2010 if (nd.contains("oiServiceRate")) {
2011 std::vector<int> cut(classes.size(), 10);
2012 if (nd.contains("oiCutoffs")) {
2013 const std::vector<double> cv = detail::num_vec_from_json<double>(nd.at("oiCutoffs"));
2014 for (std::size_t r = 0; r < cut.size() && r < cv.size(); ++r)
2015 cut[r] = static_cast<int>(std::lround(cv[r]));
2016 }
2017 std::vector<std::vector<bool> > swap;
2018 if (nd.contains("swapGraph")) {
2019 const Matrix<T> sg = detail::mat_from_json<T>(nd.at("swapGraph"));
2020 swap.assign(sg.rows(), std::vector<bool>(sg.cols(), false));
2021 for (std::size_t a = 0; a < sg.rows(); ++a)
2022 for (std::size_t b = 0; b < sg.cols(); ++b)
2023 swap[a][b] = num_traits<T>::to_double(sg(a, b)) != 0.0;
2024 }
2025 net.set_pas(idx,
2026 detail::oi_rate_from_json<T>(nd.at("oiServiceRate"), cut, classes.size()),
2027 swap);
2028 }
2029
2030 // -- The declared state -----------------------------------------------
2031 if (nd.contains("initialState") && type == "Place") {
2032 net.set_initial_marking(idx, detail::num_vec_from_json<T>(nd.at("initialState")));
2033 } else if (nd.contains("initialState") && !nd.contains("stateSpace")) {
2034 // A stateful node that is not a Place carries its declared state in
2035 // the same key, and this struct spells a declared state as a
2036 // ONE-ROW state space under a prior of one (`sn_state.h`, the CTMC
2037 // transient's own reading). MATLAB, the JAR and Python all write
2038 // the key for every stateful node, so ignoring it outside a Place
2039 // dropped the initialization of every station: a model saved after
2040 // initFromMarginal came back here as the default one, with the
2041 // difference visible only in the numbers.
2042 const std::vector<T> row = detail::num_vec_from_json<T>(nd.at("initialState"));
2043 if (!row.empty()) {
2044 Matrix<T> space(1, row.size());
2045 for (std::size_t k = 0; k < row.size(); ++k) space(0, k) = row[k];
2046 net.set_state_prior(idx, space, std::vector<T>(1, num_traits<T>::from_double(1.0)));
2047 }
2048 }
2049 if (nd.contains("statePrior") || nd.contains("stateSpace")) {
2050 if (!nd.contains("statePrior") || !nd.contains("stateSpace"))
2051 throw InputError(
2052 "network_reader: node '" + nd.at("name").get<std::string>() +
2053 "' declares one of stateSpace / statePrior without the other; the prior is a "
2054 "distribution over the ROWS of that space and means nothing alone");
2055 net.set_state_prior(idx, detail::mat_from_json<T>(nd.at("stateSpace")),
2056 detail::num_vec_from_json<T>(nd.at("statePrior")));
2057 }
2058 if (nd.contains("departureDiscipline")) {
2059 const json& dd = nd.at("departureDiscipline");
2060 for (auto it = dd.begin(); it != dd.end(); ++it) {
2061 auto cit = class_idx.find(it.key());
2062 if (cit != class_idx.end())
2064 idx, cit->second, detail::departure_from_json(it.value().get<std::string>()));
2065 }
2066 }
2067
2068 // Rate scaling. `loadDependence` carries the lldscaling vector itself,
2069 // indexed from population one; `classDependence` and `jointDependence`
2070 // carry beta_r(n) and eta_i(n) materialized over the per-class box
2071 // lattice, because a function handle cannot cross JSON.
2072 for (int kind = 0; kind < 3; ++kind) {
2073 static const char* kKeys[] = {"loadDependence", "classDependence",
2074 "jointDependence"};
2075 static const char* kTypes[] = {"loadDependent", "classDependent",
2076 "jointDependent"};
2077 if (!nd.contains(kKeys[kind])) continue;
2078 if (type != "Queue" && type != "Delay")
2079 throw UnsupportedError(
2080 std::string("network_reader: node '") + nd.at("name").get<std::string>() +
2081 "' carries '" + kKeys[kind] +
2082 "', which scales a SERVICE rate and is only meaningful at a Queue or a "
2083 "Delay");
2084 const detail::json& blk = nd.at(kKeys[kind]);
2085 if (blk.value("type", std::string(kTypes[kind])) != kTypes[kind])
2086 throw UnsupportedError(std::string("network_reader: '") + kKeys[kind] +
2087 "' at node '" + nd.at("name").get<std::string>() +
2088 "' declares type '" +
2089 blk.value("type", std::string("?")) +
2090 "', which this reader does not implement");
2091 if (!blk.contains("scaling"))
2092 throw InputError(std::string("network_reader: '") + kKeys[kind] +
2093 "' at node '" + nd.at("name").get<std::string>() +
2094 "' carries no 'scaling'");
2095 if (kind == 0) {
2096 net.set_load_dependence(idx, detail::num_vec_from_json<T>(blk.at("scaling")));
2097 continue;
2098 }
2099 // An absent `cutoffs` leaves the lattice at one job per class, which
2100 // is what the Python reader assumes for the same legacy JSON.
2101 std::vector<int> cut(classes.size(), 1);
2102 if (blk.contains("cutoffs")) {
2103 const std::vector<double> cv = detail::num_vec_from_json<double>(blk.at("cutoffs"));
2104 for (std::size_t r = 0; r < cut.size() && r < cv.size(); ++r)
2105 cut[r] = static_cast<int>(std::lround(cv[r]));
2106 }
2107 const lang::CdScaling<T> fun =
2108 detail::cd_scaling_from_json<T>(blk.at("scaling"), cut, classes.size());
2109 const std::vector<T> peak = detail::cd_peak_from_json<T>(blk, blk.at("scaling"));
2110 if (kind == 1) net.set_class_dependence(idx, fun, peak);
2111 else net.set_joint_dependence(idx, fun, peak);
2112 }
2113 // Immediate feedback, a per-class map on the node. The writers emit it
2114 // for a Queue only, and only for the classes it is set on, so an absent
2115 // entry means "not set" rather than false.
2116 if (nd.contains("immediateFeedback")) {
2117 const json& imf = nd.at("immediateFeedback");
2118 for (auto it = imf.begin(); it != imf.end(); ++it) {
2119 auto cit = class_idx.find(it.key());
2120 if (cit != class_idx.end() && it.value().get<bool>())
2121 net.set_immediate_feedback(idx, cit->second);
2122 }
2123 }
2124 }
2125
2126 // -- Pass 3: routing. ----------------------------------------------------
2127 const json& routing = model.at("routing");
2129 const json& mat = routing.at("matrix");
2130 for (auto ck = mat.begin(); ck != mat.end(); ++ck) {
2131 // Key "SrcClass,DstClass"; a bare "Class" means same-class routing.
2132 const std::string key = ck.key();
2133 const std::size_t comma = key.find(',');
2134 const std::string cs = comma == std::string::npos ? key : key.substr(0, comma);
2135 const std::string cd = comma == std::string::npos ? key : key.substr(comma + 1);
2136 const std::size_t r = class_idx.at(cs);
2137 const std::size_t s = class_idx.at(cd);
2138 for (auto si = ck.value().begin(); si != ck.value().end(); ++si) {
2139 const std::size_t from = node_idx.at(si.key());
2140 for (auto di = si.value().begin(); di != si.value().end(); ++di)
2141 P.set(r, s, from, node_idx.at(di.key()),
2142 num_traits<T>::from_double(di.value().get<double>()));
2143 }
2144 }
2145 net.link(P);
2146
2147 // -- Pass 3b: the non-probabilistic dispatchers. -------------------------
2148 //
2149 // `routing` above carries the MATRIX, which is what a PROB dispatcher is
2150 // fully described by. A RROBIN, JSQ or power-of-d dispatcher has the same
2151 // matrix and a different rule, so reading only the matrix turns every one
2152 // of them into probabilistic routing with no diagnostic. `link()` has
2153 // already run, so these overwrite the per-class strategy it defaulted to.
2154 if (model.contains("routingStrategies")) {
2155 const json& rs = model.at("routingStrategies");
2156 for (auto ni = rs.begin(); ni != rs.end(); ++ni) {
2157 auto nit = node_idx.find(ni.key());
2158 for (auto ci = ni.value().begin(); ci != ni.value().end(); ++ci) {
2159 const lang::RoutingStrategy rst =
2160 detail::routing_from_json(ci.value().get<std::string>());
2161 // DISABLED IS DERIVED, not declared: it marks a (node, class)
2162 // pair the class never visits, the refresh re-derives it, and
2163 // every reference loader skips it. Writers before BUG-94 also
2164 // emitted it for the AUTO-ADDED class-switch nodes they do not
2165 // put in `nodes`, so an entry naming an unknown node is only
2166 // tolerated for this one value -- anything else is still a
2167 // dangling reference and is named.
2168 if (nit == node_idx.end()) {
2169 if (rst == lang::RoutingStrategy::DISABLED) continue;
2170 throw InputError("network_reader: routingStrategies names node '" + ni.key() +
2171 "', which the model does not declare");
2172 }
2173 if (rst == lang::RoutingStrategy::DISABLED) continue;
2174 auto cit = class_idx.find(ci.key());
2175 if (cit == class_idx.end()) continue;
2176 net.set_routing(nit->second, cit->second, rst);
2177 }
2178 }
2179 }
2180 if (model.contains("routingWeights")) {
2181 const json& rw = model.at("routingWeights");
2182 for (auto ni = rw.begin(); ni != rw.end(); ++ni) {
2183 auto nit = node_idx.find(ni.key());
2184 if (nit == node_idx.end()) continue;
2185 for (auto ci = ni.value().begin(); ci != ni.value().end(); ++ci) {
2186 auto cit = class_idx.find(ci.key());
2187 if (cit == class_idx.end()) continue;
2188 std::map<std::size_t, double> w;
2189 for (auto di = ci.value().begin(); di != ci.value().end(); ++di) {
2190 auto dit = node_idx.find(di.key());
2191 if (dit != node_idx.end()) w[dit->second] = di.value().get<double>();
2192 }
2193 net.set_routing_weights(nit->second, cit->second, w);
2194 }
2195 }
2196 }
2197 if (model.contains("routingParams")) {
2198 const json& rp = model.at("routingParams");
2199 for (auto ni = rp.begin(); ni != rp.end(); ++ni) {
2200 auto nit = node_idx.find(ni.key());
2201 if (nit == node_idx.end()) continue;
2202 for (auto ci = ni.value().begin(); ci != ni.value().end(); ++ci) {
2203 auto cit = class_idx.find(ci.key());
2204 if (cit == class_idx.end() || !ci.value().contains("d")) continue;
2205 net.set_routing_param(nit->second, cit->second, ci.value().at("d").get<int>());
2206 }
2207 }
2208 }
2209
2210 // -- Pass 3b3: Krzesinski state-dependent routing. -----------------------
2211 //
2212 // Restored AFTER `link()` and after the dispatchers above: `link` writes the
2213 // uniform placeholder into the entry row, which the declaration supersedes,
2214 // and `routingStrategies` has already named that row SDR without saying what
2215 // the subnetwork looks like. Every centre travels by NODE NAME, so a node
2216 // reordering on the writing side cannot shift one, and branch index 1 is the
2217 // complement M-V and arrives as an empty list.
2218 // See _kb/16-state-dependent-routing.md
2219 if (model.contains("stateDepRouting")) {
2220 const json& sd = model.at("stateDepRouting");
2221 for (const char* k : {"entry", "departure", "class", "branches", "level", "C", "d"})
2222 if (!sd.contains(k))
2223 throw InputError(std::string("network_reader: 'stateDepRouting' carries no '") + k +
2224 "'");
2225 auto node_of = [&](const std::string& nm) -> std::size_t {
2226 const std::map<std::string, std::size_t>::const_iterator it = node_idx.find(nm);
2227 if (it == node_idx.end())
2228 throw InputError("network_reader: 'stateDepRouting' names node '" + nm +
2229 "', which the model does not declare");
2230 return it->second;
2231 };
2232 const std::map<std::string, std::size_t>::const_iterator ci =
2233 class_idx.find(sd.at("class").get<std::string>());
2234 if (ci == class_idx.end())
2235 throw InputError("network_reader: 'stateDepRouting' names class '" +
2236 sd.at("class").get<std::string>() +
2237 "', which the model does not declare");
2238 std::vector<std::vector<std::size_t>> branches;
2239 for (const json& b : sd.at("branches")) {
2240 std::vector<std::size_t> centres;
2241 for (const json& nm : b) centres.push_back(node_of(nm.get<std::string>()));
2242 branches.push_back(centres);
2243 }
2244 std::vector<std::size_t> level;
2245 for (const json& v : sd.at("level")) level.push_back(static_cast<std::size_t>(v.get<double>()));
2246 std::vector<double> C;
2247 for (const json& v : sd.at("C")) C.push_back(v.get<double>());
2248 const json& dj = sd.at("d");
2249 Matrix<double> d(dj.size(), dj.empty() ? 0 : dj.at(0).size(), 0.0);
2250 for (std::size_t t = 0; t < dj.size(); ++t) {
2251 if (dj.at(t).size() != d.cols())
2252 throw InputError("network_reader: 'stateDepRouting' carries a ragged coefficient "
2253 "matrix d");
2254 for (std::size_t b = 0; b < d.cols(); ++b) d(t, b) = dj.at(t).at(b).get<double>();
2255 }
2256 net.set_state_dep_routing(node_of(sd.at("entry").get<std::string>()),
2257 node_of(sd.at("departure").get<std::string>()), branches, level, C,
2258 d, ci->second);
2259 }
2260
2261 // -- Pass 3b2: the global (Whittle) dependence. --------------------------
2262 //
2263 // phi(n) is rebuilt from the materialized slot lattice written by
2264 // `network_to_json`. The slots carry station and class NAMES, resolved
2265 // through this model's own index spaces, so a reordering on the writing side
2266 // cannot silently shift a coordinate. The population is clamped to the
2267 // tabulated cutoffs, which is the same saturation the writer's box lattice
2268 // declares.
2269 if (model.contains("globalDependence")) {
2270 const json& blk = model.at("globalDependence");
2271 if (blk.value("type", std::string("globalDependent")) != "globalDependent")
2272 throw UnsupportedError(
2273 std::string("network_reader: 'globalDependence' declares type '") +
2274 blk.value("type", std::string("?")) + "', which this reader does not implement");
2275 if (!blk.contains("scaling"))
2276 throw InputError("network_reader: 'globalDependence' carries no 'scaling'");
2277 const qn::NetworkStruct<T>& sn0 = net.get_struct();
2278 const std::size_t M = sn0.nstations, K = sn0.nclasses;
2279 std::vector<std::size_t> slot_st, slot_cl;
2280 if (blk.contains("slots"))
2281 for (const json& sm : blk.at("slots")) {
2282 auto nit = node_idx.find(sm.at("station").get<std::string>());
2283 auto cit = class_idx.find(sm.at("class").get<std::string>());
2284 if (nit == node_idx.end() || cit == class_idx.end())
2285 throw InputError(
2286 "network_reader: 'globalDependence' names a station or class the model "
2287 "does not declare");
2288 slot_st.push_back(sn0.nodes[nit->second - 1].station - 1);
2289 slot_cl.push_back(cit->second - 1);
2290 }
2291 const std::size_t P = slot_st.size();
2292 std::vector<int> cuts(P, 0);
2293 if (blk.contains("cutoffs")) {
2294 const std::vector<double> cv = detail::num_vec_from_json<double>(blk.at("cutoffs"));
2295 for (std::size_t d = 0; d < P && d < cv.size(); ++d)
2296 cuts[d] = static_cast<int>(std::lround(cv[d]));
2297 }
2298 const int wcut = blk.value("cutoff", 10);
2299 std::map<std::string, std::vector<T>> tbl;
2300 for (auto it = blk.at("scaling").begin(); it != blk.at("scaling").end(); ++it)
2301 tbl[it.key()] = detail::num_vec_from_json<T>(it.value());
2302 std::vector<T> peak(M * K, num_traits<T>::from_int(1));
2303 if (blk.contains("peak")) {
2304 const std::vector<T> pv = detail::num_vec_from_json<T>(blk.at("peak"));
2305 for (std::size_t j = 0; j < peak.size() && j < pv.size(); ++j) peak[j] = pv[j];
2306 }
2307 const std::vector<T> ones(M * K, num_traits<T>::from_int(1));
2309 [slot_st, slot_cl, cuts, tbl, ones, K, P](const std::vector<T>& n) {
2310 std::string key;
2311 if (P == 0) key = "0";
2312 else
2313 for (std::size_t d = 0; d < P; ++d) {
2314 long x = std::lround(num_traits<T>::to_double(n[slot_st[d] * K + slot_cl[d]]));
2315 if (x < 0) x = 0;
2316 if (x > cuts[d]) x = cuts[d];
2317 if (d) key += ',';
2318 key += std::to_string(x);
2319 }
2320 typename std::map<std::string, std::vector<T>>::const_iterator it = tbl.find(key);
2321 return it == tbl.end() ? ones : it->second;
2322 },
2323 peak, wcut);
2324 }
2325
2326 // -- Pass 3c: the finite capacity regions. -------------------------------
2327 //
2328 // A region caps the jobs held ACROSS a set of stations, which no per-station
2329 // capacity expresses. Dropping the block is the failure this reader's key
2330 // gate was written for: an FCR model then solves as the UNBOUNDED one and
2331 // reports a confident, wrong queue length.
2332 if (model.contains("finiteCapacityRegions")) {
2333 for (const json& rj : model.at("finiteCapacityRegions")) {
2334 std::vector<std::size_t> members;
2335 // The per-station `classCap` refines the region's own cap at that
2336 // station; the struct carries ONE cap per (station, class), so the
2337 // tightest of the two is what the region actually enforces.
2338 std::vector<double> cap(classes.size(), -1.0);
2339 auto read_class_map = [&](const json& blk, std::vector<double>& dst) {
2340 for (auto it = blk.begin(); it != blk.end(); ++it) {
2341 auto cit = class_idx.find(it.key());
2342 if (cit == class_idx.end()) continue;
2343 const double v = it.value().get<double>();
2344 double& slot = dst[cit->second - 1];
2345 slot = slot == -1.0 ? v : std::min(slot, v);
2346 }
2347 };
2348 if (rj.contains("classMaxJobs")) read_class_map(rj.at("classMaxJobs"), cap);
2349 for (const json& sj : rj.at("stations")) {
2350 auto nit = node_idx.find(sj.at("node").get<std::string>());
2351 if (nit == node_idx.end())
2352 throw InputError("network_reader: finite capacity region '" +
2353 rj.value("name", std::string("?")) + "' names node '" +
2354 sj.at("node").get<std::string>() +
2355 "', which the model does not declare");
2356 members.push_back(nit->second);
2357 if (sj.contains("classCap")) read_class_map(sj.at("classCap"), cap);
2358 }
2359 std::vector<double> mem(classes.size(), -1.0);
2360 if (rj.contains("classMaxMemory")) read_class_map(rj.at("classMaxMemory"), mem);
2361 std::vector<T> size(classes.size(), num_traits<T>::from_int(1));
2362 std::vector<T> weight(classes.size(), num_traits<T>::from_int(1));
2363 for (const json& sj : rj.at("stations")) {
2364 if (sj.contains("classSize"))
2365 for (auto it = sj.at("classSize").begin(); it != sj.at("classSize").end(); ++it) {
2366 auto cit = class_idx.find(it.key());
2367 if (cit != class_idx.end())
2368 size[cit->second - 1] =
2369 num_traits<T>::from_double(it.value().get<double>());
2370 }
2371 if (sj.contains("classWeight"))
2372 for (auto it = sj.at("classWeight").begin(); it != sj.at("classWeight").end();
2373 ++it) {
2374 auto cit = class_idx.find(it.key());
2375 if (cit != class_idx.end())
2376 weight[cit->second - 1] =
2377 num_traits<T>::from_double(it.value().get<double>());
2378 }
2379 }
2380 std::vector<lang::DropStrategy> rule(classes.size(), lang::DropStrategy::WAITQ);
2381 if (rj.contains("dropRule"))
2382 for (auto it = rj.at("dropRule").begin(); it != rj.at("dropRule").end(); ++it) {
2383 auto cit = class_idx.find(it.key());
2384 if (cit != class_idx.end())
2385 rule[cit->second - 1] = detail::drop_from_json(it.value().get<std::string>());
2386 }
2387 const std::size_t reg =
2388 net.add_region(members, cap, rj.value("globalMaxJobs", -1.0), rule, mem, size,
2389 rj.value("globalMaxMemory", -1.0),
2390 rj.value("name", std::string()));
2391 net.set_region_weights(reg, weight);
2392 if (rj.contains("constraintA") && rj.contains("constraintB"))
2393 net.set_region_constraint(reg, detail::mat_from_json<T>(rj.at("constraintA")),
2394 detail::num_vec_from_json<T>(rj.at("constraintB")));
2395 }
2396 }
2397
2398 // -- Pass 4: the declared rewards. ---------------------------------------
2399 //
2400 // THE DECLARATIVE FORM ONLY, `{name, type, node, class?}`, which is all the
2401 // writers emit: a reward defined from a bare lambda has no reproducible form
2402 // and `linemodel_save` warns and omits it rather than writing something that
2403 // would be wrong on reload. Each template is rebuilt here as a function of
2404 // the AGGREGATE state row -- the per-(station, class) counts in
2405 // `(ist-1)*K + k` order that `set_reward` is defined over -- so the value is
2406 // computed from the chain's own states and not from a mean.
2407 if (model.contains("rewards")) {
2408 const std::size_t K = classes.size();
2409 for (const json& rw : model.at("rewards")) {
2410 const std::string nm = rw.at("name").get<std::string>();
2411 const std::string kind = rw.at("type").get<std::string>();
2412 const std::string node_name = rw.at("node").get<std::string>();
2413 auto nit = node_idx.find(node_name);
2414 if (nit == node_idx.end())
2415 throw InputError("network_reader: reward '" + nm + "' names node '" + node_name +
2416 "', which the model does not declare");
2417 // The struct carries station -> node and no inverse, so the station
2418 // is found by scanning it; a node with no station is refused below.
2419 std::size_t ist = 0;
2420 for (std::size_t s = 0; s < net.get_struct().station_to_node.size(); ++s)
2421 if (net.get_struct().station_to_node[s] == nit->second) ist = s + 1;
2422 if (ist == 0)
2423 throw UnsupportedError("network_reader: reward '" + nm + "' is declared at node '" +
2424 node_name +
2425 "', which is not a station and so has no job count");
2426 // A reward with no class covers EVERY class at the station; with one,
2427 // exactly that class. The two are different quantities, so the
2428 // absence of the key is carried through rather than defaulted.
2429 std::size_t cls = 0; // 0 = all classes
2430 if (rw.contains("class")) {
2431 auto cit = class_idx.find(rw.at("class").get<std::string>());
2432 if (cit == class_idx.end())
2433 throw InputError("network_reader: reward '" + nm + "' names class '" +
2434 rw.at("class").get<std::string>() +
2435 "', which the model does not declare");
2436 cls = cit->second;
2437 }
2438 const double nservers = net.get_struct().stations[ist - 1].nservers;
2439 const double cap = net.get_struct().stations[ist - 1].cap;
2440 const std::size_t base = (ist - 1) * K;
2441 // The DECLARATIVE descriptor travels with the lambda so the writer
2442 // can emit the reward back out; a reward built from a bare function
2443 // has none and is omitted at save time, as the reference does.
2444 if (kind == "QLen") {
2445 net.set_reward(nm, [base, K, cls](const std::vector<T>& n) {
2446 if (cls) return n[base + cls - 1];
2447 T s = num_traits<T>::from_int(0);
2448 for (std::size_t k = 0; k < K; ++k) s = T(s + n[base + k]);
2449 return s;
2450 }, kind, nit->second, cls);
2451 } else if (kind == "Util") {
2452 // min(jobs, nservers), the reference's own definition: the number
2453 // of BUSY servers, not a fraction.
2454 net.set_reward(nm, [base, K, cls, nservers](const std::vector<T>& n) {
2455 T s = num_traits<T>::from_int(0);
2456 if (cls) s = n[base + cls - 1];
2457 else
2458 for (std::size_t k = 0; k < K; ++k) s = T(s + n[base + k]);
2459 const T c = num_traits<T>::from_double(nservers);
2460 return num_traits<T>::to_double(s) > nservers ? c : s;
2461 }, kind, nit->second, cls);
2462 } else if (kind == "Blocking") {
2463 net.set_reward(nm, [base, K, cap](const std::vector<T>& n) {
2464 T s = num_traits<T>::from_int(0);
2465 for (std::size_t k = 0; k < K; ++k) s = T(s + n[base + k]);
2468 }, kind, nit->second, cls);
2469 } else {
2470 throw UnsupportedError(
2471 "network_reader: reward '" + nm + "' is of type '" + kind +
2472 "', and the reproducible templates are QLen, Util and Blocking; a Custom "
2473 "reward wraps an arbitrary function and is not on the wire at all");
2474 }
2475 }
2476 }
2477 return net;
2478}
2479
2480/** Parse a model.json file into a `qn::Network<T>`. */
2481template <class T>
2482qn::Network<T> read_network_json(const std::string& path) {
2483 std::ifstream in(path.c_str());
2484 if (!in) throw InputError("network_reader: cannot open " + path);
2485 detail::json root;
2486 try {
2487 in >> root;
2488 } catch (const detail::json::parse_error& e) {
2489 throw InputError("network_reader: malformed JSON in " + path + ": " + e.what());
2490 }
2491 return build_network_from_json<T>(root);
2492}
2493
2494} // namespace io
2495} // namespace line
2496
2497#endif // LINE_IO_NETWORK_READER_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::vector< NodeDef > nodes
every node, in creation order
A queueing network under construction.
void set_drop_rule(std::size_t node, std::size_t cls, DropStrategy rule)
station.setDropRule(class, rule).
void set_state_prior(std::size_t node, const Matrix< T > &space, const std::vector< T > &prior)
StatefulNode.setStatePrior(space, prior): a distribution over the rows of a DECLARED state space.
void set_departure_discipline(std::size_t node, std::size_t cls, lang::DepartureDiscipline rule)
Place.setDepartureDiscipline(class, rule).
std::size_t add_logger(const std::string &nm, const std::string &log_file=std::string())
A Logger node: a pass-through that records every job crossing it.
void set_load_dependence(std::size_t node, const std::vector< T > &alpha)
station.setLoadDependence(alpha): the rate multiplier at population 1, 2, ... The vector is indexed f...
void set_class_capacity(std::size_t node, std::size_t cls, double k)
station.setChainCapacity(class, k).
std::size_t add_source(const std::string &nm)
The external arrival station.
std::size_t add_fork(const std::string &nm, double tasks_per_link=1.0)
A Fork node.
void bind_join(std::size_t join_node, std::size_t fork_node)
Record which Fork a Join created by add_join_unbound closes.
void set_arrival_batch(std::size_t node, std::size_t cls, const Distrib< T > &dist)
Source.setArrivalBatch(class, dist): the batch-size law released at each arrival epoch.
std::size_t add_open_class(const std::string &nm, int prio=0)
An open class.
void set_class_patience(std::size_t cls, const Distrib< T > &dist, lang::ImpatienceType kind=lang::ImpatienceType::RENEGING)
JobClass.setPatience(kind, dist): the CLASS-WIDE abandonment law.
std::size_t add_delay(const std::string &nm)
An infinite-server station (a Delay, MATLAB's Delay / DelayStation).
void set_class_spawn(std::size_t cls, std::size_t spawn_cls)
JobClass.spawnClass (sn.classspawn): the class injected at the same station on every completion of cl...
void set_region_constraint(std::size_t region, const Matrix< T > &A, const std::vector< T > &b)
The optional linear constraint A n <= b a region may carry beyond its caps.
void set_class_immediate_feedback(std::size_t cls)
jobclass.setImmediateFeedback(): the same property, class-wide.
std::size_t add_router(const std::string &nm)
A stateless routing node.
void set_initial_marking(std::size_t node, const std::vector< T > &tokens)
Place.setState(marking): the initial token count of the place, per class.
void set_number_of_servers(std::size_t node, double n)
queue.setNumberOfServers(n).
void set_joint_dependence(std::size_t node, const CdScaling< T > &fun, const std::vector< T > &peak)
station.setJointDependence(eta, peakRatePerClass): MATLAB's Station.ljdScaling / ljdScalingPeak.
void set_fork_tasks_per_link(std::size_t fork_node, std::size_t jobclass, double tasks, std::size_t dest_node=0)
Variable forking levels on an existing Fork, the twin of MATLAB Fork.setTasksPerLink(jobclass,...
void set_patience(std::size_t node, std::size_t cls, const Distrib< T > &dist, lang::ImpatienceType kind=lang::ImpatienceType::RENEGING)
Queue.setPatience(class, dist, type): the abandonment timer of a job WAITING at the station,...
void set_reply_signal_class(std::size_t call_cls, std::size_t reply_cls)
JobClass.setReplySignalClass(reply) (sn.syncreply), plus the sn.replyblock the state layer needs.
std::size_t add_queue(const std::string &nm, SchedStrategy sched=SchedStrategy::FCFS)
A queueing station.
void set_routing(std::size_t node, std::size_t cls, RoutingStrategy rs)
node.setRouting(class, strategy).
void set_join_strategy(std::size_t node, lang::JoinStrategy strategy, double quorum=0.0)
Join.setStrategy(...): STD waits for every sibling, PARTIAL for a quorum.
void set_fork_branch_probability(std::size_t fork_node, std::size_t jobclass, std::size_t dest_node, double prob)
A branch that fires only with probability prob.
std::size_t add_cache(const std::string &nm, const CacheParam< T > &par)
A Cache node with its item population, list capacities and popularity.
void set_reward(const std::string &nm, const std::function< T(const std::vector< T > &)> &fn, const std::string &kind=std::string(), std::size_t node=0, std::size_t cls=0)
model.setReward(name, fn): a named reward evaluated on the AGGREGATE state row, the per-(station,...
std::size_t add_self_looping_class(const std::string &nm, double njobs, std::size_t refstat_node, int prio=0)
SelfLoopingClass(model, name, njobs, refstat, prio): a closed class whose jobs perpetually cycle at t...
void set_marked_classes(std::size_t node, const std::vector< std::size_t > &classes)
Source.markedClasses: the 1-based class carried by each mark of an MMAP.
std::size_t add_closed_class(const std::string &nm, double njobs, std::size_t refstat_node, int prio=0)
A closed class of the given population, referencing a station node.
void set_sched_param(std::size_t node, std::size_t cls, const T &weight)
The DPS / GPS weight of a class at a station.
std::size_t add_class_switch(const std::string &nm, const Matrix< T > &C)
A ClassSwitch node carrying the (nclasses x nclasses) switching matrix.
void set_fork_tasks_per_link_dist(std::size_t fork_node, std::size_t jobclass, const lang::Distrib< T > &dist, std::size_t dest_node=0)
A random jobs-per-link degree, redrawn per link and per forked job.
std::size_t add_sink(const std::string &nm)
The external departure node.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
void set_batch_reject(std::size_t node, std::size_t cls, const T &p)
Queue.setBatchRejectProbability(class, p).
void set_class_dependence(std::size_t node, const CdScaling< T > &fun, const std::vector< T > &peak=std::vector< T >())
station.setClassDependence(beta, peakRatePerClass).
void set_state_dep_routing(std::size_t entry, std::size_t departure, const std::vector< std::vector< std::size_t > > &branches, const std::vector< std::size_t > &level, const std::vector< double > &C, const Matrix< double > &d, std::size_t cls=0)
node.setStateDepRouting(class, departure, branches, level, C, d).
std::size_t add_place(const std::string &nm)
A Place: an SPN token container.
void set_routing_weights(std::size_t node, std::size_t cls, const std::map< std::size_t, double > &weights)
The per-destination weights of a WRROBIN dispatcher, per (node, class).
void set_log_path(const std::string &path)
Network.setLogPath: the directory every Logger of this model writes into.
void set_capacity(std::size_t node, double k)
station.setCapacity(k), the K of Kendall's notation.
NetworkStruct< T > & raw_struct()
The struct WITHOUT refreshing it, for a caller that is still building.
void set_switchover(std::size_t node, std::size_t cls, const Distrib< T > &so)
Queue.setSwitchover(jobclass, distrib): the switchover time of a class.
void set_immediate_feedback(std::size_t node, std::size_t cls)
queue.setImmediateFeedback(class): a completing job of that class is fed straight back into service,...
void set_balking(std::size_t node, std::size_t cls, lang::BalkingStrategy strategy, const std::vector< typename Station< T >::BalkingThreshold > &thresholds)
Queue.setBalking(class, strategy, thresholds): an arrival that refuses to JOIN, on the state it finds...
void add_server_type(std::size_t node, const typename Station< T >::ServerType &stype)
Queue.addServerType(...): one heterogeneous server pool of the station.
void set_setup_delayoff(std::size_t node, std::size_t cls, const Distrib< T > &setup, const Distrib< T > &delayoff)
Queue.setDelayOff(class, setupTime, delayoffTime): the station powers down after sitting idle for the...
void set_class_deadline(std::size_t cls, double due)
JobClass.deadline: the soft deadline EDD, EDF and JMT's tardiness use.
void link(const RoutingMatrix< T > &Pm)
model.link(P): install the routing.
void set_routing_param(std::size_t node, std::size_t cls, int d)
The d of a power-of-d (SQ) dispatcher, per (node, class).
void set_retrial(std::size_t node, std::size_t cls, const Distrib< T > &proc, const T &rate, int max_attempts=0)
Queue.setRetrial(...): a station with an ORBIT instead of a waiting line.
void set_service(std::size_t node, std::size_t cls, const Distrib< T > &d)
station.setService(class, dist).
void set_reference_class(std::size_t cls)
JobClass.setReferenceClass(true): sn.refclass(c) picks this class.
std::size_t add_join_unbound(const std::string &nm)
The Join station on its own, with the fork left to bind_join.
void set_hetero_sched_policy(std::size_t node, lang::HeteroSchedPolicy policy)
Queue.setHeteroSchedPolicy(...): how the server pools are picked among.
void set_breakdown(std::size_t node, const Distrib< T > &failure, const Distrib< T > &repair, const std::vector< Distrib< T > > &down_service=std::vector< Distrib< T > >())
Queue.setBreakdown(failure, repair, downService): the server alternates up and down on the two clocks...
void set_pas(std::size_t node, const std::function< T(const std::vector< std::size_t > &)> &mu, const std::vector< std::vector< bool > > &swap_graph=std::vector< std::vector< bool > >())
Queue.setService(@(c) ...) for a pass-and-swap / order-independent station: the total service rate mu...
std::size_t add_transition(const std::string &nm, const TransitionParam< T > &par)
A Transition: the firing rules of an SPN, as Transition in MATLAB.
void set_orbit_impatience(std::size_t node, std::size_t cls, const Distrib< T > &dist)
Queue.setOrbitImpatience(class, dist): abandonment from the retrial orbit.
void set_signal(std::size_t cls, lang::SignalType type, lang::RemovalPolicy policy=lang::RemovalPolicy::RANDOM, std::size_t target=0, const std::vector< T > &remdist=std::vector< T >())
Declare a class to be a G-network SIGNAL rather than a job.
void set_global_dependence(const GdScaling< T > &fun, const std::vector< T > &peak)
model.setGlobalDependence(phi, peak): MATLAB's Network.gdScaling.
void set_server_parallelism(std::size_t node, std::size_t cls, std::size_t n)
Queue.setServerParallelism(class, n): the servers a job seizes for the whole of its service.
void set_region_weights(std::size_t region, const std::vector< T > &weight)
FiniteCapacityRegion.setClassWeight: the per-class weight the region's global cap counts a job agains...
void set_arrival(std::size_t node, std::size_t cls, const Distrib< T > &d)
source.setArrival(class, dist): the same table, at the Source.
std::size_t add_region(const std::vector< std::size_t > &nodes, const std::vector< double > &class_max_jobs, double global_max_jobs=-1.0, const std::vector< DropStrategy > &rule=std::vector< DropStrategy >(), const std::vector< double > &class_max_memory=std::vector< double >(), const std::vector< T > &class_size=std::vector< T >(), double global_max_memory=-1.0, const std::string &name=std::string())
FiniteCapacityRegion(model, nodes): a cap on the jobs held ACROSS a set of stations.
void set_polling_type(std::size_t node, lang::PollingType rule, int par=0)
Queue.setPollingType(rule, par): the polling discipline of a POLLING station, identical across all cl...
The routing matrix a model script fills in, MATLAB's P cell array.
void set(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Definition http.h:349
qn::Network< T > read_network_json(const std::string &path)
Parse a model.json file into a qn::Network<T>.
qn::Network< T > build_network_from_json(const detail::json &root)
Build a qn::Network<T> from a parsed model.json envelope.
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:424
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:363
@ TIMED
fires after its firing distribution elapses
Definition lang_types.h:362
BalkingStrategy
Balking rules, with the values of MATLAB BalkingStrategy.
Definition lang_types.h:445
SignalType
G-network signal classes, with the values of MATLAB SignalType.
Definition lang_types.h:167
@ REPLY
completes a synchronous call, releasing a held server
Definition lang_types.h:168
@ NEGATIVE
removes a batch of jobs (Gelenbe's negative customer)
Definition lang_types.h:169
@ CATASTROPHE
removes EVERY job at the station
Definition lang_types.h:170
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:389
DepartureDiscipline
When a Place releases a served token, MATLAB DepartureDiscipline.
Definition lang_types.h:458
Distrib< T > prior_continuous(const Distrib< T > &param_dist, const std::function< Distrib< T >(const T &)> &factory)
Prior(paramDist, distFactory): the continuous form.
Definition prior.h:150
RemovalPolicy
Which job a negative signal removes, with the values of MATLAB RemovalPolicy.
Definition lang_types.h:174
@ FCFS
the oldest waiting job; servers only once nobody waits
Definition lang_types.h:176
@ LCFS
the newest waiting job; servers only once nobody waits
Definition lang_types.h:177
@ RANDOM
uniform over waiting AND in-service jobs
Definition lang_types.h:175
HeteroSchedPolicy
How a heterogeneous station picks among its server types, MATLAB HeteroSchedPolicy.
Definition lang_types.h:451
PollingType
Polling service disciplines, with the values of MATLAB PollingType.
Definition lang_types.h:370
Distrib< T > prior_discrete(const std::vector< Distrib< T > > &alternatives, const std::vector< T > &probabilities)
Prior(distributions, probabilities): the discrete form.
Definition prior.h:110
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:639
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:378
ImpatienceType
Impatience kinds, with the values of MATLAB ImpatienceType.
Definition lang_types.h:442
std::vector< T > ones(std::size_t n)
Column vector of ones, the ubiquitous e in MAP algebra.
Definition linalg.h:104
The Network constructor API: Queue, Delay, Source, Sink, Router, ClassSwitch, Cache,...
Number-type abstraction for the templated API port.
Prior: parameter uncertainty as a weighted set of alternative models.
static Distrib empirical_cdf(const std::vector< T > &x, const std::vector< T > &F)
EmpiricalCDF(x, F): the moments of the MIDPOINT rule over the CDF bins, which is what MATLAB Empirica...
static Distrib nhpp(const std::vector< T > &breakpoints, const std::vector< T > &rates, bool cyclic)
NHPP(breakpoints, rates, cyclic): a MAPt of ORDER ONE, which is what an inhomogeneous Poisson process...
static Distrib replayer(const std::vector< T > &samples)
Replayer / Trace: the samples, with their empirical first two moments.
static Distrib normal(const T &mu, const T &sigma)
Normal(mu, sigma): the Gaussian, for use as a continuous Prior's parameter density.
static Distrib dmap(const Matrix< T > &D0, const Matrix< T > &D1)
DMAP(D0, D1): a DISCRETE-time MAP, where D0 + D1 is stochastic rather than a generator.
static Distrib exp_rate(const T &r)
Definition lang_types.h:814
static Distrib phase_type(const std::vector< T > &alpha, const Matrix< T > &A, bool acyclic)
PH / APH given by (alpha, A): D0 = A and D1 = (-A e) alpha.
static Distrib mapt(const std::vector< T > &breakpoints, const std::vector< Matrix< T > > &D0segs, const std::vector< Matrix< T > > &D1segs, bool cyclic)
MAPt(breakpoints, {D0_k}, {D1_k}, cyclic): a piecewise-constant (D0(t), D1(t)).
static Distrib weibull(const T &scale, const T &shape)
static Distrib bmap(const std::vector< Matrix< T > > &D)
BMAP: the batch-size blocks D0, D1, ..., Dk, where Dj carries an arrival of batch size j.
static Distrib mmap(const Matrix< T > &D0, const std::vector< Matrix< T > > &D1k)
MMAP: D0 plus one D1 block per mark.
static Distrib disabled_dist()
Definition lang_types.h:857
static Distrib pht(const std::vector< T > &breakpoints, const std::vector< std::vector< T > > &alphas, const std::vector< Matrix< T > > &Ssegs, bool cyclic)
PHt(breakpoints, {alpha_k}, {S_k}, cyclic), stored as its equivalent MAP schedule: D0 = S and D1 = (-...
static Distrib erlang_fit(const T &m, const T &c2)
Erlang fitted to a mean and an SCV, as MATLAB's Erlang.fitMeanAndSCV.
Definition lang_types.h:904
static Distrib pareto(const T &shape, const T &scale)
Pareto(shape, scale), with the MATLAB parameter order (alpha, k).
static Distrib gamma_dist(const T &shape, const T &scale)
Gamma(shape, scale), Weibull(scale, shape) and Lognormal(mu, sigma).
static Distrib cox2(const T &mu1, const T &mu2, const T &phi1)
Cox2(mu1, mu2, phi1), MATLAB's two-phase Coxian constructor.
static Distrib map_dist(const Matrix< T > &D0, const Matrix< T > &D1, ProcessType tag)
A MAP given by its two matrices; the moments are those of its stationary phase.
static Distrib poisson(const T &lambda)
Poisson(lambda), whose SCV is 1/lambda – the count's variance is lambda and its mean is lambda,...
static Distrib bernoulli(const T &p)
Bernoulli(p): one trial, mean p and variance p(1-p).
static Distrib hyperexp_n(const std::vector< T > &p, const std::vector< T > &lambda)
HyperExp(p, lambda1, lambda2): phase i chosen with probability p_i.
Definition lang_types.h:931
static Distrib discrete_uniform(const T &a, const T &b)
DiscreteUniform(a, b) over the integers a..b inclusive.
static Distrib geometric(const T &p)
Geometric(p) on the MATLAB convention: the NUMBER OF TRIALS to the first success, support {1,...
static Distrib det(const T &m)
Definition lang_types.h:858
static Distrib rap(const Matrix< T > &H0, const Matrix< T > &H1)
RAP(H0, H1): a rational arrival process, whose moments are the MAP ones.
static Distrib uniform(const T &a, const T &b)
Uniform(a, b).
static Distrib lognormal(const T &logmean, const T &logsigma)
static Distrib hyperexp(const T &p, const T &lambda1, const T &lambda2)
Definition lang_types.h:956
static Distrib me(const std::vector< T > &alpha, const Matrix< T > &A)
ME(alpha, A): the matrix-exponential distribution, whose moments are the phase-type ones – k!
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:846
static Distrib erlang(const T &phase_rate, std::size_t r)
Erlang(alpha, r): r phases of rate alpha, as MATLAB's Erlang(phaseRate, nphases).
Definition lang_types.h:873
static Distrib exp_mean(const T &m)
Definition lang_types.h:799
static Distrib coxian(const std::vector< T > &mu, const std::vector< T > &phi)
Coxian(mu, phi): phase i completes with probability phi(i) and otherwise moves to phase i+1.
Definition lang_types.h:987
static Distrib discrete_sampler(const std::vector< T > &p, const std::vector< T > &x)
DiscreteSampler(p, x): the pmf p over the points x.
static Distrib binomial(const T &n, const T &p)
Binomial(n, p).
static Distrib zipf(const T &s, std::size_t n)
Zipf(s, n) over the ranks 1..n, with the generalized harmonic moments H(s-1,n)/H(s,...
The popularity LAW each class declared, beside the pmf it expands to.
T qlru
Delayed-hit retrieval system (Cache.setRetrievalSystem).
std::vector< Popularity > preadkind
per class, parallel to pread
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
A Logger node's trace configuration, MATLAB's Logger properties and sn.nodeparam{ind}...
std::string file_path
directory, MATLAB's model.getLogPath
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
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
std::vector< double > firingprio
firing priority per mode
std::vector< lang::TimingStrategy > timing
immediate or timed
std::vector< std::string > modenames
std::vector< lang::Distrib< T > > firingproc
firing distribution per mode
std::vector< double > nmodeservers
servers per mode, may be infinite
std::vector< T > fireweight
weight among simultaneously enabled modes
std::vector< Matrix< T > > firing
firing[m](p,r): class-r tokens mode m moves to/from place p when it fires.
std::vector< Matrix< T > > enabling
enabling[m](p,r): class-r tokens of place p (0-based node) mode m needs.
std::vector< std::function< T(const std::vector< T > &)> > firingdep
Marking-dependent firing-rate multiplier g_m(marking); an empty entry is the unit multiplier.
std::vector< Matrix< T > > inhibiting
inhibiting[m](p,r): class-r tokens of p that BLOCK mode m (Inf = never).
std::vector< std::size_t > firingphases
phase count per mode, 0 when non-Markovian