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