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