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. An EMPTY `cutoffs`
1037 * (the key absent on the wire) means no clamping. Twin of the Python
1038 * `_cd_table_to_callable`.
1039 */
1040template <class T>
1041lang::CdScaling<T> cd_scaling_from_json(const json& tbl, const std::vector<int>& cutoffs,
1042 std::size_t K) {
1043 std::map<std::string, std::vector<T> > table;
1044 for (auto it = tbl.begin(); it != tbl.end(); ++it)
1045 table[it.key()] = num_vec_from_json<T>(it.value());
1046 const std::vector<int> cut = cutoffs;
1047 return [table, cut, K](const std::vector<T>& n) {
1048 std::string key;
1049 for (std::size_t r = 0; r < K; ++r) {
1050 int v = r < n.size()
1051 ? static_cast<int>(std::lround(num_traits<T>::to_double(n[r])))
1052 : 0;
1053 if (v < 0) v = 0;
1054 if (r < cut.size() && v > cut[r]) v = cut[r];
1055 if (r) key += ',';
1056 key += std::to_string(v);
1057 }
1058 std::vector<T> out(K, num_traits<T>::from_int(1));
1059 typename std::map<std::string, std::vector<T> >::const_iterator it = table.find(key);
1060 if (it == table.end()) return out;
1061 for (std::size_t r = 0; r < K && r < it->second.size(); ++r) out[r] = it->second[r];
1062 return out;
1063 };
1064}
1065
1066/**
1067 * Rebuild the mu(c) of an order-independent / pass-and-swap station from the
1068 * macrostate table the writers materialize (`oi_rate_table` in
1069 * `linemodel_save.m`, `LineModelIO.oiServiceRate` in the JAR).
1070 *
1071 * The handle takes an ORDERED microstate -- the list of 1-based class indices
1072 * in buffer order, which is what `set_pas` is defined over -- and the table is
1073 * keyed by the per-class COUNTS, because mu is order-independent by
1074 * construction. A composition beyond the tabulated cutoffs saturates at them,
1075 * matching how the writer chose those cutoffs (the closed populations, or 10
1076 * for an open class beyond which mu is constant); a composition the table does
1077 * not list returns zero, which is the writer's own encoding of a non-finite
1078 * rate and means the macrostate is unreachable. All four readers agree on that
1079 * 0 (the JAR and Python used to answer the table maximum): a box with cutoffs
1080 * never misses a non-empty state, so a miss is a state above the buffer of a
1081 * no-cutoff simplex table, which the writers' saturation search never reads.
1082 *
1083 * An EMPTY `cutoffs` means no clamping: the table is keyed by the raw counts.
1084 * That is what an absent `oiCutoffs` says on the wire (Python `save_model`
1085 * omits it for a non-saturating mu at a finite buffer and tabulates the whole
1086 * simplex 1 <= sum(n) <= cap instead), and how MATLAB, the JAR and Python read it.
1087 */
1088template <class T>
1089std::function<T(const std::vector<std::size_t>&)> oi_rate_from_json(const json& tbl,
1090 const std::vector<int>& cutoffs,
1091 std::size_t K) {
1092 std::map<std::string, T> table;
1093 for (auto it = tbl.begin(); it != tbl.end(); ++it)
1094 table[it.key()] = num_traits<T>::from_double(it.value().get<double>());
1095 const std::vector<int> cut = cutoffs;
1096 return [table, cut, K](const std::vector<std::size_t>& micro) {
1097 std::vector<int> cnt(K, 0);
1098 for (std::size_t j = 0; j < micro.size(); ++j)
1099 if (micro[j] >= 1 && micro[j] <= K) ++cnt[micro[j] - 1];
1100 std::string key;
1101 for (std::size_t r = 0; r < K; ++r) {
1102 int v = cnt[r];
1103 if (r < cut.size() && v > cut[r]) v = cut[r];
1104 if (r) key += ',';
1105 key += std::to_string(v);
1106 }
1107 typename std::map<std::string, T>::const_iterator it = table.find(key);
1108 return it == table.end() ? num_traits<T>::from_int(0) : it->second;
1109 };
1110}
1111
1112/**
1113 * The declared peak the wire carries, or, for legacy JSON written before the
1114 * peak became mandatory, the peak DERIVED from the table.
1115 *
1116 * Deriving it is not a fabrication: the table is the whole lattice the reference
1117 * `cd_peak_scaling` would sweep, so the maximum over its rows and classes is
1118 * exactly that routine's answer. It skips the all-zero composition (the
1119 * reference's `tot > 0` guard) and any non-finite entry, which a handle may
1120 * legitimately return for an unreachable composition and which would otherwise
1121 * become the normalizer and zero every utilization at the station.
1122 */
1123template <class T>
1124std::vector<T> cd_peak_from_json(const json& blk, const json& tbl) {
1125 if (blk.contains("peak") && !blk.at("peak").empty())
1126 return num_vec_from_json<T>(blk.at("peak"));
1127 double bmax = 0;
1128 for (auto it = tbl.begin(); it != tbl.end(); ++it) {
1129 if (it.key().find_first_not_of("0,") == std::string::npos) continue;
1130 const std::vector<double> row = num_vec_from_json<double>(it.value());
1131 for (double x : row)
1132 if (std::isfinite(x) && x > bmax) bmax = x;
1133 }
1134 return std::vector<T>(1, num_traits<T>::from_double(bmax));
1135}
1136
1137/**
1138 * Refuse any top-level or node-level key that carries model semantics this
1139 * reader does not consume.
1140 *
1141 * The whitelists are the keys the passes below actually read, plus the purely
1142 * descriptive ones. Anything else is a construct the C++ model layer either
1143 * cannot represent or does not yet parse, and it is NAMED rather than dropped.
1144 */
1145inline void reject_unconsumed_model_keys(const json& model) {
1146 static const char* kModelKeys[] = {"name", "type", "nodes",
1147 "classes", "routing", "format",
1148 "version", "rewards", "finiteCapacityRegions",
1149 "routingStrategies", "routingWeights", "routingParams",
1150 "logPath", "globalDependence", "stateDepRouting"};
1151 // EXACTLY the keys a branch below reads, and nothing else. The first draft
1152 // of this list also carried `arrival`, `classCap`, `accessGraph`,
1153 // `joinStrategy`, `joinQuorum`, `fanOut` and `swapGraph`,
1154 // none of which this reader consumes -- whitelisting them would have
1155 // re-admitted the very silent drop the gate exists to stop. When adding a
1156 // key here, grep that the parser actually reads it.
1157 static const char* kNodeKeys[] = {
1158 "name", "type", "scheduling", "servers", "service",
1159 "buffer", "capacity", "dropRule", "schedParams", "pollingType",
1160 "pollingPar", "classSwitchMatrix", "csMatrix", "forkNode",
1161 "tasksPerLink", "fanOutByDest", "fanOutDist", "fanOutProb",
1162 "items", "numItems", "itemLevelCap", "popularity", "replacementStrategy",
1163 "itemSizes", "costCaps", "accessProb", "admissionProb", "accessGraph",
1164 "itemClass",
1165 "cache", "initialState",
1166 "retrievalSystem", "queues", "hitClass", "missClass",
1167 "immediateFeedback", "loadDependence",
1168 "classDependence", "jointDependence",
1169 "modes", "classCap", "departureDiscipline",
1170 "oiServiceRate", "oiCutoffs", "swapGraph",
1171 "arrivalBatch", "markedClasses",
1172 "stateSpace", "statePrior", "joinStrategy", "joinQuorum",
1173 "setupTime", "delayOffTime","switchoverTimes", "breakdown",
1174 "serverTypes","heteroSchedPolicy", "serverParallelism",
1175 "balking", "retrial", "patience", "orbitImpatience",
1176 "batchRejectProb",
1177 // Logger trace configuration; read into NodeDef::logger below.
1178 "fileName", "filePath", "startTime", "loggerName", "timestamp",
1179 "jobID", "jobClass", "timeSameClass","timeAnyClass"};
1180 // The class object. It had NO gate until every key below was found to be
1181 // read: `isReferenceClass`, `deadline`, `patience`, `spawnClass` and
1182 // `replySignalClass` were all being dropped in silence, each of them a
1183 // different wrong number rather than a diagnostic.
1184 static const char* kClassKeys[] = {
1185 "name", "type", "population", "refNode", "priority",
1186 "openOrClosed","signalType", "targetClass", "removalPolicy","removalDistribution",
1187 "isReferenceClass", "deadline", "patience", "impatienceType",
1188 "spawnClass", "replySignalClass", "immediateFeedback"};
1189 auto known = [](const char* const* tab, std::size_t n, const std::string& k) {
1190 for (std::size_t i = 0; i < n; ++i)
1191 if (k == tab[i]) return true;
1192 return false;
1193 };
1194 const std::string why =
1195 "', which this reader does not implement. Refusing rather than dropping it: a "
1196 "constraint silently discarded here would make every solver return a confident "
1197 "answer for a different model";
1198 for (auto it = model.begin(); it != model.end(); ++it)
1199 if (!known(kModelKeys, sizeof(kModelKeys) / sizeof(*kModelKeys), it.key()))
1200 throw UnsupportedError("network_reader: the model carries '" + it.key() + why);
1201 if (model.contains("classes"))
1202 for (const json& cl : model.at("classes"))
1203 for (auto it = cl.begin(); it != cl.end(); ++it)
1204 if (!known(kClassKeys, sizeof(kClassKeys) / sizeof(*kClassKeys), it.key()))
1205 throw UnsupportedError("network_reader: class '" +
1206 cl.value("name", std::string("?")) + "' carries '" +
1207 it.key() + why);
1208 if (!model.contains("nodes")) return;
1209 for (const json& nd : model.at("nodes"))
1210 for (auto it = nd.begin(); it != nd.end(); ++it)
1211 if (!known(kNodeKeys, sizeof(kNodeKeys) / sizeof(*kNodeKeys), it.key()))
1212 throw UnsupportedError("network_reader: node '" +
1213 nd.value("name", std::string("?")) + "' carries '" +
1214 it.key() + why);
1215}
1216
1217} // namespace detail
1218
1219/**
1220 * Build a `qn::Network<T>` from a parsed model.json envelope.
1221 *
1222 * Two passes: classes and nodes are declared first (a class references its
1223 * reference node by name, a Join references its Fork by name, so every node
1224 * index must exist before the cross-references are wired), then service,
1225 * arrival and routing are applied.
1226 */
1227template <class T>
1228qn::Network<T> build_network_from_json(const detail::json& root) {
1229 using detail::json;
1230 const json& model = root.contains("model") ? root.at("model") : root;
1231 const std::string mtype = model.value("type", std::string("Network"));
1232 if (mtype != "Network") {
1233 // An Environment IS solved by this port, just not by this reader, so
1234 // the refusal names the arm that reads it rather than leaving the
1235 // caller to conclude the model is unsupported.
1236 if (mtype == "Environment")
1237 throw UnsupportedError(
1238 "network_reader: this is an Environment model (a network per stage plus the "
1239 "stage transitions); solve it with -s env, which reads it through "
1240 "environment_reader.h");
1241 throw UnsupportedError("network_reader: model type '" + mtype +
1242 "' is not a Network; only Network models are solved by this path");
1243 }
1244
1245 // WHAT THIS READER DOES NOT UNDERSTAND, IT REFUSES. A key carrying model
1246 // semantics that no branch below consumes would otherwise be SILENTLY
1247 // DROPPED, and every solver would then return an answer correct for the
1248 // model received and wrong for the model intended, with no diagnostic
1249 // anywhere. Not hypothetical: `fcr_mm1kdrop` exports a
1250 // `finiteCapacityRegions` block with globalMaxJobs 3 and a drop rule, and
1251 // without this gate the C++ MVA reported QLen 4 -- exactly rho/(1-rho) for
1252 // the UNBOUNDED M/M/1 -- against the exact M/M/1/K value 1.224932. Same
1253 // rule, and same reason, as the unknown-argument refusal on the --api
1254 // boundary: a key that silently takes its default is a wrong answer.
1255 detail::reject_unconsumed_model_keys(model);
1256
1257 qn::Network<T> net(model.value("name", std::string("model")));
1258 net.set_log_path(model.value("logPath", std::string()));
1259
1260 // A ClassSwitch node needs the class count at construction and a Join needs
1261 // its Fork to exist, while a closed class needs its reference node. That
1262 // cycle is broken by ordering the declarations, not by post-hoc setters:
1263 // every node a class or a Join can depend on is created first (pass 1a),
1264 // then the classes (1b), then the dependent nodes (1c). Routing is wired by
1265 // node NAME (pass 3), so this reordering never perturbs the parity table.
1266 const json& nodes = model.at("nodes");
1267 std::map<std::string, std::size_t> node_idx;
1268 std::vector<std::string> node_type(nodes.size());
1269 for (std::size_t i = 0; i < nodes.size(); ++i)
1270 node_type[i] = nodes[i].at("type").get<std::string>();
1271
1272 // -- Pass 1a: nodes that carry no class/fork dependency. -----------------
1273 for (std::size_t i = 0; i < nodes.size(); ++i) {
1274 const json& nd = nodes[i];
1275 const std::string& type = node_type[i];
1276 // A Join is a STATION, so deferring its creation would shift every
1277 // station index after it and rotate the station rows of the result
1278 // document; it is declared here, in model order, and bound to its Fork
1279 // in pass 1c. ClassSwitch, Cache and Transition are plain nodes, so
1280 // their late creation perturbs no station index.
1281 if (type == "ClassSwitch" || type == "Cache" || type == "Transition") continue;
1282 const std::string name = nd.at("name").get<std::string>();
1283 std::size_t idx = 0;
1284 if (type == "Source") {
1285 idx = net.add_source(name);
1286 } else if (type == "Sink") {
1287 idx = net.add_sink(name);
1288 } else if (type == "Delay") {
1289 idx = net.add_delay(name);
1290 } else if (type == "Queue") {
1291 idx = net.add_queue(name,
1292 detail::sched_from_json(nd.value("scheduling", std::string("FCFS"))));
1293 } else if (type == "Router") {
1294 idx = net.add_router(name);
1295 } else if (type == "Logger" || type == "LogTunnel") {
1296 idx = net.add_logger(name, nd.value("fileName", std::string()));
1297 qn::NodeDef::LoggerParam& lg = net.raw_struct().nodes[idx - 1].logger;
1298 if (nd.contains("filePath")) lg.file_path = nd.at("filePath").get<std::string>();
1299 lg.start_time = nd.value("startTime", lg.start_time);
1300 lg.logger_name = nd.value("loggerName", lg.logger_name);
1301 lg.timestamp = nd.value("timestamp", lg.timestamp);
1302 lg.job_id = nd.value("jobID", lg.job_id);
1303 lg.job_class = nd.value("jobClass", lg.job_class);
1304 lg.time_same_class = nd.value("timeSameClass", lg.time_same_class);
1305 lg.time_any_class = nd.value("timeAnyClass", lg.time_any_class);
1306 } else if (type == "Place") {
1307 // A PLACE CARRIES A DISCIPLINE LIKE A QUEUE, and dropping it is not
1308 // cosmetic: a queueing place whose embedded FCFS queue is read as an
1309 // INF pass-through is a Delay, so a QPN loses the only timed
1310 // behaviour it has and arrives as a net of immediate transitions.
1311 // The writers emit `scheduling` for every Place (`linemodel_save`
1312 // treats Place as a Station, not as a Queue), so the field is there
1313 // to be read; INF is the ordinary place's own value and the default.
1314 idx = net.add_place(name,
1315 detail::sched_from_json(nd.value("scheduling", std::string("INF"))));
1316 } else if (type == "Join") {
1317 idx = net.add_join_unbound(name);
1318 } else if (type == "Fork") {
1319 idx = net.add_fork(name, detail::num_value(nd, "tasksPerLink", 1.0));
1320 } else {
1321 throw UnsupportedError("network_reader: unsupported node type '" + type + "' at node '" +
1322 name + "'");
1323 }
1324 node_idx[name] = idx;
1325 }
1326
1327 // -- Pass 1b: classes. ---------------------------------------------------
1328 const json& classes = model.at("classes");
1329 std::map<std::string, std::size_t> class_idx;
1330 // The signal classes, resolved after the whole class list exists: a
1331 // signal's TARGET is another class by name, which may follow it.
1332 std::vector<std::size_t> signal_classes;
1333 for (std::size_t r = 0; r < classes.size(); ++r) {
1334 const json& cl = classes[r];
1335 const std::string name = cl.at("name").get<std::string>();
1336 const std::string type = cl.at("type").get<std::string>();
1337 std::size_t idx = 0;
1338 if (type == "Open") {
1339 idx = net.add_open_class(name, cl.value("priority", 0));
1340 } else if (type == "Closed" || type == "SelfLooping") {
1341 // A `SelfLoopingClass` IS a closed class -- its jobs cycle at the
1342 // reference station, which is a property of the routing -- so it is
1343 // built as one and only tagged, exactly as `linemodel_load.m:125`
1344 // handles the two labels in one branch.
1345 const double pop = cl.at("population").get<double>();
1346 const std::string ref = cl.at("refNode").get<std::string>();
1347 auto it = node_idx.find(ref);
1348 if (it == node_idx.end())
1349 throw InputError("network_reader: class '" + name + "' references unknown node '" +
1350 ref + "'");
1351 idx = type == "SelfLooping"
1352 ? net.add_self_looping_class(name, pop, it->second, cl.value("priority", 0))
1353 : net.add_closed_class(name, pop, it->second, cl.value("priority", 0));
1354 } else if (type == "Signal") {
1355 // A G-network SIGNAL is an ordinary open or closed class that
1356 // REMOVES jobs instead of joining a queue, so it is created as its
1357 // underlying kind first and marked afterwards. `openOrClosed`
1358 // carries that kind; without it the signal would be built as a
1359 // closed class with no population.
1360 const std::string kind = cl.value("openOrClosed", std::string("Open"));
1361 if (kind == "Closed") {
1362 const std::string ref = cl.at("refNode").get<std::string>();
1363 auto it = node_idx.find(ref);
1364 if (it == node_idx.end())
1365 throw InputError("network_reader: signal class '" + name +
1366 "' references unknown node '" + ref + "'");
1367 // A CLOSED SIGNAL HAS NO POPULATION OF ITS OWN, and the writers
1368 // therefore emit no `population` for it: `ClosedSignal.m:69`
1369 // passes 0 up to ClosedClass, its jobs arriving only by class
1370 // switch from the caller. Requiring the key made every REPLY
1371 // model unreadable here with a raw json out_of_range.
1372 idx = net.add_closed_class(name, cl.value("population", 0.0), it->second,
1373 cl.value("priority", 0));
1374 } else {
1375 idx = net.add_open_class(name, cl.value("priority", 0));
1376 }
1377 signal_classes.push_back(r);
1378 } else {
1379 throw UnsupportedError("network_reader: unsupported class type '" + type +
1380 "' for class '" + name + "'");
1381 }
1382 // The class-wide spelling of immediate feedback; the node-level map is
1383 // read in pass 2 and `sn.immfeed` is the OR of the two.
1384 if (cl.value("immediateFeedback", false)) net.set_class_immediate_feedback(idx);
1385 // `setReferenceClass`: which class of a chain `sn.refclass` names. It is
1386 // the denominator of every chain visit ratio in `sn_get_demands_chain`
1387 // and `sn_get_product_form_params`, so dropping it silently rescales
1388 // the demands of every multi-class chain that declares one.
1389 if (cl.value("isReferenceClass", false)) net.set_reference_class(idx);
1390 // `JobClass.deadline`, `sn.classdeadline`: EDD and EDF order by it.
1391 if (cl.contains("deadline")) {
1392 const double due = cl.at("deadline").get<double>();
1393 if (std::isfinite(due)) net.set_class_deadline(idx, due);
1394 }
1395 // The CLASS-WIDE patience, `Queue.getPatience`'s fallback. A node-scoped
1396 // `patience` read in pass 2 overrides it at the station that names it.
1397 if (cl.contains("patience")) {
1398 const json& pt = cl.at("patience");
1399 const lang::Distrib<T> pd = detail::dist_from_json<T>(pt);
1400 if (!pd.disabled)
1402 idx, pd,
1403 cl.contains("impatienceType")
1404 ? detail::impatience_from_json(cl.at("impatienceType").get<std::string>())
1406 }
1407 class_idx[name] = idx;
1408 }
1409 // The class-to-class bindings, resolved once every class exists: either
1410 // side may be declared after the class that names it.
1411 for (std::size_t r = 0; r < classes.size(); ++r) {
1412 const json& cl = classes[r];
1413 const std::size_t idx = class_idx.at(cl.at("name").get<std::string>());
1414 if (cl.contains("spawnClass")) {
1415 const std::string sp = cl.at("spawnClass").get<std::string>();
1416 auto sit = class_idx.find(sp);
1417 if (sit == class_idx.end())
1418 throw InputError("network_reader: class '" + cl.at("name").get<std::string>() +
1419 "' spawns class '" + sp + "', which the model does not declare");
1420 net.set_class_spawn(idx, sit->second);
1421 }
1422 // Without this a REPLY signal class is INERT after a round trip:
1423 // nothing unblocks the servers waiting on it (`linemodel_save.m:797`).
1424 if (cl.contains("replySignalClass")) {
1425 const std::string rp = cl.at("replySignalClass").get<std::string>();
1426 auto rit = class_idx.find(rp);
1427 if (rit == class_idx.end())
1428 throw InputError("network_reader: class '" + cl.at("name").get<std::string>() +
1429 "' replies with class '" + rp +
1430 "', which the model does not declare");
1431 net.set_reply_signal_class(idx, rit->second);
1432 }
1433 }
1434 // Signals, now that every class the target may name exists.
1435 for (std::size_t si = 0; si < signal_classes.size(); ++si) {
1436 const json& cl = classes[signal_classes[si]];
1437 const std::string name = cl.at("name").get<std::string>();
1438 const std::string st = cl.value("signalType", std::string("negative"));
1440 if (st == "reply" || st == "REPLY") kind = lang::SignalType::REPLY;
1441 else if (st == "catastrophe" || st == "CATASTROPHE") kind = lang::SignalType::CATASTROPHE;
1442 else if (st != "negative" && st != "NEGATIVE")
1443 throw UnsupportedError("network_reader: signal class '" + name + "' is of type '" + st +
1444 "', and the kinds on the wire are negative, catastrophe and "
1445 "reply");
1447 const std::string rp = cl.value("removalPolicy", std::string("RANDOM"));
1448 if (rp == "FCFS" || rp == "fcfs") pol = lang::RemovalPolicy::FCFS;
1449 else if (rp == "LCFS" || rp == "lcfs") pol = lang::RemovalPolicy::LCFS;
1450 std::size_t target = 0;
1451 if (cl.contains("targetClass")) {
1452 auto tit = class_idx.find(cl.at("targetClass").get<std::string>());
1453 if (tit == class_idx.end())
1454 throw InputError("network_reader: signal class '" + name + "' targets class '" +
1455 cl.at("targetClass").get<std::string>() +
1456 "', which the model does not declare");
1457 target = tit->second;
1458 }
1459 std::vector<T> remdist;
1460 if (cl.contains("removalDistribution"))
1461 remdist = detail::removal_pmf_from_json<T>(cl.at("removalDistribution"));
1462 net.set_signal(class_idx.at(name), kind, pol, target, remdist);
1463 }
1464 const std::size_t K = class_idx.size();
1465
1466 // -- Pass 1c: ClassSwitch (matrix now sizeable), Join (fork exists) and
1467 // Cache (its hit/miss classes and popularity are class-indexed). --------
1468 for (std::size_t i = 0; i < nodes.size(); ++i) {
1469 const json& nd = nodes[i];
1470 const std::string& type = node_type[i];
1471 if (type != "ClassSwitch" && type != "Join" && type != "Cache" && type != "Fork") continue;
1472 const std::string name = nd.at("name").get<std::string>();
1473 std::size_t idx = 0;
1474 if (type == "Fork") {
1475 // VARIABLE FORKING LEVELS. Each list is an array of
1476 // {dest, class, ...} records, exactly as `linemodel_save.m:509-553`
1477 // writes them; `dest` is a node NAME and `class` a 1-based index.
1478 // The overrides are recorded here and replayed by `link()` in Pass 3,
1479 // because a per-destination override reads the routing.
1480 if (!nd.contains("fanOutByDest") && !nd.contains("fanOutDist") &&
1481 !nd.contains("fanOutProb"))
1482 continue;
1483 idx = node_idx.at(name);
1484 if (nd.contains("fanOutByDest")) {
1485 const json& ovs = nd.at("fanOutByDest");
1486 for (std::size_t e = 0; e < ovs.size(); ++e)
1487 net.set_fork_tasks_per_link(idx, ovs[e].at("class").get<std::size_t>(),
1488 ovs[e].at("value").get<double>(),
1489 detail::fork_dest_index(ovs[e], node_idx, name));
1490 }
1491 if (nd.contains("fanOutDist")) {
1492 const json& ovs = nd.at("fanOutDist");
1493 for (std::size_t e = 0; e < ovs.size(); ++e) {
1494 const std::vector<double> pv = ovs[e].at("p").get<std::vector<double> >();
1495 const std::vector<double> xv = ovs[e].at("x").get<std::vector<double> >();
1496 std::vector<T> p, x;
1497 for (std::size_t q = 0; q < pv.size(); ++q)
1498 p.push_back(num_traits<T>::from_double(pv[q]));
1499 for (std::size_t q = 0; q < xv.size(); ++q)
1500 x.push_back(num_traits<T>::from_double(xv[q]));
1501 net.set_fork_tasks_per_link_dist(idx, ovs[e].at("class").get<std::size_t>(),
1503 detail::fork_dest_index(ovs[e], node_idx, name));
1504 }
1505 }
1506 if (nd.contains("fanOutProb")) {
1507 const json& ovs = nd.at("fanOutProb");
1508 for (std::size_t e = 0; e < ovs.size(); ++e)
1510 idx, ovs[e].at("class").get<std::size_t>(),
1511 detail::fork_dest_index(ovs[e], node_idx, name),
1512 ovs[e].at("value").get<double>());
1513 }
1514 continue;
1515 }
1516 if (type == "Cache") {
1517 // THE WRITERS EMIT THE CACHE TWICE, by design: a nested `cache`
1518 // object and the same fields flattened onto the node, so that both
1519 // the MATLAB and the JAR readers find what they look for
1520 // (`linemodel_save.m:425-435`). The flat spelling wins where both
1521 // are present -- they are mirrored from the nested one, so they
1522 // agree -- and the nested one is consulted for anything the flat
1523 // mirror does not carry.
1524 const json empty_obj = json::object();
1525 const json& cj = nd.contains("cache") ? nd.at("cache") : empty_obj;
1527 cp.nitems = detail::has_cache_key(nd, cj, "numItems")
1528 ? detail::cache_key(nd, cj, "numItems").get<std::size_t>()
1529 : cj.at("items").get<std::size_t>();
1530 cp.itemcap = detail::has_cache_key(nd, cj, "itemLevelCap")
1531 ? detail::cache_key(nd, cj, "itemLevelCap").get<std::vector<int> >()
1532 : cj.at("capacity").get<std::vector<int> >();
1533 // Per-item storage costs and per-list cost caps (ton21cache Sec. IX).
1534 // A scalar costCaps is the single cache-wide cap, replicated per list.
1535 if (detail::has_cache_key(nd, cj, "itemSizes"))
1536 cp.itemsize = detail::cache_key(nd, cj, "itemSizes").get<std::vector<int> >();
1537 if (detail::has_cache_key(nd, cj, "costCaps")) {
1538 const json& cc = detail::cache_key(nd, cj, "costCaps");
1539 if (cc.is_array()) {
1540 cp.costcap = cc.get<std::vector<int> >();
1541 } else {
1542 cp.costcapglobal = true;
1543 cp.costcap.assign(cp.itemcap.size(), cc.get<int>());
1544 }
1545 }
1546 cp.replacestrat = detail::replacement_from_json(
1547 detail::has_cache_key(nd, cj, "replacementStrategy")
1548 ? detail::cache_key(nd, cj, "replacementStrategy").get<std::string>()
1549 : cj.value("replacement", std::string("RR")));
1550 cp.pread.assign(K, std::vector<T>());
1551 cp.hitclass.assign(K, 0);
1552 cp.missclass.assign(K, 0);
1553 cp.preadkind.assign(K, typename qn::CacheParam<T>::Popularity());
1554 cp.classitem.assign(K, 0);
1555 if (detail::has_cache_key(nd, cj, "popularity")) {
1556 const json& pop = detail::cache_key(nd, cj, "popularity");
1557 for (auto it = pop.begin(); it != pop.end(); ++it) {
1558 // A class that does not read the cache is written with a
1559 // `Disabled` popularity, and leaves `pread` empty
1560 // (`linemodel_load.m:602`).
1561 if (it.value().value("type", std::string()) == "Disabled") continue;
1562 cp.pread[class_idx.at(it.key()) - 1] =
1563 detail::pmf_from_json<T>(it.value(), cp.nitems);
1564 cp.preadkind[class_idx.at(it.key()) - 1] =
1565 detail::popularity_kind_from_json<T>(it.value(), cp.nitems);
1566 }
1567 }
1568 auto fill_switch = [&](const char* key, std::vector<std::size_t>& dst) {
1569 if (!detail::has_cache_key(nd, cj, key)) return;
1570 const json& blk = detail::cache_key(nd, cj, key);
1571 for (auto it = blk.begin(); it != blk.end(); ++it)
1572 dst[class_idx.at(it.key()) - 1] = class_idx.at(it.value().get<std::string>());
1573 };
1574 fill_switch("hitClass", cp.hitclass);
1575 fill_switch("missClass", cp.missclass);
1576 // `itemClass`: for a cache network, the item each per-item class reads
1577 // (`Cache.setItemReadClasses`). Carried so a reader need not infer it
1578 // from a one-hot pread, which a genuine single-item popularity also has.
1579 if (detail::has_cache_key(nd, cj, "itemClass")) {
1580 const json& blk = detail::cache_key(nd, cj, "itemClass");
1581 for (auto it = blk.begin(); it != blk.end(); ++it)
1582 cp.classitem[class_idx.at(it.key()) - 1] =
1583 static_cast<std::size_t>(it.value().get<double>());
1584 }
1585 // `accessProb`: the per-(class, item) access graph, `sn.nodeparam{i}.accost`.
1586 // The wire form is a class-major array of item-major arrays of
1587 // (h+1)x(h+1) matrices, with an empty entry where the pair declares
1588 // none (`linemodel_save.m:402-416`). It is READ HERE and nowhere else,
1589 // and dropping it would solve a cache whose admission and promotion
1590 // are item-dependent as if they were the linear chain.
1591 // `admissionProb`: the q-LRU admission probability, `nodeparam.qlru`.
1592 // Every Cache the reference writes carries it, defaulting to 1, so
1593 // refusing it made every MATLAB-written cache model unreadable here.
1594 if (detail::has_cache_key(nd, cj, "admissionProb"))
1595 cp.qlru = num_traits<T>::from_double(detail::cache_key(nd, cj, "admissionProb").get<double>());
1596 if (detail::has_cache_key(nd, cj, "accessProb")) {
1597 const json& ap = detail::cache_key(nd, cj, "accessProb");
1598 cp.accost.clear();
1599 for (const json& per_class : ap) {
1600 std::vector<Matrix<T> > row;
1601 for (const json& g : per_class) {
1602 if (g.is_null() || g.empty()) {
1603 row.push_back(Matrix<T>());
1604 continue;
1605 }
1606 row.push_back(detail::mat_from_json<T>(g));
1607 }
1608 cp.accost.push_back(row);
1609 }
1610 } else if (detail::has_cache_key(nd, cj, "accessGraph")) {
1611 // `accessGraph` is the SAME access cost shared by every class:
1612 // one (h+1)x(h+1) matrix per item, written when the model set
1613 // `node.graph` rather than the full per-class `accessProb`
1614 // (`linemodel_save.m:395-401`). Replicating it across the classes
1615 // here is what makes the two spellings the same model.
1616 const std::vector<Matrix<T> > shared =
1617 detail::mat_list_from_json<T>(detail::cache_key(nd, cj, "accessGraph"));
1618 cp.accost.assign(K, shared);
1619 }
1620 // The initial cache contents: the state row `[class counts | list
1621 // contents | retrieval bitmap]` the reference dumps from the node.
1622 if (detail::has_cache_key(nd, cj, "initialState"))
1623 cp.initstate = detail::num_vec_from_json<T>(detail::cache_key(nd, cj, "initialState"));
1624 // Delayed-hit retrieval system: the retrieval classes are already in
1625 // the class list (created in pass 1b) with their service and routing,
1626 // so only the cache-side maps are recovered here -- no re-creation.
1627 // Read through the same nested-or-flat helper every other cache key
1628 // uses. All three writers put this one flat today, so a direct
1629 // `nd.contains` happens to work -- but it is the shape python got
1630 // wrong (it chose ONE source per node and lost whatever the other
1631 // held), and a reader that treats one key differently from its
1632 // siblings is the trap that made that possible.
1633 if (detail::has_cache_key(nd, cj, "retrievalSystem")) {
1634 const json& rs = detail::cache_key(nd, cj, "retrievalSystem");
1635 cp.retrieval_capacity = rs.value("capacity", 0);
1636 cp.retrieval_classes.assign(cp.nitems, std::vector<std::size_t>(K, 0));
1637 if (rs.contains("byClass")) {
1638 for (auto rc = rs.at("byClass").begin(); rc != rs.at("byClass").end(); ++rc) {
1639 const std::size_t rdcls = class_idx.at(rc.key()); // 1-based read class
1640 std::vector<std::size_t> qnodes;
1641 for (const auto& qn : rc.value().at("queues"))
1642 qnodes.push_back(node_idx.at(qn.get<std::string>()));
1643 cp.retrieval_queues[rdcls - 1] = qnodes;
1644 for (auto it2 = rc.value().at("items").begin();
1645 it2 != rc.value().at("items").end(); ++it2) {
1646 const std::size_t item = std::stoul(it2.key());
1647 if (item < cp.nitems)
1648 cp.retrieval_classes[item][rdcls - 1] =
1649 class_idx.at(it2.value().get<std::string>());
1650 }
1651 }
1652 }
1653 }
1654 idx = net.add_cache(name, cp);
1655 node_idx[name] = idx;
1656 continue;
1657 }
1658 if (type == "Join") {
1659 // The pairing is a DECLARATION the Join carries; the routing does not
1660 // determine it when a model nests two fork-join pairs. A document that
1661 // omits it is refused here rather than at link(), where the message
1662 // would name the Fork rather than the Join that forgot it.
1663 if (!nd.contains("forkNode"))
1664 throw InputError("network_reader: Join '" + name +
1665 "' names no 'forkNode': the document declares no "
1666 "Fork for it to close.");
1667 auto it = node_idx.find(nd.at("forkNode").get<std::string>());
1668 if (it == node_idx.end())
1669 throw InputError("network_reader: Join '" + name +
1670 "' references unknown fork '" +
1671 nd.at("forkNode").get<std::string>() + "'");
1672 const std::size_t fork = it->second;
1673 // The station itself was declared in pass 1a, in model order, so
1674 // only the Fork it closes is recorded here.
1675 idx = node_idx.at(name);
1676 net.bind_join(idx, fork);
1677 // The declared join rule. PARTIAL fires on a quorum of siblings
1678 // rather than on all of them, so dropping it would make a partial
1679 // join wait for arrivals that never come.
1680 if (nd.contains("joinStrategy") || nd.contains("joinQuorum")) {
1681 const std::string js = nd.value("joinStrategy", std::string("STD"));
1682 if (js != "STD" && js != "PARTIAL")
1683 throw UnsupportedError("network_reader: Join '" + name + "' declares strategy '" +
1684 js + "', and the rules on the wire are STD and PARTIAL");
1685 net.set_join_strategy(idx,
1686 js == "PARTIAL" ? lang::JoinStrategy::PARTIAL
1688 nd.value("joinQuorum", 0.0));
1689 }
1690 } else {
1691 // Rows a class-switch matrix omits leave that class unchanged, so the
1692 // matrix defaults to the identity before the listed rows overwrite it.
1693 // The builder's class indices are 1-based; the matrix is 0-indexed.
1694 // `csMatrix` is the JAR writer's spelling of the same block.
1695 Matrix<T> C(K, K);
1696 for (std::size_t d = 0; d < K; ++d) C(d, d) = num_traits<T>::from_int(1);
1697 if (nd.contains("classSwitchMatrix") || nd.contains("csMatrix")) {
1698 const json& csm =
1699 nd.contains("classSwitchMatrix") ? nd.at("classSwitchMatrix") : nd.at("csMatrix");
1700 for (auto ri = csm.begin(); ri != csm.end(); ++ri) {
1701 const std::size_t rr = class_idx.at(ri.key()) - 1;
1702 for (std::size_t d = 0; d < K; ++d) C(rr, d) = num_traits<T>::from_int(0);
1703 for (auto ci = ri.value().begin(); ci != ri.value().end(); ++ci)
1704 C(rr, class_idx.at(ci.key()) - 1) =
1705 num_traits<T>::from_double(ci.value().get<double>());
1706 }
1707 }
1708 idx = net.add_class_switch(name, C);
1709 }
1710 node_idx[name] = idx;
1711 }
1712
1713 // -- Pass 1d: Transitions, whose arcs name other nodes. -------------------
1714 // A mode's enabling/inhibiting/firing arcs are written as (node, class,
1715 // count) triples, so every node must exist before they can be resolved, and
1716 // the arc matrices are sized against the FULL node count rather than the
1717 // count so far. THE CLASS IS KEPT: `TransitionParam` stores an
1718 // (nnodes x nclasses) matrix per mode, as MATLAB's `enablingConditions{m}`
1719 // does, so a mode requiring two Class1 tokens at a place is not satisfied by
1720 // Class2 tokens sitting there. An arc naming a class the model does not
1721 // declare is an error rather than a silent drop.
1722 for (std::size_t i = 0; i < nodes.size(); ++i) {
1723 if (node_type[i] != "Transition") continue;
1724 const json& nd = nodes[i];
1725 const std::string name = nd.at("name").get<std::string>();
1726 if (!nd.contains("modes") || nd.at("modes").empty())
1727 throw InputError("network_reader: transition '" + name + "' declares no mode");
1728 const json& modes = nd.at("modes");
1730 tp.nmodes = modes.size();
1731 const std::size_t nn = nodes.size();
1732 const double inf = std::numeric_limits<double>::infinity();
1733 for (std::size_t m = 0; m < modes.size(); ++m) {
1734 const json& mj = modes[m];
1735 tp.modenames.push_back(mj.value("name", std::string("Mode") + std::to_string(m + 1)));
1736 // A marking-dependent firing rate g_m(marking) crosses the wire as
1737 // the MATERIALIZED lattice `{slots, cutoffs, scaling}`, because a
1738 // handle has no JSON form: `slots` names the enabling (place,class)
1739 // pairs that g reads, `cutoffs` caps each of them, and `scaling` is
1740 // keyed by the comma-joined 0-based counts in slot order. Rebuilt
1741 // here into the same closure MATLAB's `firingdep_table_to_handle`
1742 // and the Python `_firingdep_table_to_handle` build, over the
1743 // node-indexed marking `state_events.h` passes in.
1744 tp.firingdep.push_back(std::function<T(const std::vector<T>&)>());
1745 if (mj.contains("firingRateDependence")) {
1746 const json& frm = mj.at("firingRateDependence");
1747 if (!frm.contains("slots") || !frm.contains("scaling"))
1748 throw InputError("network_reader: the marking-dependent firing rate of mode " +
1749 std::to_string(m + 1) + " of transition '" + name +
1750 "' carries no 'slots'/'scaling' lattice");
1751 std::vector<std::size_t> slot_node;
1752 for (const json& sm : frm.at("slots")) {
1753 const std::string on = sm.at("node").get<std::string>();
1754 const std::map<std::string, std::size_t>::const_iterator ni = node_idx.find(on);
1755 if (ni == node_idx.end())
1756 throw InputError("network_reader: the marking-dependent firing rate of "
1757 "transition '" +
1758 name + "' reads node '" + on +
1759 "', which the model does not declare");
1760 slot_node.push_back(ni->second - 1);
1761 }
1762 std::vector<long> cutoffs;
1763 if (frm.contains("cutoffs"))
1764 for (const json& c : frm.at("cutoffs")) cutoffs.push_back(c.get<long>());
1765 std::map<std::string, double> table;
1766 const json& sc = frm.at("scaling");
1767 for (json::const_iterator it = sc.begin(); it != sc.end(); ++it)
1768 table[it.key()] = it.value().get<double>();
1769 tp.firingdep.back() = [slot_node, cutoffs,
1770 table](const std::vector<T>& mk) -> T {
1771 std::string key;
1772 for (std::size_t s = 0; s < slot_node.size(); ++s) {
1773 long c = slot_node[s] < mk.size()
1774 ? static_cast<long>(std::llround(
1775 num_traits<T>::to_double(mk[slot_node[s]])))
1776 : 0L;
1777 if (c < 0) c = 0;
1778 if (s < cutoffs.size() && c > cutoffs[s]) c = cutoffs[s];
1779 if (s) key += ',';
1780 key += std::to_string(c);
1781 }
1782 const std::map<std::string, double>::const_iterator hit = table.find(key);
1783 // A marking outside the tabulated box is NEUTRAL, not zero:
1784 // the same default the three reference readers apply.
1785 return hit == table.end() ? num_traits<T>::from_int(1)
1786 : num_traits<T>::from_double(hit->second);
1787 };
1788 }
1789 const bool immediate =
1790 mj.value("timingStrategy", std::string("TIMED")) == "IMMEDIATE";
1791 tp.timing.push_back(immediate ? lang::TimingStrategy::IMMEDIATE
1794 if (mj.contains("distribution")) proc = detail::dist_from_json<T>(mj.at("distribution"));
1795 tp.firingproc.push_back(proc);
1796 tp.firingphases.push_back(immediate || proc.disabled ? 0
1797 : lang::dist_to_map(proc).order());
1798 tp.nmodeservers.push_back(detail::num_value(mj, "numServers", 1.0));
1799 // ONE, not zero: `Transition.addMode` gives a new mode priority 1 in
1800 // all four codebases (MATLAB Transition.m:88, python nodes.py:2660,
1801 // JAR Transition.java:114), and the three readers leave that default
1802 // in place when the key is absent. Defaulting to 0 here made an
1803 // omitted key mean a DIFFERENT mode than it means everywhere else,
1804 // and firing priority selects which immediate mode fires, so the
1805 // marking process itself changed rather than a reported decimal.
1806 tp.firingprio.push_back(mj.value("firingPriority", 1.0));
1807 tp.fireweight.push_back(num_traits<T>::from_double(mj.value("firingWeight", 1.0)));
1808 const std::size_t K = classes.size();
1809 Matrix<T> enab(nn, K, num_traits<T>::from_int(0));
1810 Matrix<T> inhib(nn, K, num_traits<T>::from_double(inf));
1811 Matrix<T> fire(nn, K, num_traits<T>::from_int(0));
1812 const char* kArcKey[3] = {"enablingConditions", "inhibitingConditions",
1813 "firingOutcomes"};
1814 for (int which = 0; which < 3; ++which) {
1815 if (!mj.contains(kArcKey[which])) continue;
1816 for (const json& arc : mj.at(kArcKey[which])) {
1817 const std::string on = arc.at("node").get<std::string>();
1818 const std::map<std::string, std::size_t>::const_iterator ni = node_idx.find(on);
1819 if (ni == node_idx.end())
1820 throw InputError("network_reader: transition '" + name + "' names node '" +
1821 on + "', which the model does not declare");
1822 // AN ARC WITHOUT A CLASS IS THE FIRST CLASS'S, which is what
1823 // a single-class document means by omitting it; a name the
1824 // model never declared is an error, because dropping the arc
1825 // would build a net with one fewer precondition.
1826 std::size_t rr = 1;
1827 if (arc.contains("class")) {
1828 const std::string cn = arc.at("class").get<std::string>();
1829 const std::map<std::string, std::size_t>::const_iterator ci =
1830 class_idx.find(cn);
1831 if (ci == class_idx.end())
1832 throw InputError("network_reader: transition '" + name +
1833 "' names class '" + cn +
1834 "', which the model does not declare");
1835 rr = ci->second;
1836 }
1837 const T cnt = num_traits<T>::from_double(arc.value("count", 1.0));
1838 const std::size_t q = ni->second - 1;
1839 if (which == 0) enab(q, rr - 1) = enab(q, rr - 1) + cnt;
1840 else if (which == 1) inhib(q, rr - 1) = cnt; // a threshold, not a count
1841 else fire(q, rr - 1) = fire(q, rr - 1) + cnt;
1842 }
1843 }
1844 tp.enabling.push_back(enab);
1845 tp.inhibiting.push_back(inhib);
1846 tp.firing.push_back(fire);
1847 }
1848 node_idx[name] = net.add_transition(name, tp);
1849 }
1850
1851 // -- Pass 2: per-node parameters (service/arrival, servers, CS matrix). ---
1852 for (std::size_t i = 0; i < nodes.size(); ++i) {
1853 const json& nd = nodes[i];
1854 const std::string& type = node_type[i];
1855 const std::size_t idx = node_idx.at(nd.at("name").get<std::string>());
1856
1857 // A QUEUEING PLACE'S EMBEDDED QUEUE HAS A SERVER COUNT TOO, and it is
1858 // the c of its M/M/c: `add_place(name, sched)` defaults it to one server
1859 // for a queueing discipline, which a `servers` field then overrides.
1860 if (nd.contains("servers") && (type == "Queue" || type == "Place"))
1861 net.set_number_of_servers(idx, detail::num_from_json(nd.at("servers")));
1862
1863 if (nd.contains("service")) {
1864 const json& svc = nd.at("service");
1865 for (auto it = svc.begin(); it != svc.end(); ++it) {
1866 auto cit = class_idx.find(it.key());
1867 if (cit == class_idx.end())
1868 throw InputError("network_reader: service names unknown class '" + it.key() +
1869 "' at node '" + nd.at("name").get<std::string>() + "'");
1870 const lang::Distrib<T> d = detail::dist_from_json<T>(it.value());
1871 if (type == "Source") net.set_arrival(idx, cit->second, d);
1872 else net.set_service(idx, cit->second, d);
1873 }
1874 }
1875
1876 if (type == "Queue" && nd.contains("scheduling") &&
1877 nd.at("scheduling").get<std::string>() == "POLLING") {
1878 if (nd.contains("pollingType"))
1879 net.set_polling_type(idx, detail::polling_from_json(nd.at("pollingType").get<std::string>()),
1880 nd.value("pollingPar", 0));
1881 }
1882
1883 // Per-class scheduling weights (DPS / GPS). Without these the AMVA DPS
1884 // path either errors (no weights) or degenerates to plain PS.
1885 if (nd.contains("schedParams") && (type == "Queue" || type == "Delay")) {
1886 const json& sp = nd.at("schedParams");
1887 for (auto it = sp.begin(); it != sp.end(); ++it) {
1888 auto cit = class_idx.find(it.key());
1889 if (cit != class_idx.end())
1890 net.set_sched_param(idx, cit->second,
1891 num_traits<T>::from_double(it.value().get<double>()));
1892 }
1893 }
1894
1895 // Finite-buffer blocking. `dropRule` per class (blockingAfterService ->
1896 // BAS makes mva_is_bas_model true, routing the model to solver_sqd) and
1897 // `buffer` the total station capacity.
1898 // The writers emit these for `isa(node,'Station')`, which is wider than
1899 // Queue and Delay: a Join and a queueing Place are stations too, and a
1900 // rule read only at a Queue is a rule silently dropped at those.
1901 const bool station_node =
1902 type == "Queue" || type == "Delay" || type == "Join" || type == "Place";
1903 if (nd.contains("dropRule") && station_node) {
1904 const json& dr = nd.at("dropRule");
1905 for (auto it = dr.begin(); it != dr.end(); ++it) {
1906 auto cit = class_idx.find(it.key());
1907 if (cit != class_idx.end())
1908 net.set_drop_rule(idx, cit->second,
1909 detail::drop_from_json(it.value().get<std::string>()));
1910 }
1911 }
1912 if (nd.contains("buffer") && station_node)
1913 net.set_capacity(idx, detail::num_from_json(nd.at("buffer")));
1914 // Per-class buffer capacity, the refinement of `buffer`.
1915 if (nd.contains("classCap")) {
1916 const json& cc = nd.at("classCap");
1917 for (auto it = cc.begin(); it != cc.end(); ++it) {
1918 auto cit = class_idx.find(it.key());
1919 if (cit != class_idx.end())
1920 net.set_class_capacity(idx, cit->second, detail::num_from_json(it.value()));
1921 }
1922 }
1923
1924 // -- Impatience, balking and the retrial orbit -----------------------
1925 //
1926 // Four DIFFERENT populations, and the reference keeps them apart: a
1927 // patience timer abandons a job that has JOINED the queue, balking
1928 // refuses to join at all, an orbit impatience abandons a retrial orbit
1929 // (never a buffer slot), and a batch rejection drops a whole arriving
1930 // batch. Folding any two together changes which jobs are counted.
1931 if (nd.contains("patience")) {
1932 const json& pt = nd.at("patience");
1933 for (auto it = pt.begin(); it != pt.end(); ++it) {
1934 auto cit = class_idx.find(it.key());
1935 if (cit == class_idx.end()) continue;
1936 const json& pj = it.value();
1937 const lang::ImpatienceType kind =
1938 pj.contains("impatienceType")
1939 ? detail::impatience_from_json(pj.at("impatienceType").get<std::string>())
1941 net.set_patience(idx, cit->second, detail::dist_from_json<T>(pj.at("distribution")),
1942 kind);
1943 }
1944 }
1945 if (nd.contains("orbitImpatience")) {
1946 const json& oi = nd.at("orbitImpatience");
1947 for (auto it = oi.begin(); it != oi.end(); ++it) {
1948 auto cit = class_idx.find(it.key());
1949 if (cit != class_idx.end())
1950 net.set_orbit_impatience(idx, cit->second, detail::dist_from_json<T>(it.value()));
1951 }
1952 }
1953 if (nd.contains("batchRejectProb")) {
1954 const json& br = nd.at("batchRejectProb");
1955 for (auto it = br.begin(); it != br.end(); ++it) {
1956 auto cit = class_idx.find(it.key());
1957 if (cit != class_idx.end())
1958 net.set_batch_reject(idx, cit->second,
1959 num_traits<T>::from_double(it.value().get<double>()));
1960 }
1961 }
1962 if (nd.contains("balking")) {
1963 const json& bk = nd.at("balking");
1964 for (auto it = bk.begin(); it != bk.end(); ++it) {
1965 auto cit = class_idx.find(it.key());
1966 if (cit == class_idx.end()) continue;
1967 const json& bj = it.value();
1968 std::vector<typename qn::Station<T>::BalkingThreshold> ths;
1969 if (bj.contains("thresholds"))
1970 for (const json& tj : bj.at("thresholds")) {
1972 th.min_jobs = tj.value("minJobs", 0.0);
1973 // -1 IS THE WIRE'S INFINITY, not a count: the writer maps
1974 // an unbounded upper end to it rather than emitting Inf,
1975 // which JSON has no literal for.
1976 th.max_jobs = tj.value("maxJobs", -1.0);
1977 th.probability = num_traits<T>::from_double(tj.value("probability", 1.0));
1978 ths.push_back(th);
1979 }
1980 net.set_balking(idx, cit->second,
1981 detail::balking_from_json(bj.at("strategy").get<std::string>()), ths);
1982 }
1983 }
1984 if (nd.contains("retrial")) {
1985 const json& rt = nd.at("retrial");
1986 for (auto it = rt.begin(); it != rt.end(); ++it) {
1987 auto cit = class_idx.find(it.key());
1988 if (cit == class_idx.end()) continue;
1989 const lang::Distrib<T> delay = detail::dist_from_json<T>(it.value().at("delay"));
1990 // `sn.retrialMu` is the RATE of that delay, which is what the
1991 // state layer multiplies the orbit population by.
1992 const T rate = T(num_traits<T>::from_int(1) / delay.mean);
1993 net.set_retrial(idx, cit->second, delay, rate,
1994 int(detail::num_value(it.value(), "maxAttempts", 0.0)));
1995 }
1996 }
1997
1998 // -- Setup / delay-off, switchover and heterogeneous servers ---------
1999 if (nd.contains("setupTime")) {
2000 if (!nd.contains("delayOffTime"))
2001 throw InputError(
2002 "network_reader: node '" + nd.at("name").get<std::string>() +
2003 "' declares a setup time with no delay-off time; a server that never powers "
2004 "down never pays the setup, so the pair is meaningless alone");
2005 const json& su = nd.at("setupTime");
2006 const json& doff = nd.at("delayOffTime");
2007 for (auto it = su.begin(); it != su.end(); ++it) {
2008 auto cit = class_idx.find(it.key());
2009 if (cit == class_idx.end() || !doff.contains(it.key())) continue;
2010 net.set_setup_delayoff(idx, cit->second, detail::dist_from_json<T>(it.value()),
2011 detail::dist_from_json<T>(doff.at(it.key())));
2012 }
2013 }
2014 // -- Server breakdown / repair ---------------------------------------
2015 // The wire form is `linemodel_save`'s: {failure, repair, downService?},
2016 // with `downService` keyed by CLASS NAME. MATLAB writes it per class
2017 // even when `setBreakdown` was given one distribution for every class,
2018 // flattening the class-independent form, so there is only one shape to
2019 // read here.
2020 if (nd.contains("breakdown")) {
2021 const json& bd = nd.at("breakdown");
2022 if (!bd.contains("failure") || !bd.contains("repair"))
2023 throw InputError(
2024 "network_reader: node '" + nd.at("name").get<std::string>() +
2025 "' declares a breakdown without both a failure and a repair time; a server "
2026 "that never recovers is an absorbing model, not a breakdown");
2027 std::vector<lang::Distrib<T> > down(class_idx.size(),
2029 if (bd.contains("downService")) {
2030 const json& ds = bd.at("downService");
2031 for (auto it = ds.begin(); it != ds.end(); ++it) {
2032 auto cit = class_idx.find(it.key());
2033 if (cit == class_idx.end()) continue;
2034 if (cit->second >= 1 && cit->second <= down.size())
2035 down[cit->second - 1] = detail::dist_from_json<T>(it.value());
2036 }
2037 }
2038 net.set_breakdown(idx, detail::dist_from_json<T>(bd.at("failure")),
2039 detail::dist_from_json<T>(bd.at("repair")), down);
2040 }
2041 if (nd.contains("switchoverTimes")) {
2042 // ONE KEY, TWO SHAPES, as `linemodel_save` writes them: under
2043 // POLLING an entry names the buffer the server LEAVES and carries
2044 // no `to`; otherwise it names the ordered class PAIR, and the
2045 // station keeps the (K x K) cell the reference keeps.
2046 //
2047 // A PAIRWISE SWITCHOVER IS CARRIED AND NOT CONSUMED, here as in
2048 // every codebase: the state machinery, the SSA events and the MVA
2049 // polling analyzer all reach `Station::switchover` through the
2050 // polling buffers, and `writeJSIM` warns and drops the pairwise
2051 // times on an ordinary Server because JMT's `Server` has no
2052 // switchover. Carrying it is still not idle -- it is what lets the
2053 // model be declared, written and read back unchanged, which is what
2054 // `switchover_basic` is.
2055 const bool polling = type == "Queue" &&
2056 nd.value("scheduling", std::string()) == "POLLING";
2057 for (const json& so : nd.at("switchoverTimes")) {
2058 auto cit = class_idx.find(so.at("from").get<std::string>());
2059 if (cit == class_idx.end()) continue;
2060 const std::string to = so.value("to", std::string());
2061 if (polling) {
2062 if (!to.empty())
2063 throw UnsupportedError(
2064 "network_reader: node '" + nd.at("name").get<std::string>() +
2065 "' is POLLING-scheduled but declares a switchover that names the "
2066 "class moved TO; a polling switchover is the walk out of one buffer");
2067 net.set_switchover(idx, cit->second,
2068 detail::dist_from_json<T>(so.at("distribution")));
2069 continue;
2070 }
2071 if (to.empty())
2072 throw UnsupportedError(
2073 "network_reader: node '" + nd.at("name").get<std::string>() +
2074 "' declares a switchover without the class moved TO, which only a "
2075 "POLLING station's per-buffer walk may omit");
2076 auto tit = class_idx.find(to);
2077 if (tit == class_idx.end()) continue;
2078 net.set_switchover(idx, cit->second, tit->second,
2079 detail::dist_from_json<T>(so.at("distribution")));
2080 }
2081 }
2082 if (nd.contains("serverTypes")) {
2083 for (const json& st : nd.at("serverTypes")) {
2084 typename qn::Station<T>::ServerType stype;
2085 stype.name = st.value("name", std::string());
2086 stype.count = st.value("count", 1.0);
2087 if (st.contains("compatibleClasses")) {
2088 stype.compatible.assign(classes.size(), false);
2089 for (const json& cn : st.at("compatibleClasses")) {
2090 auto cit = class_idx.find(cn.get<std::string>());
2091 if (cit != class_idx.end()) stype.compatible[cit->second - 1] = true;
2092 }
2093 }
2094 if (st.contains("service")) {
2095 stype.service.assign(classes.size(), lang::Distrib<T>::disabled_dist());
2096 const json& sv = st.at("service");
2097 for (auto it = sv.begin(); it != sv.end(); ++it) {
2098 auto cit = class_idx.find(it.key());
2099 if (cit != class_idx.end())
2100 stype.service[cit->second - 1] = detail::dist_from_json<T>(it.value());
2101 }
2102 }
2103 net.add_server_type(idx, stype);
2104 }
2105 if (nd.contains("heteroSchedPolicy"))
2107 idx, detail::hetero_from_json(nd.at("heteroSchedPolicy").get<std::string>()));
2108 }
2109 // Job parallelism: servers seized at once by a job, per class. Read after
2110 // the pools, which size the station and so bound the admissible values.
2111 if (nd.contains("serverParallelism")) {
2112 const json& sp = nd.at("serverParallelism");
2113 for (auto it = sp.begin(); it != sp.end(); ++it) {
2114 auto cit = class_idx.find(it.key());
2115 if (cit != class_idx.end())
2116 net.set_server_parallelism(idx, cit->second, it.value().get<std::size_t>());
2117 }
2118 }
2119
2120 // -- The Source refinements: batch size and the MMAP mark binding ----
2121 if (nd.contains("arrivalBatch")) {
2122 const json& ab = nd.at("arrivalBatch");
2123 for (auto it = ab.begin(); it != ab.end(); ++it) {
2124 auto cit = class_idx.find(it.key());
2125 if (cit != class_idx.end())
2126 net.set_arrival_batch(idx, cit->second, detail::dist_from_json<T>(it.value()));
2127 }
2128 }
2129 if (nd.contains("markedClasses")) {
2130 std::vector<std::size_t> marks;
2131 for (const json& cn : nd.at("markedClasses")) {
2132 auto cit = class_idx.find(cn.get<std::string>());
2133 if (cit == class_idx.end())
2134 throw InputError("network_reader: node '" + nd.at("name").get<std::string>() +
2135 "' binds mark " + std::to_string(marks.size() + 1) +
2136 " to class '" + cn.get<std::string>() +
2137 "', which the model does not declare");
2138 marks.push_back(cit->second);
2139 }
2140 net.set_marked_classes(idx, marks);
2141 }
2142
2143 // -- The order-independent / pass-and-swap station -------------------
2144 //
2145 // mu(c) crosses as a macrostate TABLE, so `oiCutoffs` is not decoration:
2146 // it is the lattice the table was materialized over and the point beyond
2147 // which the rate saturates. An ABSENT `oiCutoffs` means the table is
2148 // NOT clamped: Python writes a non-saturating mu at a finite buffer as
2149 // the exact simplex 1 <= sum(n) <= cap with no cutoffs, and every other
2150 // reader then looks the raw counts up. Assuming a 10-per-class box here
2151 // served such a station mu(10) for every count above 10.
2152 if (nd.contains("oiServiceRate")) {
2153 std::vector<int> cut;
2154 if (nd.contains("oiCutoffs")) {
2155 const std::vector<double> cv = detail::num_vec_from_json<double>(nd.at("oiCutoffs"));
2156 for (std::size_t r = 0; r < classes.size() && r < cv.size(); ++r)
2157 cut.push_back(static_cast<int>(std::lround(cv[r])));
2158 }
2159 std::vector<std::vector<bool> > swap;
2160 if (nd.contains("swapGraph")) {
2161 const Matrix<T> sg = detail::mat_from_json<T>(nd.at("swapGraph"));
2162 swap.assign(sg.rows(), std::vector<bool>(sg.cols(), false));
2163 for (std::size_t a = 0; a < sg.rows(); ++a)
2164 for (std::size_t b = 0; b < sg.cols(); ++b)
2165 swap[a][b] = num_traits<T>::to_double(sg(a, b)) != 0.0;
2166 }
2167 net.set_pas(idx,
2168 detail::oi_rate_from_json<T>(nd.at("oiServiceRate"), cut, classes.size()),
2169 swap);
2170 }
2171
2172 // -- The declared state -----------------------------------------------
2173 if (nd.contains("initialState") && type == "Place") {
2174 net.set_initial_marking(idx, detail::num_vec_from_json<T>(nd.at("initialState")));
2175 } else if (nd.contains("initialState") && !nd.contains("stateSpace")) {
2176 // A stateful node that is not a Place carries its declared state in
2177 // the same key, and this struct spells a declared state as a
2178 // ONE-ROW state space under a prior of one (`sn_state.h`, the CTMC
2179 // transient's own reading). MATLAB, the JAR and Python all write
2180 // the key for every stateful node, so ignoring it outside a Place
2181 // dropped the initialization of every station: a model saved after
2182 // initFromMarginal came back here as the default one, with the
2183 // difference visible only in the numbers.
2184 const std::vector<T> row = detail::num_vec_from_json<T>(nd.at("initialState"));
2185 if (!row.empty()) {
2186 Matrix<T> space(1, row.size());
2187 for (std::size_t k = 0; k < row.size(); ++k) space(0, k) = row[k];
2188 net.set_state_prior(idx, space, std::vector<T>(1, num_traits<T>::from_double(1.0)));
2189 }
2190 }
2191 if (nd.contains("statePrior") || nd.contains("stateSpace")) {
2192 if (!nd.contains("statePrior") || !nd.contains("stateSpace"))
2193 throw InputError(
2194 "network_reader: node '" + nd.at("name").get<std::string>() +
2195 "' declares one of stateSpace / statePrior without the other; the prior is a "
2196 "distribution over the ROWS of that space and means nothing alone");
2197 net.set_state_prior(idx, detail::mat_from_json<T>(nd.at("stateSpace")),
2198 detail::num_vec_from_json<T>(nd.at("statePrior")));
2199 }
2200 if (nd.contains("departureDiscipline")) {
2201 const json& dd = nd.at("departureDiscipline");
2202 for (auto it = dd.begin(); it != dd.end(); ++it) {
2203 auto cit = class_idx.find(it.key());
2204 if (cit != class_idx.end())
2206 idx, cit->second, detail::departure_from_json(it.value().get<std::string>()));
2207 }
2208 }
2209
2210 // Rate scaling. `loadDependence` carries the lldscaling vector itself,
2211 // indexed from population one; `classDependence` and `jointDependence`
2212 // carry beta_r(n) and eta_i(n) materialized over the per-class box
2213 // lattice, because a function handle cannot cross JSON.
2214 for (int kind = 0; kind < 3; ++kind) {
2215 static const char* kKeys[] = {"loadDependence", "classDependence",
2216 "jointDependence"};
2217 static const char* kTypes[] = {"loadDependent", "classDependent",
2218 "jointDependent"};
2219 if (!nd.contains(kKeys[kind])) continue;
2220 if (type != "Queue" && type != "Delay")
2221 throw UnsupportedError(
2222 std::string("network_reader: node '") + nd.at("name").get<std::string>() +
2223 "' carries '" + kKeys[kind] +
2224 "', which scales a SERVICE rate and is only meaningful at a Queue or a "
2225 "Delay");
2226 const detail::json& blk = nd.at(kKeys[kind]);
2227 if (blk.value("type", std::string(kTypes[kind])) != kTypes[kind])
2228 throw UnsupportedError(std::string("network_reader: '") + kKeys[kind] +
2229 "' at node '" + nd.at("name").get<std::string>() +
2230 "' declares type '" +
2231 blk.value("type", std::string("?")) +
2232 "', which this reader does not implement");
2233 if (!blk.contains("scaling"))
2234 throw InputError(std::string("network_reader: '") + kKeys[kind] +
2235 "' at node '" + nd.at("name").get<std::string>() +
2236 "' carries no 'scaling'");
2237 if (kind == 0) {
2238 net.set_load_dependence(idx, detail::num_vec_from_json<T>(blk.at("scaling")));
2239 continue;
2240 }
2241 // An ABSENT `cutoffs` means NO clamping: the table is keyed by the
2242 // raw counts, as MATLAB, the JAR and Python read it. The writers omit
2243 // it when the station's buffer truncates the box (sum(n) <= cap), and
2244 // every reachable count is then tabulated. Clamping at one job per
2245 // class here served beta(1) for every larger count.
2246 std::vector<int> cut;
2247 if (blk.contains("cutoffs")) {
2248 const std::vector<double> cv = detail::num_vec_from_json<double>(blk.at("cutoffs"));
2249 for (std::size_t r = 0; r < classes.size() && r < cv.size(); ++r)
2250 cut.push_back(static_cast<int>(std::lround(cv[r])));
2251 }
2252 const lang::CdScaling<T> fun =
2253 detail::cd_scaling_from_json<T>(blk.at("scaling"), cut, classes.size());
2254 const std::vector<T> peak = detail::cd_peak_from_json<T>(blk, blk.at("scaling"));
2255 if (kind == 1) net.set_class_dependence(idx, fun, peak);
2256 else net.set_joint_dependence(idx, fun, peak);
2257 }
2258 // Immediate feedback, a per-class map on the node. The writers emit it
2259 // for a Queue only, and only for the classes it is set on, so an absent
2260 // entry means "not set" rather than false.
2261 if (nd.contains("immediateFeedback")) {
2262 const json& imf = nd.at("immediateFeedback");
2263 for (auto it = imf.begin(); it != imf.end(); ++it) {
2264 auto cit = class_idx.find(it.key());
2265 if (cit != class_idx.end() && it.value().get<bool>())
2266 net.set_immediate_feedback(idx, cit->second);
2267 }
2268 }
2269 }
2270
2271 // -- Pass 3: routing. ----------------------------------------------------
2272 const json& routing = model.at("routing");
2274 const json& mat = routing.at("matrix");
2275 for (auto ck = mat.begin(); ck != mat.end(); ++ck) {
2276 // Key "SrcClass,DstClass"; a bare "Class" means same-class routing.
2277 const std::string key = ck.key();
2278 const std::size_t comma = key.find(',');
2279 const std::string cs = comma == std::string::npos ? key : key.substr(0, comma);
2280 const std::string cd = comma == std::string::npos ? key : key.substr(comma + 1);
2281 const std::size_t r = class_idx.at(cs);
2282 const std::size_t s = class_idx.at(cd);
2283 for (auto si = ck.value().begin(); si != ck.value().end(); ++si) {
2284 const std::size_t from = node_idx.at(si.key());
2285 for (auto di = si.value().begin(); di != si.value().end(); ++di)
2286 P.set(r, s, from, node_idx.at(di.key()),
2287 num_traits<T>::from_double(di.value().get<double>()));
2288 }
2289 }
2290 net.link(P);
2291
2292 // -- Pass 3b: the non-probabilistic dispatchers. -------------------------
2293 //
2294 // `routing` above carries the MATRIX, which is what a PROB dispatcher is
2295 // fully described by. A RROBIN, JSQ or power-of-d dispatcher has the same
2296 // matrix and a different rule, so reading only the matrix turns every one
2297 // of them into probabilistic routing with no diagnostic. `link()` has
2298 // already run, so these overwrite the per-class strategy it defaulted to.
2299 if (model.contains("routingStrategies")) {
2300 const json& rs = model.at("routingStrategies");
2301 for (auto ni = rs.begin(); ni != rs.end(); ++ni) {
2302 auto nit = node_idx.find(ni.key());
2303 for (auto ci = ni.value().begin(); ci != ni.value().end(); ++ci) {
2304 const lang::RoutingStrategy rst =
2305 detail::routing_from_json(ci.value().get<std::string>());
2306 // DISABLED IS DERIVED, not declared: it marks a (node, class)
2307 // pair the class never visits, the refresh re-derives it, and
2308 // every reference loader skips it. Writers before BUG-94 also
2309 // emitted it for the AUTO-ADDED class-switch nodes they do not
2310 // put in `nodes`, so an entry naming an unknown node is only
2311 // tolerated for this one value -- anything else is still a
2312 // dangling reference and is named.
2313 if (nit == node_idx.end()) {
2314 if (rst == lang::RoutingStrategy::DISABLED) continue;
2315 throw InputError("network_reader: routingStrategies names node '" + ni.key() +
2316 "', which the model does not declare");
2317 }
2318 if (rst == lang::RoutingStrategy::DISABLED) continue;
2319 auto cit = class_idx.find(ci.key());
2320 if (cit == class_idx.end()) continue;
2321 net.set_routing(nit->second, cit->second, rst);
2322 }
2323 }
2324 }
2325 if (model.contains("routingWeights")) {
2326 const json& rw = model.at("routingWeights");
2327 for (auto ni = rw.begin(); ni != rw.end(); ++ni) {
2328 auto nit = node_idx.find(ni.key());
2329 if (nit == node_idx.end()) continue;
2330 for (auto ci = ni.value().begin(); ci != ni.value().end(); ++ci) {
2331 auto cit = class_idx.find(ci.key());
2332 if (cit == class_idx.end()) continue;
2333 std::map<std::size_t, double> w;
2334 for (auto di = ci.value().begin(); di != ci.value().end(); ++di) {
2335 auto dit = node_idx.find(di.key());
2336 if (dit != node_idx.end()) w[dit->second] = di.value().get<double>();
2337 }
2338 net.set_routing_weights(nit->second, cit->second, w);
2339 }
2340 }
2341 }
2342 if (model.contains("routingParams")) {
2343 const json& rp = model.at("routingParams");
2344 for (auto ni = rp.begin(); ni != rp.end(); ++ni) {
2345 auto nit = node_idx.find(ni.key());
2346 if (nit == node_idx.end()) continue;
2347 for (auto ci = ni.value().begin(); ci != ni.value().end(); ++ci) {
2348 auto cit = class_idx.find(ci.key());
2349 if (cit == class_idx.end() || !ci.value().contains("d")) continue;
2350 net.set_routing_param(nit->second, cit->second, ci.value().at("d").get<int>());
2351 }
2352 }
2353 }
2354
2355 // -- Pass 3b3: Krzesinski state-dependent routing. -----------------------
2356 //
2357 // Restored AFTER `link()` and after the dispatchers above: `link` writes the
2358 // uniform placeholder into the entry row, which the declaration supersedes,
2359 // and `routingStrategies` has already named that row SDR without saying what
2360 // the subnetwork looks like. Every centre travels by NODE NAME, so a node
2361 // reordering on the writing side cannot shift one, and branch index 1 is the
2362 // complement M-V and arrives as an empty list.
2363 // See _kb/16-state-dependent-routing.md
2364 if (model.contains("stateDepRouting")) {
2365 const json& sd = model.at("stateDepRouting");
2366 for (const char* k : {"entry", "departure", "class", "branches", "level", "C", "d"})
2367 if (!sd.contains(k))
2368 throw InputError(std::string("network_reader: 'stateDepRouting' carries no '") + k +
2369 "'");
2370 auto node_of = [&](const std::string& nm) -> std::size_t {
2371 const std::map<std::string, std::size_t>::const_iterator it = node_idx.find(nm);
2372 if (it == node_idx.end())
2373 throw InputError("network_reader: 'stateDepRouting' names node '" + nm +
2374 "', which the model does not declare");
2375 return it->second;
2376 };
2377 const std::map<std::string, std::size_t>::const_iterator ci =
2378 class_idx.find(sd.at("class").get<std::string>());
2379 if (ci == class_idx.end())
2380 throw InputError("network_reader: 'stateDepRouting' names class '" +
2381 sd.at("class").get<std::string>() +
2382 "', which the model does not declare");
2383 std::vector<std::vector<std::size_t>> branches;
2384 for (const json& b : sd.at("branches")) {
2385 std::vector<std::size_t> centres;
2386 for (const json& nm : b) centres.push_back(node_of(nm.get<std::string>()));
2387 branches.push_back(centres);
2388 }
2389 std::vector<std::size_t> level;
2390 for (const json& v : sd.at("level")) level.push_back(static_cast<std::size_t>(v.get<double>()));
2391 std::vector<double> C;
2392 for (const json& v : sd.at("C")) C.push_back(v.get<double>());
2393 const json& dj = sd.at("d");
2394 Matrix<double> d(dj.size(), dj.empty() ? 0 : dj.at(0).size(), 0.0);
2395 for (std::size_t t = 0; t < dj.size(); ++t) {
2396 if (dj.at(t).size() != d.cols())
2397 throw InputError("network_reader: 'stateDepRouting' carries a ragged coefficient "
2398 "matrix d");
2399 for (std::size_t b = 0; b < d.cols(); ++b) d(t, b) = dj.at(t).at(b).get<double>();
2400 }
2401 net.set_state_dep_routing(node_of(sd.at("entry").get<std::string>()),
2402 node_of(sd.at("departure").get<std::string>()), branches, level, C,
2403 d, ci->second);
2404 }
2405
2406 // -- Pass 3b2: the global (Whittle) dependence. --------------------------
2407 //
2408 // phi(n) is rebuilt from the materialized slot lattice written by
2409 // `network_to_json`. The slots carry station and class NAMES, resolved
2410 // through this model's own index spaces, so a reordering on the writing side
2411 // cannot silently shift a coordinate. The population is clamped to the
2412 // tabulated cutoffs, which is the same saturation the writer's box lattice
2413 // declares.
2414 if (model.contains("globalDependence")) {
2415 const json& blk = model.at("globalDependence");
2416 if (blk.value("type", std::string("globalDependent")) != "globalDependent")
2417 throw UnsupportedError(
2418 std::string("network_reader: 'globalDependence' declares type '") +
2419 blk.value("type", std::string("?")) + "', which this reader does not implement");
2420 if (!blk.contains("scaling"))
2421 throw InputError("network_reader: 'globalDependence' carries no 'scaling'");
2422 const qn::NetworkStruct<T>& sn0 = net.get_struct();
2423 const std::size_t M = sn0.nstations, K = sn0.nclasses;
2424 std::vector<std::size_t> slot_st, slot_cl;
2425 if (blk.contains("slots"))
2426 for (const json& sm : blk.at("slots")) {
2427 auto nit = node_idx.find(sm.at("station").get<std::string>());
2428 auto cit = class_idx.find(sm.at("class").get<std::string>());
2429 if (nit == node_idx.end() || cit == class_idx.end())
2430 throw InputError(
2431 "network_reader: 'globalDependence' names a station or class the model "
2432 "does not declare");
2433 slot_st.push_back(sn0.nodes[nit->second - 1].station - 1);
2434 slot_cl.push_back(cit->second - 1);
2435 }
2436 const std::size_t P = slot_st.size();
2437 std::vector<int> cuts(P, 0);
2438 if (blk.contains("cutoffs")) {
2439 const std::vector<double> cv = detail::num_vec_from_json<double>(blk.at("cutoffs"));
2440 for (std::size_t d = 0; d < P && d < cv.size(); ++d)
2441 cuts[d] = static_cast<int>(std::lround(cv[d]));
2442 }
2443 const int wcut = blk.value("cutoff", 10);
2444 std::map<std::string, std::vector<T>> tbl;
2445 for (auto it = blk.at("scaling").begin(); it != blk.at("scaling").end(); ++it)
2446 tbl[it.key()] = detail::num_vec_from_json<T>(it.value());
2447 std::vector<T> peak(M * K, num_traits<T>::from_int(1));
2448 if (blk.contains("peak")) {
2449 const std::vector<T> pv = detail::num_vec_from_json<T>(blk.at("peak"));
2450 for (std::size_t j = 0; j < peak.size() && j < pv.size(); ++j) peak[j] = pv[j];
2451 }
2452 const std::vector<T> ones(M * K, num_traits<T>::from_int(1));
2454 [slot_st, slot_cl, cuts, tbl, ones, K, P](const std::vector<T>& n) {
2455 std::string key;
2456 if (P == 0) key = "0";
2457 else
2458 for (std::size_t d = 0; d < P; ++d) {
2459 long x = std::lround(num_traits<T>::to_double(n[slot_st[d] * K + slot_cl[d]]));
2460 if (x < 0) x = 0;
2461 if (x > cuts[d]) x = cuts[d];
2462 if (d) key += ',';
2463 key += std::to_string(x);
2464 }
2465 typename std::map<std::string, std::vector<T>>::const_iterator it = tbl.find(key);
2466 return it == tbl.end() ? ones : it->second;
2467 },
2468 peak, wcut);
2469 }
2470
2471 // -- Pass 3c: the finite capacity regions. -------------------------------
2472 //
2473 // A region caps the jobs held ACROSS a set of stations, which no per-station
2474 // capacity expresses. Dropping the block is the failure this reader's key
2475 // gate was written for: an FCR model then solves as the UNBOUNDED one and
2476 // reports a confident, wrong queue length.
2477 if (model.contains("finiteCapacityRegions")) {
2478 for (const json& rj : model.at("finiteCapacityRegions")) {
2479 std::vector<std::size_t> members;
2480 // The per-station `classCap` refines the region's own cap at that
2481 // station; the struct carries ONE cap per (station, class), so the
2482 // tightest of the two is what the region actually enforces.
2483 std::vector<double> cap(classes.size(), -1.0);
2484 auto read_class_map = [&](const json& blk, std::vector<double>& dst) {
2485 for (auto it = blk.begin(); it != blk.end(); ++it) {
2486 auto cit = class_idx.find(it.key());
2487 if (cit == class_idx.end()) continue;
2488 const double v = it.value().get<double>();
2489 double& slot = dst[cit->second - 1];
2490 slot = slot == -1.0 ? v : std::min(slot, v);
2491 }
2492 };
2493 if (rj.contains("classMaxJobs")) read_class_map(rj.at("classMaxJobs"), cap);
2494 for (const json& sj : rj.at("stations")) {
2495 auto nit = node_idx.find(sj.at("node").get<std::string>());
2496 if (nit == node_idx.end())
2497 throw InputError("network_reader: finite capacity region '" +
2498 rj.value("name", std::string("?")) + "' names node '" +
2499 sj.at("node").get<std::string>() +
2500 "', which the model does not declare");
2501 members.push_back(nit->second);
2502 if (sj.contains("classCap")) read_class_map(sj.at("classCap"), cap);
2503 }
2504 std::vector<double> mem(classes.size(), -1.0);
2505 if (rj.contains("classMaxMemory")) read_class_map(rj.at("classMaxMemory"), mem);
2506 std::vector<T> size(classes.size(), num_traits<T>::from_int(1));
2507 std::vector<T> weight(classes.size(), num_traits<T>::from_int(1));
2508 for (const json& sj : rj.at("stations")) {
2509 if (sj.contains("classSize"))
2510 for (auto it = sj.at("classSize").begin(); it != sj.at("classSize").end(); ++it) {
2511 auto cit = class_idx.find(it.key());
2512 if (cit != class_idx.end())
2513 size[cit->second - 1] =
2514 num_traits<T>::from_double(it.value().get<double>());
2515 }
2516 if (sj.contains("classWeight"))
2517 for (auto it = sj.at("classWeight").begin(); it != sj.at("classWeight").end();
2518 ++it) {
2519 auto cit = class_idx.find(it.key());
2520 if (cit != class_idx.end())
2521 weight[cit->second - 1] =
2522 num_traits<T>::from_double(it.value().get<double>());
2523 }
2524 }
2525 std::vector<lang::DropStrategy> rule(classes.size(), lang::DropStrategy::WAITQ);
2526 if (rj.contains("dropRule"))
2527 for (auto it = rj.at("dropRule").begin(); it != rj.at("dropRule").end(); ++it) {
2528 auto cit = class_idx.find(it.key());
2529 if (cit != class_idx.end())
2530 rule[cit->second - 1] = detail::drop_from_json(it.value().get<std::string>());
2531 }
2532 const std::size_t reg =
2533 net.add_region(members, cap, rj.value("globalMaxJobs", -1.0), rule, mem, size,
2534 rj.value("globalMaxMemory", -1.0),
2535 rj.value("name", std::string()));
2536 net.set_region_weights(reg, weight);
2537 if (rj.contains("constraintA") && rj.contains("constraintB"))
2538 net.set_region_constraint(reg, detail::mat_from_json<T>(rj.at("constraintA")),
2539 detail::num_vec_from_json<T>(rj.at("constraintB")));
2540 }
2541 }
2542
2543 // -- Pass 4: the declared rewards. ---------------------------------------
2544 //
2545 // THE DECLARATIVE FORM ONLY, `{name, type, node, class?}`, which is all the
2546 // writers emit: a reward defined from a bare lambda has no reproducible form
2547 // and `linemodel_save` warns and omits it rather than writing something that
2548 // would be wrong on reload. Each template is rebuilt here as a function of
2549 // the AGGREGATE state row -- the per-(station, class) counts in
2550 // `(ist-1)*K + k` order that `set_reward` is defined over -- so the value is
2551 // computed from the chain's own states and not from a mean.
2552 if (model.contains("rewards")) {
2553 const std::size_t K = classes.size();
2554 for (const json& rw : model.at("rewards")) {
2555 const std::string nm = rw.at("name").get<std::string>();
2556 const std::string kind = rw.at("type").get<std::string>();
2557 const std::string node_name = rw.at("node").get<std::string>();
2558 auto nit = node_idx.find(node_name);
2559 if (nit == node_idx.end())
2560 throw InputError("network_reader: reward '" + nm + "' names node '" + node_name +
2561 "', which the model does not declare");
2562 // The struct carries station -> node and no inverse, so the station
2563 // is found by scanning it; a node with no station is refused below.
2564 std::size_t ist = 0;
2565 for (std::size_t s = 0; s < net.get_struct().station_to_node.size(); ++s)
2566 if (net.get_struct().station_to_node[s] == nit->second) ist = s + 1;
2567 if (ist == 0)
2568 throw UnsupportedError("network_reader: reward '" + nm + "' is declared at node '" +
2569 node_name +
2570 "', which is not a station and so has no job count");
2571 // A reward with no class covers EVERY class at the station; with one,
2572 // exactly that class. The two are different quantities, so the
2573 // absence of the key is carried through rather than defaulted.
2574 std::size_t cls = 0; // 0 = all classes
2575 if (rw.contains("class")) {
2576 auto cit = class_idx.find(rw.at("class").get<std::string>());
2577 if (cit == class_idx.end())
2578 throw InputError("network_reader: reward '" + nm + "' names class '" +
2579 rw.at("class").get<std::string>() +
2580 "', which the model does not declare");
2581 cls = cit->second;
2582 }
2583 const double nservers = net.get_struct().stations[ist - 1].nservers;
2584 const double cap = net.get_struct().stations[ist - 1].cap;
2585 const std::size_t base = (ist - 1) * K;
2586 // The DECLARATIVE descriptor travels with the lambda so the writer
2587 // can emit the reward back out; a reward built from a bare function
2588 // has none and is omitted at save time, as the reference does.
2589 if (kind == "QLen") {
2590 net.set_reward(nm, [base, K, cls](const std::vector<T>& n) {
2591 if (cls) return n[base + cls - 1];
2592 T s = num_traits<T>::from_int(0);
2593 for (std::size_t k = 0; k < K; ++k) s = T(s + n[base + k]);
2594 return s;
2595 }, kind, nit->second, cls);
2596 } else if (kind == "Util") {
2597 // min(jobs, nservers), the reference's own definition: the number
2598 // of BUSY servers, not a fraction.
2599 net.set_reward(nm, [base, K, cls, nservers](const std::vector<T>& n) {
2600 T s = num_traits<T>::from_int(0);
2601 if (cls) s = n[base + cls - 1];
2602 else
2603 for (std::size_t k = 0; k < K; ++k) s = T(s + n[base + k]);
2604 const T c = num_traits<T>::from_double(nservers);
2605 return num_traits<T>::to_double(s) > nservers ? c : s;
2606 }, kind, nit->second, cls);
2607 } else if (kind == "Blocking") {
2608 net.set_reward(nm, [base, K, cap](const std::vector<T>& n) {
2609 T s = num_traits<T>::from_int(0);
2610 for (std::size_t k = 0; k < K; ++k) s = T(s + n[base + k]);
2613 }, kind, nit->second, cls);
2614 } else {
2615 throw UnsupportedError(
2616 "network_reader: reward '" + nm + "' is of type '" + kind +
2617 "', and the reproducible templates are QLen, Util and Blocking; a Custom "
2618 "reward wraps an arbitrary function and is not on the wire at all");
2619 }
2620 }
2621 }
2622 return net;
2623}
2624
2625/** Parse a model.json file into a `qn::Network<T>`. */
2626template <class T>
2627qn::Network<T> read_network_json(const std::string& path) {
2628 std::ifstream in(path.c_str());
2629 if (!in) throw InputError("network_reader: cannot open " + path);
2630 detail::json root;
2631 try {
2632 in >> root;
2633 } catch (const detail::json::parse_error& e) {
2634 throw InputError("network_reader: malformed JSON in " + path + ": " + e.what());
2635 }
2636 return build_network_from_json<T>(root);
2637}
2638
2639} // namespace io
2640} // namespace line
2641
2642#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