LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
network_struct.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_LANG_QN_NETWORK_STRUCT_H
6#define LINE_LANG_QN_NETWORK_STRUCT_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * A queueing network and its refreshed NetworkStruct.
12 *
13 * SCOPE. This is the `sn` of `matlab/src/lang/@@MNetwork/refreshStruct.m`, held
14 * together with the model it was refreshed from, because every consumer of a
15 * struct in this port also mutates the model and re-derives it (SolverLN
16 * re-parameterises service processes between outer iterations, the fork-join
17 * transform rewrites routing). MATLAB does the same through the `sn` its
18 * Network caches.
19 *
20 * Fields are filled in as the solvers that read them are ported; a model
21 * needing an unported field is refused where the field would be read.
22 *
23 * NODES vs STATIONS vs STATEFUL NODES. There are three nested index spaces,
24 * exactly as `sn` has, and they stopped coinciding the moment fork/join
25 * arrived:
26 *
27 * nodes everything routing passes through: the stations, plus a Fork, its
28 * output Routers, a Join, a Sink
29 * stateful the nodes that hold jobs: every node except the Fork
30 * stations the nodes that serve jobs, including a Join (which serves at rate
31 * Inf) and a Source (whose service process is the arrival process)
32 *
33 * Routing (`P`, and the `rtnodes` derived from it) lives at NODE level; `rt` is
34 * its stochastic complement over the stateful nodes, which is what eliminates
35 * the Fork; `visits` is indexed by stateful node and `nodevisits` by node.
36 *
37 * ClassSwitch NODES ARE MATERIALISED, as `@MNetwork/link.m:225-329` does it:
38 * `link()` inserts one `CS_<i>_to_<j>` per ordered pair of linked nodes whose
39 * routing switches class, folds the switching probability into the first leg
40 * and leaves every surviving route SAME-CLASS. The stochastic complement in
41 * `rt` removes them again, since they are not stateful, so `rt` is unchanged by
42 * their presence; `rtnodes` and `nnodes` carry the extra hop, which is what the
43 * node table reports and what the JSIM export needs (`jmt_writer.h` can only
44 * emit a ClassSwitch for a node whose type IS ClassSwitch). Keeping the switch
45 * on the edge instead makes JMT silently simulate the UNSWITCHED model.
46 *
47 * ARITHMETIC. Rates, service times, routing probabilities and visits are T.
48 * Populations and server counts are `double`, since they are counts that may
49 * be infinite (a delay station, an open class).
50 */
51
53#include <algorithm>
54#include <cmath>
55#include <complex>
56#include <functional>
57#include <limits>
58#include <map>
59#include <string>
60#include <vector>
61
71#include "line/num/number.h"
72#include "line/util/error.h"
73#include "line/util/matrix.h"
74
75namespace line {
76namespace qn {
77
78using lang::CdScaling;
79using lang::GdScaling;
80using lang::Distrib;
82using lang::GlobalConstants;
85using lang::NodeType;
90
91/** One fork firing synchronization: `sn.fjsync{k}`. */
92template <class T>
93struct FjSync {
94 std::size_t fork = 0; ///< 1-based Fork node
95 std::size_t join = 0; ///< 1-based Join node that closes it
96 std::size_t cls = 0; ///< 1-based ORIGINAL class being forked
97 std::size_t tag = 0; ///< 1-based tag this entry allocates
98 std::vector<std::size_t> branchheads; ///< 1-based node per branch
99 std::vector<std::size_t> auxclasses; ///< the tag's auxiliary class per branch
100 /** (B x T) every auxiliary class of this (fork, class), for the tag scan. */
101 std::vector<std::vector<std::size_t>> auxall;
102 std::size_t weight = 1; ///< tasksPerLink: siblings emitted per branch
103 /**
104 * Per-branch tasksPerLink, EMPTY when every branch carries `weight`.
105 *
106 * A fork may send a different number of tasks down each link
107 * (`Fork.setTasksPerLink(class, n, dest)`), and the count is what sizes the
108 * auxiliary class capacity and the join's required count, so it cannot be
109 * collapsed to the node-wide mean. It stays empty in the uniform case so
110 * that a plain fork carries exactly the shape it always did; the two paths
111 * emit the same interleaved order there, so this is a shape convention and
112 * not a behavioural fork.
113 */
114 std::vector<std::size_t> weightlink;
116};
117
118/**
119 * `sn.nodeparam{j}.fj`: what a Join node needs to fire on identity.
120 *
121 * `auxmatrix[r]` is the (B x T) matrix of auxiliary sibling classes minted for
122 * original class r -- row b is branch b, column t is tag t -- and `required[r][b]`
123 * is how many siblings of branch b a firing consumes (`tasksPerLink` under the
124 * only join strategy the exact implementation accepts). It lives here rather
125 * than in `fj_tag.h` because `NetworkStruct` carries it and the event layer reads
126 * it; `fj_tag.h` is what FILLS it.
127 */
129 std::size_t fork = 0;
130 std::vector<std::size_t> origclasses;
131 std::map<std::size_t, std::vector<std::vector<std::size_t>>> auxmatrix;
132 std::map<std::size_t, std::vector<std::size_t>> required;
133};
134
135/**
136 * Variable forking levels, the twin of MATLAB `sn.nodeparam{f}.fanOutLink` /
137 * `.fanOutProb` / `.fanOutDist`.
138 *
139 * All three are (nnodes x nclasses) and indexed by DESTINATION NODE rather than
140 * by link ordinal, because the link order is an artefact of `connmatrix`
141 * traversal and renumbers whenever the model is relinked.
142 *
143 * `fan_out_link(k,r)`: expected tasks sent to destination k for class r, zero
144 * on a link class r does not take. `fan_out_prob(k,r)`: probability the branch
145 * fires at all, so the SIBLING COUNT is random even when each link carries a
146 * fixed number. `fan_out_dist[k][r]`: the jobs-per-link distribution, with an
147 * unset (DISABLED) entry meaning degenerate at `fan_out_link(k,r)`.
148 *
149 * This is a per-node BLOCK keyed by node index -- the shape `joindecl` already
150 * uses, and the shape MATLAB's `nodeparam{f}` has -- rather than three fields on
151 * `NodeDef`, because `NodeDef` is not templated on the arithmetic and a
152 * `Matrix<T>` cannot live there. A fork with no override has no entry at all,
153 * which is how a consumer tells the classic case from the variable one without
154 * inspecting a single number.
155 */
156template <class T>
157struct ForkParam {
160 std::vector<std::vector<lang::Distrib<T> > > fan_out_dist;
161};
162
163/**
164 * A node of the network.
165 *
166 * `station` is the 1-based station index when the node serves jobs and 0 when
167 * it does not (a Fork, a Router, a ClassSwitch, a Sink). `stateful` is false
168 * for the nodes that hold no jobs and are eliminated by the stochastic
169 * complement that builds rt.
170 */
171struct NodeDef {
172 std::string name;
173 NodeType nodetype = NodeType::Queue;
174 /**
175 * Declared as a Queue although its INF discipline makes `nodetype` Delay (`Network::add_queue`). MATLAB keeps
176 * the node's CLASS apart from sn.nodetype, and the TikZ exporter and linemodel_save read the class: such a node
177 * is drawn as a buffer with an infinite server and saved with type "Queue". Not part of the reference sn.
178 */
179 bool queue_object = false;
180 bool stateful = true;
181 std::size_t station = 0;
182 /**
183 * `sn.routing`, per class. PROB means the routing block P carries the
184 * probabilities as given; RAND and RROBIN mean the refresh spreads them
185 * uniformly over the nodes this one is connected to, which is the routing
186 * MATRIX of a round-robin dispatcher (its determinism lives in the higher
187 * moments, see refresh_routing). Anything else is state dependent, and the
188 * refresh refuses it by name: a state-dependent strategy silently treated
189 * as PROB returns a product-form answer for a model that has none.
190 */
191 std::vector<RoutingStrategy> routing;
192 /**
193 * The per-destination weights of a WRROBIN dispatcher, per class: a map
194 * from 1-based destination NODE index to its weight.
195 *
196 * MATLAB and the JAR keep these on the node object and the native Python
197 * struct calls the field `sn.routingweights`; this port carries them here
198 * so a WRROBIN model round-trips through model.json without loss. The
199 * refresh still refuses WRROBIN by name, because spreading the weights into
200 * a routing MATRIX would answer a state-independent model instead.
201 */
202 std::vector<std::map<std::size_t, double>> routing_weights;
203 /**
204 * The scalar parameter of a parameterized dispatcher, per class: the d of
205 * a power-of-d (SQ) choice. Zero means the strategy takes none. Carried for
206 * the same reason as `routing_weights`.
207 */
208 std::vector<int> routing_param;
209 /**
210 * `Fork.output.tasksPerLink` == MATLAB `sn.nodeparam{f}.fanOut`: how many
211 * tasks a fork emits per outgoing link. It scales both the MMT auxiliary
212 * arrival rate and the synchronisation delay; the default 1 leaves a plain
213 * fork unchanged.
214 *
215 * For a fork whose degree is RANDOM this is the MEAN, so a consumer that
216 * only knows this field gets E[tasks per link] rather than a number the
217 * fork never emits.
218 */
219 double tasks_per_link = 1.0;
220
221 /**
222 * A Logger node's trace configuration, MATLAB's `Logger` properties and
223 * `sn.nodeparam{ind}` for a Logger.
224 *
225 * The defaults are `Logger.m`'s constructor: timestamp, job id and job
226 * class are recorded, the wall-clock start time, the logger name and the
227 * two inter-departure columns are not. They are carried on the node rather
228 * than derived because a Logger with no file name exports to JMT as a
229 * LogTunnel that writes nowhere -- the solver runs, and the trace the user
230 * asked for is silently absent.
231 */
232 struct LoggerParam {
233 std::string file_name; ///< base name, no directory
234 std::string file_path; ///< directory, MATLAB's model.getLogPath
235 bool start_time = false;
236 bool logger_name = false;
237 bool timestamp = true;
238 bool job_id = true;
239 bool job_class = true;
240 bool time_same_class = false;
241 bool time_any_class = false;
242 };
244};
245
246/**
247 * The parameters of a Transition node: MATLAB `sn.nodeparam{ind}` for an SPN
248 * transition, as `refreshPetriNetNodes.m` writes them.
249 *
250 * A transition has MODES, not classes, but its ARCS carry a class: MATLAB's
251 * `enablingConditions{m}` is an (nnodes x nclasses) matrix, so one mode can
252 * require two tokens of Class1 at a place while another requires one of Class2
253 * at the same place. Each mode carries its own enabling and inhibiting
254 * conditions over the input places, its firing effect, and its own firing
255 * process -- which is why State.fromMarginal treats a Transition's state as
256 * per-mode and cannot reuse the per-class station encoding.
257 *
258 * THE CLASS DIMENSION IS NOT DECORATION. Collapsing the arcs onto the place lets
259 * a token of one class satisfy another class's pre-arc -- a DIFFERENT net, not
260 * an approximation of this one. Every consumer therefore reads a (node, class)
261 * pair, and the few that are genuinely class-blind (the MDD level aggregation,
262 * the S-invariants) say so by refusing a multiclass net rather than by summing
263 * it.
264 */
265template <class T>
267 std::size_t nmodes = 0;
268 std::vector<std::string> modenames;
269 /** enabling[m](p,r): class-r tokens of place p (0-based node) mode m needs. */
270 std::vector<Matrix<T>> enabling;
271 /** inhibiting[m](p,r): class-r tokens of p that BLOCK mode m (Inf = never). */
272 std::vector<Matrix<T>> inhibiting;
273 /** firing[m](p,r): class-r tokens mode m moves to/from place p when it fires. */
274 std::vector<Matrix<T>> firing;
275
276 /**
277 * The arcs of one mode summed over classes, for a consumer that is class
278 * blind BECAUSE THE NET IS SINGLE CLASS.
279 *
280 * `is_multiclass()` is what makes that safe, and a caller that cannot honour
281 * the class dimension must test it and refuse: the sum of a multiclass net's
282 * arcs describes a net whose tokens are interchangeable, which is not this
283 * one.
284 *
285 * A NON-FINITE ENTRY IS SKIPPED, and a row of nothing but non-finite entries
286 * stays non-finite: on an enabling arc Inf is JMT's "any number of tokens"
287 * and on an inhibiting one it is the ABSENCE of the arc, so adding it in
288 * would turn either into a row that no marking satisfies.
289 */
290 static std::vector<T> arc_total(const std::vector<Matrix<T>>& a, std::size_t m) {
291 std::vector<T> v;
292 if (m >= a.size()) return v;
293 v.assign(a[m].rows(), num_traits<T>::from_int(0));
294 for (std::size_t p = 0; p < a[m].rows(); ++p) {
295 double s = 0.0;
296 bool any = false;
297 for (std::size_t r = 0; r < a[m].cols(); ++r) {
298 const double x = num_traits<T>::to_double(a[m](p, r));
299 if (!std::isfinite(x)) continue;
300 s += x;
301 any = true;
302 }
303 v[p] = any || a[m].cols() == 0
305 : num_traits<T>::from_double(std::numeric_limits<double>::infinity());
306 }
307 return v;
308 }
309
310 /**
311 * The inhibiting THRESHOLD of one mode per place, class blind.
312 *
313 * A threshold is not a count and does not add up: the mode is blocked as
314 * soon as ANY class reaches its own bound, so the class-blind reduction is
315 * the smallest finite threshold, and Inf where no class declares one.
316 */
317 static std::vector<T> inhibit_total(const std::vector<Matrix<T>>& a, std::size_t m) {
318 std::vector<T> v;
319 if (m >= a.size()) return v;
320 const double inf = std::numeric_limits<double>::infinity();
321 v.assign(a[m].rows(), num_traits<T>::from_double(inf));
322 for (std::size_t p = 0; p < a[m].rows(); ++p) {
323 double best = inf;
324 for (std::size_t r = 0; r < a[m].cols(); ++r) {
325 const double x = num_traits<T>::to_double(a[m](p, r));
326 if (std::isfinite(x) && x < best) best = x;
327 }
328 v[p] = num_traits<T>::from_double(best);
329 }
330 return v;
331 }
332
333 /** True when any mode's arcs touch more than one class. */
334 bool is_multiclass() const {
335 const std::vector<Matrix<T>>* all[3] = {&enabling, &inhibiting, &firing};
336 for (int w = 0; w < 3; ++w)
337 for (std::size_t m = 0; m < all[w]->size(); ++m) {
338 std::size_t touched = 0;
339 for (std::size_t r = 0; r < (*all[w])[m].cols(); ++r) {
340 bool any = false;
341 for (std::size_t p = 0; p < (*all[w])[m].rows() && !any; ++p) {
342 const double v = num_traits<T>::to_double((*all[w])[m](p, r));
343 // An inhibiting Inf is the ABSENCE of an arc, not one.
344 any = w == 1 ? (std::isfinite(v) && v > 0.0) : v > 0.0;
345 }
346 if (any) ++touched;
347 }
348 if (touched > 1) return true;
349 }
350 return false;
351 }
352
353 /** The one class every arc of every mode touches, 1-based; 0 when none does. */
354 std::size_t single_class() const {
355 const std::vector<Matrix<T>>* all[3] = {&enabling, &inhibiting, &firing};
356 for (int w = 0; w < 3; ++w)
357 for (std::size_t m = 0; m < all[w]->size(); ++m)
358 for (std::size_t r = 0; r < (*all[w])[m].cols(); ++r)
359 for (std::size_t p = 0; p < (*all[w])[m].rows(); ++p) {
360 const double v = num_traits<T>::to_double((*all[w])[m](p, r));
361 if (w == 1 ? (std::isfinite(v) && v > 0.0) : v > 0.0) return r + 1;
362 }
363 return 0;
364 }
365 std::vector<double> nmodeservers; ///< servers per mode, may be infinite
366 std::vector<double> firingprio; ///< firing priority per mode
367 std::vector<T> fireweight; ///< weight among simultaneously enabled modes
368 std::vector<lang::TimingStrategy> timing; ///< immediate or timed
369 std::vector<lang::Distrib<T>> firingproc; ///< firing distribution per mode
370 std::vector<std::size_t> firingphases; ///< phase count per mode, 0 when non-Markovian
371 /**
372 * Marking-dependent firing-rate multiplier g_m(marking); an empty entry is
373 * the unit multiplier. Transition.setFiringRateDependence. The argument is the
374 * (nnodes x nclasses) marking flattened node-major, entry (i-1)*K + (r-1) for
375 * 1-based node i and class r, as MATLAB's g(M); single-class, it is node-indexed.
376 */
377 std::vector<std::function<T(const std::vector<T>&)>> firingdep;
378
379 /** One fluid arc of a mode: `node` is the 1-based fluid place, `value` a rate or an impulse. */
380 struct FluidArc {
381 std::size_t node = 0;
382 double value = 0.0;
383 };
384 /** A fluid guard: the mode is enabled only while level(node) >= thr (sgn = +1) or <= thr (sgn = -1). */
385 struct FluidGuard {
386 std::size_t node = 0;
387 int sgn = 1;
388 double thr = 0.0;
389 };
390 /** A fluid rate dependence: the firing rate is multiplied by fun(level(node)). */
391 struct FluidDep {
392 std::size_t node = 0;
393 std::function<double(double)> fun;
394 };
395 /**
396 * A marking-dependent fluid flow (`setFluidFlowDependence`): the flow of the mode on fluid place `node` is
397 * multiplied by fun(M). M is the (nnodes x nclasses) marking flattened row-major, entry (i-1)*nclasses + (r-1)
398 * for 1-based node i and class r, holding the token places `read` and 0 in every other row. `read` names the
399 * mode's input places unless `read_given`; the builder resolves it at `get_struct()`, as refreshPetriNetNodes.
400 */
402 std::size_t node = 0;
403 std::function<double(const std::vector<double>&)> fun;
404 std::vector<std::size_t> read;
405 bool read_given = false;
406 };
407 /**
408 * FSPN arcs per mode, MATLAB `sn.transparam{ind}.fluidrate/fluidguard/fluidimpulse/fluiddep`. A reader that
409 * builds the per-mode vectors itself (pnml, jsim) may leave these shorter than `nmodes`, so read them through
410 * the accessors below, which answer an empty list for a mode that declares nothing.
411 */
412 std::vector<std::vector<FluidArc>> fluidrate;
413 std::vector<std::vector<FluidGuard>> fluidguard;
414 std::vector<std::vector<FluidArc>> fluidimpulse;
415 std::vector<std::vector<FluidDep>> fluiddep;
416 std::vector<std::vector<FluidFlowDep>> fluidflowdep;
417
418 const std::vector<FluidArc>& fluid_rates(std::size_t m) const {
419 static const std::vector<FluidArc> none;
420 return m < fluidrate.size() ? fluidrate[m] : none;
421 }
422 const std::vector<FluidGuard>& fluid_guards(std::size_t m) const {
423 static const std::vector<FluidGuard> none;
424 return m < fluidguard.size() ? fluidguard[m] : none;
425 }
426 const std::vector<FluidArc>& fluid_impulses(std::size_t m) const {
427 static const std::vector<FluidArc> none;
428 return m < fluidimpulse.size() ? fluidimpulse[m] : none;
429 }
430 const std::vector<FluidDep>& fluid_deps(std::size_t m) const {
431 static const std::vector<FluidDep> none;
432 return m < fluiddep.size() ? fluiddep[m] : none;
433 }
434 const std::vector<FluidFlowDep>& fluid_flow_deps(std::size_t m) const {
435 static const std::vector<FluidFlowDep> none;
436 return m < fluidflowdep.size() ? fluidflowdep[m] : none;
437 }
438 /** `Transition.hasFluidArcs`: any mode declares a fluid rate, guard, impulse, rate or flow dependence. */
439 bool has_fluid_arcs() const {
440 for (std::size_t m = 0; m < nmodes; ++m)
441 if (!fluid_rates(m).empty() || !fluid_guards(m).empty() || !fluid_impulses(m).empty() ||
442 !fluid_deps(m).empty() || !fluid_flow_deps(m).empty())
443 return true;
444 return false;
445 }
446};
447
448/**
449 * The parameters of a retrial station: MATLAB `sn.retrialProc` and friends.
450 *
451 * A retrial station has NO waiting line. An arrival that finds every server
452 * busy joins an ORBIT and re-attempts at the retrial rate, so its state is an
453 * (in-service, orbit) split rather than an ordered buffer -- State.fromMarginal
454 * enumerates that split, and the freed server is NOT filled on a completion.
455 */
456template <class T>
458 /** retrial_proc[r] is the class-r retrial process; empty = not a retrial class. */
459 std::vector<lang::Distrib<T>> retrial_proc;
460 std::vector<T> retrial_rate; ///< mu_r, the per-class orbit retry rate
461 std::vector<int> max_attempts; ///< 0 = unbounded
462 /** MATLAB `sn.retrialPolicy`: 1 = LINEAR (per-job clocks), 2 = CONSTANT (one clock per orbit). */
463 std::vector<int> policy;
464 /** MATLAB `sn.orbitMaxJobs`: -1 = unbounded. The capacity already bounds service plus orbit. */
465 std::vector<int> max_orbit;
466};
467
468/**
469 * Setup and delay-off of a station that powers down when it falls idle.
470 *
471 * MATLAB `Queue.setDelayOff(class, setupTime, delayoffTime)`, which
472 * refreshLocalVars copies into `sn.nodeparam{node}{class}`. The server starts
473 * a delay-off timer when it empties and shuts down when the timer expires; the
474 * next arrival to a shut-down server pays the setup time before service.
475 *
476 * Stored per class because the reference stores it per class, but every
477 * consumer reads ONE pair per station -- `solver_mam_basic.m` takes the LAST
478 * class's, so `last()` below is what a solver should call rather than picking
479 * a class itself.
480 */
481template <class T>
483 std::vector<lang::Distrib<T>> setup; ///< per class, disabled = not declared
484 std::vector<lang::Distrib<T>> delayoff; ///< per class
485 /** The pair the solvers use: the last class that declares one. */
486 bool last(lang::Distrib<T>& su, lang::Distrib<T>& doff) const {
487 for (std::size_t r = setup.size(); r > 0; --r)
488 if (!setup[r - 1].disabled) {
489 su = setup[r - 1];
490 doff = r - 1 < delayoff.size() ? delayoff[r - 1] : lang::Distrib<T>::disabled_dist();
491 return true;
492 }
493 return false;
494 }
495};
496
497/**
498 * Server breakdown and repair of a station whose server fails and is repaired.
499 *
500 * MATLAB `Queue.setBreakdown(failure, repair, downService)`, which
501 * `refreshStruct` spreads over `sn.hasbreakdown` (per NODE),
502 * `sn.breakdownMu` / `sn.repairMu` / `sn.breakdownProc` / `sn.repairProc` (per
503 * STATION) and `sn.downServiceRates` (per station and class). They are one
504 * feature and are kept as one record here, keyed by station, for the same
505 * reason `SetupDelayOffParam` is: presence IS the flag.
506 *
507 * WHAT A BREAKDOWN IS, and what it is not. The server alternates up and down on
508 * independent clocks. A job IN SERVICE when the server goes down is NOT evicted
509 * and does not restart: it holds its residual work across the outage, so the
510 * outage is a pure interruption. `down_service_rates(r)` is a DEGRADED server,
511 * not a stopped one -- a positive rate there means class r keeps being served
512 * while the server is down, more slowly. Zero means no service at all, which is
513 * the ordinary reading of "broken".
514 *
515 * THE DEGRADED SERVICE MUST BE EXPONENTIAL, and the reference refuses anything
516 * else by name: a phase-type degraded service would need its own phase block in
517 * the joint chain, which no codebase builds. Only the RATE is stored for that
518 * reason -- there is no distribution left to carry.
519 */
520template <class T>
522 lang::Distrib<T> failure; ///< time to failure of an up server
523 lang::Distrib<T> repair; ///< time to repair of a down server
524 T failure_rate = num_traits<T>::from_int(0); ///< `sn.breakdownMu`: 1 / mean failure time
525 T repair_rate = num_traits<T>::from_int(0); ///< `sn.repairMu`: 1 / mean repair time
526 /** `sn.downServiceRates(ist, :)`: per class, 0 = no service while down. */
527 std::vector<T> down_service_rates;
528
529 T down_rate_of(std::size_t cls_1based) const {
530 if (cls_1based == 0 || cls_1based > down_service_rates.size())
531 return num_traits<T>::from_int(0);
532 return down_service_rates[cls_1based - 1];
533 }
534};
535
536/**
537 * The parameters of a Cache node, MATLAB's `sn.nodeparam{ind}` for a Cache.
538 *
539 * `itemcap` is the capacity of each of the `h` cache lists (a single-level
540 * cache has one entry). `nitems` is the item population `n`.
541 *
542 * `pread[v]` is the read distribution of class v over the n items; an EMPTY row
543 * is MATLAB's `NaN` placeholder, "class v does not read this cache", and leaves
544 * that class's rates at zero. `accost[v][k]` is the ((h+1) x (h+1)) list-to-list
545 * routing matrix of class v on item k; an EMPTY `accost` selects the reference
546 * default, the linear cache in which an item moves from list l to l+1 on a hit.
547 * These two match `da::CacheParam` exactly, so `da_cache_isolate` consumes them
548 * without a conversion.
549 *
550 * `hitclass` and `missclass` are the 1-based classes a job of class r switches
551 * into on a hit and on a miss, 0 meaning the transition is disabled.
552 */
553template <class T>
555 std::size_t nitems = 0;
556 std::vector<int> itemcap;
557 /**
558 * Item read by each per-item class of a cache network (MATLAB
559 * `Cache.setItemReadClasses`, `sn.nodeparam{i}.classitem`), 1-based, 0 where
560 * the class is not one. Stored rather than inferred from a one-hot pread,
561 * which is ambiguous against a genuine single-item popularity.
562 */
563 std::vector<std::size_t> classitem;
564 /**
565 * Per-item storage cost (size) and per-list cap on the total cost of the
566 * resident items (ton21cache Sec. IX). BOTH EMPTY = unconstrained, the
567 * classic model. `costcapglobal` records that the caps came from a single
568 * cache-wide value.
569 */
570 std::vector<int> itemsize;
571 std::vector<int> costcap;
572 bool costcapglobal = false;
573 std::vector<std::vector<T>> pread; ///< (u) x (n), empty row = NaN
574 /**
575 * The popularity LAW each class declared, beside the pmf it expands to.
576 *
577 * Every solver reads `pread`, which is why the pmf is what the reader
578 * materializes. The law is kept because JMT's Cache section takes a
579 * PARAMETRIC popularity -- a Zipf exponent or a uniform range -- and
580 * cannot be given a pmf; without this, exporting a Zipf cache would have to
581 * either guess the exponent back out of the pmf or drop the popularity.
582 * `type` is NONE when the pmf was supplied directly.
583 */
584 struct Popularity {
586 double s = 0.0; ///< Zipf exponent
587 std::size_t n = 0; ///< support size of a Zipf or a DiscreteSampler
588 };
589 std::vector<Popularity> preadkind; ///< per class, parallel to `pread`
591 std::vector<std::size_t> hitclass, missclass;
592 std::vector<std::vector<Matrix<T>>> accost; ///< (u) x (n) of (h+1)x(h+1), or empty
593 /** q-LRU admission probability; 1 admits every miss (plain LRU). */
595 /**
596 * Delayed-hit retrieval system (Cache.setRetrievalSystem). Zero capacity =
597 * none. `retrieval_queues[r]` are the 1-based retrieval-station node indices
598 * a read of class r (0-based key) circulates on a miss; `retrieval_classes`
599 * is (nitems x nclasses), item i of read class r -> the per-item retrieval
600 * class (1-based), 0 where none. Built by set_retrieval_system.
601 */
603 std::map<std::size_t, std::vector<std::size_t>> retrieval_queues; ///< read class(0-based)->nodes
604 std::vector<std::vector<std::size_t>> retrieval_classes; ///< (nitems x nclasses), 1-based
605 /**
606 * The DECLARED initial contents of the cache, as the reference dumps the
607 * node's state row: the per-class job counts, then the list contents, then
608 * the retrieval bitmap. Empty means the cache starts empty, which is what
609 * `initDefault` builds; a warm cache is not derivable from anything else.
610 */
611 std::vector<T> initstate;
612 /**
613 * Truncation level of block B: how many secondary requests may be merged
614 * onto the in-flight fetches of this cache at once. -1 is UNBOUNDED, which
615 * is what a sample path needs and what `State.afterEventCache` calls the
616 * `isSimulation` branch; a non-negative value is the enumeration bound an
617 * exact solver generates its local state space under.
618 */
620};
621
622/**
623 * Port of `State.cacheRetrievalClassMap`: the canonical order of a cache's
624 * retrieval classes, which is the column order of block B.
625 *
626 * Block B keys the merged requests by RETRIEVAL CLASS rather than by item so
627 * that the originating class, hence its hit class, is recoverable when the
628 * fetch completes and the merged requests are released as delayed hits.
629 */
630template <class T>
631void cache_retrieval_class_map(const CacheParam<T>& cp, std::vector<std::size_t>& rc_list,
632 std::vector<std::size_t>& rc_items,
633 std::vector<std::size_t>& rc_orig) {
634 rc_list.clear();
635 rc_items.clear();
636 rc_orig.clear();
637 for (std::size_t k = 0; k < cp.retrieval_classes.size(); ++k)
638 for (std::size_t c = 0; c < cp.retrieval_classes[k].size(); ++c)
639 if (cp.retrieval_classes[k][c] != 0) {
640 rc_list.push_back(cp.retrieval_classes[k][c]);
641 rc_items.push_back(k + 1);
642 rc_orig.push_back(c + 1);
643 }
644 // The reference sorts by the retrieval class index and carries the two
645 // parallel arrays along; a plain index sort reproduces that permutation.
646 std::vector<std::size_t> ord(rc_list.size());
647 for (std::size_t i = 0; i < ord.size(); ++i) ord[i] = i;
648 std::stable_sort(ord.begin(), ord.end(),
649 [&](std::size_t a, std::size_t b) { return rc_list[a] < rc_list[b]; });
650 std::vector<std::size_t> l(rc_list.size()), it(rc_list.size()), oc(rc_list.size());
651 for (std::size_t i = 0; i < ord.size(); ++i) {
652 l[i] = rc_list[ord[i]];
653 it[i] = rc_items[ord[i]];
654 oc[i] = rc_orig[ord[i]];
655 }
656 rc_list.swap(l);
657 rc_items.swap(it);
658 rc_orig.swap(oc);
659}
660
661/** One station of the network. */
662template <class T>
663struct Station {
664 std::string name;
665 NodeType nodetype = NodeType::Queue;
666 SchedStrategy sched = SchedStrategy::FCFS;
667 double nservers = 1.0; ///< may be infinite (a Delay, or an inf-scheduled task)
668 bool attr_ishost = false;
669 std::size_t attr_idx = 0; ///< LQN element this station stands for
670
671 /**
672 * `sn.schedparam`, per class: the DPS / GPS weight, or the SEPT / LEPT rank.
673 * Empty means the discipline takes no parameter; the refresh fills it with
674 * ones for DPS and GPS, which is MATLAB's default weight.
675 */
676 std::vector<T> schedparam;
677 /**
678 * Station capacity in Kendall's K, as `setCapacity` sets it. Infinite means
679 * unbounded; `sn.cap` is derived from it and from the class capacities.
680 */
681 double cap = std::numeric_limits<double>::infinity();
682 /** Per-class buffer from `setChainCapacity`; infinite where unset. */
683 std::vector<double> classcap;
684 /**
685 * Node-level immediate feedback, per class; empty when the station sets
686 * none. Only a Queue carries it in the reference, which is also the only
687 * node the JSON writer emits it for.
688 */
689 std::vector<bool> immfeed;
690 /**
691 * Per-class blocking rule as an INT, with 0 meaning "not set".
692 *
693 * The sentinel is MATLAB's: DropStrategy has no member with value 0, so a
694 * zero entry is what an unset rule looks like there, and the refresh
695 * derives those from the capacity. It matters that the two are
696 * distinguishable, because an EXPLICIT WAITQ for an open class at a finite
697 * buffer is rejected while the derived one is not.
698 */
699 std::vector<int> droprule;
700 /**
701 * `sn.lldscaling` for this station: the multiplier at population 1, 2, ...
702 * Empty when the station is not load dependent.
703 */
704 std::vector<T> lldscaling;
705 /** `sn.cdscaling` for this station: the class-dependence map, empty when unset. */
707 /**
708 * `sn.cdscalingpeak` for this station: the DECLARED peak rate scaling per
709 * class, empty when the station is not class dependent.
710 *
711 * It is not derivable from `cdscaling`: finding max_n beta_r(n) would mean
712 * sweeping the whole population lattice, and the reference does not. It is
713 * what utilization at a class-dependent station is normalized by, so that
714 * U = T*S/peak keeps the T*S/c convention of an ordinary multiserver
715 * station; without it a beta emulating two servers reports twice the true
716 * utilization. MATLAB's `setClassDependence` requires it.
717 */
718 std::vector<T> cdscalingpeak;
719 /**
720 * `sn.jdscaling` for this station: MATLAB's `Station.ljdScaling`, the JOINT
721 * dependence map eta_i(n), empty when unset.
722 *
723 * It has the same signature as `cdscaling` and is folded into it
724 * multiplicatively wherever a rate is scaled, exactly as
725 * `State.afterEventInit` does. It is kept as a SEPARATE field rather than
726 * pre-multiplied into `cdscaling` because the two carry different modelling
727 * claims: a `cdscaling` beta_r(n) keeps the product form (it is the
728 * class-dependent rate lattice `pfqn_cdfun` evaluates), while an eta_i(n)
729 * does not, and `pfqn_mvajd` / `pfqn_ncjd` are selected on that distinction.
730 */
732 /**
733 * `sn.jdscalingpeak` for this station: the declared peak joint-dependent
734 * scaling per class. `setJointDependence` makes it mandatory for the same
735 * reason `setClassDependence` does -- utilization is reported as T*S/peak.
736 */
737 std::vector<T> jdscalingpeak;
738
739 /**
740 * `sn.nodeparam{ind}.svcRateFun` for a PAS / OI station: the TOTAL service
741 * rate as a function of the ordered microstate, a 1-based list of class
742 * indices in queue order.
743 *
744 * A pass-and-swap or order-independent queue is parameterized by mu(c) as a
745 * whole; there is no per-class service distribution, and MATLAB's
746 * `setServiceRateFunction` rejects one. The refresh still derives a
747 * representative per-class rate mu([r]) so the ordinary rate machinery
748 * stays consistent, exactly as `Queue.setServiceRateFunction` does.
749 */
750 std::function<T(const std::vector<std::size_t>&)> svc_rate_fun;
751
752 /**
753 * Polling parameters for a POLLING station, MATLAB's `pollingType`,
754 * `switchoverTime` and `pollingPar` on the Queue.
755 *
756 * `polling_type[r]` is the discipline of class r's buffer (the reference
757 * assumes it is identical across buffers), `switchover[r]` its switchover
758 * distribution, and `polling_par` the K of a K-limited discipline. Empty
759 * `polling_type` means the station is not a polling station.
760 */
761 std::vector<lang::PollingType> polling_type;
762 std::vector<Distrib<T>> switchover;
763 int polling_par = 0;
764 /**
765 * `Queue.setSwitchover(fromClass, toClass, distrib)`: the walk the server
766 * takes when it turns from serving class r to serving class s, MATLAB's
767 * (K x K) `switchoverTime` cell. Empty means none declared at all;
768 * otherwise it is (K x K) and an undeclared pair is DISABLED, so it is not
769 * serialized. MATLAB fills its cell with Immediate and writes all K^2
770 * entries instead; nothing reads either, and the sparse form is the one
771 * that round-trips a document unchanged.
772 *
773 * IT IS CARRIED, NOT CONSUMED, and that is parity rather than an omission.
774 * No solver in any codebase reads a pairwise switchover: the MVA polling
775 * analyzer and the state machinery reach `switchover` above through the
776 * polling buffers, and `writeJSIM` warns and DROPS the pairwise times on an
777 * ordinary Server because JMT's `Server` has no switchover of its own. What
778 * the field buys is that the model can be declared, serialized and read
779 * back unchanged -- which is what `switchover_basic` exercises.
780 */
781 std::vector<std::vector<Distrib<T>>> switchover_pair;
782 /**
783 * `sn.nodeparam{ind}.swapGraph`: which class a departing job promotes the
784 * jobs behind it into. All zero is the ORDER-INDEPENDENT case, where no
785 * swapping happens and the station is product-form; a nonzero entry makes
786 * it a genuine pass-and-swap station, which the OI analyzer refuses.
787 */
789
790 // ---- impatience, balking and heterogeneous servers ---------------------
791 //
792 // These are declared per class on the station, as the reference declares
793 // them, and they are all OPTIONAL: an empty vector means the station
794 // declares none, which is not the same as declaring a disabled one.
795 // `used_lang_features` emits Reneging / Balking / HeteroServers from them,
796 // so a solver that does not implement one refuses the model by name
797 // instead of solving a station without it.
798
799 /**
800 * `Queue.setPatience(class, dist)`: the abandonment timer of a WAITING job,
801 * with `impatience[r]` naming which rule it is. A disabled entry is a class
802 * that declares none.
803 */
804 std::vector<Distrib<T>> patience;
805 std::vector<lang::ImpatienceType> impatience;
806 /**
807 * `Queue.setOrbitImpatience(class, dist)`: abandonment from the RETRIAL
808 * ORBIT, which is a different population from the waiting line above -- a
809 * job that gave up retrying never occupied a buffer slot.
810 */
811 std::vector<Distrib<T>> orbit_impatience;
812 /** `Queue.setBatchRejectProbability`: per-class rejection of a whole batch. */
813 std::vector<T> batch_reject;
814
815 /**
816 * One balking threshold: with `min_jobs <= n <= max_jobs` at the station,
817 * an arriving job of the class refuses to join with `probability`.
818 * `max_jobs = -1` is the wire's spelling of an unbounded upper end.
819 */
821 double min_jobs = 0.0;
822 double max_jobs = -1.0;
824 };
825 /** Per class; `strategy == NONE` is a class that declares no balking. */
830 std::vector<BalkingParam> balking;
831
832 /**
833 * A heterogeneous server pool: `count` servers that serve only
834 * `compatible` classes, each with its own service law.
835 *
836 * The station's own `service` row stays the class-level default and is what
837 * every homogeneous consumer reads; `server_types` is the refinement, and
838 * `hetero_policy` says how the pools are picked among.
839 */
840 struct ServerType {
841 std::string name;
842 double count = 1.0;
843 std::vector<bool> compatible; ///< per class; empty = every class
844 std::vector<Distrib<T>> service; ///< per class
845 };
846 std::vector<ServerType> server_types;
848
849 /**
850 * `Queue.setServerParallelism(class, n)`: the servers a job seizes for the
851 * whole of its service, JMT's job parallelism. Per class, empty or all ones
852 * when every job seizes one server.
853 */
854 std::vector<std::size_t> server_parallelism;
855
856 /**
857 * `Source.setArrivalBatch(class, dist)`: the batch-size law released at
858 * each arrival epoch. It does NOT space the epochs -- the arrival process
859 * in `service` does -- so the two are separate and both are needed.
860 */
861 std::vector<Distrib<T>> arrival_batch;
862 /** `Source.markedClasses`: the 1-based class of each mark of an MMAP arrival. */
863 std::vector<std::size_t> marked_classes;
864 /** `Place.departureDiscipline`, per class. */
865 std::vector<lang::DepartureDiscipline> departure_discipline;
866 /**
867 * `Place.setFluid` (MATLAB `sn.fluid`): the place holds a continuous LEVEL rather than tokens, the fluid
868 * place of a Fluid Stochastic Petri Net (Horton, Kulkarni, Nicol and Trivedi, 1998). Tokens never enter it:
869 * timed modes pump it at a constant rate while enabled, any mode may move it by an impulse when it fires,
870 * and its level gates modes through guards. `fluidcap` is the level ceiling (`sn.fluidcap`, Inf when
871 * unbounded) and `fluidinit` the level at time zero (`sn.fluidinit`).
872 */
873 bool fluid = false;
874 double fluidcap = std::numeric_limits<double>::infinity();
875 double fluidinit = 0.0;
876};
877
878/** One job class of the network. */
879struct JobClass {
880 std::string name;
881 JobClassType type = JobClassType::CLOSED;
882 double population = 0.0; ///< infinite for an open class
883 std::size_t refstat = 1; ///< 1-based reference station
884 /**
885 * Whether passage through the reference station is a COMPLETION.
886 *
887 * TRUE BY DEFAULT, as in `JobClass.m:34`, `JobClass.java:108` and the native
888 * Python `classes.py:37` -- every other codebase constructs a class that
889 * completes, and nothing on the model.json wire carries the flag, so a
890 * `false` default here made the SAME model mean different things in this port
891 * than in the three it is a port of. The visible consequence was that every
892 * response-time law refused: `solver_ctmc_cdf_respt` needs one completing
893 * class in the tagged chain to have an event to end the passage at, so
894 * `getCdfRespT` on an ordinary two-station model reported that no class
895 * completes. The paths that need a NON-completing class -- an auxiliary
896 * fork-join sibling, an LN pseudo-class -- set it to false explicitly, and
897 * did so already.
898 */
899 bool completes = true;
900 bool is_ref_class = false; ///< marks the chain's reference class
901 int attr_kind = -1; ///< LayeredNetworkElement of the element it stands for
902 std::size_t attr_idx = 0; ///< index of that element
903 int prio = 0;
904 /**
905 * Class-level immediate feedback, ORed with the station's own setting into
906 * `sn.immfeed`. It is the class-wide spelling of the same property, and the
907 * JSON wire carries it as a bare `"immediateFeedback": true` on the class
908 * where the node-level form is a per-class map on the node.
909 */
910 bool immfeed = false;
911 /**
912 * `sn.classdeadline(r)`: the soft deadline EDD and EDF order by, and the
913 * tardiness JMT reports. Infinite where the class declares none, which is
914 * `NetworkStruct.m:17`'s "Inf = no deadline" sentinel.
915 */
916 double deadline = std::numeric_limits<double>::infinity();
917 /**
918 * `sn.classspawn(r)`: the 1-based class injected at the SAME station on
919 * every completion of this class, 0 where none. MATLAB stores -1 for none;
920 * the 0 here is this port's usual absent-index sentinel.
921 */
922 std::size_t spawn = 0;
923 /**
924 * A `SelfLoopingClass`: a closed class that perpetually cycles at its
925 * reference station. It carries no state beyond `ClosedClass`, so it is
926 * built as one; the marker exists so the writer does not silently downgrade
927 * the wire type to `Closed` and so `getUsedLangFeatures` can name it.
928 */
929 bool self_looping = false;
930};
931
932/**
933 * CTMC bookkeeping of a heterogeneous-server station, written by `ctmc::ctmc_pools`
934 * on the solver's private copy of the struct (MATLAB `sn.nodeparam{ind}.ctmcpool`).
935 *
936 * The class-r service law there is a block-diagonal phase-type law with one block
937 * per compatible pool, in ascending pool order, so the phase of a job in service
938 * names its pool. All indices below are 1-based pools and classes, as in MATLAB.
939 */
940template <class T>
941struct CtmcPool {
942 std::size_t ntypes = 0;
943 std::vector<double> count; ///< servers per pool
944 std::vector<std::vector<bool>> compat; ///< compat[t-1][r-1]
946 std::vector<std::vector<std::size_t>> pools; ///< pools[r-1]: compatible pools of r, ascending
947 std::vector<std::vector<std::size_t>> off, len; ///< offset and length of block k of class r
948 std::vector<std::vector<std::vector<T>>> alpha; ///< entry vector of block k of class r
949 std::vector<std::vector<std::vector<T>>> exit; ///< exit rates of block k of class r
950 std::vector<std::vector<Matrix<T>>> D0; ///< D0 of block k of class r
951 std::vector<std::vector<double>> fsfrate; ///< fsfrate[t-1][r-1] = 1/mean (FSF)
952 std::vector<std::size_t> alfsorder; ///< pools by ascending class count (ALFS)
953 bool rotate = false; ///< ALIS/FAIRNESS order is part of the state
954 std::vector<std::vector<std::size_t>> perms; ///< every pool order, identity first
955 /** 1-based index of `order` in `perms`, 0 when absent. */
956 std::size_t perm_index(const std::vector<std::size_t>& order) const {
957 for (std::size_t p = 0; p < perms.size(); ++p)
958 if (perms[p] == order) return p + 1;
959 return 0;
960 }
961};
962
963/**
964 * CTMC pair form of a MAP service at an FCFS station, written by `ctmc::ctmc_mapc` on the
965 * solver's private copy of the struct (MATLAB `sn.ctmcmapc{ist,r}`). JMT and both LDES
966 * engines chain ONE sampler per (station, class) in service-start order, so with
967 * V = (-D0)^-1 D1 a busy server is a pair (i,j): current phase i, landing phase j drawn at
968 * the start. The class memory variable holds the 1-based landing of the most recently
969 * STARTED draw; a start from h enters pair (h,j) w.p. V(h,j) and sets it to j.
970 *
971 * A SINGLE server keeps the original law instead (`single`, no pairs), as the reference
972 * does: the memory is the phase the last completion left the MAP in, a start enters it,
973 * and while the class is in service the memory tracks the phase of that one job.
974 */
975template <class T>
976struct CtmcMapc {
977 std::size_t p = 0; ///< phases of the original MAP
978 bool single = false; ///< c = 1: carried phase, no pairs
979 std::vector<std::pair<std::size_t, std::size_t>> pairs; ///< 0-based (i,j), i-major, V(i,j) > 0
980 Matrix<T> V; ///< landing law (-D0)^-1 D1
981 std::vector<T> done; ///< completion rate D1(i,j)/V(i,j)
982};
983
984/**
985 * Refuses RENEGING on a class of finite population (`LINE:input:Model`; MATLAB `closedRenegingRefusal.m`, one
986 * sentence for `set_patience`, the JSON reader that calls it, and `refresh_struct`). A renege REMOVES the waiting
987 * job from the network, which a closed class cannot do: on Delay -> FCFS, N=4, Exp(0.5) patience, CTMC dropped the
988 * transition while SSA, JMT and LDES destroyed jobs until the one in service was left (_kb/06, "Reneging is
989 * open-class only"). `input`, not `unsupported`: no solver can help, so AUTO must not fall back.
990 *
991 * @param classname the class
992 * @param where "" or ` at queue "<name>"`
993 */
994[[noreturn]] inline void closed_reneging_refusal(const std::string& classname, const std::string& where) {
995 throw InputError(ErrorKind{"Model"},
996 "Class \"" + classname + "\" is closed, but reneging" + where +
997 " removes a waiting job from the network, which would change a closed population. Reneging "
998 "is defined for open classes only; model the abandonment as a routing (e.g. a probabilistic "
999 "exit back to the reference station) or use an open class.");
1000}
1001
1002/**
1003 * A network plus its refreshed NetworkStruct.
1004 *
1005 * The two halves are one object because every caller that mutates the model
1006 * (service processes, class populations, routing probabilities) re-derives the
1007 * struct, exactly as MATLAB's Network does through its cached `sn`. The refresh
1008 * entry points mirror the MATLAB ones and have the same granularity, which
1009 * matters for cost: `refresh_rates` is called on every outer iteration of
1010 * SolverLN for every layer, `refresh_chains` only where the routing changed.
1011 */
1012template <class T>
1014public:
1015 std::string name;
1016 /**
1017 * `Network.setLogPath` / `getLogPath`: the directory every Logger writes
1018 * into, and the `logPath` attribute of an exported JMT model.
1019 *
1020 * Model-level rather than per-Logger because that is where the reference
1021 * keeps it -- `Logger`'s constructor REFUSES when it is unset -- and
1022 * because the JSIM header carries one such path for the whole model.
1023 */
1024 std::string log_path;
1025 std::vector<NodeDef> nodes; ///< every node, in creation order
1026 std::vector<Station<T>> stations; ///< stations[k-1] is the k-th station
1027 std::vector<std::size_t> station_to_node; ///< (nstations) 1-based node index
1028 std::vector<std::size_t> stateful_nodes; ///< 1-based node indices, ascending
1029 std::vector<JobClass> classes;
1030 /** service[i][r], 0-based station and class; a disabled entry marks a pair never visited. */
1031 std::vector<std::vector<Distrib<T>>> service;
1032
1033 /**
1034 * Does (station i, class r) have a service law an analyzer may convert?
1035 * 0-based, and the ONLY correct precondition for `dist_to_map(service[i][r])`.
1036 *
1037 * `disabled` alone is not enough, and the gap is not hypothetical. It is the
1038 * twin of MATLAB's NaN-in-`sn.rates` sentinel, so it answers "does the class
1039 * visit this station". A JOIN is visited -- `refreshRates` gives it
1040 * `rates = Inf`, `scv = 0` -- and yet it has NO service law:
1041 * `refreshProcessRepresentations` hands it a `Coxian(NaN,NaN)`, which every
1042 * MATLAB analyzer then skips through a SECOND and separate `any(isnan(D0))`
1043 * guard. Rational has no NaN, so the C++ twin of that Coxian is a DISABLED
1044 * `Distrib`, and a guard that tests only `disabled[i][r] || rates <= 0` sails
1045 * past a Join (Inf > 0) straight into `dist_to_map`, which throws
1046 * "the distribution is disabled" with no station named. `fj_basic_open`
1047 * failed exactly this way under `-s mam`.
1048 *
1049 * So: `disabled[i][r]` is about the VISIT, `service[i][r].disabled` is about
1050 * the LAW, and a station can be visited without having one.
1051 */
1052 bool has_service_law(std::size_t i, std::size_t r) const {
1053 return !disabled[i][r] && !service[i][r].disabled;
1054 }
1055
1056 /**
1057 * Is station `i` (0-based) a QUEUEING PLACE, i.e. a Place with an embedded
1058 * queue? `Place.isQueueing()` in MATLAB.
1059 *
1060 * KEYED ON THE SERVICE LAW, not on the discipline, because that is the
1061 * reference's own rule: `Place.queueing` is raised by `setService` and by
1062 * nothing else, so a place constructed with a discipline and never given a
1063 * law is an ordinary place. The reverse cannot happen -- an ordinary place
1064 * carries a `ServiceTunnel` and no law -- so the two tests agree wherever
1065 * both are defined, and only this one survives the JSON round trip, where a
1066 * `queueing` flag would have to be carried separately and kept in step.
1067 */
1068 bool is_queueing_place(std::size_t i) const {
1069 if (i >= stations.size() || stations[i].nodetype != NodeType::Place) return false;
1070 for (std::size_t r = 0; r < service[i].size(); ++r)
1071 if (!service[i][r].disabled) return true;
1072 return false;
1073 }
1074
1075 /** Is 1-based node `ind` a FLUID place (`Place.setFluid`, MATLAB `sn.fluid(ind)`)? */
1076 bool is_fluid_node(std::size_t ind) const {
1077 if (ind == 0 || ind > nodes.size()) return false;
1078 const std::size_t st = nodes[ind - 1].station;
1079 return st > 0 && st <= stations.size() && stations[st - 1].nodetype == NodeType::Place &&
1080 stations[st - 1].fluid;
1081 }
1082 /** `sn_has_fluid_places`: does the net hold a fluid place, or a transition declaring a fluid arc? */
1083 bool has_fluid_places() const {
1084 for (const auto& s : stations)
1085 if (s.nodetype == NodeType::Place && s.fluid) return true;
1086 for (const auto& tp : transparam)
1087 if (tp.second.has_fluid_arcs()) return true;
1088 return false;
1089 }
1090 /** P[(r,s)] is an (nnodes x nnodes) block; absent means all zero. */
1091 std::map<std::pair<std::size_t, std::size_t>, Matrix<T>> P;
1092 /**
1093 * The routing after refresh_routing() has expanded the non-PROB strategies
1094 * and folded the class switches in. EMPTY when the expansion is the
1095 * identity, which is the case for every model whose routing is given as
1096 * probabilities and has no ClassSwitch node -- and then route_eff() reads P
1097 * directly, so the two representations never drift apart.
1098 */
1099 std::map<std::pair<std::size_t, std::size_t>, Matrix<T>> Peff;
1100 /**
1101 * Krzesinski (1987) product-form state-dependent routing, in 0-based
1102 * STATION indices; empty unless a node declares it with
1103 * `set_state_dep_routing`. Branch index 1 denotes the complement M-V and is
1104 * unused. `sdr_nodes` is the same structure in 0-based NODE indices.
1105 *
1106 * The routing is state dependent yet keeps a product form of its own, so
1107 * `sn_has_sd_routing` is true for it while the normalizing-constant solver
1108 * still accepts it. See _kb/16-state-dependent-routing.md
1109 */
1111 /** Node-indexed twin of `sdr`. */
1113 /** fj(f,j): the Join node j that closes the Fork node f, 1-based. */
1114 std::vector<std::pair<std::size_t, std::size_t>> fj;
1115 /**
1116 * `sn.isfjaugmented`: this struct came out of `fj_tag`, so its Fork nodes are
1117 * STATEFUL and its Join nodes carry a per-class sibling count instead of the
1118 * ordinary buffer/server split.
1119 *
1120 * It is a flag and not an inference from `fj` being non-empty, because the
1121 * un-augmented struct of the SAME model also has `fj` populated: what
1122 * distinguishes them is the auxiliary class block, and the event layer must
1123 * not take the Join branch before it exists.
1124 */
1125 bool isfjaugmented = false;
1126 /**
1127 * `sn.fjclassmap`: the ORIGINAL class of each auxiliary sibling class,
1128 * 0 for an original class. Empty unless `isfjaugmented`.
1129 */
1130 std::vector<std::size_t> fjclassmap;
1131 /**
1132 * `sn.fjauxclass`: (nclasses+1, 1-based) true where the class is an MMT
1133 * AUXILIARY OPEN class, i.e. one `fj_mmt` added to carry parallelism at rate
1134 * (fanout-1)*forkLambda. Such a class has infinite njobs but its arrivals come
1135 * from the finite closed population, so a finite-population correction still
1136 * applies to it. Empty on every struct the user's own model produces, and on
1137 * the H-T arm, whose auxiliary classes are CLOSED and need no such marker.
1138 * Distinct from `fjclassmap`/`isfjaugmented`, which describe the CTMC tag
1139 * augmentation.
1140 */
1141 std::vector<bool> fjauxclass;
1142 /**
1143 * `sn.nodeparam{j}.fj` for each Join node: the tag matrix and the required
1144 * sibling multiplicity `after_event_join` fires on. Declared as an opaque
1145 * map here and defined in `fj_tag.h`, which owns its layout.
1146 */
1147 std::map<std::size_t, FjJoinParam> fjjoinparam;
1148
1149 // ---- the Source/Sink pair, when the model has open classes -------------
1150 std::size_t sourceIdx = 0; ///< 1-based station index of the Source, 0 = none
1151 std::size_t sinkNode = 0; ///< 1-based NODE index of the Sink, 0 = none (it is not a station)
1152
1153 /** Cache parameters by 1-based NODE index; only Cache nodes have an entry. */
1154 std::map<std::size_t, CacheParam<T>> nodeparam;
1155 /**
1156 * Heterogeneous server pools rewritten for the CTMC, by 1-based NODE index.
1157 * Empty except on the private copy `ctmc::ctmc_pools` returns; the rotating
1158 * ALIS/FAIRNESS pool order, when present, is the single local variable
1159 * (`nvars` column 2R+1) and holds a 1-based index into `perms`.
1160 */
1161 std::map<std::size_t, CtmcPool<T>> ctmcpool;
1162 /**
1163 * MAP services in CTMC pair form, keyed by (1-based station, 1-based class). Empty except
1164 * on the private copy `ctmc::ctmc_mapc` returns; see `CtmcMapc`.
1165 */
1166 std::map<std::pair<std::size_t, std::size_t>, CtmcMapc<T>> ctmcmapc;
1167
1168 /**
1169 * The DECLARED initial state of a stateful node, by 1-based node index.
1170 *
1171 * `initmarking` is a Place's token count per class, which is the initial
1172 * marking of an SPN and is not derivable from anything else -- an SPN with
1173 * no tokens anywhere is a dead net, so dropping it changes the answer to
1174 * "nothing ever fires". `stateprior` and `statespace` are the pair MATLAB
1175 * writes together (`StatefulNode.statePrior` over `StatefulNode.space`):
1176 * the prior is a distribution over the ROWS of that space, so neither is
1177 * meaningful without the other and the reader refuses a lone one.
1178 */
1179 std::map<std::size_t, std::vector<T>> initmarking;
1180 std::map<std::size_t, std::vector<T>> stateprior;
1181 std::map<std::size_t, Matrix<T>> statespace;
1182
1183 /**
1184 * The DECLARED join rule of a Join node, by 1-based node index.
1185 *
1186 * STD waits for every sibling; PARTIAL fires on `quorum` of them. The
1187 * quorum is what `FjJoinParam::required` holds once the fork-join tagging
1188 * has run, but that runs on the augmented struct: this is the declaration
1189 * as the model.json carries it, so the two do not replace each other.
1190 */
1191 struct JoinDecl {
1193 double quorum = 0.0; ///< 0 = every sibling
1194 };
1195 std::map<std::size_t, JoinDecl> joindecl;
1196
1197 /** Variable forking levels, by 1-based Fork node; absent on a plain fork. */
1198 std::map<std::size_t, ForkParam<T> > forkparam;
1199
1200 /** The fork's override block, or null when it declares none. */
1201 const ForkParam<T>* fork_param_of(std::size_t node) const {
1202 const typename std::map<std::size_t, ForkParam<T> >::const_iterator it =
1203 forkparam.find(node);
1204 return it == forkparam.end() ? static_cast<const ForkParam<T>*>(0) : &it->second;
1205 }
1206
1207 /**
1208 * `sn.reward`: the user-declared reward functions, MATLAB's
1209 * `model.setReward(name, fn)`.
1210 *
1211 * `fn` is evaluated on the AGGREGATE state row -- the per-(station, class)
1212 * job counts in `(ist-1)*K + k` order, which is what
1213 * `ctmc_state_space_aggr` builds -- and not on the detailed state. That is
1214 * the reference's `RewardState` contract, and it is what makes a reward
1215 * portable across disciplines: the caller writes `state[q]` for a queue
1216 * length without having to know whether the buffer stores class tags or
1217 * per-class counts.
1218 */
1219 struct Reward {
1220 std::string name;
1221 std::function<T(const std::vector<T>&)> fn;
1222 /**
1223 * The DECLARATIVE form the reward was built from, when it was: the
1224 * template name (QLen, Util, Blocking), the 1-based node it is declared
1225 * at, and the 1-based class it covers (0 = every class).
1226 *
1227 * A lambda cannot be written back out, so a reward built from one has
1228 * an empty `kind` and the writer omits it -- which is what
1229 * `linemodel_save` does, with a warning, rather than emitting a
1230 * definition that would be wrong on reload.
1231 */
1232 std::string kind;
1233 std::size_t node = 0;
1234 std::size_t cls = 0;
1235 };
1236 std::vector<Reward> reward;
1237
1238 /**
1239 * FINITE CAPACITY REGIONS, MATLAB's `refreshRegions` output.
1240 *
1241 * A region caps the jobs (and the memory) held ACROSS a set of stations,
1242 * which no per-station capacity can express: three stations each able to
1243 * hold 5 jobs but at most 6 between them is a region, not three caps.
1244 *
1245 * `cap(i,r)` is the class-r bound at station i, `cap(i,K)` the station's
1246 * global bound, and -1 means unbounded -- the reference's sentinel, kept
1247 * rather than translated to infinity because it is compared with `~= -1`
1248 * to test MEMBERSHIP as well as boundedness.
1249 *
1250 * `rule(r)` decides what an arrival that would violate the region does:
1251 * DROP loses it, WAITQ parks it outside every station's own queue, which is
1252 * why the CTMC needs a separate waiting-room state per region and not just
1253 * a filter on the enumerated space.
1254 */
1255 struct Region {
1256 /** The region's declared name, as the wire carries it; a generated one otherwise. */
1257 std::string name;
1258 std::vector<std::vector<double>> cap; ///< (nstations x nclasses+1), -1 = unbounded
1259 std::vector<double> maxmem; ///< per member station, -1 = unbounded
1260 std::vector<bool> members; ///< membership, independent of the caps
1261 std::vector<DropStrategy> rule; ///< per class
1262 std::vector<T> weight, size; ///< per class; size is the memory footprint
1263 Matrix<T> lincon_A; ///< optional linear constraint A n <= b
1264 std::vector<T> lincon_b;
1265 };
1266 std::vector<Region> regions;
1267 /** Transition (SPN) parameters, keyed by 1-based node index. */
1268 std::map<std::size_t, TransitionParam<T>> transparam;
1269 /** Retrial parameters, keyed by 1-based STATION index. */
1270 std::map<std::size_t, RetrialParam<T>> retrialparam;
1271 /**
1272 * Setup / delay-off, keyed by 1-based STATION index.
1273 *
1274 * Presence IS MATLAB's `sn.hassetup(ist)`: the reference sets that flag
1275 * from `~isempty(station.setupTime)`, so a station appears here exactly
1276 * when it is a setup task's server.
1277 */
1278 std::map<std::size_t, SetupDelayOffParam<T>> setupparam;
1279 /**
1280 * Server breakdown / repair, keyed by 1-based STATION index.
1281 *
1282 * Presence IS MATLAB's `sn.hasbreakdown(node)`, which the reference sets
1283 * from `~isempty(node.breakdownFailure) && ~isempty(node.breakdownRepair)`
1284 * -- BOTH, since a server that fails and is never repaired is a different
1285 * model and the reference declines to infer one.
1286 */
1287 std::map<std::size_t, BreakdownParam<T>> breakdownparam;
1288
1289 /** `sn.hasbreakdown(ind)`: does the NODE's server break down? */
1290 bool has_breakdown_node(std::size_t ind) const {
1291 if (ind == 0 || ind > nodes.size()) return false;
1292 const std::size_t ist = nodes[ind - 1].station;
1293 return ist != 0 && breakdownparam.find(ist) != breakdownparam.end();
1294 }
1295 /** Any station at all, the guard every solver gate needs first. */
1296 bool has_breakdown() const { return !breakdownparam.empty(); }
1297 /**
1298 * The class-switch matrix of a ClassSwitch node, by 1-based NODE index.
1299 *
1300 * (nclasses x nclasses), row-stochastic. It is applied on the way OUT of
1301 * the node, exactly as MATLAB's ClassSwitch does, and the node is not
1302 * stateful, so the stochastic complement folds it into the edges around it.
1303 */
1304 std::map<std::size_t, Matrix<T>> csmatrix;
1305
1306 // ---- refreshed struct -------------------------------------------------
1307 std::size_t nstations = 0, nclasses = 0, nchains = 0;
1308 /**
1309 * `sn.gdscaling`: the network-level globally state-dependent (Whittle)
1310 * rate scaling phi(n). Its argument is the FULL (nstations x nclasses)
1311 * population matrix in row-major order, NOT one station's slice, so unlike
1312 * `Station::jdscaling` it lives on the struct rather than on a station.
1313 * Empty when the model declares none. See `set_global_dependence`.
1314 */
1316 /**
1317 * `sn.gdscalingpeak`: the declared (nstations x nclasses) peak of
1318 * `gdscaling`, row-major, used to report Util = T*S/peak.
1319 */
1320 std::vector<T> gdscalingpeak;
1321 /**
1322 * `sn.gdscalingcutoff`: the per-slot OPEN-class truncation used when
1323 * `gdscaling` is materialized onto the JSON wire (closed classes are
1324 * tabulated up to their own population). Solving ignores it entirely.
1325 */
1327 /**
1328 * (nstations x nclasses) service rates and SCVs, with a PARALLEL disabled
1329 * flag instead of MATLAB's NaN sentinel.
1330 *
1331 * MATLAB writes NaN into sn.rates for a (station, class) pair the class
1332 * never visits, and every consumer tests isnan. Rational has no NaN -- it
1333 * is a field, not a floating-point format -- so the marker has to be
1334 * carried out of band or the exact instantiation could not represent a
1335 * disabled pair at all. The flag is the marker; rates and scv hold zero
1336 * there, and no consumer may read them without consulting `disabled`.
1337 */
1339 std::vector<std::vector<bool>> disabled;
1340
1341 /**
1342 * `sn.immfeed`: (nstations x nclasses) IMMEDIATE FEEDBACK, the reference's
1343 * `refreshStruct` field.
1344 *
1345 * A job of class r completing at station i is fed straight back into
1346 * service, HOLDING THE SERVER, instead of being routed out and re-queued.
1347 * The flag is the OR of the station's own setting (Queue only) and the
1348 * class-level one, exactly as MATLAB `@@MNetwork/refreshStruct.m` computes
1349 * it, so either spelling reaches the same matrix.
1350 *
1351 * IT IS NOT A `Feature`, deliberately. `feature_name` is byte-for-byte the
1352 * MATLAB registry field name, and the reference has no such entry: MVA and
1353 * NC WARN that they approximate it as class-switching with re-queueing,
1354 * while CTMC and SSA implement the sample-path semantics. Making it a
1355 * feature would invent a name no other codebase carries and would turn the
1356 * reference's warning into a refusal.
1357 */
1358 std::vector<std::vector<bool>> immfeed;
1359 std::vector<std::vector<bool>> chains; ///< (nchains x nclasses)
1360 std::vector<std::vector<bool>> csmask; ///< (nclasses x nclasses) class r can switch into s, diagonal set
1361 std::vector<std::vector<std::size_t>> inchain;///< 1-based class indices per chain
1362 std::vector<std::size_t> refclass; ///< (nchains) 1-based class, 0 = none
1363 std::vector<Matrix<T>> visits; ///< (nchains) each (nstateful x nclasses)
1364 std::vector<Matrix<T>> nodevisits; ///< (nchains) each (nnodes x nclasses)
1365
1366 /**
1367 * `sn.cap` and `sn.classcap`: the total and per-class buffers.
1368 *
1369 * Derived by refresh_capacity() from the station capacities and the chain
1370 * populations, exactly as MATLAB's refreshCapacity does, so a station with
1371 * no explicit capacity still carries the population bound of the chains
1372 * that reach it.
1373 */
1374 std::vector<double> cap;
1375 std::vector<std::vector<double>> classcap;
1376 std::vector<std::vector<DropStrategy>> droprule;
1377
1378 /**
1379 * `sn.rt` and `sn.rtnodes`: the class-expanded routing.
1380 *
1381 * rt is (nstateful*nclasses) square over the STATEFUL nodes -- the
1382 * stochastic complement that removes Fork, ClassSwitch and Router nodes --
1383 * and rtnodes is (nnodes*nclasses) square over every node. Row (i-1)*K + r
1384 * is node i in class r, which is MATLAB's ordering.
1385 */
1387
1388 /**
1389 * `sn.nvars`, (nnodes x 3R+1): the LOCAL VARIABLE columns each node appends
1390 * to its state, beyond the buffer and the servers. Layout, verbatim from
1391 * refreshLocalVars:
1392 *
1393 * 1 .. R modulating phase, one per class with a MAP/MMPP2 process
1394 * R+1 .. 2R routing variable, one per class routed round-robin
1395 * 2R+1 the SHARED node block: cache width, or the BAS blocked
1396 * marker, or the breakdown status, or the polling
1397 * controller. They share one column and are therefore
1398 * mutually exclusive -- refreshLocalVars rejects the
1399 * combinations rather than widening the state.
1400 * 2R+1+r the REPLY blocked-server counter for calling class r
1401 *
1402 * The trailing sum(nvars(ind,:)) columns are what every state slicer must
1403 * take clear of before reading the server block.
1404 */
1405 std::vector<std::vector<std::size_t>> nvars;
1406
1407 /**
1408 * `sn.isbasblocking`, per NODE: true where the node is the BLOCKING
1409 * (upstream) side of a true-BAS relation and therefore carries the blocked
1410 * marker in `nvars` column 2R+1.
1411 *
1412 * TRUE BAS is not a drop rule, it is a HELD JOB. Under blocking after
1413 * service the job that finished at this station cannot leave because its
1414 * destination is full; it stays at the server, occupying it, until room
1415 * frees. That needs a state bit -- "the front job here is completed and
1416 * waiting" -- which no queue length can express, and it is what separates
1417 * BAS from DROP (where the arrival is lost) and from WAITQ (where the
1418 * arrival waits at the region gate instead).
1419 *
1420 * IT IS KEYED ON THE NODE, NOT ON THE DROP RULE, and the distinction is
1421 * load-bearing. BAS may be DECLARED on either side: upstream, on the station
1422 * that will hold the job, or on the full destination (the JMT/LDES
1423 * convention). Both resolve to the same blocking station, because the held
1424 * job sits upstream either way, so keying enumeration on the station's own
1425 * `droprule` misses every destination-declared model.
1426 */
1427 std::vector<bool> isbasblocking;
1428 /**
1429 * `sn.isbasdestination`, (nstations x nclasses): true where a refusal at
1430 * this station must BLOCK an upstream BAS station rather than drop the job.
1431 *
1432 * `arrival_is_lost` needs it because it sees only the station where the
1433 * refusal happens. Under the upstream declaration form that station carries
1434 * no BAS rule of its own, so without this mask an open class refused there
1435 * would be declared lost, the become-blocked edge would never fire, and the
1436 * blocking station would behave as if its destination were unbounded.
1437 */
1438 std::vector<std::vector<bool>> isbasdestination;
1439
1440 /**
1441 * The pass-and-swap / order-independent parameters of a PAS station, keyed
1442 * by station index (Dorsman and Gardner 2024, Sect. 2).
1443 *
1444 * `svc_rate_fun` is the total service rate mu(c) of an ordered list of
1445 * class indices; the rate of the token at position p is the INCREMENT
1446 * mu(c1..cp) - mu(c1..c_{p-1}), which is what makes the station
1447 * order-independent. `swap_graph` is the (R x R) adjacency saying which
1448 * class may take another's place. An OI station is the special case of an
1449 * empty swap graph.
1450 */
1451 struct PasParam {
1452 std::function<T(const std::vector<std::size_t>&)> svc_rate_fun;
1453 std::vector<std::vector<bool>> swap_graph;
1454 };
1455 std::map<std::size_t, PasParam> pasparam;
1456
1457 /**
1458 * `sn.replyblock` (nnodes x nclasses) and `sn.syncreply` (nclasses).
1459 *
1460 * A SYNCHRONOUS call: a job of a calling class leaves this station for the
1461 * callee but KEEPS its server, released only when the matching REPLY class
1462 * arrives back. `replyblock` marks the (node, calling class) pairs that
1463 * hold such a server, and `syncreply[r]` is the 1-based reply class the
1464 * calling class r expects (0 = none). A CTMC has no job identity to key on
1465 * as LDES does, so the state carries COUNTS of held servers.
1466 */
1467 std::vector<std::vector<bool>> replyblock;
1468 std::vector<std::size_t> syncreply;
1469
1470 /**
1471 * The polling controller of a POLLING station, keyed by station index.
1472 *
1473 * `switchover[r]` is the walk into buffer r, taken from the buffer the
1474 * server LEAVES to get there. An Immediate switchover is NOT represented as
1475 * a state: taking its ~1e8 rate literally would make the generator stiff
1476 * and add a spurious state per buffer, so `pollingNext` folds it into the
1477 * enclosing transition instead.
1478 */
1481 std::size_t pk = 1; ///< the K of K-LIMITED
1482 std::vector<lang::Distrib<T>> switchover;
1483 };
1484 std::map<std::size_t, PollingParam> pollingparam;
1485
1486 /**
1487 * The polling controller of station `ist`, from whichever API declared it.
1488 *
1489 * TWO APIS WRITE THE SAME CONTROLLER. `set_polling` fills `pollingparam`;
1490 * `Queue.setPollingType` / `Queue.setSwitchover` -- the MATLAB-faithful pair
1491 * the JSON reader also uses -- fill `Station::polling_type`, `switchover`
1492 * and `polling_par`. Reading only the first left a station built the second
1493 * way with NO controller at all, which is not a degraded model but an
1494 * unrepresentable one: the state handlers then index an empty `polled`.
1495 *
1496 * A POLLING station that declares neither still HAS a controller, exactly as
1497 * `State.pollingInfo` builds one: EXHAUSTIVE service, every switchover
1498 * immediate. That is a discipline, not an absence.
1499 */
1500 PollingParam effective_polling(std::size_t ist) const {
1501 PollingParam pp;
1502 if (ist == 0 || ist > stations.size()) return pp;
1503 const typename std::map<std::size_t, PollingParam>::const_iterator it =
1504 pollingparam.find(ist);
1505 if (it != pollingparam.end()) return it->second;
1506 const Station<T>& st = stations[ist - 1];
1507 if (!st.polling_type.empty()) {
1508 pp.ptype = st.polling_type[0];
1509 if (pp.ptype == lang::PollingType::KLIMITED && st.polling_par >= 1)
1510 pp.pk = static_cast<std::size_t>(st.polling_par);
1511 }
1512 pp.switchover = st.switchover;
1513 return pp;
1514 }
1515
1516 /**
1517 * The G-network signal declaration, per CLASS. `issignal` is the gate: the
1518 * remaining vectors are only read where it is true.
1519 *
1520 * `signaltarget` is the 1-based class a NEGATIVE signal may remove, or 0
1521 * for the classic untargeted Gelenbe customer, which is eligible against
1522 * every non-signal class. `signalremdist` is the batch-size pmf indexed by
1523 * batch size 0,1,2,...; an empty entry means "remove exactly one".
1524 */
1525 std::vector<bool> issignal;
1526 std::vector<lang::SignalType> signaltype;
1527 std::vector<std::size_t> signaltarget;
1528 std::vector<lang::RemovalPolicy> signalrempolicy;
1529 std::vector<std::vector<T>> signalremdist;
1530
1531 /** Total local-variable width of node `ind` (1-based). */
1532 std::size_t nvars_of(std::size_t ind) const {
1533 // A node index is 1-BASED, so 0 means "no node" and must return 0. The
1534 // old guard `nvars.size() < ind` is vacuously false at ind == 0 because
1535 // both sides are unsigned, and the loop below then indexed nvars[-1]:
1536 // heap corruption, surfacing as a SIGABRT far from here.
1537 if (ind == 0 || ind > nvars.size()) return 0;
1538 std::size_t w = 0;
1539 for (std::size_t j = 0; j < nvars[ind - 1].size(); ++j) w += nvars[ind - 1][j];
1540 return w;
1541 }
1542
1543 std::size_t nof_stations() const { return stations.size(); }
1544 std::size_t nof_classes() const { return classes.size(); }
1545 std::size_t nof_nodes() const { return nodes.size(); }
1546 std::size_t nof_stateful() const { return stateful_nodes.size(); }
1547
1548 /** `sn.njobs`: the population of each class, infinite for an open one. */
1549 std::vector<double> njobs() const {
1550 std::vector<double> v;
1551 v.reserve(classes.size());
1552 for (const JobClass& c : classes) v.push_back(c.population);
1553 return v;
1554 }
1555
1556 /** `sn.nclosedjobs`: the total population of the closed classes. */
1557 double nclosedjobs() const { return total_jobs(); }
1558
1559 // -----------------------------------------------------------------------
1560 // MATLAB-SHAPED VIEWS of the fields this port derives instead of storing.
1561 //
1562 // Each accessor below assembles one whole `sn` field in MATLAB's layout and
1563 // values (refreshStruct.m, refreshProcessPhases.m,
1564 // refreshProcessRepresentations.m, refreshProcessTypes.m, refreshLST.m), so
1565 // the NetworkStruct contract (sn_contract.h) can place the field on it. They
1566 // are computed on demand from the stored records, as the per-element
1567 // accessors (`phases_of`, `procid(i,r)`, `stateful_index`...) already are,
1568 // and are never stored beside them. Indices are 1-based and NaN marks "none",
1569 // as in MATLAB. They are views for the contract and for parity checks; the
1570 // engines keep reading the per-element accessors, and two of those differ
1571 // from the MATLAB field on purpose -- see `phases()` and `phasessz()`.
1572 // -----------------------------------------------------------------------
1573
1574 /** `sn.nregions`: the number of finite capacity regions. */
1575 std::size_t nregions() const { return regions.size(); }
1576
1577 /** `sn.nodeToStateful` (1 x N): the stateful index of each node, NaN when it is not stateful. */
1578 std::vector<double> node_to_stateful() const {
1579 std::vector<double> v(nodes.size(), std::nan(""));
1580 for (std::size_t k = 0; k < stateful_nodes.size(); ++k)
1581 if (stateful_nodes[k] >= 1 && stateful_nodes[k] <= nodes.size())
1582 v[stateful_nodes[k] - 1] = static_cast<double>(k + 1);
1583 return v;
1584 }
1585
1586 /** `sn.stationToStateful` (1 x M): the stateful index of each station. */
1587 std::vector<double> station_to_stateful() const {
1588 const std::vector<double> nsf = node_to_stateful();
1589 std::vector<double> v(nstations, std::nan(""));
1590 for (std::size_t ist = 1; ist <= nstations && ist <= station_to_node.size(); ++ist) {
1591 const std::size_t nd = station_to_node[ist - 1];
1592 if (nd >= 1 && nd <= nsf.size()) v[ist - 1] = nsf[nd - 1];
1593 }
1594 return v;
1595 }
1596
1597 /** `sn.statefulToStation` (1 x S): the station index of each stateful node, NaN when it is not a station. */
1598 std::vector<double> stateful_to_station() const {
1599 std::vector<double> v(stateful_nodes.size(), std::nan(""));
1600 for (std::size_t k = 0; k < stateful_nodes.size(); ++k) {
1601 const std::size_t nd = stateful_nodes[k];
1602 if (nd >= 1 && nd <= nodes.size() && nodes[nd - 1].station != 0)
1603 v[k] = static_cast<double>(nodes[nd - 1].station);
1604 }
1605 return v;
1606 }
1607
1608 /**
1609 * `sn.connmatrix` (N x N): 1 where some class routes node i to node j with
1610 * positive probability. MATLAB records each such arc through `addLink` as
1611 * `link` installs P; this port keeps P itself, so the arcs are read off it,
1612 * the same class-independent topology `refresh_routing` builds for the
1613 * RAND/RROBIN spreads. The Sink -> Source arcs are left out: they are the
1614 * open-chain closure `apply_sink_closure` writes into P, which the reference
1615 * adds after link.m has recorded its links, so they never reach its matrix.
1616 */
1618 const std::size_t I = nodes.size();
1619 const T zero = num_traits<T>::from_int(0);
1620 Matrix<double> c(I, I, 0.0);
1621 for (const auto& kv : P) {
1622 const Matrix<T>& B = kv.second;
1623 if (B.rows() < I || B.cols() < I) continue; // get_route reads such a block as zero
1624 for (std::size_t a = 0; a < I; ++a)
1625 for (std::size_t b = 0; b < I; ++b)
1626 if (B(a, b) > zero &&
1627 !(nodes[a].nodetype == NodeType::Sink && nodes[b].nodetype == NodeType::Source))
1628 c(a, b) = 1.0;
1629 }
1630 return c;
1631 }
1632
1633 /**
1634 * `sn.isstatedep` (N x 3): column 1 the input section (never set by the
1635 * reference), column 2 state-dependent service (a Cache), column 3
1636 * state-dependent routing (RROBIN, WRROBIN, JSQ, SQ, SDR for some class).
1637 */
1639 const std::size_t I = nodes.size();
1640 Matrix<double> m(I, 3, 0.0);
1641 for (std::size_t ind = 0; ind < I; ++ind) {
1642 if (nodes[ind].nodetype == NodeType::Cache) m(ind, 1) = 1.0;
1643 for (RoutingStrategy rs : nodes[ind].routing)
1644 if (rs == RoutingStrategy::RROBIN || rs == RoutingStrategy::WRROBIN || rs == RoutingStrategy::JSQ ||
1645 rs == RoutingStrategy::SQ || rs == RoutingStrategy::SDR)
1646 m(ind, 2) = 1.0;
1647 }
1648 return m;
1649 }
1650
1651 /** `sn.hasbreakdown` (N x 1): 1 where the node's server is subject to breakdowns. */
1652 std::vector<double> hasbreakdown() const {
1653 std::vector<double> v(nodes.size(), 0.0);
1654 for (std::size_t ind = 1; ind <= nodes.size(); ++ind)
1655 if (has_breakdown_node(ind)) v[ind - 1] = 1.0;
1656 return v;
1657 }
1658
1659 /** `sn.iscatastrophe` (K x 1): true where class r is a catastrophe signal. */
1660 std::vector<bool> iscatastrophe() const {
1661 std::vector<bool> v(nclasses, false);
1662 for (std::size_t r = 0; r < nclasses && r < signaltype.size(); ++r)
1663 v[r] = r < issignal.size() && issignal[r] && signaltype[r] == lang::SignalType::CATASTROPHE;
1664 return v;
1665 }
1666
1667 /**
1668 * `sn.procid` (M x K): the ProcessType id of each (station, class) pair, as
1669 * `refreshProcessTypes.m` writes it: a Join serves IMMEDIATE (its server is a
1670 * ServiceTunnel the reference names so), an ordinary Place is NaN (a
1671 * ServiceTunnel the reference leaves unset), every other pair the type of its
1672 * law. The ids are this port's enum values, which are MATLAB's.
1673 */
1675 Matrix<double> m(nstations, nclasses, std::nan(""));
1676 for (std::size_t i = 0; i < nstations && i < service.size(); ++i)
1677 for (std::size_t r = 0; r < nclasses && r < service[i].size(); ++r) {
1678 if (stations[i].nodetype == NodeType::Join)
1679 m(i, r) = static_cast<double>(static_cast<int>(ProcessType::IMMEDIATE));
1680 else if (stations[i].nodetype == NodeType::Place && !is_queueing_place(i))
1681 continue;
1682 else
1683 m(i, r) = static_cast<double>(static_cast<int>(service[i][r].type));
1684 }
1685 return m;
1686 }
1687
1688 private:
1689 /** Is (0-based i, r) a pair whose law MATLAB's getServiceRates has no arm for (an empty `sn.proc` entry)? */
1690 bool proc_is_empty(std::size_t i, std::size_t r) const {
1691 const Distrib<T>& d = service[i][r];
1692 if (d.disabled) return stations[i].nodetype == NodeType::Place && !is_queueing_place(i);
1693 return d.is_prior();
1694 }
1695 /** Is (0-based i, r) a pair whose `sn.proc` entry is MATLAB's disabled `{NaN, NaN}`? */
1696 bool proc_is_nan(std::size_t i, std::size_t r) const {
1697 return service[i][r].disabled && !proc_is_empty(i, r);
1698 }
1699 /** The (D0, D1, D_k...) of `sn.proc{i}{r}` after refreshProcessRepresentations' Erlang conversion. */
1700 std::vector<Matrix<T>> proc_blocks(std::size_t i, std::size_t r) const {
1701 const Distrib<T>& d = service[i][r];
1702 std::vector<Matrix<T>> b;
1703 if (d.has_map()) {
1704 b.push_back(d.D0);
1705 b.push_back(d.D1);
1706 for (const Matrix<T>& Dk : d.Dmark) b.push_back(Dk);
1707 } else {
1708 const mam::Map<T> m = lang::dist_to_map(d);
1709 b.push_back(m.D0);
1710 b.push_back(m.D1);
1711 }
1712 return b;
1713 }
1714
1715 public:
1716 /**
1717 * `sn.phases` (M x K), as refreshProcessRepresentations.m leaves it: 0 for a
1718 * disabled pair, 1 for an empty entry (an ordinary Place, a Prior) and for an
1719 * NHPP, the order of the representation otherwise -- which for a
1720 * non-Markovian law is the order of its Erlang conversion (`dist_to_map`).
1721 * `phases_of(i, r)` answers 1 for such a law instead, because this port
1722 * converts it only when a solver asks (sn_nonmarkov_toph.h); the two agree on
1723 * every Markovian law.
1724 */
1727 for (std::size_t i = 0; i < nstations && i < service.size(); ++i)
1728 for (std::size_t r = 0; r < nclasses && r < service[i].size(); ++r) {
1729 const Distrib<T>& d = service[i][r];
1730 if (proc_is_empty(i, r) || d.type == ProcessType::NHPP)
1731 m(i, r) = 1.0;
1732 else if (!d.disabled)
1733 m(i, r) = static_cast<double>(proc_blocks(i, r)[0].rows());
1734 }
1735 return m;
1736 }
1737
1738 /**
1739 * `sn.phasessz` (M x K) = max(phases, 1), with MATLAB's two exceptions: a
1740 * Join keeps its 0 and a non-carrier mark of a marked arrival is 1. The
1741 * engines size their state rows with `phasessz_of`, which gives a Join 1 and
1742 * is built on `phases_of` (see `phases()`).
1743 */
1745 Matrix<double> m = phases();
1746 for (std::size_t i = 0; i < m.rows(); ++i)
1747 for (std::size_t r = 0; r < m.cols(); ++r) {
1748 if (stations[i].nodetype != NodeType::Join && m(i, r) < 1.0) m(i, r) = 1.0;
1749 if (markidx_of(i + 1, r + 1) > 1) m(i, r) = 1.0;
1750 }
1751 return m;
1752 }
1753
1754 /** `sn.phaseshift` (M x K+1) = [0, cumsum(phasessz, 2)]. */
1756 const Matrix<double> sz = phasessz();
1757 Matrix<double> m(sz.rows(), sz.cols() + 1, 0.0);
1758 for (std::size_t i = 0; i < sz.rows(); ++i)
1759 for (std::size_t r = 0; r < sz.cols(); ++r) m(i, r + 1) = m(i, r) + sz(i, r);
1760 return m;
1761 }
1762
1763 /**
1764 * `sn.mu` (M x K cells): the total outgoing rate of each phase, MATLAB's
1765 * getServiceRates/getSourceRates. NaN for a disabled pair, empty for an
1766 * empty entry, the time-average rate for an NHPP, 1/mean for a DMAP (the
1767 * per-slot event rate), `Distrib::mu_vec` otherwise.
1768 */
1769 std::vector<std::vector<std::vector<double>>> mu() const { return rate_cells(true); }
1770
1771 /** `sn.phi` (M x K cells): the completion probability of each phase; 1 where `mu` is a single rate. */
1772 std::vector<std::vector<std::vector<double>>> phi() const { return rate_cells(false); }
1773
1774 /**
1775 * `sn.pie` (M x K cells): the phase an arriving job enters, `map_pie` of
1776 * `sn.proc{i}{r}` (the Erlang conversion for a non-Markovian law, the
1777 * time-average nominal for MAPt/PHt). NaN for a disabled or empty entry and
1778 * for an NHPP, whose rate schedule has no phase.
1779 */
1780 std::vector<std::vector<std::vector<double>>> pie() const {
1781 std::vector<std::vector<std::vector<double>>> out(nstations, std::vector<std::vector<double>>(nclasses));
1782 for (std::size_t i = 0; i < nstations && i < service.size(); ++i)
1783 for (std::size_t r = 0; r < nclasses && r < service[i].size(); ++r) {
1784 const Distrib<T>& d = service[i][r];
1785 if (d.disabled || d.is_prior() || d.type == ProcessType::NHPP) {
1786 out[i][r] = {std::nan("")};
1787 continue;
1788 }
1789 const std::vector<Matrix<T>> b = proc_blocks(i, r);
1790 mam::Map<T> m;
1791 m.D0 = b[0];
1792 m.D1 = b[1];
1793 for (const T& x : mam::map_pie(m)) out[i][r].push_back(num_traits<T>::to_double(x));
1794 }
1795 return out;
1796 }
1797
1798 /**
1799 * `sn.isph` (M x K): true where `sn.proc{i}{r}` with `sn.pie{i}{r}` admits a
1800 * phase-type reading (`sn_is_phasetype`); a disabled or empty entry is
1801 * uninspectable and reads true, as in the reference.
1802 */
1805 const auto pies = pie();
1806 for (std::size_t i = 0; i < nstations && i < service.size(); ++i)
1807 for (std::size_t r = 0; r < nclasses && r < service[i].size(); ++r) {
1808 const Distrib<T>& d = service[i][r];
1809 if (d.disabled || d.is_prior() || d.type == ProcessType::NHPP) continue;
1810 std::vector<T> p;
1811 for (double x : pies[i][r]) p.push_back(num_traits<T>::from_double(x));
1812 m(i, r) = api::sn_is_phasetype(proc_blocks(i, r), p) ? 1.0 : 0.0;
1813 }
1814 return m;
1815 }
1816
1817 /** One LST handle: E[exp(-sX)] at a complex argument. */
1818 using LstHandle = std::function<std::complex<double>(const std::complex<double>&)>;
1819
1820 /**
1821 * `sn.lst` (M x K handles), refreshLST.m: the Laplace-Stieltjes transform of
1822 * each law, admitting a complex argument (`lang::dist_lst`). EMPTY at a Fork,
1823 * a Join and a disabled Source class, as the reference leaves them; a
1824 * disabled class at any other station answers NaN, which is what the
1825 * reference's handle on a Disabled law returns. Under exact arithmetic a
1826 * handle accepts only a real argument and evaluates the exact transform.
1827 */
1828 std::vector<std::vector<LstHandle>> lst() const {
1829 std::vector<std::vector<LstHandle>> out(nstations, std::vector<LstHandle>(nclasses));
1830 for (std::size_t i = 0; i < nstations && i < service.size(); ++i)
1831 for (std::size_t r = 0; r < nclasses && r < service[i].size(); ++r) {
1832 const NodeType nt = stations[i].nodetype;
1833 const Distrib<T>& d = service[i][r];
1834 if (nt == NodeType::Fork || nt == NodeType::Join) continue;
1835 if (d.disabled) {
1836 if (nt != NodeType::Source)
1837 out[i][r] = [](const std::complex<double>&) { return std::complex<double>(std::nan(""), 0.0); };
1838 continue;
1839 }
1840 out[i][r] = [d](const std::complex<double>& s) -> std::complex<double> {
1841 if constexpr (num_traits<T>::has_transcendental) {
1842 return lang::dist_lst(d, s);
1843 } else {
1844 if (s.imag() != 0.0)
1845 throw UnsupportedError(ErrorKind{"Arithmetic"},
1846 "sn.lst: a complex argument needs transcendental arithmetic; solve in double");
1847 return std::complex<double>(
1849 }
1850 };
1851 }
1852 return out;
1853 }
1854
1855 private:
1856 std::vector<std::vector<std::vector<double>>> rate_cells(bool want_mu) const {
1857 std::vector<std::vector<std::vector<double>>> out(nstations, std::vector<std::vector<double>>(nclasses));
1858 for (std::size_t i = 0; i < nstations && i < service.size(); ++i)
1859 for (std::size_t r = 0; r < nclasses && r < service[i].size(); ++r) {
1860 const Distrib<T>& d = service[i][r];
1861 if (proc_is_empty(i, r)) continue;
1862 if (d.disabled) {
1863 out[i][r] = {std::nan("")};
1864 } else if (d.type == ProcessType::NHPP || d.type == ProcessType::DMAP) {
1865 out[i][r] = {want_mu ? num_traits<T>::to_double(d.rate()) : 1.0};
1866 } else {
1867 for (const T& x : want_mu ? d.mu_vec() : d.phi_vec())
1868 out[i][r].push_back(num_traits<T>::to_double(x));
1869 }
1870 }
1871 return out;
1872 }
1873
1874 public:
1875
1876 /** `sn.procid(i,r)`: the process type of a (station, class) pair. */
1877 ProcessType procid(std::size_t ist, std::size_t r) const {
1878 return service[ist - 1][r - 1].type;
1879 }
1880
1881 /**
1882 * `sn.phases(i,r)`: the order of the process representation.
1883 *
1884 * A PLACE HOLDS TOKENS IN ONE PHASE, and that is not cosmetic. MATLAB's
1885 * `refreshProcessRepresentations` decides the count from what `ph{ist}{r}`
1886 * turned out to be, and it has TWO ways of having no service:
1887 * - `isempty(ph{ist}{r})` -> `phases = 1`, its "fluid fails otherwise" arm
1888 * - a process that is all NaN -> `phases = 0`
1889 * A non-queueing Place carries a `ServiceTunnel`, so its entry is EMPTY and
1890 * it gets 1; a Join is handed an explicit `Coxian(NaN,NaN)` and gets 0.
1891 * C++ spells both "no service" as a disabled `Distrib`, whose `phases()` is
1892 * 0, so a Place must be answered 1 explicitly: with 0 phases
1893 * `from_marginal_node_first` builds no row and every closed SPN is refused.
1894 *
1895 * A QUEUEING Place is untouched: it has a real server and a real law, so it
1896 * is not disabled and answers from its representation as before.
1897 */
1898 std::size_t phases_of(std::size_t ist, std::size_t r) const {
1899 const Distrib<T>& d = service[ist - 1][r - 1];
1900 if (d.disabled && stations[ist - 1].nodetype == NodeType::Place) return 1;
1901 return d.phases();
1902 }
1903 /**
1904 * `sn.phasessz(i,r) = max(sn.phases(i,r),1)`: THE WIDTH of class r's phase
1905 * block in a state row, as opposed to `phases_of`, which is its CONTENT.
1906 *
1907 * The two differ exactly where a class is disabled at a station, and the
1908 * reference keeps the column anyway: a disabled process is stored as a
1909 * `1 x 1 NaN`, never as an empty, so `length(sn.proc{ist}{r}{1})` is 1 and
1910 * `State.fromMarginal` emits one always-zero column for it. A Source
1911 * serving one of six classes therefore writes `[Inf 1 0 0 0 0 0]`. A narrow
1912 * row is self-consistent but misdecodes a MATLAB-exported `model.json`.
1913 *
1914 * Use this wherever a WIDTH or an OFFSET is computed -- `row_layout`,
1915 * `to_marginal`, every `from_marginal*` builder -- and `phases_of` wherever
1916 * the question is whether the class has a process at all, or how many real
1917 * phases it has to iterate over.
1918 */
1919 std::size_t phasessz_of(std::size_t ist, std::size_t r) const {
1920 // A NON-CARRIER MARK OWNS NO PHASE BLOCK. Every mark of a marked
1921 // arrival shares ONE modulating chain, and `set_marked_arrival` binds
1922 // the same `Distrib` to each marked class, so asking each of them for
1923 // its own width would lay K independent copies of the chain into the
1924 // state row -- K independent streams, at K times the offered load.
1925 // The chain lives in the CARRIER's block (mark 1) and a later mark
1926 // contributes one always-zero column, which is MATLAB's
1927 // `sn.phasessz(sn.markidx > 1) = 1`.
1928 if (markidx_of(ist, r) > 1) return 1;
1929 const std::size_t p = phases_of(ist, r);
1930 return p > 0 ? p : 1;
1931 }
1932 /**
1933 * `sn.markidx(i,r)`: the 1-based MARK that class r carries in station i's
1934 * marked arrival process, or 0 when the class carries no mark.
1935 *
1936 * DERIVED from `stations[i-1].marked_classes` rather than stored beside it,
1937 * since that vector already IS the binding -- mark k names class
1938 * `marked_classes[k-1]` -- and a second copy could disagree with it after a
1939 * class removal.
1940 */
1941 std::size_t markidx_of(std::size_t ist, std::size_t r) const {
1942 if (ist == 0 || ist > stations.size()) return 0;
1943 const std::vector<std::size_t>& mc = stations[ist - 1].marked_classes;
1944 for (std::size_t k = 0; k < mc.size(); ++k)
1945 if (mc[k] == r) return k + 1;
1946 return 0;
1947 }
1948 /**
1949 * The CARRIER of station i's marked arrival: the class of mark 1, whose
1950 * phase block holds the one modulating chain every mark shares. 0 when the
1951 * station has no marked process.
1952 */
1953 std::size_t mark_carrier_of(std::size_t ist) const {
1954 if (ist == 0 || ist > stations.size()) return 0;
1955 const std::vector<std::size_t>& mc = stations[ist - 1].marked_classes;
1956 return mc.empty() ? 0 : mc[0];
1957 }
1958 bool has_fork() const {
1959 for (const NodeDef& n : nodes)
1960 if (n.nodetype == NodeType::Fork) return true;
1961 return false;
1962 }
1963 /**
1964 * MATLAB's `any(sn.isstatedep(:,3))` NARROWED TO THE ONE STRATEGY THIS PORT
1965 * EVALUATES PER STATE: Krzesinski's SDR.
1966 *
1967 * RROBIN, WRROBIN, JSQ and SQ are state dependent too and `sn_has_sd_routing`
1968 * reports them. Round-robin carries its pointer in the state instead, and a
1969 * generator asking "must I re-evaluate the routing at every state?" asks
1970 * `has_rtfun_routing`, which adds JSQ and SQ to SDR.
1971 */
1972 bool has_sdr_routing() const {
1973 for (const NodeDef& n : nodes)
1974 for (RoutingStrategy rs : n.routing)
1975 if (rs == RoutingStrategy::SDR) return true;
1976 return false;
1977 }
1978 /**
1979 * True when some routing must be re-read at every state from `rt_state`: SDR, and the JSQ and SQ(d)
1980 * dispatchers, whose split is a function of the queue lengths the departing job sees (`sub_jsq` /
1981 * `sub_sq` of refreshRoutingMatrix.m). Round-robin is not here: its pointer is a state coordinate.
1982 */
1983 bool has_rtfun_routing() const {
1984 for (const NodeDef& n : nodes)
1985 for (RoutingStrategy rs : n.routing)
1986 if (rs == RoutingStrategy::SDR || rs == RoutingStrategy::JSQ || rs == RoutingStrategy::SQ)
1987 return true;
1988 return false;
1989 }
1990 /**
1991 * ROUND-ROBIN DISPATCH, the state that makes it deterministic.
1992 *
1993 * A round-robin dispatcher is not a coin: which link the next job takes is
1994 * decided by a POINTER the node carries between departures, and without it
1995 * `refresh_routing`'s uniform expansion answers a random-routing model under
1996 * a dispatcher's name. The pointer lives in the node's local-variable block,
1997 * one column per class that routes RROBIN or WRROBIN
1998 * (`refresh_local_vars` allocates it as `nvars[ind-1][R+r-1]`).
1999 *
2000 * RROBIN stores the DESTINATION NODE INDEX in its slot; WRROBIN stores a
2001 * POSITION in the weighted cycle, because a repeated outlink must advance
2002 * once per repetition and a destination value could not tell the copies
2003 * apart. Both are the reference's encodings (`refreshLocalVars.m:318-355`,
2004 * `afterEventStation.m:632-658`, `afterEventRouter.m:22-52`).
2005 */
2006 std::vector<std::size_t> rr_outlinks(std::size_t ind, std::size_t r) const {
2007 std::vector<std::size_t> out;
2008 const T zero = num_traits<T>::from_int(0);
2009 const std::size_t K = classes.size(), I = nodes.size();
2010 for (std::size_t j = 1; j <= I; ++j) {
2011 bool linked = false;
2012 for (std::size_t s = 1; s <= K && !linked; ++s)
2013 if (get_route(r, s, ind, j) > zero) linked = true;
2014 if (linked) out.push_back(j);
2015 }
2016 return out;
2017 }
2018
2019 /** The WRROBIN cycle: each outlink repeated by its weight, weight 0 once. */
2020 std::vector<std::size_t> rr_weighted_outlinks(std::size_t ind, std::size_t r) const {
2021 const std::vector<std::size_t> ol = rr_outlinks(ind, r);
2022 const std::map<std::size_t, double>* w =
2023 (ind <= nodes.size() && nodes[ind - 1].routing_weights.size() >= r)
2024 ? &nodes[ind - 1].routing_weights[r - 1]
2025 : NULL;
2026 std::vector<std::size_t> cycle;
2027 for (std::size_t d = 0; d < ol.size(); ++d) {
2028 long reps = 1;
2029 if (w != NULL) {
2030 const std::map<std::size_t, double>::const_iterator it = w->find(ol[d]);
2031 if (it != w->end() && it->second > 0.0)
2032 reps = std::lround(it->second) < 1 ? 1 : std::lround(it->second);
2033 }
2034 for (long q = 0; q < reps; ++q) cycle.push_back(ol[d]);
2035 }
2036 return cycle;
2037 }
2038
2039 /** The pair form of class r (1-based) at station ist (1-based), or null when there is none. */
2040 const CtmcMapc<T>* mapc_of(std::size_t ist, std::size_t r) const {
2041 if (ctmcmapc.empty()) return nullptr;
2042 const auto it = ctmcmapc.find(std::make_pair(ist, r));
2043 return it == ctmcmapc.end() ? nullptr : &it->second;
2044 }
2045
2046 /** 0-based column of class r's MAP memory inside node ind's local-variable block. */
2047 std::size_t map_var_slot(std::size_t ind, std::size_t r) const {
2048 std::size_t slot = 0;
2049 for (std::size_t j = 0; j + 1 < r && j < nvars[ind - 1].size(); ++j) slot += nvars[ind - 1][j];
2050 return slot;
2051 }
2052
2053 /**
2054 * 1-BASED index of the pointer of (ind, r) INSIDE the node's local-variable
2055 * block, or 0 when that pair does not dispatch round-robin.
2056 *
2057 * `sum(nvars(ind, 1:(R+class)))`, which counts over the phase columns first:
2058 * the reference indexes `space_var` with exactly that, and reading the
2059 * pointer as "the r-th trailing column" instead is wrong the moment a class
2060 * carries a modulating phase or only some classes dispatch.
2061 */
2062 std::size_t rr_var_slot(std::size_t ind, std::size_t r) const {
2063 const std::size_t R = classes.size();
2064 if (ind == 0 || ind > nodes.size() || r == 0 || r > R) return 0;
2065 const std::vector<RoutingStrategy>& rt_i = nodes[ind - 1].routing;
2066 if (rt_i.size() < r) return 0;
2067 if (rt_i[r - 1] != RoutingStrategy::RROBIN && rt_i[r - 1] != RoutingStrategy::WRROBIN)
2068 return 0;
2069 if (nvars.size() < ind) return 0;
2070 std::size_t slot = 0;
2071 for (std::size_t j = 0; j < R + r && j < nvars[ind - 1].size(); ++j)
2072 slot += nvars[ind - 1][j];
2073 return slot;
2074 }
2075
2076 /**
2077 * The destination node the pointer in VARROW names, or 0 when (ind, r) does
2078 * not dispatch round-robin or the pointer is out of range.
2079 *
2080 * @param varrow the node's local-variable columns, as `after_event` slices them
2081 */
2082 std::size_t rr_dest(std::size_t ind, std::size_t r, const std::vector<T>& varrow) const {
2083 const std::size_t slot = rr_var_slot(ind, r);
2084 if (slot == 0 || slot > varrow.size()) return 0;
2085 const long v = std::lround(num_traits<T>::to_double(varrow[slot - 1]));
2086 if (nodes[ind - 1].routing[r - 1] == RoutingStrategy::RROBIN)
2087 return v > 0 ? static_cast<std::size_t>(v) : 0;
2088 const std::vector<std::size_t> cycle = rr_weighted_outlinks(ind, r);
2089 if (v < 1 || static_cast<std::size_t>(v) > cycle.size()) return 0;
2090 return cycle[static_cast<std::size_t>(v) - 1];
2091 }
2092
2093 /**
2094 * Advance the pointer of (ind, r) in VARROW by one position, cyclically.
2095 *
2096 * A no-op where the pair does not dispatch round-robin, so a caller can run
2097 * it unconditionally on every departure.
2098 */
2099 void rr_advance(std::size_t ind, std::size_t r, std::vector<T>& varrow) const {
2100 const std::size_t slot = rr_var_slot(ind, r);
2101 if (slot == 0 || slot > varrow.size()) return;
2102 if (nodes[ind - 1].routing[r - 1] == RoutingStrategy::RROBIN) {
2103 const std::vector<std::size_t> ol = rr_outlinks(ind, r);
2104 if (ol.empty()) return;
2105 const long cur = std::lround(num_traits<T>::to_double(varrow[slot - 1]));
2106 std::size_t idx = ol.size(); // "not found" -> restart at the first
2107 for (std::size_t d = 0; d < ol.size(); ++d)
2108 if (static_cast<long>(ol[d]) == cur) { idx = d; break; }
2109 const std::size_t nxt = (idx + 1 < ol.size()) ? idx + 1 : 0;
2110 varrow[slot - 1] = num_traits<T>::from_int(static_cast<long>(ol[nxt]));
2111 return;
2112 }
2113 const std::vector<std::size_t> cycle = rr_weighted_outlinks(ind, r);
2114 if (cycle.empty()) return;
2115 const long pos = std::lround(num_traits<T>::to_double(varrow[slot - 1]));
2116 const long nxt = (pos < 1 || static_cast<std::size_t>(pos) >= cycle.size()) ? 1 : pos + 1;
2117 varrow[slot - 1] = num_traits<T>::from_int(nxt);
2118 }
2119
2120 /** Whether ANY (node, class) pair dispatches round-robin. */
2121 bool has_rr_routing() const {
2122 for (const NodeDef& n : nodes)
2123 for (RoutingStrategy rs : n.routing)
2124 if (rs == RoutingStrategy::RROBIN || rs == RoutingStrategy::WRROBIN) return true;
2125 return false;
2126 }
2127
2128 /**
2129 * Whether immediate feedback is EFFECTIVE anywhere in the model.
2130 *
2131 * A DECLARATION ALONE IS NOT ENOUGH. `Queue.setImmediateFeedback` marks a
2132 * station and `JobClass.setImmediateFeedback` marks a class -- and the class
2133 * spelling marks EVERY station, since `immfeed` is the OR of the two -- so a
2134 * model with no self-loop at all can carry a full `immfeed` matrix while the
2135 * feature changes nothing. Reading the raw matrix made every solver that
2136 * consults it warn, or refuse, on a plain M/M/1 that merely mentioned the flag.
2137 *
2138 * Immediate feedback is effective at (station i, class r) when `immfeed[i][r]`
2139 * holds AND the routing table has a self-loop INTO (i, r) from some class s at
2140 * the same station, which is the only way a job can come back to the server it
2141 * just left. A class switch on the way round is folded into `rt` by
2142 * `refresh_routing`, so the incoming class s need not be r.
2143 *
2144 * Solvers that HANDLE immediate feedback look at the SYNCHRONIZATION instead;
2145 * see `qn::immfeed_self_loop`, which applies the same test per sync.
2146 */
2148 bool declared = false;
2149 for (const std::vector<bool>& row : immfeed) {
2150 for (bool b : row)
2151 if (b) {
2152 declared = true;
2153 break;
2154 }
2155 if (declared) break;
2156 }
2157 if (!declared) return false;
2158 // Without a routing table there is nothing to qualify the declaration
2159 // with, so report it as declared rather than silently dropping it.
2160 if (rt.rows() == 0 || rt.cols() == 0) return true;
2161 const std::size_t R = nclasses;
2162 for (std::size_t ist = 1; ist <= immfeed.size(); ++ist) {
2163 const std::size_t ind = node_of_station(ist);
2164 if (ind == 0 || ind > nodes.size()) continue;
2165 const std::size_t isf = stateful_index(ind);
2166 if (isf == 0) continue;
2167 for (std::size_t r = 1; r <= immfeed[ist - 1].size() && r <= R; ++r) {
2168 if (!immfeed[ist - 1][r - 1]) continue;
2169 const std::size_t col = (isf - 1) * R + (r - 1);
2170 if (col >= rt.cols()) continue;
2171 for (std::size_t sc = 0; sc < R; ++sc) {
2172 const std::size_t row = (isf - 1) * R + sc;
2173 if (row < rt.rows() &&
2174 num_traits<T>::to_double(rt(row, col)) > 0)
2175 return true;
2176 }
2177 }
2178 }
2179 return false;
2180 }
2181 /** 1-based node index of station `st`, 0 when the map has no entry. */
2182 std::size_t node_of_station(std::size_t st) const {
2183 // A Layer built by the LQN path can carry fewer map entries than
2184 // stations; returning 0 says "no node" rather than reading past the end.
2185 if (st == 0 || st > station_to_node.size()) return 0;
2186 return station_to_node[st - 1];
2187 }
2188 /** 1-based stateful index of node `ind`, 0 when the node is not stateful. */
2189 std::size_t stateful_index(std::size_t ind) const {
2190 for (std::size_t k = 0; k < stateful_nodes.size(); ++k)
2191 if (stateful_nodes[k] == ind) return k + 1;
2192 return 0;
2193 }
2194
2195 std::size_t stateful_of_station(std::size_t st) const {
2196 const std::size_t nd = station_to_node[st - 1];
2197 for (std::size_t k = 0; k < stateful_nodes.size(); ++k)
2198 if (stateful_nodes[k] == nd) return k + 1;
2199 throw InputError("network: a station is not a stateful node");
2200 }
2201
2202 /** Add a station, which is also a node, and grow the service table. */
2203 std::size_t add_station(const Station<T>& st) {
2204 stations.push_back(st);
2205 service.emplace_back(classes.size(), Distrib<T>::disabled_dist());
2206 NodeDef nd;
2207 nd.name = st.name;
2208 nd.nodetype = st.nodetype;
2209 nd.stateful = true;
2210 nd.station = stations.size();
2211 nodes.push_back(nd);
2212 station_to_node.push_back(nodes.size());
2213 stateful_nodes.push_back(nodes.size());
2214 grow_routing();
2215 return stations.size();
2216 }
2217
2218 /** Add a non-station node (a Fork, a Router). Returns its 1-based index. */
2219 std::size_t add_node(const std::string& nm, NodeType ty, bool stateful) {
2220 NodeDef nd;
2221 nd.name = nm;
2222 nd.nodetype = ty;
2223 nd.stateful = stateful;
2224 nd.station = 0;
2225 nodes.push_back(nd);
2226 if (stateful) stateful_nodes.push_back(nodes.size());
2227 grow_routing();
2228 return nodes.size();
2229 }
2230
2231 /** Add a class and grow the service table. */
2232 std::size_t add_class(const JobClass& cl) {
2233 classes.push_back(cl);
2234 for (auto& row : service) row.emplace_back(Distrib<T>::disabled_dist());
2235 return classes.size();
2236 }
2237
2238 void set_service(std::size_t station, std::size_t cls, const Distrib<T>& d) {
2239 service[station - 1][cls - 1] = d;
2240 }
2241
2242 /** P{r,s}(i,j) = p, with 1-based NODE and class indices. */
2243 void set_route(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T& p) {
2244 auto key = std::make_pair(r, s);
2245 auto it = P.find(key);
2246 if (it == P.end())
2247 it = P.emplace(key, Matrix<T>(nodes.size(), nodes.size(), num_traits<T>::from_int(0)))
2248 .first;
2249 if (it->second.rows() != nodes.size()) grow_block(it->second);
2250 it->second(i - 1, j - 1) = p;
2251 }
2252
2253 /**
2254 * Write into the routing the consumers actually read.
2255 *
2256 * The Sink -> Source closure is derived by the refresh, not given by the
2257 * user, so it belongs in the effective routing; writing it into P alone
2258 * would make it invisible on any model whose routing was expanded.
2259 */
2260 void set_route_effective(std::size_t r, std::size_t s, std::size_t i, std::size_t j,
2261 const T& p) {
2262 if (Peff.empty()) {
2263 set_route(r, s, i, j, p);
2264 return;
2265 }
2266 auto key = std::make_pair(r, s);
2267 auto it = Peff.find(key);
2268 if (it == Peff.end())
2269 it = Peff.emplace(key, Matrix<T>(nodes.size(), nodes.size(), num_traits<T>::from_int(0)))
2270 .first;
2271 if (it->second.rows() != nodes.size()) {
2272 Matrix<T> g(nodes.size(), nodes.size(), num_traits<T>::from_int(0));
2273 for (std::size_t a = 0; a < it->second.rows(); ++a)
2274 for (std::size_t b = 0; b < it->second.cols(); ++b) g(a, b) = it->second(a, b);
2275 it->second = g;
2276 }
2277 it->second(i - 1, j - 1) = p;
2278 }
2279
2280 /**
2281 * P{r,s}(i,j), AS THE USER SET IT.
2282 *
2283 * Every consumer of the routing reads route_eff() instead, which is this
2284 * matrix once refresh_routing() has expanded the strategies that are not
2285 * literal probabilities. The two agree on a model whose routing is entirely
2286 * PROB, which is every model the LN layer builder produces.
2287 */
2288 T get_route(std::size_t r, std::size_t s, std::size_t i, std::size_t j) const {
2289 auto it = P.find(std::make_pair(r, s));
2290 if (it == P.end() || it->second.rows() < nodes.size()) return num_traits<T>::from_int(0);
2291 return it->second(i - 1, j - 1);
2292 }
2293
2294 /** The routing actually in force: the expansion when there is one, else P. */
2295 T route_eff(std::size_t r, std::size_t s, std::size_t i, std::size_t j) const {
2296 if (Peff.empty()) return get_route(r, s, i, j);
2297 auto it = Peff.find(std::make_pair(r, s));
2298 if (it == Peff.end() || it->second.rows() < nodes.size()) return num_traits<T>::from_int(0);
2299 return it->second(i - 1, j - 1);
2300 }
2301
2302 // -----------------------------------------------------------------------
2303 // Refresh
2304 // -----------------------------------------------------------------------
2305
2306 /**
2307 * The whole chain, in MATLAB's refreshStruct order.
2308 *
2309 * The order is load bearing: the routing expansion must precede the chains
2310 * (they are read off the class-switch structure of the routing), the chains
2311 * must precede the capacities (a station's buffer is bounded by the
2312 * population of the chains that reach it), and the visits must precede
2313 * nothing but must follow the sink closure, which needs the chains.
2314 */
2317 // A struct built any other way than through set_patience (direct field writes) is caught here.
2319 line::util::LineConsole::compile_detail("refreshing service and arrival processes");
2321 refresh_rates();
2323 // BEFORE the routing table: a round-robin Router must be STATEFUL, or
2324 // the stochastic complement that builds `rt` erases it and the pointer
2325 // has nowhere to live.
2327 line::util::LineConsole::compile_detail("computing the routing table");
2329 line::util::LineConsole::compile_detail("computing the chains and the visit ratios");
2332 "found %s over %s",
2333 line::util::LineConsole::plural(static_cast<long>(this->nchains), "chain", "chains").c_str(),
2334 line::util::LineConsole::plural(static_cast<long>(this->classes.size()), "class", "classes").c_str());
2336 refresh_rt();
2337 line::util::LineConsole::compile_detail("refreshing node parameters and state-dependent routing");
2340 // Needs the visit ratios, so it runs after refresh_chains.
2342 }
2343
2344 /** Refuses RENEGING declared for a class of finite population at any station (`closed_reneging_refusal`). */
2346 for (std::size_t i = 0; i < stations.size(); ++i)
2347 for (std::size_t r = 0; r < stations[i].impatience.size() && r < classes.size(); ++r)
2348 if (stations[i].impatience[r] == lang::ImpatienceType::RENEGING &&
2349 std::isfinite(classes[r].population))
2350 closed_reneging_refusal(classes[r].name, " at queue \"" + stations[i].name + "\"");
2351 }
2352
2353 /**
2354 * True when node `ind` (1-based) holds a server across a synchronous call
2355 * whose reply class is `r` (1-based), i.e. `r` returns here to release a
2356 * server rather than to be served by one.
2357 */
2358 bool holds_reply_for(std::size_t ind, std::size_t r) const {
2359 if (ind == 0 || ind > replyblock.size()) return false;
2360 for (std::size_t k = 0; k < syncreply.size(); ++k)
2361 if (syncreply[k] == r && k < replyblock[ind - 1].size() && replyblock[ind - 1][k])
2362 return true;
2363 return false;
2364 }
2365
2366 /**
2367 * Whether a job of class `r` (0-based) can LEAVE node `ind` (1-based) again.
2368 *
2369 * True for anything that is not a service station, and for a station that
2370 * serves `r`, declares heterogeneous server types (its per-class process is
2371 * disabled by construction there) or holds a server across a synchronous
2372 * call whose reply class is `r`. False only for the flow sink itself, which
2373 * is what stops the walk in `reached_node_classes` -- the same three
2374 * exemptions the guard applies, kept in one place so the walk and the
2375 * verdict cannot drift apart.
2376 */
2377 bool serves_class(std::size_t ind, std::size_t r) const {
2378 if (ind == 0 || ind > nodes.size()) return true;
2379 const NodeType nt = nodes[ind - 1].nodetype;
2380 if (nt != NodeType::Queue && nt != NodeType::Delay) return true;
2381 const std::size_t sti = nodes[ind - 1].station;
2382 if (sti == 0 || sti > stations.size()) return true;
2383 if (!stations[sti - 1].server_types.empty()) return true;
2384 if (procid(sti, r + 1) != ProcessType::DISABLED) return true;
2385 return holds_reply_for(ind, r + 1);
2386 }
2387
2388 /**
2389 * The (node, class) pairs a job can actually ARRIVE at, 0-based on both axes.
2390 *
2391 * A forward walk of `rtnodes` from the feed points. See
2392 * `check_service_reachable` for why the evidence is the UNMASKED routing
2393 * kernel rather than `nodevisits`, and for the three rules -- absorbing
2394 * Sink, Source expanded only as a seed, unservable pair reached but not
2395 * expanded -- that keep the walk from over-approximating.
2396 *
2397 * Returns an empty vector when `rtnodes` is not the expected (N*K) square,
2398 * which leaves the caller checking nothing, exactly as before.
2399 */
2400 std::vector<std::vector<bool>> reached_node_classes() const {
2401 const std::size_t N = nodes.size(), K = nclasses;
2402 std::vector<std::vector<bool>> reached;
2403 if (N == 0 || K == 0) return reached;
2404 if (rtnodes.rows() < N * K || rtnodes.cols() < N * K) return reached;
2405 reached.assign(N, std::vector<bool>(K, false));
2406 std::vector<std::vector<bool>> seed(N, std::vector<bool>(K, false));
2407 for (std::size_t i = 0; i < stations.size(); ++i) {
2408 if (stations[i].nodetype != NodeType::Source) continue;
2409 const std::size_t ind = node_of_station(i + 1);
2410 if (ind == 0 || ind > N) continue;
2411 for (std::size_t r = 0; r < K; ++r)
2412 if (procid(i + 1, r + 1) != ProcessType::DISABLED) seed[ind - 1][r] = true;
2413 }
2414 for (std::size_t r = 0; r < K && r < classes.size(); ++r) {
2415 const double pop = classes[r].population;
2416 if (!std::isfinite(pop) || pop <= 0.0) continue;
2417 const std::size_t ind = node_of_station(classes[r].refstat);
2418 if (ind == 0 || ind > N) continue;
2419 seed[ind - 1][r] = true;
2420 }
2421 std::vector<std::pair<std::size_t, std::size_t>> stack;
2422 for (std::size_t i = 0; i < N; ++i)
2423 for (std::size_t r = 0; r < K; ++r)
2424 if (seed[i][r]) {
2425 reached[i][r] = true;
2426 stack.push_back(std::make_pair(i, r));
2427 }
2428 while (!stack.empty()) {
2429 const std::size_t i = stack.back().first;
2430 const std::size_t r = stack.back().second;
2431 stack.pop_back();
2432 const NodeType nt = nodes[i].nodetype;
2433 if (nt == NodeType::Sink) continue;
2434 if (nt == NodeType::Source && !seed[i][r]) continue;
2435 if (!serves_class(i + 1, r)) continue;
2436 const std::size_t row = i * K + r;
2437 for (std::size_t col = 0; col < N * K; ++col) {
2438 if (num_traits<T>::to_double(rtnodes(row, col)) <=
2440 continue;
2441 const std::size_t j = col / K, sIdx = col % K;
2442 if (!reached[j][sIdx]) {
2443 reached[j][sIdx] = true;
2444 stack.push_back(std::make_pair(j, sIdx));
2445 }
2446 }
2447 }
2448 return reached;
2449 }
2450
2451 /**
2452 * Refuses a class that is ROUTED TO a station which cannot serve it.
2453 *
2454 * The reference's `sanitize` disables the OUTGOING routing of a class a
2455 * station cannot serve, which is what keeps it out of that station's visit
2456 * ratios -- but nothing stopped the class being routed IN, and a class that
2457 * arrives where it cannot be served is a flow sink: it enters and never
2458 * leaves. The station-level guard next to it cannot see this, because it
2459 * asks whether the station serves ANY class, not whether it serves the
2460 * classes that reach it.
2461 *
2462 * Unguarded, such a model gets a different silent wrong answer from each of
2463 * MVA, CTMC and SSA.
2464 *
2465 * READS `rtnodes`, WALKED FORWARD FROM THE FEED POINTS. Do not route this
2466 * guard through `nodevisits` or `visits`: both carry the `served` mask,
2467 * which zeroes exactly the (station, class) cell a flow sink shows up in,
2468 * so a masked visit vector cannot answer this question. See
2469 * _kb/07-cross-language-parity.md.
2470 *
2471 * `rtnodes` on its own over-approximates -- it says where a class WOULD go
2472 * if one existed -- and the WALK is what removes the slack. It starts only
2473 * at (Source, class) pairs whose arrival process is not DISABLED, and at the
2474 * reference station of each closed class with a positive population, so the
2475 * disabled-arrival row a class-switching Source carries is never entered.
2476 * Three rules keep it honest: a Sink is ABSORBING (`rtnodes` wraps it back
2477 * to the Source to close the kernel, and following that wrap re-enters every
2478 * Source row, including the disabled ones the seeding just excluded); a
2479 * Source is expanded ONLY AS A SEED, for the same reason; and an unservable
2480 * (station, class) is REACHED BUT NOT EXPANDED, since nothing leaves it --
2481 * that is the whole complaint -- so nothing downstream of it is evidence of
2482 * anything.
2483 *
2484 * A chain no job can enter has no seed, so its rows are never walked.
2485 */
2486 /**
2487 * A signal spends no time at the queue it reaches: it acts on arrival and leaves, so an unset slot is
2488 * Immediate. Left Disabled, the routed-without-service check demanded a service no solver reads.
2489 * MATLAB sanitize, the JAR and python fill the same slot.
2490 */
2492 for (std::size_t i = 0; i < nstations; ++i) {
2493 const NodeType nt = stations[i].nodetype;
2494 if (nt != NodeType::Queue && nt != NodeType::Delay) continue;
2495 for (std::size_t r = 0; r < classes.size() && r < issignal.size(); ++r)
2496 if (issignal[r] && procid(i + 1, r + 1) == ProcessType::DISABLED)
2497 set_service(i + 1, r + 1, Distrib<T>::immediate());
2498 }
2499 }
2500
2502 if (nclasses == 0) return;
2503 // Same exemption as the reference's sanitize checks: a station of a
2504 // cache, Petri-net or fork-join model legitimately carries no per-class
2505 // service.
2506 for (std::size_t ind = 0; ind < nodes.size(); ++ind) {
2507 const NodeType nt = nodes[ind].nodetype;
2508 if (nt == NodeType::Cache || nt == NodeType::Place ||
2509 nt == NodeType::Transition || nt == NodeType::Fork ||
2510 nt == NodeType::Join)
2511 return;
2512 }
2513 const std::size_t K = nclasses;
2514 const std::vector<std::vector<bool>> reached = reached_node_classes();
2515 if (reached.empty()) return;
2516 for (std::size_t i = 0; i < nstations; ++i) {
2517 const NodeType nt = stations[i].nodetype;
2518 if (nt != NodeType::Queue && nt != NodeType::Delay) continue;
2519 // A heterogeneous pool carries its service on the server types, so
2520 // the per-class process is legitimately disabled there. Skipped in
2521 // all four codebases.
2522 if (!stations[i].server_types.empty()) continue;
2523 const std::size_t ind = node_of_station(i + 1);
2524 if (ind == 0) continue;
2525 for (std::size_t r = 0; r < K; ++r) {
2526 if (procid(i + 1, r + 1) != ProcessType::DISABLED) continue;
2527 // A SYNCHRONOUS REPLY is not served by the station it returns
2528 // to: it releases the server that station held across the call,
2529 // which is the whole content of set_sync_reply. Its Disabled
2530 // service there is the marker of the feature, not a flow sink.
2531 if (holds_reply_for(ind, r + 1)) continue;
2532 if (ind - 1 >= reached.size() || r >= reached[ind - 1].size()) continue;
2533 if (!reached[ind - 1][r]) continue;
2534 const std::string kind = (nt == NodeType::Delay) ? "Delay" : "Queue";
2535 throw InputError(kind + " '" + stations[i].name +
2536 "' has no service configured for job class '" +
2537 classes[r].name +
2538 "', but the class is routed to it. Jobs would arrive and "
2539 "never leave. Configure a service for that class, or route "
2540 "it elsewhere.");
2541 }
2542 }
2543 }
2544
2545 /**
2546 * Port of the `sn.immfeed` block of `@@MNetwork/refreshStruct.m`.
2547 *
2548 * The station's own per-class setting OR the class-level one, which is the
2549 * reference's `stationHas || classHas`. Sized here rather than in
2550 * `refresh_rates` because it is a property of the model's topology and not
2551 * of the service processes, so it must not be cleared when only the rates
2552 * are re-derived (SolverLN calls `refresh_rates` per layer, per iteration).
2553 */
2555 const std::size_t M = stations.size(), R = classes.size();
2556 immfeed.assign(M, std::vector<bool>(R, false));
2557 for (std::size_t i = 0; i < M; ++i)
2558 for (std::size_t r = 0; r < R; ++r) {
2559 const bool station_has =
2560 r < stations[i].immfeed.size() && stations[i].immfeed[r];
2561 immfeed[i][r] = station_has || classes[r].immfeed;
2562 }
2563 }
2564
2565 /**
2566 * Make node `ind` stateful, keeping `stateful_nodes` ascending. Idempotent.
2567 *
2568 * Used by the fork-join transforms: the Router they put where a Fork stood
2569 * is a `Router` OBJECT in the reference, hence a `StatefulNode`, and it has
2570 * to stay one here. Its chain block is deliberately NOT stochastic -- an
2571 * auxiliary class keeps the OTHER fork's full fan-out, so that row sums to
2572 * the fan-out -- and on such a block the stochastic complement is not
2573 * visit-preserving, so complementing the node away moves every downstream
2574 * visit ratio off the reference's.
2575 */
2576 void promote_stateful(std::size_t ind) {
2577 if (ind == 0 || ind > nodes.size() || nodes[ind - 1].stateful) return;
2578 nodes[ind - 1].stateful = true;
2579 stateful_nodes.insert(std::lower_bound(stateful_nodes.begin(), stateful_nodes.end(), ind),
2580 ind);
2581 }
2582
2583 /**
2584 * A Router that DISPATCHES (round-robin, JSQ or SQ(d)) holds state, so it must
2585 * survive the stochastic complement that removes the stateless nodes: a
2586 * pointer for round-robin, and for JSQ/SQ a departure the generator can flag
2587 * as state dependent, which a complemented-away node would not have.
2588 *
2589 * The reference makes EVERY Router stateful (`refreshStruct.m:155`); this
2590 * port promotes only the dispatching ones, because a PROB or RAND Router
2591 * genuinely holds nothing and complementing it away is both correct and
2592 * cheaper -- it keeps the state space of every existing model unchanged.
2593 * The promotion happens at refresh rather than at `add_router`, since
2594 * `set_routing` is called after the node exists.
2595 */
2597 for (std::size_t ind = 1; ind <= nodes.size(); ++ind) {
2598 NodeDef& nd = nodes[ind - 1];
2599 if (nd.stateful || nd.nodetype != NodeType::Router) continue;
2600 bool dispatch = false;
2601 for (std::size_t r = 0; r < nd.routing.size(); ++r)
2602 if (nd.routing[r] == RoutingStrategy::RROBIN ||
2603 nd.routing[r] == RoutingStrategy::WRROBIN ||
2604 nd.routing[r] == RoutingStrategy::JSQ || nd.routing[r] == RoutingStrategy::SQ)
2605 dispatch = true;
2606 if (!dispatch) continue;
2607 nd.stateful = true;
2608 stateful_nodes.insert(
2609 std::lower_bound(stateful_nodes.begin(), stateful_nodes.end(), ind), ind);
2610 }
2611 }
2612
2613 /**
2614 * Port of MNetwork.refreshLocalVars: the per-node local-variable widths.
2615 *
2616 * A column is reserved only where the model actually needs one, because the
2617 * width is part of the state encoding -- a spurious column widens every
2618 * state row and makes it unmatchable against the enumerated space.
2619 */
2621 const std::size_t R = classes.size();
2622 // One entry per class, 0 = none, as MATLAB's -ones(nclasses,1): a class added after the last binding
2623 // (LQN2QN's Ph2End destructors) or a model with no binding at all would otherwise leave it short
2624 syncreply.resize(R, 0);
2626 nvars.assign(nodes.size(), std::vector<std::size_t>(3 * R + 1, 0));
2627 for (std::size_t ind = 1; ind <= nodes.size(); ++ind) {
2628 const std::size_t ist = nodes[ind - 1].station;
2629 // A Markov-modulated process restarts from the phase it was left
2630 // in, so that phase has to survive in the state between services.
2631 if (ist != 0)
2632 for (std::size_t r = 1; r <= R; ++r) {
2633 const ProcessType pt = procid(ist, r);
2634 if (pt == ProcessType::MAP || pt == ProcessType::MMPP2)
2635 nvars[ind - 1][r - 1] += 1;
2636 }
2637 // Round-robin routing is stateful: the pointer to the next outgoing
2638 // link is what makes it deterministic rather than random.
2639 const std::vector<RoutingStrategy>& rt_i = nodes[ind - 1].routing;
2640 for (std::size_t r = 1; r <= R && r <= rt_i.size(); ++r)
2641 if (rt_i[r - 1] == RoutingStrategy::RROBIN ||
2642 rt_i[r - 1] == RoutingStrategy::WRROBIN)
2643 nvars[ind - 1][R + r - 1] += 1;
2644 // The shared node block. A Cache stores its list contents here;
2645 // the BAS marker, the breakdown status and the polling controller
2646 // claim the same column, which is why the reference rejects those
2647 // combinations instead of widening the state.
2648 // The polling controller shares the node block with the cache,
2649 // the BAS marker and the breakdown status, which is why those
2650 // combinations are rejected rather than the state widened.
2651 // The SAME resolution polling_info reads, or its offset misses nvars.
2652 if (ist != 0 && stations[ist - 1].sched == SchedStrategy::POLLING) {
2653 const PollingParam pp = effective_polling(ist);
2654 bool anysw = false;
2655 for (std::size_t r = 0; r < pp.switchover.size(); ++r) {
2656 const lang::Distrib<T>& d = pp.switchover[r];
2657 if (!d.disabled && d.D0.rows() > 0 && d.type != ProcessType::IMMEDIATE)
2658 anysw = true;
2659 }
2660 std::size_t w = anysw ? 2 : 0; // pos and swk
2661 if (pp.ptype != lang::PollingType::EXHAUSTIVE) w += 1; // ctr
2662 nvars[ind - 1][2 * R] = w;
2663 }
2664 // The REPLY block: one counter column per (node, calling class).
2665 for (std::size_t r = 1; r <= R; ++r)
2666 if (replyblock.size() >= ind && replyblock[ind - 1].size() >= r &&
2667 replyblock[ind - 1][r - 1])
2668 nvars[ind - 1][2 * R + r] = 1;
2669 // The SERVER STATUS of a station that breaks down (1 = up, 0 = down),
2670 // `refreshLocalVars.m`. It takes the shared node block, so a polling
2671 // controller there is refused as the reference refuses it; the BAS
2672 // marker is refused by `refresh_bas_blocking`. The status must be
2673 // the TRAILING column, which is where the DEP, PHASE, FAILURE and
2674 // REPAIR handlers read it, so a reply block (whose counters trail
2675 // the node block in this port) is refused too.
2676 if (ist != 0 && breakdownparam.find(ist) != breakdownparam.end()) {
2677 if (stations[ist - 1].sched == SchedStrategy::POLLING)
2678 throw UnsupportedError(ErrorKind{"Breakdown"},
2679 "Server breakdowns are not supported at a polling station (" +
2680 nodes[ind - 1].name +
2681 "): the polling controller and the breakdown status share one "
2682 "local-state column.");
2683 for (std::size_t r = 1; r <= R; ++r)
2684 if (replyblock.size() >= ind && replyblock[ind - 1].size() >= r &&
2685 replyblock[ind - 1][r - 1])
2686 throw UnsupportedError(ErrorKind{"Breakdown"},
2687 "Server breakdowns are not supported at station '" +
2688 nodes[ind - 1].name +
2689 "', which also holds servers for a synchronous reply: the reply "
2690 "counters would trail the breakdown status the service handlers read");
2691 nvars[ind - 1][2 * R] = 1;
2692 }
2693 const typename std::map<std::size_t, CacheParam<T>>::const_iterator ci =
2694 nodeparam.find(ind);
2695 if (ci != nodeparam.end()) {
2696 // Contents, plus block A (a per-item occupancy bitmap) and block
2697 // B (a per-retrieval-class count of merged secondary requests)
2698 // when a delayed-hit retrieval system is attached.
2699 std::size_t w = 0;
2700 for (std::size_t u = 0; u < ci->second.itemcap.size(); ++u)
2701 if (ci->second.itemcap[u] > 0)
2702 w += static_cast<std::size_t>(ci->second.itemcap[u]);
2703 if (ci->second.retrieval_capacity > 0) {
2704 w += ci->second.nitems;
2705 std::vector<std::size_t> rcl, rci, rco;
2706 cache_retrieval_class_map(ci->second, rcl, rci, rco);
2707 w += rcl.size();
2708 }
2709 nvars[ind - 1][2 * R] = w;
2710 }
2711 }
2713 }
2714
2715 /**
2716 * `sn.replyblock`, DERIVED from `sn.syncreply` and the routing.
2717 *
2718 * Port of `refreshLocalVars.m:340-386`. A server is held wherever the REPLY
2719 * class can arrive: every node that is a station, is not the Source, and is
2720 * not an infinite server -- an INF station has a server per job, so holding
2721 * one is immaterial and needs no state. Every such station must be FCFS,
2722 * because a held server is encoded as a per-class COUNTER, which is exact
2723 * only where the servers are interchangeable.
2724 *
2725 * Derived rather than declared because the model layer carries only the
2726 * class-to-class binding (`JobClass.setReplySignalClass`), and a caller
2727 * naming the nodes itself would silently disagree with the reference on any
2728 * model whose routing sends the reply somewhere it did not think of.
2729 */
2731 const std::size_t R = classes.size(), N = nodes.size();
2732 bool any = false;
2733 for (std::size_t r = 0; r < R && r < syncreply.size(); ++r)
2734 if (syncreply[r] >= 1 && syncreply[r] <= R) any = true;
2735 if (!any) return;
2736 replyblock.assign(N, std::vector<bool>(R, false));
2737 for (std::size_t r = 1; r <= R; ++r) {
2738 if (r > syncreply.size()) break;
2739 const std::size_t s = syncreply[r - 1];
2740 if (s < 1 || s > R) continue;
2741 for (std::size_t ind = 1; ind <= N; ++ind) {
2742 const std::size_t ist = nodes[ind - 1].station;
2743 if (ist == 0 || nodes[ind - 1].nodetype == lang::NodeType::Source) continue;
2744 if (stations[ist - 1].sched == SchedStrategy::INF) continue;
2745 bool arrives_here = false;
2746 for (std::size_t i = 1; i <= N && !arrives_here; ++i)
2747 for (std::size_t q = 1; q <= R; ++q)
2748 if (rtnodes.rows() >= N * R &&
2750 rtnodes((i - 1) * R + q - 1, (ind - 1) * R + s - 1)) > 0) {
2751 arrives_here = true;
2752 break;
2753 }
2754 if (!arrives_here) continue;
2755 if (stations[ist - 1].sched != SchedStrategy::FCFS)
2756 throw InputError(
2757 "network '" + name +
2758 "': synchronous calls (REPLY signals) are supported only at FCFS "
2759 "stations, but '" +
2760 nodes[ind - 1].name +
2761 "' uses another discipline. A held server is encoded as a per-class "
2762 "counter, which is exact only where servers are interchangeable (FCFS) "
2763 "or unlimited (INF); the other disciplines are not yet encoded rather than "
2764 "infeasible. Set this station to FCFS or INF, or simulate the layered model "
2765 "directly with SolverLDES.");
2766 replyblock[ind - 1][r - 1] = true;
2767 }
2768 }
2769 }
2770
2771 /**
2772 * The nodes directly downstream of `ind`, walking THROUGH stateless nodes
2773 * and stopping at the first station on each path.
2774 *
2775 * `downstreamStations` in `refreshLocalVars.m`. A blocked job is held for its
2776 * IMMEDIATE destination, so the walk stops at the first station: a Router or
2777 * ClassSwitch in between is a routing decision, not a place to wait.
2778 *
2779 * The reference reads `sn.connmatrix`; this port has no such field and reads
2780 * `rtnodes` instead, which is the same graph after the refresh has resolved
2781 * the routing strategies. The two differ only on a link the model declares
2782 * and then routes zero mass over, and such a link cannot fill a destination,
2783 * so it cannot block anything either.
2784 */
2785 std::vector<std::size_t> downstream_stations(std::size_t ind) const {
2786 const std::size_t R = classes.size();
2787 const std::size_t N = nodes.size();
2788 std::vector<std::size_t> out;
2789 if (rtnodes.rows() < N * R) return out;
2790 std::vector<bool> seen(N + 1, false);
2791 std::vector<std::size_t> frontier(1, ind);
2792 seen[ind] = true;
2793 while (!frontier.empty()) {
2794 const std::size_t i = frontier.back();
2795 frontier.pop_back();
2796 for (std::size_t j = 1; j <= N; ++j) {
2797 if (seen[j]) continue;
2798 bool linked = false;
2799 for (std::size_t r = 0; r < R && !linked; ++r)
2800 for (std::size_t s = 0; s < R && !linked; ++s)
2802 rtnodes((i - 1) * R + r, (j - 1) * R + s)) > 0)
2803 linked = true;
2804 if (!linked) continue;
2805 seen[j] = true;
2806 if (nodes[j - 1].station != 0) {
2807 out.push_back(j); // a station terminates this path
2808 } else {
2809 frontier.push_back(j); // walk through the stateless node
2810 }
2811 }
2812 }
2813 return out;
2814 }
2815
2816 /**
2817 * Port of `refreshLocalVars`' true-BAS block and its `declaresBlockedMarker`
2818 * helper: which nodes carry the blocked marker, and where a refusal blocks.
2819 */
2821 const std::size_t R = classes.size();
2822 const std::size_t N = nodes.size();
2823 isbasblocking.assign(N, false);
2824 isbasdestination.assign(stations.size(), std::vector<bool>(R, false));
2825 if (droprule.empty()) return;
2826 for (std::size_t ind = 1; ind <= N; ++ind) {
2827 const std::size_t ist = nodes[ind - 1].station;
2828 if (ist == 0) continue;
2829 const NodeType nt = nodes[ind - 1].nodetype;
2830 if (nt == NodeType::Source || nt == NodeType::Cache) continue;
2831 const std::vector<std::size_t> dests = downstream_stations(ind);
2832 bool declares = false;
2833 for (std::size_t r = 1; r <= R; ++r) {
2834 const bool here_bas = droprule.size() >= ist && droprule[ist - 1].size() >= r &&
2835 droprule[ist - 1][r - 1] == DropStrategy::BAS;
2836 for (std::size_t d = 0; d < dests.size(); ++d) {
2837 const std::size_t jst = nodes[dests[d] - 1].station;
2838 if (jst == 0 || nodes[dests[d] - 1].nodetype == NodeType::Source) continue;
2839 // The destination must be able to FILL. An unbounded queue
2840 // never refuses, so nothing upstream of it can ever block,
2841 // and reserving the marker would widen the state for nothing.
2842 //
2843 // THE DECLARED CAPACITY, not the refreshed `cap`. The refresh
2844 // clamps an unbounded station to the total closed population,
2845 // which is finite, so reading `cap` here would find every
2846 // destination in a closed model "able to fill" and reserve a
2847 // marker on every upstream station. The reference reads
2848 // `dnode.cap`, the value the user set.
2849 const double dcap = stations[jst - 1].cap;
2850 if (!std::isfinite(dcap) || !(dcap > 0)) continue;
2851 const bool there_bas =
2852 droprule.size() >= jst && droprule[jst - 1].size() >= r &&
2853 droprule[jst - 1][r - 1] == DropStrategy::BAS;
2854 if (!here_bas && !there_bas) continue;
2855 // Do NOT stop at the first hit: every such destination has to
2856 // be recorded, since a refusal at ANY of them must block here
2857 // rather than drop.
2858 declares = true;
2859 isbasdestination[jst - 1][r - 1] = true;
2860 }
2861 }
2862 if (!declares) continue;
2863 // The marker shares nvars column 2R+1 with the cache contents, the
2864 // breakdown status and the polling controller, so the reference
2865 // rejects the combinations rather than widening the state.
2866 if (breakdownparam.find(ist) != breakdownparam.end())
2867 throw UnsupportedError(ErrorKind{"Breakdown"},
2868 "station '" + nodes[ind - 1].name +
2869 "' combines server breakdowns with true-BAS blocking: the breakdown status "
2870 "and the BAS blocked marker share one local-state column. Remove the BAS drop "
2871 "rule or the breakdown");
2872 if (stations[ist - 1].sched == SchedStrategy::POLLING)
2873 throw UnsupportedError(ErrorKind{"SchedStrategy_POLLING"},
2874 "true BAS blocking is not supported at the polling station '" +
2875 nodes[ind - 1].name +
2876 "': the polling controller and the BAS blocked marker share one local-state "
2877 "column. Use a non-polling discipline at the blocking station, or remove the "
2878 "BAS drop rule");
2879 for (std::size_t r = 1; r <= R; ++r)
2880 if (replyblock.size() >= ind && replyblock[ind - 1].size() >= r &&
2881 replyblock[ind - 1][r - 1])
2882 throw UnsupportedError(ErrorKind{"SyncCall"},
2883 "true BAS blocking is not supported at station '" + nodes[ind - 1].name +
2884 "', which also holds servers for a synchronous reply: fromMarginal appends "
2885 "the reply counters AFTER the blocked marker, so the marker would no "
2886 "longer be the trailing column the departure handler reads");
2887 nvars[ind - 1][2 * R] = 1;
2888 isbasblocking[ind - 1] = true;
2889 }
2890 }
2891
2892 /**
2893 * Port of MNetwork.refreshScheduling's schedparam half.
2894 *
2895 * DPS and GPS take a per-class weight, defaulting to 1; SEPT and LEPT take
2896 * the rank of the class's mean service time among the distinct means, which
2897 * is what the reference computes here rather than at the solver.
2898 *
2899 * EVERY OTHER DISCIPLINE DEFAULTS TO 1, NOT 0. Measured against MATLAB
2900 * R2025a: FCFS, PS, LPS, SIRO, LCFS, HOL and SJF all report schedparam 1
2901 * per class on a two-class model, and only SEPT/LEPT differ. Defaulting to
2902 * zero here made every consumer that normalizes by the weight total divide
2903 * 0/0: a load-dependent PS station reported Util exactly 0 against the
2904 * reference's 0.400822578299582, and `ctmc_signal_busy` shares the pattern.
2905 */
2907 const T one = num_traits<T>::from_int(1);
2908 for (std::size_t i = 0; i < stations.size(); ++i) {
2909 Station<T>& st = stations[i];
2910 if (st.sched == SchedStrategy::DPS || st.sched == SchedStrategy::GPS) {
2911 if (st.schedparam.size() != classes.size())
2912 st.schedparam.assign(classes.size(), one);
2913 continue;
2914 }
2915 if (st.sched == SchedStrategy::SEPT || st.sched == SchedStrategy::LEPT) {
2916 if (st.schedparam.size() == classes.size()) continue;
2917 std::vector<double> means;
2918 for (std::size_t r = 0; r < classes.size(); ++r)
2919 means.push_back(num_traits<T>::to_double(service[i][r].mean));
2920 std::vector<double> sorted = means;
2921 std::sort(sorted.begin(), sorted.end());
2922 sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end());
2923 if (st.sched == SchedStrategy::LEPT)
2924 std::reverse(sorted.begin(), sorted.end());
2925 st.schedparam.assign(classes.size(), num_traits<T>::from_int(0));
2926 for (std::size_t r = 0; r < classes.size(); ++r)
2927 for (std::size_t k = 0; k < sorted.size(); ++k)
2928 if (sorted[k] == means[r])
2929 st.schedparam[r] = num_traits<T>::from_int(static_cast<long>(k) + 1);
2930 continue;
2931 }
2932 if (st.schedparam.empty()) st.schedparam.assign(classes.size(), one);
2933 }
2934 }
2935
2936 /**
2937 * Port of the part of MNetwork.refreshRoutingMatrix this port reaches: the
2938 * expansion of a routing STRATEGY into the probabilities `rt` is built from.
2939 *
2940 * PROB is already probabilities and is copied through. RAND and RROBIN
2941 * spread the mass uniformly over the nodes the user connected this one to,
2942 * which is what MATLAB's `RoutingStrategy.RAND` means once `link` has
2943 * recorded the connections, and what `getRoutingMatrix.m:117` does for both
2944 * in the same branch: a round-robin pointer visits every outgoing link
2945 * equally often, so the ROUTING PROBABILITIES it induces are uniform and
2946 * only the higher moments of the split are deterministic. Recovering that
2947 * determinism is the consumer's job -- `npfqn_traffic_split_rr` gives QNA
2948 * and MNA the split degree, and a solver that needs the pointer itself must
2949 * carry it in the state. A solver that does neither must not declare
2950 * `RoutingStrategy_RROBIN` in its feature set, or it answers a random-
2951 * routing model under a round-robin name.
2952 * A ClassSwitch node's outgoing mass is multiplied by its
2953 * class-switch matrix, so a job leaving it in class r continues as class s
2954 * with probability C(r,s) -- the node itself is not stateful, and the
2955 * stochastic complement folds it into the edges around it.
2956 *
2957 * WRROBIN spreads by its weights, and JSQ, SQ and SDR uniformly: for those
2958 * three the expansion is a PLACEHOLDER whose support is right and whose
2959 * values are not, and a solver that declares them re-reads the row at every
2960 * state from `rt_state`. Any other strategy (FIRING, say) is REFUSED by
2961 * name: silently treating one as PROB returns a product-form answer for a
2962 * model that does not have one.
2963 */
2965 const T zero = num_traits<T>::from_int(0);
2966 const std::size_t K = classes.size(), I = nodes.size();
2967 Peff.clear();
2968 bool trivial = true;
2969 for (const NodeDef& nd : nodes)
2970 for (RoutingStrategy rs : nd.routing)
2971 if (rs != RoutingStrategy::PROB && rs != RoutingStrategy::DISABLED) trivial = false;
2972 // A (node, class) PAIR THE CALLER NEVER ROUTED IS NOT TRIVIAL EITHER,
2973 // because the reference FILLS it -- see the unrouted branch below. This
2974 // port has no DISABLED strategy to key on (every pair defaults to PROB),
2975 // so the condition is the one MATLAB's DISABLED case IS: no outgoing
2976 // mass for this class at this node, at a node that has connections.
2977
2978 bool has_cs = false;
2979 for (const NodeDef& nd : nodes)
2980 if (nd.nodetype == NodeType::ClassSwitch) has_cs = true;
2981 // A Cache node is an implicit class switch: its read class routes on to
2982 // the hit and miss classes, so the routing is never trivial.
2983 const bool has_cache = !nodeparam.empty();
2984 if (trivial && !has_cs && !has_cache) return; // P is already the effective routing
2985
2986 for (const auto& kv : P) Peff[kv.first] = kv.second;
2987 // The class-INDEPENDENT topology (`sn.connmatrix`), built once from the arcs P holds. Probing
2988 // get_route over every class pair per (node, class, destination) was I^2 K^3 lookups, which
2989 // never finished on a flattened LQN (25 nodes, 146 classes: ~2e9 map finds).
2990 std::vector<char> conn(I * I, 0);
2991 for (const auto& kv : P) {
2992 const Matrix<T>& M = kv.second;
2993 if (M.rows() < I || M.cols() < I) continue; // get_route reads such a block as zero
2994 for (std::size_t a = 0; a < I; ++a)
2995 for (std::size_t b = 0; b < I; ++b)
2996 if (M(a, b) > zero) conn[a * I + b] = 1;
2997 }
2998 auto eff_at = [&](std::size_t r, std::size_t s) -> Matrix<T>& {
2999 auto key = std::make_pair(r, s);
3000 auto it = Peff.find(key);
3001 if (it == Peff.end())
3002 it = Peff.emplace(key, Matrix<T>(I, I, zero)).first;
3003 if (it->second.rows() != I) {
3004 Matrix<T> g(I, I, zero);
3005 for (std::size_t a = 0; a < it->second.rows(); ++a)
3006 for (std::size_t b = 0; b < it->second.cols(); ++b) g(a, b) = it->second(a, b);
3007 it->second = g;
3008 }
3009 return it->second;
3010 };
3011
3012 for (std::size_t i = 1; i <= I; ++i) {
3013 const NodeDef& nd = nodes[i - 1];
3014 for (std::size_t r = 1; r <= K; ++r) {
3015 const RoutingStrategy rs =
3016 nd.routing.size() >= r ? nd.routing[r - 1] : RoutingStrategy::PROB;
3017 if (rs == RoutingStrategy::PROB || rs == RoutingStrategy::DISABLED) continue;
3018 if (rs != RoutingStrategy::RAND && rs != RoutingStrategy::RROBIN &&
3019 rs != RoutingStrategy::JSQ && rs != RoutingStrategy::SQ &&
3020 rs != RoutingStrategy::WRROBIN && rs != RoutingStrategy::SDR)
3021 throw UnsupportedError(ErrorKind{"RoutingStrategy"}, std::string("network: routing strategy '") +
3022 routing_to_text(rs) + "' at node '" + nd.name +
3023 "' has no routing-matrix expansion in this port");
3024 // THE DESTINATIONS ARE THE NODE'S CONNECTIONS, NOT THIS CLASS'S
3025 // OWN ARCS. `getRoutingMatrix.m:117-135` enumerates `sn.connmatrix`
3026 // -- a class-INDEPENDENT topology -- and routes same-class over
3027 // it, which is what makes a class that declared no arc at a node
3028 // it nevertheless reaches get a routing at all. Reading the
3029 // class's own entries instead left such a row EMPTY, and an empty
3030 // row is not a visit: on `gallery_erlerl1`, where Class1 declares
3031 // Source->Queue and Class2 declares Queue->Sink, the Queue came
3032 // out unvisited by Class1 and every metric of the model was zero.
3033 // The `served` mask in `refresh_visits` is what keeps the fill
3034 // honest -- it drops the (station, class) pairs with no service.
3035 //
3036 // A CLOSED CLASS IS NOT ROUTED INTO A SINK, and not out of a
3037 // Source at all, which is the same branch's rule: a sink would
3038 // absorb a job the population must conserve.
3039 const bool open_class = !std::isfinite(
3040 num_traits<T>::to_double(classes[r - 1].population));
3041 const bool from_source = nd.nodetype == NodeType::Source;
3042 const bool from_sink = nd.nodetype == NodeType::Sink;
3043 if (!open_class && (from_source || from_sink)) continue;
3044 std::vector<std::pair<std::size_t, std::size_t>> dest; // (node, class)
3045 for (std::size_t j = 1; j <= I; ++j) {
3046 if (!open_class && nodes[j - 1].nodetype == NodeType::Sink) continue;
3047 if (conn[(i - 1) * I + (j - 1)]) dest.emplace_back(j, r);
3048 }
3049 if (dest.empty()) continue;
3050 // WRROBIN SPREADS BY ITS WEIGHTS, everything else uniformly.
3051 // That is `getRoutingMatrix.m`'s split and it keeps the FIRST
3052 // MOMENT right for a weighted dispatcher, which a uniform
3053 // expansion would not.
3054 std::vector<T> share(dest.size(),
3056 num_traits<T>::from_int(static_cast<long>(dest.size()))));
3057 if (rs == RoutingStrategy::WRROBIN) {
3058 const std::map<std::size_t, double>* w =
3059 nd.routing_weights.size() >= r ? &nd.routing_weights[r - 1] : NULL;
3060 double total = 0.0;
3061 std::vector<double> raw(dest.size(), 0.0);
3062 if (w != NULL)
3063 for (std::size_t d = 0; d < dest.size(); ++d) {
3064 const std::map<std::size_t, double>::const_iterator it =
3065 w->find(dest[d].first);
3066 raw[d] = (it == w->end()) ? 0.0 : it->second;
3067 total += raw[d];
3068 }
3069 if (total > 0.0)
3070 for (std::size_t d = 0; d < dest.size(); ++d)
3071 share[d] = num_traits<T>::from_double(raw[d] / total);
3072 }
3073 // Clear row i of every (r,s) block that exists: an absent block already reads as zero,
3074 // and materialising all K^2 of them only to zero one row is K^2 I^2 of memory
3075 for (std::size_t s = 1; s <= K; ++s) {
3076 if (Peff.find(std::make_pair(r, s)) == Peff.end()) continue;
3077 Matrix<T>& B = eff_at(r, s);
3078 for (std::size_t j = 1; j <= I; ++j) B(i - 1, j - 1) = zero;
3079 }
3080 for (std::size_t d = 0; d < dest.size(); ++d)
3081 eff_at(r, dest[d].second)(i - 1, dest[d].first - 1) = share[d];
3082 }
3083 }
3084
3085 // ClassSwitch: split the outgoing mass across the class-switch matrix.
3086 for (const auto& kv : csmatrix) {
3087 const std::size_t cs = kv.first;
3088 const Matrix<T>& C = kv.second;
3089 if (C.rows() != K || C.cols() != K)
3090 throw InputError("network: the class-switch matrix of node '" +
3091 nodes[cs - 1].name + "' is not (nclasses x nclasses)");
3092 // WHERE A SWITCHED JOB GOES IS THE ARRIVAL CLASS'S ROUTING, NOT THE
3093 // DEPARTURE CLASS'S. `getRoutingMatrix.m`'s StatelessClassSwitcher
3094 // block sets rtnodes(r, (j-1)*K+s) = Pcs(r,s) * Pij(s,s): the
3095 // destination distribution is read off the DIAGONAL, i.e. from the
3096 // row class s routes on with, and only the mass is Pcs(r,s).
3097 // Reading class r's own outgoing row instead is the same matrix
3098 // only when every class of the chain leaves the switch the same
3099 // way. Where they do not -- `Delay -> CS1 -> Queue -> CS2 -> Delay`
3100 // with each class Disabled at the station it does not visit, so
3101 // each class is routed on one arc of the cycle only -- class r has
3102 // NO outgoing arc at the switch it enters, the whole block comes
3103 // out zero, and the two classes then fall into separate chains with
3104 // empty visits. The state space collapses to its initial state and
3105 // the CTMC, NC and MVA answers are an empty table, not an error.
3106 std::vector<std::vector<T>> diag(K, std::vector<T>(I, zero));
3107 for (std::size_t s = 1; s <= K; ++s) {
3108 if (!Peff.empty() && Peff.find(std::make_pair(s, s)) == Peff.end()) continue;
3109 for (std::size_t j = 1; j <= I; ++j)
3110 diag[s - 1][j - 1] =
3111 Peff.empty() ? get_route(s, s, cs, j) : eff_at(s, s)(cs - 1, j - 1);
3112 }
3113 // Zero row cs only in blocks that exist: an absent block reads as zero, and eff_at on all K^2 pairs materialised them
3114 for (std::size_t r = 1; r <= K; ++r)
3115 for (std::size_t s = 1; s <= K; ++s) {
3116 if (Peff.find(std::make_pair(r, s)) == Peff.end()) continue;
3117 Matrix<T>& B = eff_at(r, s);
3118 for (std::size_t j = 1; j <= I; ++j) B(cs - 1, j - 1) = zero;
3119 }
3120 for (std::size_t r = 1; r <= K; ++r)
3121 for (std::size_t s = 1; s <= K; ++s) {
3122 if (!(C(r - 1, s - 1) > zero)) continue;
3123 Matrix<T>& B = eff_at(r, s);
3124 for (std::size_t j = 1; j <= I; ++j)
3125 if (diag[s - 1][j - 1] > zero)
3126 B(cs - 1, j - 1) = T(diag[s - 1][j - 1] * C(r - 1, s - 1));
3127 }
3128 }
3129
3130 // Cache: the read (input) class self-switches at the cache node to the
3131 // hit and the miss class, which then follow their own routing. The
3132 // reference leaves the split unresolved (NaN) in the base struct and the
3133 // solver decides it; for the visit equations that back the offered
3134 // arrival rate and residence time it resolves to a uniform 1/2 - 1/2,
3135 // which is what makes ArvR the OFFERED rate (Tput is the carried one the
3136 // cacheqn decomposition produces). A half of the read mass reaching each
3137 // of hit/miss is a same-node class switch, hence a self-loop edge.
3138 const T half = T(num_traits<T>::from_int(1) / num_traits<T>::from_int(2));
3139 for (const auto& kv : nodeparam) {
3140 const std::size_t ci = kv.first; // 1-based cache node
3141 const CacheParam<T>& cp = kv.second;
3142 for (std::size_t r = 0; r < cp.hitclass.size() && r < K; ++r) {
3143 if (cp.hitclass[r] == 0) continue;
3144 // clear the read class's own outgoing routing at the cache
3145 for (std::size_t s = 1; s <= K; ++s) {
3146 Matrix<T>& B = eff_at(r + 1, s);
3147 for (std::size_t j = 1; j <= I; ++j) B(ci - 1, j - 1) = zero;
3148 }
3149 eff_at(r + 1, cp.hitclass[r])(ci - 1, ci - 1) = half;
3150 if (r < cp.missclass.size() && cp.missclass[r] != 0)
3151 eff_at(r + 1, cp.missclass[r])(ci - 1, ci - 1) = half;
3152 }
3153 }
3154 }
3155
3156 /**
3157 * The number of siblings the fork-join pair ending at Join node `joinNode`
3158 * (1-based) emits per parent job: the matched Fork's out-degree times its
3159 * tasksPerLink, with the Join's in-degree as the fallback when no Fork is
3160 * matched. Port of `matlab/src/api/fj/sn_join_siblings.m`.
3161 *
3162 * Read off the DECLARED routing `P` rather than off `rtnodes`, because
3163 * `refresh_capacity` asks this question and runs BEFORE `refresh_rt`. The
3164 * two differ only on a declared link that carries no mass, which forks no
3165 * sibling either.
3166 */
3167 std::size_t join_siblings(std::size_t joinNode, std::size_t r = 0) const {
3168 const std::size_t I = nodes.size();
3169 if (joinNode == 0 || joinNode > I) return 0;
3170 std::size_t forkNode = 0;
3171 for (std::size_t a = 0; a < fj.size(); ++a)
3172 if (fj[a].second == joinNode) {
3173 forkNode = fj[a].first;
3174 break;
3175 }
3176 const T zero = num_traits<T>::from_int(0);
3177 std::vector<bool> seen(I + 1, false);
3178 std::size_t deg = 0;
3179 for (typename std::map<std::pair<std::size_t, std::size_t>, Matrix<T> >::const_iterator
3180 it = P.begin();
3181 it != P.end(); ++it) {
3182 if (it->second.rows() < I || it->second.cols() < I) continue;
3183 for (std::size_t b = 1; b <= I; ++b) {
3184 if (seen[b]) continue;
3185 const T v = (forkNode != 0) ? it->second(forkNode - 1, b - 1)
3186 : it->second(b - 1, joinNode - 1);
3187 if (v > zero) {
3188 seen[b] = true;
3189 ++deg;
3190 }
3191 }
3192 }
3193 if (forkNode == 0 || forkNode > I) return deg;
3194
3195 // THE COUNT IS PER LINK when the fork carries a VARIABLE FORKING LEVEL.
3196 // `fan_out_link(d,r)` is the expected tasks towards destination d for
3197 // class r (the DISTRIBUTION case stores its mean there) and
3198 // `fan_out_prob(d,r)` whether the branch fires at all, so the expected
3199 // sibling count is sum_d fan_out_prob(d,r)*fan_out_link(d,r). A fork
3200 // with no override has no `forkparam` entry, which is how the classic
3201 // out-degree times `tasks_per_link` case is told apart.
3202 const ForkParam<T>* fp = fork_param_of(forkNode);
3203 if (fp != 0 && fp->fan_out_link.rows() > 0) {
3204 const Matrix<T>& fol = fp->fan_out_link;
3205 const bool haveProb = fp->fan_out_prob.rows() == fol.rows() &&
3206 fp->fan_out_prob.cols() == fol.cols();
3207 const std::size_t lo = (r != 0 && r <= fol.cols()) ? r - 1 : 0;
3208 const std::size_t hi = (r != 0 && r <= fol.cols()) ? r - 1 : fol.cols() - 1;
3209 double best = 0.0;
3210 for (std::size_t c = lo; c <= hi && c < fol.cols(); ++c) {
3211 double acc = 0.0;
3212 for (std::size_t d = 0; d < fol.rows(); ++d) {
3213 const double link = num_traits<T>::to_double(fol(d, c));
3214 if (!(link > 0.0)) continue;
3215 acc += link * (haveProb ? num_traits<T>::to_double(fp->fan_out_prob(d, c))
3216 : 1.0);
3217 }
3218 if (acc > best) best = acc;
3219 }
3220 if (best > 0.0) return static_cast<std::size_t>(best + 0.5);
3221 }
3222
3223 double w = nodes[forkNode - 1].tasks_per_link;
3224 if (!(w >= 1.0)) w = 1.0;
3225 return deg * static_cast<std::size_t>(w + 0.5);
3226 }
3227
3228 /**
3229 * The 1-based Join nodes that fire on a STRICT quorum, i.e. on FEWER
3230 * siblings than are forked. A model holding one is not
3231 * population-conserving at the sibling level: the join releases the parent
3232 * at the k-th of n siblings and the n-k stragglers stay in their branches,
3233 * so the parent forks again while they are still in flight.
3234 */
3235 std::vector<std::size_t> quorum_joins() const {
3236 std::vector<std::size_t> out;
3237 for (typename std::map<std::size_t, JoinDecl>::const_iterator it = joindecl.begin();
3238 it != joindecl.end(); ++it) {
3239 if (it->second.strategy != lang::JoinStrategy::PARTIAL) continue;
3240 if (it->second.quorum <= 0.0) continue;
3241 const std::size_t n = join_siblings(it->first);
3242 const std::size_t k = static_cast<std::size_t>(it->second.quorum + 0.5);
3243 if (n == 0 || k < n) out.push_back(it->first);
3244 }
3245 return out;
3246 }
3247
3248 /**
3249 * Port of MNetwork.refreshCapacity.
3250 *
3251 * `classcap(i,r)` is the population of r's CHAIN, cut down by any explicit
3252 * per-class or per-station buffer, and 0 where the class does not visit the
3253 * station; `cap(i)` is the explicit station buffer when there is one, and
3254 * otherwise the smaller of the chain and class sums.
3255 *
3256 * `chaincap` IS K COLUMNS WIDE, NOT nchains, exactly as the reference sizes
3257 * it (`chaincap = Inf*ones(M,K)`). The last class of a chain wins the write,
3258 * so a chain with a class disabled at station i can leave chaincap(i,c) at
3259 * 0; under class switching, where nchains < K, the untouched columns stay
3260 * Inf and carry the sum, which is what stops that 0 from capping a station
3261 * that holds the whole chain population at nothing.
3262 *
3263 * The derived drop rule follows the reference exactly, including that WAITQ
3264 * means "never consulted" at an unbounded station and "unsettled" for a
3265 * closed class at a bounded one: only an OPEN class at a real finite buffer
3266 * gets DROP.
3267 *
3268 * A PLACE IS EXEMPT FROM THE ZEROING. `disabled` is this port's marker for
3269 * the reference's `isnan(sn.rates(i,r))`, and a Place holds a marking
3270 * rather than serving, so it has no service process and every class reads
3271 * as disabled there. Zeroing it leaves a token container that cannot hold a
3272 * token: `cap` and `classcap` both come out 0, JMT is handed a Storage
3273 * section of capacity 0, and the net is dead on arrival. `refreshCapacity.m`
3274 * carries the same `~= NodeType.Place` guard on its `isnan` test.
3275 */
3277 const std::size_t M = stations.size(), K = classes.size();
3278 const double inf = std::numeric_limits<double>::infinity();
3279 cap.assign(M, 0.0);
3280 classcap.assign(M, std::vector<double>(K, inf));
3281 droprule.assign(M, std::vector<DropStrategy>(K, DropStrategy::WAITQ));
3282 std::vector<std::vector<double>> chaincap(M, std::vector<double>(std::max(nchains, K), inf));
3283
3284 // A class routed into a QUORUM Join is not population-conserving: the
3285 // stragglers of an already-fired parent are still in their branches
3286 // when it forks again, and nothing bounds that backlog, so a branch
3287 // station holds no more than the class population only under a STANDARD
3288 // join. Capping it at the chain population makes the engine drop a
3289 // closed job. see _kb/05-solvers-overview.md
3290 std::vector<char> quorum_class(K + 1, 0);
3291 {
3292 const std::vector<std::size_t> qj = quorum_joins();
3293 const T zero = num_traits<T>::from_int(0);
3294 for (std::size_t x = 0; x < qj.size(); ++x)
3295 for (typename std::map<std::pair<std::size_t, std::size_t>,
3296 Matrix<T> >::const_iterator it = P.begin();
3297 it != P.end(); ++it) {
3298 if (it->second.rows() < nodes.size() || qj[x] > nodes.size()) continue;
3299 for (std::size_t a = 1; a <= nodes.size(); ++a)
3300 if (it->second(a - 1, qj[x] - 1) > zero) {
3301 if (it->first.first <= K) quorum_class[it->first.first] = 1;
3302 if (it->first.second <= K) quorum_class[it->first.second] = 1;
3303 }
3304 }
3305 }
3306
3307 // A fork with tasksPerLink = w > 1 puts w tasks of the SAME parent on one
3308 // link, so a branch station can hold w jobs per circulating parent and the
3309 // chain population is no longer its bound. The multiplier is the PRODUCT
3310 // over the forks, because a fork nested in another's branch multiplies
3311 // again; that is an upper bound for forks in series, where a cap that never
3312 // binds costs nothing, and exact for the single-fork case. Without it this
3313 // engine drops a closed job at a branch station.
3314 double fork_task_factor = 1.0;
3315 for (std::size_t i = 0; i < nodes.size(); ++i) {
3316 if (nodes[i].nodetype != NodeType::Fork) continue;
3317 const double w = nodes[i].tasks_per_link;
3318 if (w >= 1.0 && std::isfinite(w)) fork_task_factor *= std::floor(w + 0.5);
3319 }
3320
3321 for (std::size_t c = 0; c < nchains; ++c) {
3322 double chain_cap = 0.0;
3323 bool open = false;
3324 bool quorum = false;
3325 for (std::size_t r : inchain[c]) {
3326 if (std::isinf(classes[r - 1].population)) open = true;
3327 else chain_cap += classes[r - 1].population;
3328 if (r <= K && quorum_class[r]) quorum = true;
3329 }
3330 // A chain holding a spawn TARGET gains a job on every trigger completion, so it is not population-conserving
3331 // either (refreshCapacity.m spawnFedChain); capping it at its population makes the engine drop phase-2 jobs
3332 bool spawn_fed = false;
3333 for (std::size_t q = 0; q < K && !spawn_fed; ++q)
3334 for (std::size_t r : inchain[c])
3335 if (classes[q].spawn == r) spawn_fed = true;
3336 chain_cap *= fork_task_factor;
3337 if (open || quorum || spawn_fed) chain_cap = inf;
3338 for (std::size_t r : inchain[c])
3339 for (std::size_t i = 0; i < M; ++i) {
3340 const Station<T>& st = stations[i];
3341 const bool user_rule = st.droprule.size() >= r && st.droprule[r - 1] != 0;
3342 if (st.nodetype != NodeType::Source) {
3343 const bool cap_finite = st.cap >= 0.0 && !std::isinf(st.cap);
3344 const bool classcap_finite = st.classcap.size() >= r &&
3345 st.classcap[r - 1] > 0.0 &&
3346 !std::isinf(st.classcap[r - 1]);
3347 if (user_rule &&
3348 st.droprule[r - 1] == static_cast<int>(DropStrategy::WAITQ) &&
3349 std::isinf(classes[r - 1].population) &&
3350 (cap_finite || classcap_finite))
3351 throw UnsupportedError(ErrorKind{"FiniteCapacity"},
3352 "station '" + st.name + "' declares setDropRule(WAITQ) for the "
3353 "open class '" + classes[r - 1].name + "' at a finite capacity: "
3354 "LINE does not implement waiting-room blocking for an open "
3355 "arrival at a plain finite buffer. Use DropStrategy.DROP for a "
3356 "loss station, or BAS / BBS / RSRD for blocking between "
3357 "stations");
3358 if (user_rule) {
3359 droprule[i][r - 1] = static_cast<DropStrategy>(st.droprule[r - 1]);
3360 } else if (std::isinf(st.cap)) {
3361 droprule[i][r - 1] = DropStrategy::WAITQ;
3362 } else if (!std::isinf(classes[r - 1].population)) {
3363 droprule[i][r - 1] = DropStrategy::WAITQ;
3364 } else {
3365 droprule[i][r - 1] = DropStrategy::DROP;
3366 }
3367 }
3368 // A Place has no service process, so `disabled` is not absence.
3369 if (disabled[i][r - 1] && st.nodetype != NodeType::Place) {
3370 classcap[i][r - 1] = 0.0;
3371 chaincap[i][c] = 0.0;
3372 continue;
3373 }
3374 chaincap[i][c] = chain_cap;
3375 classcap[i][r - 1] = chain_cap;
3376 if (st.classcap.size() >= r && st.classcap[r - 1] >= 0.0)
3377 classcap[i][r - 1] = std::min(classcap[i][r - 1], st.classcap[r - 1]);
3378 if (st.cap >= 0.0) classcap[i][r - 1] = std::min(classcap[i][r - 1], st.cap);
3379 }
3380 }
3381 for (std::size_t i = 0; i < M; ++i) {
3382 if (stations[i].cap >= 0.0 && !std::isinf(stations[i].cap)) {
3383 cap[i] = stations[i].cap;
3384 continue;
3385 }
3386 double sc = 0.0, scl = 0.0;
3387 for (std::size_t c = 0; c < chaincap[i].size(); ++c) sc += chaincap[i][c];
3388 for (std::size_t r = 0; r < K; ++r) scl += classcap[i][r];
3389 cap[i] = std::min(sc, scl);
3390 }
3391 }
3392
3393 /**
3394 * `sn.rt` and `sn.rtnodes`: the class-expanded routing matrices.
3395 *
3396 * rtnodes is the routing over every node; rt is its stochastic complement
3397 * over the stateful ones, which is the same construction the per-chain
3398 * visits use -- but over ALL classes at once rather than one chain at a
3399 * time, because that is what `sn.rt` means and what solver_qna reads.
3400 */
3401 void refresh_rt() {
3402 const T zero = num_traits<T>::from_int(0);
3403 const std::size_t K = classes.size(), I = nodes.size();
3404 std::vector<std::size_t> all(K);
3405 for (std::size_t r = 0; r < K; ++r) all[r] = r + 1;
3406 rtnodes = Matrix<T>(I * K, I * K, zero);
3407 // route_eff over every entry is (I*K)^2 map finds; the blocks that exist hold every nonzero
3408 for_each_route_block([&](std::size_t r, std::size_t s, const Matrix<T>& M) {
3409 for (std::size_t a = 0; a < I; ++a)
3410 for (std::size_t b = 0; b < I; ++b) rtnodes(a * K + r - 1, b * K + s - 1) = M(a, b);
3411 });
3412 rt = station_routing(all);
3413 }
3414
3415 /**
3416 * Re-resolve the cache read self-switch from the offered 1/2-1/2 to the
3417 * ACTUAL hit/miss probabilities the cacheqn decomposition converged on, then
3418 * recompute `rt` and the visits. The runner reads ArvR and ResidT off the
3419 * result, matching MATLAB whose `sn.visits` carry the actual
3420 * (setResultHitProb) split, not the offered one. Served metrics are
3421 * unaffected: they come from the analyzer's own over-routed inner solve.
3422 * `hitprob`/`missprob` are (ncaches x nclasses), indexed by cache order and
3423 * input class, exactly as `da_cacheqn` returns them.
3424 */
3425 void refresh_cacheqn_actual_visits(const Matrix<T>& hitprob, const Matrix<T>& missprob) {
3426 const T zero = num_traits<T>::from_int(0);
3427 const std::size_t K = classes.size();
3428 std::size_t cidx = 0;
3429 for (const auto& kv : nodeparam) {
3430 const std::size_t ci = kv.first; // 1-based cache node
3431 const CacheParam<T>& cp = kv.second;
3432 for (std::size_t r = 0; r < cp.hitclass.size() && r < K; ++r) {
3433 if (cp.hitclass[r] == 0) continue;
3434 set_route_effective(r + 1, cp.hitclass[r], ci, ci, hitprob(cidx, r));
3435 if (r < cp.missclass.size() && cp.missclass[r] != 0)
3436 set_route_effective(r + 1, cp.missclass[r], ci, ci, missprob(cidx, r));
3437 }
3438 ++cidx;
3439 }
3440 refresh_rt();
3441 const bool fork = has_fork();
3442 visits.assign(nchains, Matrix<T>(stateful_nodes.size(), nclasses, zero));
3443 nodevisits.assign(nchains, Matrix<T>(nodes.size(), nclasses, zero));
3444 for (std::size_t c = 0; c < nchains; ++c) {
3445 visits[c] = chain_visits(c, stateful_nodes, true, fork);
3446 nodevisits[c] = chain_visits(c, all_nodes(), false, fork);
3447 }
3448 }
3449
3450 /**
3451 * Port of MNetwork.refreshRates: lower each service process onto a rate and
3452 * an SCV. A disabled (station, class) pair becomes NaN, which is the marker
3453 * every downstream consumer keys on to mean "this class never visits here".
3454 */
3456 nstations = stations.size();
3457 nclasses = classes.size();
3458 const T zero = num_traits<T>::from_int(0);
3460 scv = Matrix<T>(nstations, nclasses, zero);
3461 disabled.assign(nstations, std::vector<bool>(nclasses, true));
3462 for (std::size_t i = 0; i < nstations; ++i)
3463 for (std::size_t r = 0; r < nclasses; ++r) {
3464 // Join infinite-rate rationale: see _kb/04-networkstruct.md (cpp port notes)
3465 if (stations[i].nodetype == NodeType::Join) {
3466 disabled[i][r] = false;
3468 std::numeric_limits<double>::infinity());
3469 scv(i, r) = zero;
3470 continue;
3471 }
3472 const Distrib<T>& d = service[i][r];
3473 if (d.disabled) continue;
3474 disabled[i][r] = false;
3475 // A MARKED class receives ONLY ITS MARK'S stream, so its rate is
3476 // the mark's, not the shared process's. `set_marked_arrival`
3477 // binds one `Distrib` to every marked class, so `d.rate()` is
3478 // `1/mean` of the AGGREGATE and reports the whole stream K times
3479 // over -- 2.5 apiece on `cache_mmap_rr_env` against 1.9 and 0.6.
3480 const std::size_t mk = markidx_of(i + 1, r + 1);
3481 if (mk > 0 && mk <= d.Dmark.size()) {
3482 const std::pair<T, T> ms = mark_marginal_moments(d, mk);
3483 rates(i, r) = num_traits<T>::to_double(ms.first) > 0
3484 ? T(num_traits<T>::from_int(1) / ms.first)
3486 scv(i, r) = ms.second;
3487 continue;
3488 }
3489 rates(i, r) = d.rate();
3490 scv(i, r) = d.scv;
3491 }
3492 }
3493
3494 /**
3495 * Mean and SCV of ONE MARK'S MARGINAL MAP, `MarkedMAP.toMAPs(k)`.
3496 *
3497 * The marginal moves the OTHER marks' arrivals into the hidden part:
3498 * `D0' = D0 + (D1 - D1k)`, `D1' = D1k` (`MarkedMAP.m:130-140`,
3499 * `refreshRates.m:56-63`). It is what a consumer that sees only class k
3500 * experiences, and it is NOT the aggregate restricted -- an arrival of
3501 * another mark still moves the shared chain, it just is not an arrival here.
3502 *
3503 * `D0' + D1' = D0 + D1`, so the marginal keeps the aggregate's phase
3504 * generator and its rate reduces to `pi D1k e`; only the SCV needs the
3505 * second moment, which is why this goes through `map_scv` rather than
3506 * stopping at the rate.
3507 */
3508 std::pair<T, T> mark_marginal_moments(const Distrib<T>& d, std::size_t mk) const {
3509 mam::Map<T> m;
3510 m.D0 = d.D0;
3511 m.D1 = d.Dmark[mk - 1];
3512 for (std::size_t a = 0; a < m.D0.rows(); ++a)
3513 for (std::size_t b = 0; b < m.D0.cols(); ++b)
3514 m.D0(a, b) += T(d.D1(a, b) - m.D1(a, b));
3515 return std::make_pair(mam::map_mean(m), mam::map_scv(m));
3516 }
3517
3518 /**
3519 * Port of MNetwork.refreshChains followed by sn_refresh_visits.
3520 *
3521 * The class-switch mask is (r == s) or "some link carries r into s", which
3522 * is what link() records in csMatrix and what refreshChains then re-derives
3523 * from `rt`; the chains are the connected components of that mask read as
3524 * an undirected graph. MATLAB orders the chains by `sortrows(...,'descend')`
3525 * on the indicator rows, which for disjoint components is the same as
3526 * ordering by their smallest class index, and that is what is done here.
3527 */
3529 refresh_rates();
3530 const T zero = num_traits<T>::from_int(0);
3531 const std::size_t K = nclasses;
3532
3533 // class-switch mask rationale: see _kb/04-networkstruct.md (cpp port notes)
3534 std::vector<std::vector<bool>> cs(K, std::vector<bool>(K, false));
3535 for (std::size_t r = 0; r < K; ++r) cs[r][r] = true;
3536 for (const auto& kv : (Peff.empty() ? P : Peff)) {
3537 bool any = false;
3538 for (std::size_t a = 0; a < kv.second.rows() && !any; ++a)
3539 for (std::size_t b = 0; b < kv.second.cols() && !any; ++b)
3540 if (kv.second(a, b) > zero) any = true;
3541 if (any) cs[kv.first.first - 1][kv.first.second - 1] = true;
3542 }
3543 // A ClassSwitch NODE couples classes too, and once `link()` synthesizes
3544 // one the coupling lives ONLY here: the rewrite folds `P{r,s}(i,j)` into
3545 // a same-class pair of legs, so the r != s block that used to carry it
3546 // is gone from P. `link.m` accumulates the same thing --
3547 // `csMatrix = csMatrix | nodes{ind}.server.csMatrix > 0` -- and without
3548 // it every switched class falls into its own chain.
3549 for (const auto& kv : csmatrix) {
3550 const Matrix<T>& C = kv.second;
3551 for (std::size_t r = 0; r < K && r < C.rows(); ++r)
3552 for (std::size_t s = 0; s < K && s < C.cols(); ++s)
3553 if (C(r, s) > zero) cs[r][s] = true;
3554 }
3555 // A Cache node couples each input class with the classes it switches to
3556 // on a hit and a miss; that coupling lives in nodeparam, not in P, so it
3557 // must be added here or the read/hit/miss classes fall into separate
3558 // chains and the switched (open) classes get no arrivals.
3559 for (const auto& kv : nodeparam) {
3560 const CacheParam<T>& cp = kv.second;
3561 for (std::size_t r = 0; r < cp.hitclass.size() && r < K; ++r) {
3562 if (cp.hitclass[r] != 0) cs[r][cp.hitclass[r] - 1] = true;
3563 if (r < cp.missclass.size() && cp.missclass[r] != 0)
3564 cs[r][cp.missclass[r] - 1] = true;
3565 }
3566 // A RETRIEVAL class is also a class the cache switches the read
3567 // class into on a miss -- it just lives in `retrieval_classes`
3568 // rather than in `missclass`, because there is one per ITEM. The
3569 // coupling above missed them, so on a closed delayed-hit model each
3570 // retrieval class formed its own singleton chain: 4 chains where
3571 // MATLAB has 1, with zero visits at the fetch station and the whole
3572 // population parked at the delay. The rule in the comment above
3573 // applies to them unchanged.
3574 for (const std::vector<std::size_t>& row : cp.retrieval_classes)
3575 for (std::size_t r = 0; r < row.size() && r < K; ++r)
3576 if (row[r] != 0) cs[r][row[r] - 1] = true;
3577 }
3578 csmask = cs;
3579 std::vector<std::size_t> comp(K, K);
3580 std::size_t ncomp = 0;
3581 for (std::size_t r = 0; r < K; ++r) {
3582 if (comp[r] != K) continue;
3583 std::vector<std::size_t> stack{r};
3584 comp[r] = ncomp;
3585 while (!stack.empty()) {
3586 const std::size_t v = stack.back();
3587 stack.pop_back();
3588 for (std::size_t w = 0; w < K; ++w)
3589 if (comp[w] == K && (cs[v][w] || cs[w][v])) {
3590 comp[w] = ncomp;
3591 stack.push_back(w);
3592 }
3593 }
3594 ++ncomp;
3595 }
3596 // components are already discovered in order of their smallest member,
3597 // which is the order sortrows(...,'descend') produces
3598 nchains = ncomp;
3599 chains.assign(nchains, std::vector<bool>(K, false));
3600 inchain.assign(nchains, {});
3601 for (std::size_t r = 0; r < K; ++r) {
3602 chains[comp[r]][r] = true;
3603 inchain[comp[r]].push_back(r + 1);
3604 }
3605
3606 // ---- reference class per chain -------------------------------------
3607 refclass.assign(nchains, 0);
3608 for (std::size_t c = 0; c < nchains; ++c)
3609 for (std::size_t k : inchain[c])
3610 if (classes[k - 1].is_ref_class) refclass[c] = k;
3611
3612 for (std::size_t c = 0; c < nchains; ++c) {
3613 const std::size_t rs = classes[inchain[c][0] - 1].refstat;
3614 for (std::size_t k : inchain[c])
3615 if (classes[k - 1].refstat != rs)
3616 throw InputError("network '" + name + "': classes within a chain have different "
3617 "reference stations");
3618 }
3619
3621
3622 // ---- visits, at stateful-node and at node level ---------------------
3623 const bool fork = has_fork();
3624 visits.assign(nchains, Matrix<T>(stateful_nodes.size(), nclasses, zero));
3625 nodevisits.assign(nchains, Matrix<T>(nodes.size(), nclasses, zero));
3626 for (std::size_t c = 0; c < nchains; ++c) {
3627 visits[c] = chain_visits(c, stateful_nodes, true, fork);
3628 nodevisits[c] = chain_visits(c, all_nodes(), false, fork);
3629 }
3630 }
3631
3632 /**
3633 * Route every open chain from the Sink back into the Source, as the tail of
3634 * MATLAB's getRoutingMatrix does before it takes the stochastic complement.
3635 *
3636 * WHY IT IS NOT OPTIONAL. `visits` is the stationary vector of the chain's
3637 * routing DTMC. An open chain that ends at the Sink has no such vector --
3638 * the Sink is absorbing, so the solve returns the point mass there and
3639 * every station comes out with zero visits. The closing arc turns the open
3640 * chain into a recurrent one whose stationary vector, renormalised by the
3641 * Source, is the visit count PER ARRIVAL, which is what the chain demands
3642 * are built from.
3643 *
3644 * The destination class is drawn in proportion to the ARRIVAL RATES of the
3645 * chain's classes, so a job leaving the Sink re-enters as the class the
3646 * Source would have generated. A chain whose arrivals are all disabled has
3647 * no such proportion, and the reference falls back to the uniform choice
3648 * rather than dividing by zero -- those chains carry no traffic and only
3649 * need to stay well posed.
3650 *
3651 * The arcs are rebuilt on every refresh, and the previous ones cleared
3652 * first: the rates move between passes of the fork-join fixed point, and a
3653 * stale arc would leave two closures with different weights in place.
3654 */
3656 if (sourceIdx == 0 || sinkNode == 0) return;
3657 const T zero = num_traits<T>::from_int(0);
3658 const T one = num_traits<T>::from_int(1);
3659 const std::size_t src = station_to_node[sourceIdx - 1];
3660 // closure-clearing rationale: see _kb/04-networkstruct.md (cpp port notes)
3661 for (auto& kv : P) {
3662 if (kv.second.rows() < nodes.size()) continue;
3663 kv.second(sinkNode - 1, src - 1) = zero;
3664 }
3665 for (auto& kv : Peff) {
3666 if (kv.second.rows() < nodes.size()) continue;
3667 kv.second(sinkNode - 1, src - 1) = zero;
3668 }
3669 for (std::size_t c = 0; c < nchains; ++c) {
3670 bool open = false;
3671 for (std::size_t k : inchain[c])
3672 if (std::isinf(classes[k - 1].population)) open = true;
3673 if (!open) continue;
3674 T tot = zero;
3675 for (std::size_t k : inchain[c])
3676 if (!disabled[sourceIdx - 1][k - 1]) tot += rates(sourceIdx - 1, k - 1);
3677 const T uniform =
3678 T(one / num_traits<T>::from_int(static_cast<long>(inchain[c].size())));
3679 for (std::size_t s : inchain[c]) {
3680 T p = uniform;
3681 if (tot > zero) {
3682 const T ar =
3683 disabled[sourceIdx - 1][s - 1] ? zero : rates(sourceIdx - 1, s - 1);
3684 p = T(ar / tot);
3685 }
3686 if (!(p > zero)) continue;
3687 for (std::size_t r : inchain[c]) set_route_effective(r, s, sinkNode, src, p);
3688 }
3689 }
3690 }
3691
3692 /** Every node index, 1-based, for the node-level visit computation. */
3693 std::vector<std::size_t> all_nodes() const {
3694 std::vector<std::size_t> v(nodes.size());
3695 for (std::size_t i = 0; i < nodes.size(); ++i) v[i] = i + 1;
3696 return v;
3697 }
3698
3699 /**
3700 * Port of the per-chain body of sn_refresh_visits, over an arbitrary node
3701 * subset -- the stateful nodes for `visits`, all nodes for `nodevisits`.
3702 *
3703 * The FORK CORRECTIONS are the reference's, and they are blunt on purpose.
3704 * A Fork row sums to its fan-out rather than to one, so the rows are
3705 * renormalised before the DTMC solve; afterwards, rather than trusting the
3706 * resulting stationary vector, a population-preserving SPN argument sets
3707 * EVERY visited entry to 1 (and a Join entry to its in-degree, one per
3708 * incoming branch). Reproduced exactly: the corrected visits are what the
3709 * chain demands are built from, so an "improved" version would disagree
3710 * with every other codebase.
3711 */
3712 /**
3713 * sn_refresh_visits.m chainSeeds: the positions refpos*nIC+x, in a chain block over `sel`, of the chain
3714 * classes holding jobs at the reference station: a populated closed class, or an open class with a
3715 * positive, finite rate at its source.
3716 */
3717 std::vector<std::size_t> chain_seeds(std::size_t c, const std::vector<std::size_t>& sel) const {
3718 const std::vector<std::size_t>& ic = inchain[c];
3719 const std::size_t refst = classes[ic[0] - 1].refstat; // 1-based
3720 const std::size_t refnode = station_to_node[refst - 1];
3721 std::vector<std::size_t> seeds;
3722 std::size_t refpos = sel.size();
3723 for (std::size_t a = 0; a < sel.size(); ++a)
3724 if (sel[a] == refnode) refpos = a;
3725 if (refpos == sel.size()) return seeds;
3726 for (std::size_t x = 0; x < ic.size(); ++x) {
3727 const JobClass& jc = classes[ic[x] - 1];
3728 bool held;
3729 if (std::isfinite(jc.population)) {
3730 held = jc.population > 0;
3731 } else {
3732 const bool dis = refst - 1 < disabled.size() && ic[x] - 1 < disabled[refst - 1].size() &&
3733 disabled[refst - 1][ic[x] - 1];
3734 const double rate = (refst - 1 < rates.rows() && ic[x] - 1 < rates.cols())
3735 ? num_traits<T>::to_double(rates(refst - 1, ic[x] - 1)) : 0.0;
3736 held = !dis && std::isfinite(rate) && rate > 0;
3737 }
3738 if (held) seeds.push_back(refpos * ic.size() + x);
3739 }
3740 return seeds;
3741 }
3742
3743 /**
3744 * sn_refresh_visits.m retrievalEdges: extra [from, to] chain-block positions for a retrieval cache, which
3745 * switches a read class into a per-item retrieval class INSIDE the node. The routing does not carry that
3746 * switch, so without these edges the retrieval classes, and the stations only they visit, look unreachable.
3747 */
3748 std::vector<std::pair<std::size_t, std::size_t>> chain_retrieval_edges(const std::vector<std::size_t>& ic,
3749 const std::vector<std::size_t>& sel) const {
3750 std::vector<std::pair<std::size_t, std::size_t>> edges;
3751 const std::size_t nIC = ic.size();
3752 for (const auto& kv : nodeparam) {
3753 const std::vector<std::vector<std::size_t>>& rc = kv.second.retrieval_classes;
3754 if (rc.empty()) continue;
3755 std::size_t pos = sel.size();
3756 for (std::size_t a = 0; a < sel.size(); ++a)
3757 if (sel[a] == kv.first) pos = a;
3758 if (pos == sel.size()) continue;
3759 for (std::size_t x = 0; x < nIC; ++x)
3760 for (const std::vector<std::size_t>& row : rc) {
3761 if (ic[x] - 1 >= row.size() || row[ic[x] - 1] == 0) continue;
3762 for (std::size_t y = 0; y < nIC; ++y)
3763 if (ic[y] == row[ic[x] - 1]) edges.emplace_back(pos * nIC + x, pos * nIC + y);
3764 }
3765 }
3766 return edges;
3767 }
3768
3769 /** sn_refresh_visits.m chainReachable: states reachable from the seeds along nonzero entries and the
3770 * extra `edges`; all states when there is no seed. A NaN entry counts as an edge, as `~= 0` does in the
3771 * reference. */
3772 static std::vector<bool> chain_reachable(const Matrix<T>& P, const std::vector<std::size_t>& seeds,
3773 const std::vector<std::pair<std::size_t, std::size_t>>& edges) {
3774 const std::size_t n = P.rows();
3775 if (seeds.empty()) return std::vector<bool>(n, true);
3776 std::vector<std::vector<std::size_t>> extra(n);
3777 for (const std::pair<std::size_t, std::size_t>& e : edges)
3778 if (e.first < n && e.second < n) extra[e.first].push_back(e.second);
3779 std::vector<bool> reach(n, false);
3780 std::vector<std::size_t> frontier;
3781 for (std::size_t sd : seeds)
3782 if (sd < n && !reach[sd]) {
3783 reach[sd] = true;
3784 frontier.push_back(sd);
3785 }
3786 while (!frontier.empty()) {
3787 std::vector<std::size_t> nxt;
3788 for (std::size_t row : frontier) {
3789 for (std::size_t col = 0; col < n; ++col) {
3790 const double v = num_traits<T>::to_double(P(row, col));
3791 if (!reach[col] && v != 0.0) {
3792 reach[col] = true;
3793 nxt.push_back(col);
3794 }
3795 }
3796 for (std::size_t col : extra[row])
3797 if (!reach[col]) {
3798 reach[col] = true;
3799 nxt.push_back(col);
3800 }
3801 }
3802 frontier.swap(nxt);
3803 }
3804 return reach;
3805 }
3806
3807 Matrix<T> chain_visits(std::size_t c, const std::vector<std::size_t>& sel, bool complement,
3808 bool fork) const {
3809 const T zero = num_traits<T>::from_int(0);
3810 const std::vector<std::size_t>& ic = inchain[c];
3811 const std::size_t nIC = ic.size();
3812 const std::size_t dim = sel.size() * nIC;
3813 Matrix<T> Pc(dim, dim, zero);
3814 if (complement) {
3815 const Matrix<T> rt = station_routing(ic);
3816 Pc = rt;
3817 } else {
3818 // Block-wise over the routing map: route_eff per entry was dim^2 map finds per chain
3819 std::vector<std::size_t> cpos(nclasses + 1, nIC);
3820 for (std::size_t x = 0; x < nIC; ++x) cpos[ic[x]] = x;
3821 for_each_route_block([&](std::size_t r, std::size_t s, const Matrix<T>& M) {
3822 if (r > nclasses || s > nclasses) return;
3823 const std::size_t x = cpos[r], y = cpos[s];
3824 if (x == nIC || y == nIC) return;
3825 for (std::size_t a = 0; a < sel.size(); ++a)
3826 for (std::size_t b = 0; b < sel.size(); ++b)
3827 Pc(a * nIC + x, b * nIC + y) = M(sel[a] - 1, sel[b] - 1);
3828 });
3829 }
3830
3831 // THE `served` MASK of sn_refresh_visits.m, ON BOTH CHAINS.
3832 // `getRoutingMatrix` leaves a JMT-oriented UNIFORM FILL on (station,
3833 // class) pairs the class has no service at, and those states are not
3834 // reachable: a class with no service law cannot be served there. Left
3835 // in, they leak mass between what are otherwise separate recurrent
3836 // branches -- on cs_transient_class that moved Queue1 from 0.1875 to
3837 // 0.22917 and Queue2 from 0.3125 to 0.27083, against an absorption of
3838 // 0.375/0.625 halved over each two-station cycle.
3839 //
3840 // It matters MORE on the node chain, which the reference used to leave
3841 // untouched: at station level an unserved (station,class) is a dead end,
3842 // while the node kernel keeps the class-switch nodes between the
3843 // stations, so the disabled states close into a whole spurious CYCLE. A
3844 // materialised LQN replica is exactly that -- replica 2's stations still
3845 // carry replica 1's classes in rtnodes -- and the reducible solve then
3846 // splits the mass between the real chain and the phantom one, giving
3847 // every node of replica 2 a visit in replica 1's classes.
3848 //
3849 // A Place, a Transition and a station declaring heterogeneous server
3850 // types are EXEMPT, exactly as the reference exempts them: all three
3851 // carry NaN station rates by construction, so the test would read as
3852 // "not served" for a station that plainly is.
3853 std::vector<bool> served(dim, true);
3854 {
3855 for (std::size_t a = 0; a < sel.size(); ++a) {
3856 const std::size_t sti = nodes[sel[a] - 1].station;
3857 if (sti == 0 || sti > stations.size()) continue;
3858 const NodeType nt = stations[sti - 1].nodetype;
3859 if (nt == NodeType::Place || nt == NodeType::Transition) continue;
3860 if (!stations[sti - 1].server_types.empty()) continue;
3861 if (sti > disabled.size()) continue;
3862 // The reference tests `isnan(sn.rates(...))`; THIS PORT'S MARKER
3863 // IS `disabled`. refresh_rates leaves a disabled pair's rate at
3864 // 0 rather than NaN (its comment says otherwise and is stale),
3865 // so an isnan test here would never fire and the mask would be
3866 // silently inert. `disabled` is what every other consumer keys
3867 // on and it carries exactly MATLAB's meaning.
3868 for (std::size_t x = 0; x < nIC; ++x)
3869 if (ic[x] <= disabled[sti - 1].size() && disabled[sti - 1][ic[x] - 1])
3870 served[a * nIC + x] = false;
3871 }
3872 for (std::size_t row = 0; row < dim; ++row)
3873 if (!served[row])
3874 for (std::size_t col = 0; col < dim; ++col) Pc(row, col) = zero;
3875 for (std::size_t col = 0; col < dim; ++col)
3876 if (!served[col])
3877 for (std::size_t row = 0; row < dim; ++row) Pc(row, col) = zero;
3878 }
3879
3880 // keep only states a chain job can reach: an unreachable pass-through row whose successor is dropped
3881 // would otherwise leak mass out of the solved block (sn_refresh_visits.m chainReachable)
3882 const std::vector<bool> reach = chain_reachable(Pc, chain_seeds(c, sel), chain_retrieval_edges(ic, sel));
3883 std::vector<std::size_t> visited;
3884 std::vector<T> rowsum(dim, zero);
3885 for (std::size_t row = 0; row < dim; ++row) {
3886 T s = zero;
3887 for (std::size_t col = 0; col < dim; ++col) s += Pc(row, col);
3888 rowsum[row] = s;
3889 if (s > zero && reach[row]) visited.push_back(row);
3890 }
3891 bool oversum = false;
3892 if (fork) {
3893 for (std::size_t row = 0; row < dim; ++row) {
3894 if (num_traits<T>::to_double(rowsum[row]) > 1.0 + GlobalConstants::FineTol)
3895 oversum = true;
3897 for (std::size_t col = 0; col < dim; ++col)
3898 Pc(row, col) = T(Pc(row, col) / rowsum[row]);
3899 }
3900 }
3901
3902 // Detect a genuinely reducible chain up front, so its DISABLED-routing
3903 // fillers and the reducible solver apply ONLY here -- every irreducible
3904 // chain, i.e. the whole existing test surface, stays on the exact
3905 // dtmc_solve path below. In MATLAB the singular normalization solve
3906 // returns NaN here, tripping the same fallback; this reproduces that
3907 // without changing the shared solver.
3908 //
3909 // "Genuinely reducible" is more than one recurrent class AND all of them
3910 // inside ONE weak component, which is the shape MATLAB's solve actually
3911 // fails on. `ctmc_solve` (MATLAB's, the JAR's and this port's alike)
3912 // SPLITS ON WEAK COMPONENTS first, solves each on its own and
3913 // renormalizes the union, so a chain whose recurrent classes sit in
3914 // SEPARATE weak components has an answer there, and an exact one: each
3915 // component gets equal weight.
3916 //
3917 // A delayed-hit retrieval cache is exactly that second shape. Its
3918 // per-item fetch cycle (cache -> retrieval station -> cache, in that
3919 // item's retrieval class) shares no state with the read class's own
3920 // loop, because the class switch that feeds it happens INSIDE the cache
3921 // node rather than on a routing arc. Calling that reducible sends it to
3922 // `dtmc_solve_reducible`, which hands the read loop all the mass and
3923 // leaves every retrieval station with ZERO visits -- and a station with
3924 // no visits is given no capacity by `space_capacity_c`, so the CTMC then
3925 // enumerates a state space with no in-flight fetch in it at all and
3926 // reports NaN hit/miss shares. Measured on retrieval_simple before this
3927 // qualifier: 12 states where MATLAB, the JAR and native Python have 144.
3928 bool reducible = false;
3929 if (!fork && !visited.empty()) {
3930 Matrix<T> Pv0(visited.size(), visited.size(), zero);
3931 for (std::size_t a = 0; a < visited.size(); ++a)
3932 for (std::size_t b = 0; b < visited.size(); ++b) Pv0(a, b) = Pc(visited[a], visited[b]);
3933 const mc::SccResult scc = mc::stronglyconncomp(Pv0);
3934 std::size_t nrec = 0;
3935 for (bool rc : scc.recurrent)
3936 if (rc) ++nrec;
3937 reducible = (nrec > 1) && mc::detail::weak_components(Pv0).size() == 1;
3938 }
3939 if (reducible) {
3940 // DISABLED-routing filler (getRoutingMatrix.m DISABLED case): a
3941 // (node,class) with no outgoing routing at a physically connected node
3942 // routes SAME-CLASS to each neighbour at 1/nconn. These 0-visit
3943 // transient states give the reducible solver the multiple transient
3944 // SCCs MATLAB has, so it weights the recurrent classes correctly
3945 // (11:13) instead of the single-transient absorption split (3:5).
3946 std::vector<std::size_t> nconn(sel.size(), 0);
3947 std::vector<std::vector<bool>> conn(sel.size(), std::vector<bool>(sel.size(), false));
3948 for (std::size_t a = 0; a < sel.size(); ++a)
3949 for (std::size_t b = 0; b < sel.size(); ++b) {
3950 bool cbit = false;
3951 for (std::size_t x = 0; x < nIC && !cbit; ++x)
3952 for (std::size_t y = 0; y < nIC && !cbit; ++y)
3953 if (Pc(a * nIC + x, b * nIC + y) > zero) cbit = true;
3954 conn[a][b] = cbit;
3955 if (cbit) ++nconn[a];
3956 }
3957 for (std::size_t a = 0; a < sel.size(); ++a) {
3958 if (nconn[a] == 0) continue;
3959 for (std::size_t x = 0; x < nIC; ++x) {
3960 T outsum = zero;
3961 for (std::size_t col = 0; col < dim; ++col) outsum += Pc(a * nIC + x, col);
3962 if (outsum > zero) continue; // class already routes onward here
3963 const T p = num_traits<T>::from_int(1) /
3964 num_traits<T>::from_int(static_cast<long>(nconn[a]));
3965 for (std::size_t b = 0; b < sel.size(); ++b)
3966 if (conn[a][b]) Pc(a * nIC + x, b * nIC + x) = p;
3967 }
3968 }
3969 // AND THE FILLER IS MASKED AGAIN, which is the whole point of the
3970 // mask. The reference's order is: getRoutingMatrix lays the fill
3971 // down, THEN `served` zeroes those rows and columns -- so an
3972 // unserved state never carries fill in the matrix that is solved.
3973 // Synthesising the fill here and stopping would restore exactly what
3974 // the mask was ported to remove: on cs_transient_class the two
3975 // recurrent branches came out 11:13 (0.22917 / 0.27083) instead of
3976 // the absorption split 3:5 (0.1875 / 0.3125) that MATLAB, Java and
3977 // Python all report and that the routing implies.
3978 for (std::size_t row = 0; row < dim; ++row)
3979 if (!served[row])
3980 for (std::size_t col = 0; col < dim; ++col) Pc(row, col) = zero;
3981 for (std::size_t col = 0; col < dim; ++col)
3982 if (!served[col])
3983 for (std::size_t row = 0; row < dim; ++row) Pc(row, col) = zero;
3984 visited.clear();
3985 for (std::size_t row = 0; row < dim; ++row) {
3986 T s = zero;
3987 for (std::size_t col = 0; col < dim; ++col) s += Pc(row, col);
3988 if (s > zero && reach[row]) visited.push_back(row);
3989 }
3990 }
3991
3992 std::vector<T> alpha(dim, zero);
3993 if (!visited.empty()) {
3994 Matrix<T> Pv(visited.size(), visited.size(), zero);
3995 for (std::size_t a = 0; a < visited.size(); ++a)
3996 for (std::size_t b = 0; b < visited.size(); ++b)
3997 Pv(a, b) = Pc(visited[a], visited[b]);
3998 std::vector<T> av;
3999 bool ok = false;
4000 if (!reducible) {
4001 try {
4002 av = mc::dtmc_solve(Pv);
4003 ok = true;
4004 bool allzero = true, hasnan = false;
4005 for (const T& x : av) {
4006 if (x != zero) allzero = false;
4007 if (num_traits<T>::to_double(x) != num_traits<T>::to_double(x)) hasnan = true;
4008 }
4009 if (allzero || hasnan) ok = false;
4010 } catch (const Error&) {
4011 ok = false;
4012 }
4013 }
4014 if (!ok) {
4015 // DTMC solver order: see _kb/04-networkstruct.md (cpp port notes) and _kb/11-conventions-and-gotchas.md
4017 }
4018 for (std::size_t a = 0; a < visited.size(); ++a) alpha[visited[a]] = av[a];
4019
4020 // A TRANSIENT STATE HAS VISIT RATIO EXACTLY ZERO, and saying so by
4021 // the graph rather than by a tolerance is what keeps this port on
4022 // the reference's answer. `pi P = pi` puts all of its mass on the
4023 // recurrent classes, so a state whose SCC has an edge leaving it
4024 // carries none; whether the LU lands on 0 or on an eps residue is a
4025 // pivot-order accident, and MATLAB's backslash happens to land on 0
4026 // where this LU lands on 3e-17.
4027 //
4028 // That accident is not cosmetic downstream. RN is QN/TN, so a class
4029 // with an eps visit at a station reads 0/0 -- NaN in the reference,
4030 // which the H-T order statistic DROPS, and the station's full
4031 // residence time here, which it COUNTS. On the two-span H-T model
4032 // that handed Delay1 and the other fork's auxiliary delay a
4033 // residence time of 2 each for classes that never reach either, and
4034 // `ri` came out 5.747 against the reference's 1.747 -- exactly the
4035 // 2 + 2 those two rows contribute.
4036 {
4037 const mc::SccResult scc = mc::stronglyconncomp(Pv);
4038 for (std::size_t a = 0; a < visited.size(); ++a)
4039 if (!scc.recurrent[scc.scc[a] - 1]) alpha[visited[a]] = zero;
4040 }
4041 }
4042
4043 if (fork && oversum) {
4044 for (std::size_t idx = 0; idx < dim; ++idx) {
4045 if (!(num_traits<T>::to_double(alpha[idx]) > GlobalConstants::FineTol)) continue;
4046 const std::size_t nd = sel[idx / nIC];
4047 if (!complement && nodes[nd - 1].nodetype == NodeType::Join) {
4048 // a Join is entered once per incoming branch. The reference
4049 // counts POSITIVE ROWS of rtnodes in column (nd,r), i.e.
4050 // (source node, source class) PAIRS over every class -- two
4051 // classes entering the Join as r from the same predecessor
4052 // are two branches, so neither the break nor the restriction
4053 // to the chain's own classes belongs here.
4054 const std::size_t r = ic[idx % nIC];
4055 std::size_t nsrc = 0;
4056 for (std::size_t src = 1; src <= nodes.size(); ++src)
4057 for (std::size_t q = 1; q <= nclasses; ++q)
4058 if (num_traits<T>::to_double(route_eff(q, r, src, nd)) >
4060 ++nsrc;
4061 alpha[idx] = num_traits<T>::from_int(static_cast<long>(nsrc));
4062 } else {
4063 alpha[idx] = num_traits<T>::from_int(1);
4064 }
4065 }
4066 }
4067
4068 Matrix<T> out(sel.size(), nclasses, zero);
4069 for (std::size_t a = 0; a < sel.size(); ++a)
4070 for (std::size_t x = 0; x < nIC; ++x) out(a, ic[x] - 1) = alpha[a * nIC + x];
4071
4072 // normalise by the total visits of the chain at its reference node
4073 const std::size_t rstat = classes[ic[0] - 1].refstat;
4074 const std::size_t refnode = station_to_node[rstat - 1];
4075 std::size_t refrow = sel.size();
4076 for (std::size_t a = 0; a < sel.size(); ++a)
4077 if (sel[a] == refnode) refrow = a;
4078 if (refrow < sel.size()) {
4079 T norm = zero;
4080 for (std::size_t x = 0; x < nIC; ++x) norm += out(refrow, ic[x] - 1);
4082 for (std::size_t a = 0; a < sel.size(); ++a)
4083 for (std::size_t x = 0; x < nIC; ++x)
4084 out(a, ic[x] - 1) = T(out(a, ic[x] - 1) / norm);
4085 }
4086 for (std::size_t a = 0; a < sel.size(); ++a)
4087 for (std::size_t x = 0; x < nIC; ++x)
4088 if (out(a, ic[x] - 1) < zero) out(a, ic[x] - 1) = T(-out(a, ic[x] - 1));
4089 return out;
4090 }
4091
4092 /**
4093 * The visit ratios of one chain from an already-formed chain routing block
4094 * `Pc` (dim = sel.size() * nIC), the no-fork body of sn_refresh_visits: solve
4095 * the embedded DTMC over the visited states and normalise by the total visits
4096 * at the reference node. NaN entries (a Cache class switch can leave one) are
4097 * given the row's residual probability spread uniformly, as the reference
4098 * does. Fork models keep the route_eff path (`chain_visits`); this is used by
4099 * the cacheqn driver, which rewrites `rtnodes` directly and has no fork.
4100 */
4101 Matrix<T> visits_from_block(const Matrix<T>& Pc_in, const std::vector<std::size_t>& sel,
4102 const std::vector<std::size_t>& ic, std::size_t refnode) const {
4103 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
4104 const std::size_t nIC = ic.size();
4105 const std::size_t dim = Pc_in.rows();
4106 Matrix<T> Pc = Pc_in;
4107 // NaN handling: distribute the row's residual mass over the NaN columns.
4108 for (std::size_t row = 0; row < dim; ++row) {
4109 T nonnan = zero;
4110 std::size_t nnan = 0;
4111 for (std::size_t col = 0; col < dim; ++col) {
4112 const double v = num_traits<T>::to_double(Pc(row, col));
4113 if (v != v) ++nnan;
4114 else nonnan = T(nonnan + Pc(row, col));
4115 }
4116 if (nnan == 0) continue;
4117 const double rem = 1.0 - num_traits<T>::to_double(nonnan);
4118 const T fill = (rem > 0.0) ? num_traits<T>::from_double(rem / double(nnan)) : zero;
4119 for (std::size_t col = 0; col < dim; ++col) {
4120 const double v = num_traits<T>::to_double(Pc(row, col));
4121 if (v != v) Pc(row, col) = fill;
4122 }
4123 }
4124 std::size_t chain = nchains;
4125 for (std::size_t c = 0; c < nchains; ++c)
4126 if (inchain[c] == ic) chain = c;
4127 const std::vector<bool> reach =
4128 chain_reachable(Pc, chain < nchains ? chain_seeds(chain, sel) : std::vector<std::size_t>(),
4129 chain_retrieval_edges(ic, sel));
4130 std::vector<std::size_t> visited;
4131 for (std::size_t row = 0; row < dim; ++row) {
4132 T s = zero;
4133 for (std::size_t col = 0; col < dim; ++col) s += Pc(row, col);
4134 if (s > zero && reach[row]) visited.push_back(row);
4135 }
4136 std::vector<T> alpha(dim, zero);
4137 if (!visited.empty()) {
4138 Matrix<T> Pv(visited.size(), visited.size(), zero);
4139 for (std::size_t a = 0; a < visited.size(); ++a)
4140 for (std::size_t b = 0; b < visited.size(); ++b) Pv(a, b) = Pc(visited[a], visited[b]);
4141 std::vector<T> av;
4142 bool ok = false;
4143 try {
4144 av = mc::dtmc_solve(Pv);
4145 ok = true;
4146 bool allzero = true, hasnan = false;
4147 for (const T& x : av) {
4148 if (x != zero) allzero = false;
4149 if (num_traits<T>::to_double(x) != num_traits<T>::to_double(x)) hasnan = true;
4150 }
4151 if (allzero || hasnan) ok = false;
4152 } catch (const Error&) {
4153 ok = false;
4154 }
4156 for (std::size_t a = 0; a < visited.size(); ++a) alpha[visited[a]] = av[a];
4157 }
4158 Matrix<T> out(sel.size(), nclasses, zero);
4159 for (std::size_t a = 0; a < sel.size(); ++a)
4160 for (std::size_t x = 0; x < nIC; ++x) out(a, ic[x] - 1) = alpha[a * nIC + x];
4161 std::size_t refrow = sel.size();
4162 for (std::size_t a = 0; a < sel.size(); ++a)
4163 if (sel[a] == refnode) refrow = a;
4164 if (refrow < sel.size()) {
4165 T norm = zero;
4166 for (std::size_t x = 0; x < nIC; ++x) norm += out(refrow, ic[x] - 1);
4168 for (std::size_t a = 0; a < sel.size(); ++a)
4169 for (std::size_t x = 0; x < nIC; ++x)
4170 out(a, ic[x] - 1) = T(out(a, ic[x] - 1) / norm);
4171 }
4172 for (std::size_t a = 0; a < sel.size(); ++a)
4173 for (std::size_t x = 0; x < nIC; ++x)
4174 if (out(a, ic[x] - 1) < zero) out(a, ic[x] - 1) = T(-out(a, ic[x] - 1));
4175 return out;
4176 }
4177
4178 /**
4179 * Recompute `rt`, `visits` and `nodevisits` after the caller has rewritten
4180 * `rtnodes` in place -- the cacheqn driver's per-sweep refresh. `rt` is the
4181 * stochastic complement of `rtnodes` over the stateful (node, class) rows;
4182 * the per-chain visits then read directly off `rt` (stateful) and `rtnodes`
4183 * (all nodes), with the chains held FIXED (the reference does not recompute
4184 * them inside the sweep). No-fork only; a fork keeps the refresh path.
4185 */
4187 const T zero = num_traits<T>::from_int(0);
4188 const std::size_t K = nclasses, I = nodes.size(), M = stateful_nodes.size();
4189 for (const NodeDef& nd : nodes)
4190 if (nd.nodetype == NodeType::Fork)
4191 throw UnsupportedError(ErrorKind{"Fork"}, "da_recompute_visits_from_rtnodes: fork models keep the "
4192 "route_eff visit path");
4193 std::vector<std::size_t> keep;
4194 keep.reserve(M * K);
4195 for (std::size_t p = 0; p < M; ++p) {
4196 const std::size_t base = (stateful_nodes[p] - 1) * K;
4197 for (std::size_t r = 0; r < K; ++r) keep.push_back(base + r);
4198 }
4199 rt = mc::dtmc_stochcomp(rtnodes, keep);
4200
4201 std::vector<std::size_t> allnodes(I);
4202 for (std::size_t a = 0; a < I; ++a) allnodes[a] = a + 1;
4203 visits.assign(nchains, Matrix<T>(M, K, zero));
4204 nodevisits.assign(nchains, Matrix<T>(I, K, zero));
4205 for (std::size_t c = 0; c < nchains; ++c) {
4206 const std::vector<std::size_t>& ic = inchain[c];
4207 const std::size_t nIC = ic.size();
4208 // stateful visits from rt (ordered by stateful position)
4209 Matrix<T> Ps(M * nIC, M * nIC, zero);
4210 for (std::size_t a = 0; a < M; ++a)
4211 for (std::size_t x = 0; x < nIC; ++x)
4212 for (std::size_t b = 0; b < M; ++b)
4213 for (std::size_t y = 0; y < nIC; ++y)
4214 Ps(a * nIC + x, b * nIC + y) = rt(a * K + (ic[x] - 1), b * K + (ic[y] - 1));
4215 const std::size_t rstat = classes[ic[0] - 1].refstat;
4216 const std::size_t refnode = station_to_node[rstat - 1];
4217 // sel for the stateful block is the stateful node list; its "refnode"
4218 // position must be matched by node index, so pass stateful_nodes.
4219 visits[c] = visits_from_block(Ps, stateful_nodes, ic, refnode);
4220 // node visits from rtnodes (ordered by node)
4221 Matrix<T> Pn(I * nIC, I * nIC, zero);
4222 for (std::size_t a = 0; a < I; ++a)
4223 for (std::size_t x = 0; x < nIC; ++x)
4224 for (std::size_t b = 0; b < I; ++b)
4225 for (std::size_t y = 0; y < nIC; ++y)
4226 Pn(a * nIC + x, b * nIC + y) =
4227 rtnodes(a * K + (ic[x] - 1), b * K + (ic[y] - 1));
4228 nodevisits[c] = visits_from_block(Pn, allnodes, ic, refnode);
4229 }
4230 }
4231
4232 /**
4233 * The chain-restricted routing over the STATEFUL nodes: the node-level
4234 * routing with the non-stateful nodes eliminated by a stochastic
4235 * complement, S = P11 + P12 (I - P22)^-1 P21. That is MATLAB's
4236 * dtmc_stochcomp, and it is what removes a Fork (and, there, the auto-added
4237 * ClassSwitch nodes) from the visit equations.
4238 */
4239 Matrix<T> station_routing(const std::vector<std::size_t>& ic) const {
4240 const T zero = num_traits<T>::from_int(0);
4241 const std::size_t nIC = ic.size();
4242 const std::size_t I = nodes.size();
4243 Matrix<T> full(I * nIC, I * nIC, zero);
4244 std::vector<std::size_t> pos(classes.size() + 1, 0); // class -> 1-based position in ic
4245 for (std::size_t x = 0; x < nIC; ++x) pos[ic[x]] = x + 1;
4246 for_each_route_block([&](std::size_t r, std::size_t s, const Matrix<T>& M) {
4247 if (r >= pos.size() || s >= pos.size() || pos[r] == 0 || pos[s] == 0) return;
4248 const std::size_t x = pos[r] - 1, y = pos[s] - 1;
4249 for (std::size_t a = 0; a < I; ++a)
4250 for (std::size_t b = 0; b < I; ++b) full(a * nIC + x, b * nIC + y) = M(a, b);
4251 });
4252 return stoch_comp_stateful(full, nIC);
4253 }
4254
4255 /**
4256 * Visit every (r, s) block of the routing in force, the one route_eff reads: Peff when the
4257 * expansion ran, else P. A block smaller than nnodes reads as zero there, so it is skipped.
4258 */
4259 template <class F>
4260 void for_each_route_block(F f) const {
4261 const std::map<std::pair<std::size_t, std::size_t>, Matrix<T>>& src = Peff.empty() ? P : Peff;
4262 for (const auto& kv : src)
4263 if (kv.second.rows() >= nodes.size() && kv.second.cols() >= nodes.size())
4264 f(kv.first.first, kv.first.second, kv.second);
4265 }
4266
4267 /**
4268 * The stochastic complement of a NODE-level routing block over the stateful
4269 * nodes, S = P11 + P12 (I - P22)^-1 P21.
4270 *
4271 * Split out of `station_routing` because the STATE-DEPENDENT routing table
4272 * (`rt_state`, state.h) is the same complement of a block whose SDR rows
4273 * have been re-evaluated at one state: the two must eliminate the stateless
4274 * nodes identically, or the per-state table and `rt` would disagree on a
4275 * model that merely has a Router in it.
4276 *
4277 * @param full node-major (nnodes * nIC) square block
4278 * @param nIC number of classes carried per node in that block
4279 */
4280 Matrix<T> stoch_comp_stateful(const Matrix<T>& full, std::size_t nIC) const {
4281 const T zero = num_traits<T>::from_int(0);
4282 const T one = num_traits<T>::from_int(1);
4283 const std::size_t I = nodes.size();
4284 std::vector<std::size_t> keep, drop;
4285 for (std::size_t a = 0; a < I; ++a) {
4286 const bool st = nodes[a].stateful;
4287 for (std::size_t x = 0; x < nIC; ++x) (st ? keep : drop).push_back(a * nIC + x);
4288 }
4289 if (drop.empty()) return full;
4290 // Only the stateless states a stateful one can reach enter S = P11 + P12 X, and that set is
4291 // closed under P22, so its own sub-system gives X exactly. On a flattened LQN nearly every
4292 // (node, class) pair is unreachable, and eliminating them all was cubic in nnodes*nclasses.
4293 {
4294 std::vector<char> isdrop(full.rows(), 0), seen(full.rows(), 0);
4295 for (std::size_t d : drop) isdrop[d] = 1;
4296 std::vector<std::size_t> stack;
4297 for (std::size_t k : keep)
4298 for (std::size_t d : drop)
4299 if (!seen[d] && full(k, d) != zero) { seen[d] = 1; stack.push_back(d); }
4300 while (!stack.empty()) {
4301 const std::size_t u = stack.back();
4302 stack.pop_back();
4303 for (std::size_t d : drop)
4304 if (!seen[d] && full(u, d) != zero) { seen[d] = 1; stack.push_back(d); }
4305 }
4306 std::vector<std::size_t> reach;
4307 for (std::size_t d : drop)
4308 if (seen[d]) reach.push_back(d);
4309 drop.swap(reach);
4310 }
4311 if (drop.empty()) {
4312 Matrix<T> S(keep.size(), keep.size(), zero);
4313 for (std::size_t a = 0; a < keep.size(); ++a)
4314 for (std::size_t b = 0; b < keep.size(); ++b) S(a, b) = full(keep[a], keep[b]);
4315 return S;
4316 }
4317
4318 const std::size_t nk = keep.size(), nd = drop.size();
4319 Matrix<T> P11(nk, nk, zero), P12(nk, nd, zero), P21(nd, nk, zero), A(nd, nd, zero);
4320 for (std::size_t a = 0; a < nk; ++a) {
4321 for (std::size_t b = 0; b < nk; ++b) P11(a, b) = full(keep[a], keep[b]);
4322 for (std::size_t b = 0; b < nd; ++b) P12(a, b) = full(keep[a], drop[b]);
4323 }
4324 for (std::size_t a = 0; a < nd; ++a) {
4325 for (std::size_t b = 0; b < nk; ++b) P21(a, b) = full(drop[a], keep[b]);
4326 for (std::size_t b = 0; b < nd; ++b)
4327 A(a, b) = T((a == b ? one : zero) - full(drop[a], drop[b]));
4328 }
4329 // X = A^-1 P21 by Gaussian elimination with partial pivoting
4330 Matrix<T> X = P21;
4331 std::vector<std::size_t> piv(nd);
4332 for (std::size_t i = 0; i < nd; ++i) piv[i] = i;
4333 for (std::size_t col = 0; col < nd; ++col) {
4334 std::size_t best = col;
4335 double bv = std::fabs(num_traits<T>::to_double(A(col, col)));
4336 for (std::size_t r2 = col + 1; r2 < nd; ++r2) {
4337 const double v = std::fabs(num_traits<T>::to_double(A(r2, col)));
4338 if (v > bv) { bv = v; best = r2; }
4339 }
4340 if (best != col) {
4341 for (std::size_t b = 0; b < nd; ++b) std::swap(A(col, b), A(best, b));
4342 for (std::size_t b = 0; b < nk; ++b) std::swap(X(col, b), X(best, b));
4343 }
4344 if (A(col, col) == zero)
4345 throw NumericError(ErrorKind{"Singular"}, "network: the stochastic complement is singular");
4346 // The update touches only the pivot row's nonzero columns; a zero one would change nothing
4347 std::vector<std::size_t> nzA, nzX;
4348 for (std::size_t b = 0; b < nd; ++b)
4349 if (A(col, b) != zero) nzA.push_back(b);
4350 for (std::size_t b = 0; b < nk; ++b)
4351 if (X(col, b) != zero) nzX.push_back(b);
4352 for (std::size_t r2 = 0; r2 < nd; ++r2) {
4353 if (r2 == col) continue;
4354 const T f = T(A(r2, col) / A(col, col));
4355 if (f == zero) continue;
4356 for (std::size_t b : nzA) A(r2, b) = T(A(r2, b) - f * A(col, b));
4357 for (std::size_t b : nzX) X(r2, b) = T(X(r2, b) - f * X(col, b));
4358 }
4359 }
4360 for (std::size_t r2 = 0; r2 < nd; ++r2)
4361 for (std::size_t b = 0; b < nk; ++b) X(r2, b) = T(X(r2, b) / A(r2, r2));
4362
4363 // Same summation order as the dense product, with the zero terms of P12 skipped
4364 Matrix<T> S = P11;
4365 std::vector<T> acc(nk, zero);
4366 for (std::size_t a = 0; a < nk; ++a) {
4367 std::fill(acc.begin(), acc.end(), zero);
4368 for (std::size_t m = 0; m < nd; ++m) {
4369 const T w = P12(a, m);
4370 if (w == zero) continue;
4371 for (std::size_t b = 0; b < nk; ++b) acc[b] += w * X(m, b);
4372 }
4373 for (std::size_t b = 0; b < nk; ++b) S(a, b) = T(S(a, b) + acc[b]);
4374 }
4375 return S;
4376 }
4377
4378 // ---- predicates, ports of the sn_has_* family -------------------------
4379
4380 bool has_open_classes() const {
4381 for (const JobClass& c : classes)
4382 if (std::isinf(c.population)) return true;
4383 return false;
4384 }
4385
4386 /**
4387 * `sn_is_open_model`: EVERY class is open, which is not `has_open_classes`.
4388 * A mixed model passes that predicate and fails this one, and an analyzer
4389 * that confuses the two hands a closed chain to a solver with no level for
4390 * its population. An empty class list is not an open model either.
4391 */
4392 bool is_open_model() const {
4393 for (const JobClass& c : classes)
4394 if (!std::isinf(c.population)) return false;
4395 return !classes.empty();
4396 }
4397
4398 bool has_multi_server() const {
4399 for (const Station<T>& s : stations)
4400 if (std::isfinite(s.nservers) && s.nservers > 1.0) return true;
4401 return false;
4402 }
4403
4405 for (const JobClass& c : classes)
4406 if (std::isfinite(c.population) && c.population != std::floor(c.population + 0.5))
4407 return true;
4408 return false;
4409 }
4410
4411 bool has_class_switching() const { return nclasses != nchains; }
4412
4413 bool has_priorities() const {
4414 for (const JobClass& c : classes)
4415 if (c.prio > 0) return true;
4416 return false;
4417 }
4418
4419 /**
4420 * Whether the classes carry more than one priority level.
4421 *
4422 * NOT `has_priorities()`, which asks whether any priority is nonzero: a
4423 * model whose classes all sit at level 1 has priorities by that test and
4424 * nothing to distinguish, and the reference's warning below keys on the
4425 * distinction rather than on the magnitude.
4426 */
4428 if (classes.empty()) return false;
4429 for (const JobClass& c : classes)
4430 if (c.prio != classes.front().prio) return true;
4431 return false;
4432 }
4433
4434 /**
4435 * Whether some station runs a discipline that READS the class priorities.
4436 *
4437 * The list is exactly the `*PRIO` family. Priority-awareness is a property
4438 * of the DECLARED policy and is never inferred from the data: a base policy
4439 * is not upgraded because the classes it was handed carry unequal
4440 * priorities. `afterEventStation` did infer it once, and plain LCFS with
4441 * distinct priorities then behaved as none of the three policies involved.
4442 */
4444 for (const Station<T>& s : stations) {
4445 switch (s.sched) {
4446 case SchedStrategy::HOL:
4447 case SchedStrategy::PSPRIO:
4448 case SchedStrategy::DPSPRIO:
4449 case SchedStrategy::GPSPRIO:
4450 case SchedStrategy::LCFSPRIO:
4451 case SchedStrategy::LCFSPRPRIO:
4452 case SchedStrategy::LCFSPIPRIO:
4453 case SchedStrategy::FCFSPRPRIO:
4454 case SchedStrategy::FCFSPIPRIO:
4455 case SchedStrategy::SRPTPRIO:
4456 return true;
4457 default:
4458 break;
4459 }
4460 }
4461 return false;
4462 }
4463
4464 /** Priorities were declared and no station will read them. */
4465 bool priorities_ignored() const {
4467 }
4468
4469 /**
4470 * Port of sn_has_homogeneous_scheduling: true when EVERY station uses
4471 * `strategy`, the reference's `length(findstring(sn.sched, strategy)) ==
4472 * sn.nstations` (the count of stations whose discipline is `strategy`
4473 * equals the station count), as the JAR and Python ports evaluate it and
4474 * as solver_amva's "trivial models" dispatch reads it. This port used to
4475 * reduce it to `nstations == 1`; it now follows the reference.
4476 */
4478 std::size_t count = 0;
4479 for (std::size_t i = 0; i < nstations && i < stations.size(); ++i)
4480 if (stations[i].sched == strategy) ++count;
4481 return count == nstations;
4482 }
4483
4485 bool bad = false;
4486 for (std::size_t i = 0; i < nstations; ++i) {
4487 if (stations[i].sched != SchedStrategy::FCFS) continue;
4488 bool any = false;
4490 for (std::size_t r = 0; r < nclasses; ++r) {
4491 if (disabled[i][r]) continue; // MATLAB drops the NaN entries here
4492 if (!any) {
4493 lo = hi = rates(i, r);
4494 any = true;
4495 } else {
4496 if (rates(i, r) < lo) lo = rates(i, r);
4497 if (rates(i, r) > hi) hi = rates(i, r);
4498 }
4499 }
4500 // Relative FineTol, not hi > lo: a one-ulp spread in LN layer rates flipped exact MVA vs AMVA
4501 if (any && num_traits<T>::to_double(hi - lo) >
4503 bad = true;
4504 }
4505 return bad;
4506 }
4507
4509 for (const Station<T>& s : stations)
4510 if (!(s.sched == SchedStrategy::INF || s.sched == SchedStrategy::PS ||
4511 s.sched == SchedStrategy::FCFS || s.sched == SchedStrategy::LCFSPR ||
4512 s.sched == SchedStrategy::LCFS || s.sched == SchedStrategy::EXT))
4513 return false;
4514 return true;
4515 }
4516
4517 /**
4518 * BCMP type 1 asks the FCFS service to be exponential. `has_multi_class_heter_fcfs`
4519 * compares the class MEANS only, so a class-homogeneous Erlang, hyper-exponential or
4520 * deterministic FCFS station used to pass this gate and be dispatched to exact MVA,
4521 * which reads the means alone and returns the exponential answer with no warning.
4522 */
4524 for (std::size_t i = 0; i < nstations; ++i) {
4525 if (stations[i].sched != SchedStrategy::FCFS) continue;
4526 for (std::size_t r = 0; r < nclasses; ++r) {
4527 const double v = num_traits<T>::to_double(scv(i, r));
4528 if (std::isinf(v) || !(v > 0.0)) continue;
4529 if (!(v > 1.0 - GlobalConstants::FineTol && v < 1.0 + GlobalConstants::FineTol))
4530 return false;
4531 }
4532 }
4533 return true;
4534 }
4535
4536 /**
4537 * Kendall's K of station IST (1-based), +inf when unbounded. Implementation of
4538 * api/sn/sn_get_buffer_size.h, which delegates here so the solvers' member calls and
4539 * the api free function cannot drift; the traps are documented on that header.
4540 */
4541 double buffer_size(std::size_t ist) const {
4542 double k = std::numeric_limits<double>::infinity();
4543 if (cap[ist - 1] >= 0.0) k = std::min(k, cap[ist - 1]);
4544 double ccap = 0.0;
4545 bool anyServed = false;
4546 for (std::size_t r = 0; r < nclasses; ++r)
4547 if (classcap[ist - 1][r] > 0.0) {
4548 ccap += classcap[ist - 1][r];
4549 anyServed = true;
4550 }
4551 if (anyServed) k = std::min(k, ccap);
4552 // Only the classes served here count, as in the reference: with none (every chain
4553 // reaching the station has population 0, e.g. a chain parked by sn_zero_capacity)
4554 // nothing can arrive, and summing every class instead read the zero cap as blocking.
4555 double reachable = 0.0;
4556 for (std::size_t r = 0; r < nclasses; ++r)
4557 if (classcap[ist - 1][r] > 0.0) reachable += classes[r].population;
4558 return (k >= reachable) ? std::numeric_limits<double>::infinity() : k;
4559 }
4560
4561 /** Implementation of api::sn_is_mm1k_loss; that free function delegates here. */
4562 bool is_mm1k_loss() const {
4563 if (nclasses != 1 || nodes.size() != 3) return false;
4564 for (std::size_t k = 0; k < classes.size(); ++k)
4565 if (std::isfinite(classes[k].population)) return false; // nclosedjobs ~= 0
4566 std::size_t nq = 0, nsrc = 0, nsink = 0, qnode = 0, snode = 0;
4567 for (std::size_t a = 0; a < nodes.size(); ++a) {
4568 switch (nodes[a].nodetype) {
4569 case NodeType::Queue: ++nq; qnode = a + 1; break;
4570 case NodeType::Source: ++nsrc; snode = a + 1; break;
4571 case NodeType::Sink: ++nsink; break;
4572 default: return false;
4573 }
4574 }
4575 if (nq != 1 || nsrc != 1 || nsink != 1) return false;
4576 const std::size_t qist = nodes[qnode - 1].station;
4577 const std::size_t sist = nodes[snode - 1].station;
4578 if (qist == 0 || sist == 0) return false;
4579 if (stations[qist - 1].nservers != 1.0) return false;
4580 if (droprule.size() < qist || droprule[qist - 1].empty() ||
4581 droprule[qist - 1][0] != DropStrategy::DROP)
4582 return false;
4583 if (!(qist <= cap.size()) || !std::isfinite(cap[qist - 1]) || !(cap[qist - 1] > 0.0))
4584 return false;
4585 if (std::fabs(num_traits<T>::to_double(scv(sist - 1, 0)) - 1.0) > 1e-6) return false;
4586 if (std::fabs(num_traits<T>::to_double(scv(qist - 1, 0)) - 1.0) > 1e-6) return false;
4587 // A DECLARED STATE DEPENDENCE IS NOT AN M/M/1/K. Both closed forms this
4588 // predicate gates read ONE service rate -- `qsys_mm1k_loss` a scalar mu,
4589 // `qsys_mg1k_loss_mgs` the first two moments of one service law -- so a
4590 // station carrying lldscaling, cdscaling or jdscaling would be answered with
4591 // the UNSCALED queue: a wrong number, not a coarse one, and silent.
4592 const qn::Station<T>& qst = stations[qist - 1];
4593 if (!qst.lldscaling.empty() || static_cast<bool>(qst.cdscaling) ||
4594 static_cast<bool>(qst.jdscaling))
4595 return false;
4596 return true;
4597 }
4598
4599 /**
4600 * Some station can REFUSE a job: its own buffer BINDS, or a finite capacity region
4601 * caps a set of stations jointly. Implementation of api::sn_has_blocking, which
4602 * delegates here; the rule and its two exemptions are documented on that function.
4603 */
4604 bool has_blocking() const {
4605 if (!regions.empty()) return true;
4606 for (std::size_t a = 0; a < nodes.size(); ++a)
4607 if (nodes[a].nodetype == NodeType::Cache) return false;
4608 if (is_mm1k_loss()) return false;
4609 for (std::size_t ist = 1; ist <= stations.size(); ++ist)
4610 if (std::isfinite(buffer_size(ist))) return true;
4611 return false;
4612 }
4613
4614 /** `sn_has_sd_routing`: RROBIN, WRROBIN, JSQ, SQ or SDR at some node. */
4615 bool has_sd_routing() const {
4616 for (const NodeDef& n : nodes)
4617 for (RoutingStrategy rs : n.routing)
4618 if (rs == RoutingStrategy::RROBIN || rs == RoutingStrategy::WRROBIN ||
4619 rs == RoutingStrategy::JSQ || rs == RoutingStrategy::SQ || rs == RoutingStrategy::SDR)
4620 return true;
4621 return false;
4622 }
4623
4624 /**
4625 * `sn_has_product_form`, conjunct for conjunct. A surviving Fork/Join pair (`fj`, which the
4626 * MMT and H-T transforms clear) and state-dependent routing are not product form: without
4627 * them the exact NC and MVA gates offered 'exact' on a fork-join model the reference refuses.
4628 */
4629 bool has_product_form() const {
4630 // BCMP asks for infinite buffers: without this conjunct a BAS-blocked station or
4631 // any binding finite buffer read as product form, though its truncation couples
4632 // the station occupancies.
4634 fj.empty() && !has_sd_routing() && !has_blocking() && has_exponential_fcfs();
4635 }
4636
4637 /**
4638 * Port of sn_has_product_form_not_het_fcfs: LCFS is excluded, and at FCFS the service
4639 * must be exponential AND class-independent, which is what BCMP type 1 asks for. With
4640 * unequal per-class means the product-form solve returns a wait proportional to each
4641 * class's own demand where FCFS makes every class wait behind the same queue. The mean
4642 * comparison is between CHAIN service times (visit-weighted over the classes that
4643 * actually visit the station): a class that never visits cannot break product form,
4644 * and within-chain heterogeneity is invisible to both the product-form and the qd
4645 * branch, which deaggregate a chain result proportionally to each class's own demand,
4646 * so only between-chain heterogeneity warrants the divert. LN layers carry seeded
4647 * rates for classes with zero visits, which a raw per-class comparison mistakes for
4648 * heterogeneity.
4649 *
4650 * CHECK_MEANS drops the mean test; pass false only for an algorithm
4651 * that models class-dependent FCFS itself (ab, schmidt, schmidt-ext).
4652 */
4653 bool has_product_form_not_het_fcfs(bool check_means = true) const {
4654 for (const Station<T>& s : stations)
4655 if (!(s.sched == SchedStrategy::INF || s.sched == SchedStrategy::PS ||
4656 s.sched == SchedStrategy::FCFS || s.sched == SchedStrategy::LCFSPR ||
4657 s.sched == SchedStrategy::EXT))
4658 return false;
4659 if (has_priorities()) return false;
4660 for (std::size_t i = 0; i < nstations; ++i) {
4661 if (stations[i].sched != SchedStrategy::FCFS) continue;
4662 for (std::size_t r = 0; r < nclasses; ++r) {
4663 if (disabled[i][r]) continue;
4664 const double v = num_traits<T>::to_double(scv(i, r));
4665 if (!std::isinf(v) && v > 0.0 &&
4666 !(v > 1.0 - GlobalConstants::FineTol && v < 1.0 + GlobalConstants::FineTol))
4667 return false;
4668 }
4669 if (!check_means || visits.empty()) continue;
4670 const std::size_t isf = stateful_of_station(i + 1) - 1;
4671 double stmin = 0.0, stmax = 0.0;
4672 bool anyserved = false;
4673 for (std::size_t c = 0; c < nchains && c < visits.size(); ++c) {
4674 double num = 0.0, den = 0.0;
4675 for (std::size_t r = 0; r < nclasses; ++r) {
4676 if (!chains.empty() && !chains[c][r]) continue;
4677 if (disabled[i][r]) continue;
4678 const double w = num_traits<T>::to_double(visits[c](isf, r));
4679 const double rate = num_traits<T>::to_double(rates(i, r));
4680 if (w > GlobalConstants::Zero && std::isfinite(rate) && rate > 0.0) {
4681 num += w / rate;
4682 den += w;
4683 }
4684 }
4685 if (den > 0.0) {
4686 const double st = num / den;
4687 if (!anyserved) {
4688 stmin = stmax = st;
4689 anyserved = true;
4690 } else {
4691 stmin = std::min(stmin, st);
4692 stmax = std::max(stmax, st);
4693 }
4694 }
4695 }
4696 if (anyserved && stmax - stmin > GlobalConstants::CoarseTol * stmax) return false;
4697 }
4698 return true;
4699 }
4700
4701 /** Grow every routing block to the current node count. */
4703 for (auto& kv : P) grow_block(kv.second);
4704 }
4705 void grow_block(Matrix<T>& B) const {
4706 if (B.rows() == nodes.size()) return;
4707 Matrix<T> g(nodes.size(), nodes.size(), num_traits<T>::from_int(0));
4708 for (std::size_t a = 0; a < B.rows(); ++a)
4709 for (std::size_t b = 0; b < B.cols(); ++b) g(a, b) = B(a, b);
4710 B = g;
4711 }
4712
4713 /** Total population, as MATLAB's getNumberOfJobs summed. */
4714 double total_jobs() const {
4715 double s = 0.0;
4716 for (const JobClass& c : classes)
4717 if (std::isfinite(c.population)) s += c.population;
4718 return s;
4719 }
4720};
4721
4722/**
4723 * The swap graph of a PAS / OI station, with the defaults `refreshLocalVars.m`
4724 * installs applied.
4725 *
4726 * MATLAB fills `sn.nodeparam{ind}.swapGraph` at refresh time and NEVER leaves it
4727 * empty: an OI station always gets `zeros(R,R)`, a PAS station with no explicit
4728 * graph gets the complete compatibility graph `ones(R,R) - eye(R)`, and an
4729 * explicit graph is taken as given. The builder here stores the raw user graph,
4730 * so the defaulting has to happen on read; doing it here rather than in the
4731 * builder keeps it independent of whether the classes were added before or
4732 * after `set_service_rate_function`.
4733 *
4734 * @param sn the struct
4735 * @param ist 1-based station index
4736 */
4737template <class T>
4739 const Station<T>& st = sn.stations[ist - 1];
4740 const std::size_t R = sn.nclasses;
4741 const T zero = num_traits<T>::from_int(0);
4742 if (st.sched == SchedStrategy::OI) return Matrix<T>(R, R, zero);
4743 if (!st.swap_graph.empty()) return st.swap_graph;
4744 if (st.sched != SchedStrategy::PAS) return Matrix<T>(R, R, zero);
4746 for (std::size_t r = 0; r < R; ++r) g(r, r) = zero;
4747 return g;
4748}
4749
4750/** True when the station's materialized swap graph is entirely zero. */
4751template <class T>
4752bool station_swap_graph_is_zero(const NetworkStruct<T>& sn, std::size_t ist) {
4753 const Matrix<T> g = station_swap_graph(sn, ist);
4754 const T zero = num_traits<T>::from_int(0);
4755 for (std::size_t a = 0; a < g.rows(); ++a)
4756 for (std::size_t b = 0; b < g.cols(); ++b)
4757 if (g(a, b) != zero) return false;
4758 return true;
4759}
4760
4761} // namespace qn
4762} // namespace line
4763
4764#endif // LINE_LANG_QN_NETWORK_STRUCT_H
Base error for the multiprecision C++ port.
Definition error.h:73
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
void set_route(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
P{r,s}(i,j) = p, with 1-based NODE and class indices.
std::size_t sourceIdx
1-based station index of the Source, 0 = none
Matrix< double > phases() const
sn.phases (M x K), as refreshProcessRepresentations.m leaves it: 0 for a disabled pair,...
bool has_multi_class_heter_fcfs() const
bool has_distinct_priorities() const
Whether the classes carry more than one priority level.
std::size_t nvars_of(std::size_t ind) const
Total local-variable width of node ind (1-based).
pfqn::SdrStruct sdr_nodes
Node-indexed twin of sdr.
std::size_t add_class(const JobClass &cl)
Add a class and grow the service table.
T get_route(std::size_t r, std::size_t s, std::size_t i, std::size_t j) const
P{r,s}(i,j), AS THE USER SET IT.
std::vector< Matrix< T > > nodevisits
(nchains) each (nnodes x nclasses)
void refresh_rates()
Port of MNetwork.refreshRates: lower each service process onto a rate and an SCV.
std::vector< double > node_to_stateful() const
sn.nodeToStateful (1 x N): the stateful index of each node, NaN when it is not stateful.
bool has_fluid_places() const
sn_has_fluid_places: does the net hold a fluid place, or a transition declaring a fluid arc?
std::function< std::complex< double >(const std::complex< double > &)> LstHandle
One LST handle: E[exp(-sX)] at a complex argument.
std::vector< std::size_t > chain_seeds(std::size_t c, const std::vector< std::size_t > &sel) const
Port of the per-chain body of sn_refresh_visits, over an arbitrary node subset – the stateful nodes f...
Matrix< T > chain_visits(std::size_t c, const std::vector< std::size_t > &sel, bool complement, bool fork) const
std::size_t stateful_index(std::size_t ind) const
1-based stateful index of node ind, 0 when the node is not stateful.
void refresh_cacheqn_actual_visits(const Matrix< T > &hitprob, const Matrix< T > &missprob)
Re-resolve the cache read self-switch from the offered 1/2-1/2 to the ACTUAL hit/miss probabilities t...
std::vector< std::vector< std::vector< double > > > pie() const
sn.pie (M x K cells): the phase an arriving job enters, map_pie of sn.proc{i}{r} (the Erlang conversi...
bool has_immediate_feedback() const
Whether immediate feedback is EFFECTIVE anywhere in the model.
std::vector< lang::RemovalPolicy > signalrempolicy
bool has_rtfun_routing() const
True when some routing must be re-read at every state from rt_state: SDR, and the JSQ and SQ(d) dispa...
std::vector< std::vector< bool > > immfeed
sn.immfeed: (nstations x nclasses) IMMEDIATE FEEDBACK, the reference's refreshStruct field.
const ForkParam< T > * fork_param_of(std::size_t node) const
The fork's override block, or null when it declares none.
std::size_t stateful_of_station(std::size_t st) const
std::vector< std::size_t > downstream_stations(std::size_t ind) const
The nodes directly downstream of ind, walking THROUGH stateless nodes and stopping at the first stati...
std::size_t nof_nodes() const
std::size_t nof_stateful() const
bool has_rr_routing() const
Whether ANY (node, class) pair dispatches round-robin.
Matrix< double > phaseshift() const
sn.phaseshift (M x K+1) = [0, cumsum(phasessz, 2)].
std::map< std::pair< std::size_t, std::size_t >, Matrix< T > > P
P[(r,s)] is an (nnodes x nnodes) block; absent means all zero.
std::size_t phases_of(std::size_t ist, std::size_t r) const
sn.phases(i,r): the order of the process representation.
std::map< std::size_t, TransitionParam< T > > transparam
Transition (SPN) parameters, keyed by 1-based node index.
void check_closed_reneging() const
Refuses RENEGING declared for a class of finite population at any station (closed_reneging_refusal).
std::size_t mark_carrier_of(std::size_t ist) const
The CARRIER of station i's marked arrival: the class of mark 1, whose phase block holds the one modul...
std::vector< std::vector< bool > > replyblock
sn.replyblock (nnodes x nclasses) and sn.syncreply (nclasses).
std::vector< std::size_t > all_nodes() const
Every node index, 1-based, for the node-level visit computation.
std::vector< std::vector< bool > > isbasdestination
sn.isbasdestination, (nstations x nclasses): true where a refusal at this station must BLOCK an upstr...
bool is_mm1k_loss() const
Implementation of api::sn_is_mm1k_loss; that free function delegates here.
void for_each_route_block(F f) const
Visit every (r, s) block of the routing in force, the one route_eff reads: Peff when the expansion ra...
bool has_sdr_routing() const
MATLAB's any(sn.isstatedep(:,3)) NARROWED TO THE ONE STRATEGY THIS PORT EVALUATES PER STATE: Krzesins...
std::vector< Reward > reward
std::vector< std::size_t > stateful_nodes
1-based node indices, ascending
Matrix< double > phasessz() const
sn.phasessz (M x K) = max(phases, 1), with MATLAB's two exceptions: a Join keeps its 0 and a non-carr...
bool sched_has_priority_aware() const
Whether some station runs a discipline that READS the class priorities.
std::vector< bool > iscatastrophe() const
sn.iscatastrophe (K x 1): true where class r is a catastrophe signal.
bool holds_reply_for(std::size_t ind, std::size_t r) const
True when node ind (1-based) holds a server across a synchronous call whose reply class is r (1-based...
void refresh_replyblock()
sn.replyblock, DERIVED from sn.syncreply and the routing.
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
void refresh_chains()
Port of MNetwork.refreshChains followed by sn_refresh_visits.
std::vector< std::vector< bool > > csmask
(nclasses x nclasses) class r can switch into s, diagonal set
void refresh_capacity()
Port of MNetwork.refreshCapacity.
std::vector< std::vector< bool > > chains
(nchains x nclasses)
void apply_sink_closure()
Route every open chain from the Sink back into the Source, as the tail of MATLAB's getRoutingMatrix d...
std::vector< std::size_t > refclass
(nchains) 1-based class, 0 = none
int gdscalingcutoff
sn.gdscalingcutoff: the per-slot OPEN-class truncation used when gdscaling is materialized onto the J...
std::map< std::size_t, SetupDelayOffParam< T > > setupparam
Setup / delay-off, keyed by 1-based STATION index.
std::map< std::size_t, BreakdownParam< T > > breakdownparam
Server breakdown / repair, keyed by 1-based STATION index.
bool has_exponential_fcfs() const
BCMP type 1 asks the FCFS service to be exponential.
void check_service_reachable() const
bool has_breakdown() const
Any station at all, the guard every solver gate needs first.
GdScaling< T > gdscaling
sn.gdscaling: the network-level globally state-dependent (Whittle) rate scaling phi(n).
std::vector< bool > fjauxclass
sn.fjauxclass: (nclasses+1, 1-based) true where the class is an MMT AUXILIARY OPEN class,...
std::vector< std::vector< std::size_t > > nvars
sn.nvars, (nnodes x 3R+1): the LOCAL VARIABLE columns each node appends to its state,...
bool has_service_law(std::size_t i, std::size_t r) const
Does (station i, class r) have a service law an analyzer may convert?
std::vector< std::vector< bool > > disabled
Matrix< T > visits_from_block(const Matrix< T > &Pc_in, const std::vector< std::size_t > &sel, const std::vector< std::size_t > &ic, std::size_t refnode) const
The visit ratios of one chain from an already-formed chain routing block Pc (dim = sel....
pfqn::SdrStruct sdr
Krzesinski (1987) product-form state-dependent routing, in 0-based STATION indices; empty unless a no...
std::vector< std::vector< bool > > reached_node_classes() const
The (node, class) pairs a job can actually ARRIVE at, 0-based on both axes.
void refresh_sched_param()
Port of MNetwork.refreshScheduling's schedparam half.
std::map< std::size_t, FjJoinParam > fjjoinparam
sn.nodeparam{j}.fj for each Join node: the tag matrix and the required sibling multiplicity after_eve...
std::vector< std::vector< LstHandle > > lst() const
sn.lst (M x K handles), refreshLST.m: the Laplace-Stieltjes transform of each law,...
bool serves_class(std::size_t ind, std::size_t r) const
Whether a job of class r (0-based) can LEAVE node ind (1-based) again.
double total_jobs() const
Total population, as MATLAB's getNumberOfJobs summed.
Matrix< T > rt
sn.rt and sn.rtnodes: the class-expanded routing.
void fill_signal_services()
Refuses a class that is ROUTED TO a station which cannot serve it.
const CtmcMapc< T > * mapc_of(std::size_t ist, std::size_t r) const
The pair form of class r (1-based) at station ist (1-based), or null when there is none.
std::map< std::size_t, RetrialParam< T > > retrialparam
Retrial parameters, keyed by 1-based STATION index.
void promote_stateful(std::size_t ind)
Make node ind stateful, keeping stateful_nodes ascending.
std::vector< bool > issignal
The G-network signal declaration, per CLASS.
void grow_routing()
Grow every routing block to the current node count.
std::map< std::size_t, CacheParam< T > > nodeparam
Cache parameters by 1-based NODE index; only Cache nodes have an entry.
Matrix< double > procid() const
sn.procid (M x K): the ProcessType id of each (station, class) pair, as refreshProcessTypes....
void grow_block(Matrix< T > &B) const
std::size_t map_var_slot(std::size_t ind, std::size_t r) const
0-based column of class r's MAP memory inside node ind's local-variable block.
std::size_t add_node(const std::string &nm, NodeType ty, bool stateful)
Add a non-station node (a Fork, a Router).
std::map< std::size_t, PasParam > pasparam
std::vector< double > cap
sn.cap and sn.classcap: the total and per-class buffers.
std::map< std::pair< std::size_t, std::size_t >, CtmcMapc< T > > ctmcmapc
MAP services in CTMC pair form, keyed by (1-based station, 1-based class).
std::vector< JobClass > classes
void rr_advance(std::size_t ind, std::size_t r, std::vector< T > &varrow) const
Advance the pointer of (ind, r) in VARROW by one position, cyclically.
std::vector< bool > isbasblocking
sn.isbasblocking, per NODE: true where the node is the BLOCKING (upstream) side of a true-BAS relatio...
bool has_product_form_not_het_fcfs(bool check_means=true) const
Port of sn_has_product_form_not_het_fcfs: LCFS is excluded, and at FCFS the service must be exponenti...
std::size_t sinkNode
1-based NODE index of the Sink, 0 = none (it is not a station)
std::size_t nregions() const
sn.nregions: the number of finite capacity regions.
Matrix< double > isph() const
sn.isph (M x K): true where sn.proc{i}{r} with sn.pie{i}{r} admits a phase-type reading (sn_is_phaset...
std::vector< std::vector< std::vector< double > > > mu() const
sn.mu (M x K cells): the total outgoing rate of each phase, MATLAB's getServiceRates/getSourceRates.
Matrix< double > connmatrix() const
sn.connmatrix (N x N): 1 where some class routes node i to node j with positive probability.
std::map< std::size_t, JoinDecl > joindecl
std::vector< std::size_t > signaltarget
std::vector< std::vector< std::vector< double > > > phi() const
sn.phi (M x K cells): the completion probability of each phase; 1 where mu is a single rate.
void refresh_local_vars()
Port of MNetwork.refreshLocalVars: the per-node local-variable widths.
ProcessType procid(std::size_t ist, std::size_t r) const
sn.procid(i,r): the process type of a (station, class) pair.
std::size_t rr_dest(std::size_t ind, std::size_t r, const std::vector< T > &varrow) const
The destination node the pointer in VARROW names, or 0 when (ind, r) does not dispatch round-robin or...
PollingParam effective_polling(std::size_t ist) const
The polling controller of station ist, from whichever API declared it.
std::vector< T > gdscalingpeak
sn.gdscalingpeak: the declared (nstations x nclasses) peak of gdscaling, row-major,...
std::size_t rr_var_slot(std::size_t ind, std::size_t r) const
1-BASED index of the pointer of (ind, r) INSIDE the node's local-variable block, or 0 when that pair ...
void refresh_bas_blocking()
Port of refreshLocalVars' true-BAS block and its declaresBlockedMarker helper: which nodes carry the ...
std::vector< Station< T > > stations
stations[k-1] is the k-th station
std::vector< std::size_t > rr_outlinks(std::size_t ind, std::size_t r) const
ROUND-ROBIN DISPATCH, the state that makes it deterministic.
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
void refresh_struct()
The whole chain, in MATLAB's refreshStruct order.
T route_eff(std::size_t r, std::size_t s, std::size_t i, std::size_t j) const
The routing actually in force: the expansion when there is one, else P.
bool is_queueing_place(std::size_t i) const
Is station i (0-based) a QUEUEING PLACE, i.e.
void set_service(std::size_t station, std::size_t cls, const Distrib< T > &d)
std::vector< std::size_t > syncreply
std::map< std::pair< std::size_t, std::size_t >, Matrix< T > > Peff
The routing after refresh_routing() has expanded the non-PROB strategies and folded the class switche...
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
Matrix< double > isstatedep() const
sn.isstatedep (N x 3): column 1 the input section (never set by the reference), column 2 state-depend...
std::size_t node_of_station(std::size_t st) const
1-based node index of station st, 0 when the map has no entry.
void da_recompute_visits_from_rtnodes()
Recompute rt, visits and nodevisits after the caller has rewritten rtnodes in place – the cacheqn dri...
std::vector< NodeDef > nodes
every node, in creation order
std::vector< std::vector< double > > classcap
bool has_sd_routing() const
sn_has_sd_routing: RROBIN, WRROBIN, JSQ, SQ or SDR at some node.
std::vector< std::size_t > rr_weighted_outlinks(std::size_t ind, std::size_t r) const
The WRROBIN cycle: each outlink repeated by its weight, weight 0 once.
std::map< std::size_t, Matrix< T > > csmatrix
The class-switch matrix of a ClassSwitch node, by 1-based NODE index.
std::vector< double > njobs() const
sn.njobs: the population of each class, infinite for an open one.
std::vector< std::pair< std::size_t, std::size_t > > chain_retrieval_edges(const std::vector< std::size_t > &ic, const std::vector< std::size_t > &sel) const
sn_refresh_visits.m retrievalEdges: extra [from, to] chain-block positions for a retrieval cache,...
bool has_breakdown_node(std::size_t ind) const
sn.hasbreakdown(ind): does the NODE's server break down?
std::vector< double > hasbreakdown() const
sn.hasbreakdown (N x 1): 1 where the node's server is subject to breakdowns.
void refresh_router_stateful()
A Router that DISPATCHES (round-robin, JSQ or SQ(d)) holds state, so it must survive the stochastic c...
bool is_open_model() const
sn_is_open_model: EVERY class is open, which is not has_open_classes.
bool sched_is_product_form() const
std::vector< std::vector< DropStrategy > > droprule
std::string log_path
Network.setLogPath / getLogPath: the directory every Logger writes into, and the logPath attribute of...
bool has_blocking() const
Some station can REFUSE a job: its own buffer BINDS, or a finite capacity region caps a set of statio...
void refresh_rt()
sn.rt and sn.rtnodes: the class-expanded routing matrices.
bool priorities_ignored() const
Priorities were declared and no station will read them.
std::vector< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
double buffer_size(std::size_t ist) const
Kendall's K of station IST (1-based), +inf when unbounded.
std::map< std::size_t, std::vector< T > > initmarking
The DECLARED initial state of a stateful node, by 1-based node index.
std::pair< T, T > mark_marginal_moments(const Distrib< T > &d, std::size_t mk) const
Mean and SCV of ONE MARK'S MARGINAL MAP, MarkedMAP.toMAPs(k).
std::vector< double > station_to_stateful() const
sn.stationToStateful (1 x M): the stateful index of each station.
std::map< std::size_t, std::vector< T > > stateprior
bool has_homogeneous_scheduling(SchedStrategy strategy) const
Port of sn_has_homogeneous_scheduling: true when EVERY station uses strategy, the reference's length(...
std::map< std::size_t, CtmcPool< T > > ctmcpool
Heterogeneous server pools rewritten for the CTMC, by 1-based NODE index.
double nclosedjobs() const
sn.nclosedjobs: the total population of the closed classes.
std::vector< std::pair< std::size_t, std::size_t > > fj
fj(f,j): the Join node j that closes the Fork node f, 1-based.
std::size_t nof_stations() const
void set_route_effective(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
Write into the routing the consumers actually read.
std::size_t join_siblings(std::size_t joinNode, std::size_t r=0) const
The number of siblings the fork-join pair ending at Join node joinNode (1-based) emits per parent job...
std::vector< double > stateful_to_station() const
sn.statefulToStation (1 x S): the station index of each stateful node, NaN when it is not a station.
std::size_t phasessz_of(std::size_t ist, std::size_t r) const
sn.phasessz(i,r) = max(sn.phases(i,r),1): THE WIDTH of class r's phase block in a state row,...
bool has_product_form() const
sn_has_product_form, conjunct for conjunct.
std::vector< lang::SignalType > signaltype
std::vector< std::size_t > fjclassmap
sn.fjclassmap: the ORIGINAL class of each auxiliary sibling class, 0 for an original class.
std::map< std::size_t, PollingParam > pollingparam
bool is_fluid_node(std::size_t ind) const
Is 1-based node ind a FLUID place (Place.setFluid, MATLAB sn.fluid(ind))?
std::vector< std::size_t > quorum_joins() const
The 1-based Join nodes that fire on a STRICT quorum, i.e.
bool has_fractional_populations() const
std::map< std::size_t, Matrix< T > > statespace
std::size_t markidx_of(std::size_t ist, std::size_t r) const
sn.markidx(i,r): the 1-based MARK that class r carries in station i's marked arrival process,...
Matrix< T > stoch_comp_stateful(const Matrix< T > &full, std::size_t nIC) const
The stochastic complement of a NODE-level routing block over the stateful nodes, S = P11 + P12 (I - P...
std::size_t add_station(const Station< T > &st)
Add a station, which is also a node, and grow the service table.
std::vector< std::vector< T > > signalremdist
Matrix< T > station_routing(const std::vector< std::size_t > &ic) const
The chain-restricted routing over the STATEFUL nodes: the node-level routing with the non-stateful no...
bool isfjaugmented
sn.isfjaugmented: this struct came out of fj_tag, so its Fork nodes are STATEFUL and its Join nodes c...
std::vector< std::size_t > station_to_node
(nstations) 1-based node index
static std::vector< bool > chain_reachable(const Matrix< T > &P, const std::vector< std::size_t > &seeds, const std::vector< std::pair< std::size_t, std::size_t > > &edges)
sn_refresh_visits.m chainReachable: states reachable from the seeds along nonzero entries and the ext...
void refresh_routing()
Port of the part of MNetwork.refreshRoutingMatrix this port reaches: the expansion of a routing STRAT...
std::vector< Region > regions
std::size_t nof_classes() const
void refresh_immfeed()
Port of the sn.immfeed block of @@MNetwork/refreshStruct.m.
std::map< std::size_t, ForkParam< T > > forkparam
Variable forking levels, by 1-based Fork node; absent on a plain fork.
static std::string plural(long n, const std::string &singular, const std::string &plural_form)
Count with an agreeing noun, e.g.
static void compiling(const std::string &name)
Announce the compilation of a model structure.
static void compile_detail(const char *fmt,...)
One stage line of a structure compile: silenced inside a Quiet scope, and inside an open run,...
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
Limiting distribution of a discrete-time Markov chain whose transition matrix may be reducible.
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
int dispatch(const Options &in, bool allow_server, bool no_args)
Everything main did, with the arguments already in hand.
Running progress log of a LINE solver run (the "solver console").
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
bool sn_is_phasetype(const std::vector< Matrix< T > > &maps, const std::vector< T > &pie)
Whether a (D0, D1, ...) list is a valid phase-type / MAP representation.
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:431
BalkingStrategy
Balking rules, with the values of MATLAB BalkingStrategy.
Definition lang_types.h:452
@ CATASTROPHE
removes EVERY job at the station
Definition lang_types.h:174
JoinStrategy
Join rules, with the values of MATLAB JoinStrategy.
Definition lang_types.h:468
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:396
T dist_lst(const Distrib< T > &d, const T &s)
sn.lst: the Laplace-Stieltjes transform E[exp(-sX)].
HeteroSchedPolicy
How a heterogeneous station picks among its server types, MATLAB HeteroSchedPolicy.
Definition lang_types.h:458
PollingType
Polling service disciplines, with the values of MATLAB PollingType.
Definition lang_types.h:377
@ KLIMITED
serve at most K per visit (K in pollingPar)
Definition lang_types.h:380
@ EXHAUSTIVE
serve until the queue empties
Definition lang_types.h:379
JobClassType
Job class kinds, with the values of MATLAB JobClassType.
Definition lang_types.h:374
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
std::function< std::vector< T >(const std::vector< T > &)> GdScaling
A globally state-dependent scaling, sn.gdscaling.
Definition lang_types.h:749
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:736
const char * routing_to_text(RoutingStrategy r)
Definition lang_types.h:409
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:385
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
T map_scv(const Map< T > &m)
Squared coefficient of variation.
Definition map_moment.h:140
ReducibleResult< T > dtmc_solve_reducible(const Matrix< T > &P, const std::vector< T > &pin, double zeroColTol=1e-12)
Limiting distribution of a discrete-time Markov chain whose transition matrix may be reducible.
Matrix< T > dtmc_stochcomp(const Matrix< T > &P, const std::vector< std::size_t > &keep)
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
SccResult stronglyconncomp(const Matrix< T > &A)
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
std::vector< T > dtmc_solve(const Matrix< T > &P)
Stationary distribution of a stochastic matrix P.
Definition dtmc_solve.h:106
void cache_retrieval_class_map(const CacheParam< T > &cp, std::vector< std::size_t > &rc_list, std::vector< std::size_t > &rc_items, std::vector< std::size_t > &rc_orig)
Port of State.cacheRetrievalClassMap: the canonical order of a cache's retrieval classes,...
Matrix< T > station_swap_graph(const NetworkStruct< T > &sn, std::size_t ist)
The swap graph of a PAS / OI station, with the defaults refreshLocalVars.m installs applied.
bool station_swap_graph_is_zero(const NetworkStruct< T > &sn, std::size_t ist)
True when the station's materialized swap graph is entirely zero.
const char * routing_to_text(RoutingStrategy r)
Definition lang_types.h:409
void closed_reneging_refusal(const std::string &classname, const std::string &where)
Refuses RENEGING on a class of finite population (LINE:input:Model; MATLAB closedRenegingRefusal....
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
Product-form state-dependent routing.
Whether a (D0, D1, ...) list is a valid phase-type / MAP representation.
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The DECLARED join rule of a Join node, by 1-based node index.
The pass-and-swap / order-independent parameters of a PAS station, keyed by station index (Dorsman an...
The polling controller of a POLLING station, keyed by station index.
FINITE CAPACITY REGIONS, MATLAB's refreshRegions output.
sn.reward: the user-declared reward functions, MATLAB's model.setReward(name, fn).
std::vector< T > mu_vec() const
Phase rates, MATLAB's getMu: the total outgoing rate of each phase.
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:863
std::vector< Matrix< T > > Dmark
MMAP per-class D1 blocks / BMAP per-batch-size blocks; empty otherwise.
Definition lang_types.h:873
T rate() const
The rate MATLAB's refreshRates would store: 1/mean, with the Immediate singleton short-circuited to i...
std::vector< T > phi_vec() const
Completion probabilities, MATLAB's getPhi: (D1 e) .
std::size_t phases() const
The order of the representation, MATLAB's sn.phases.
bool is_prior() const
Definition lang_types.h:888
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
std::vector< bool > recurrent
recurrent[c-1] is true when component c has no edge leaving it.
std::vector< std::size_t > scc
Component index of each state, 1-based as in MATLAB (0 is never used).
Topology and coefficients of a state-dependent routing subnetwork.
Definition pfqn_sdr.h:59
Server breakdown and repair of a station whose server fails and is repaired.
T failure_rate
sn.breakdownMu: 1 / mean failure time
T down_rate_of(std::size_t cls_1based) const
lang::Distrib< T > repair
time to repair of a down server
lang::Distrib< T > failure
time to failure of an up server
T repair_rate
sn.repairMu: 1 / mean repair time
std::vector< T > down_service_rates
sn.downServiceRates(ist, :): per class, 0 = no service while down.
The popularity LAW each class declared, beside the pmf it expands to.
std::size_t n
support size of a Zipf or a DiscreteSampler
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
int retrieval_capacity
Delayed-hit retrieval system (Cache.setRetrievalSystem).
T qlru
q-LRU admission probability; 1 admits every miss (plain LRU).
std::vector< Popularity > preadkind
per class, parallel to pread
std::vector< T > initstate
The DECLARED initial contents of the cache, as the reference dumps the node's state row: the per-clas...
std::vector< int > itemsize
Per-item storage cost (size) and per-list cap on the total cost of the resident items (ton21cache Sec...
std::vector< int > costcap
std::map< std::size_t, std::vector< std::size_t > > retrieval_queues
read class(0-based)->nodes
std::vector< std::vector< Matrix< T > > > accost
(u) x (n) of (h+1)x(h+1), or empty
long max_pending_retrieval
Truncation level of block B: how many secondary requests may be merged onto the in-flight fetches of ...
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
CTMC pair form of a MAP service at an FCFS station, written by ctmc::ctmc_mapc on the solver's privat...
Matrix< T > V
landing law (-D0)^-1 D1
std::size_t p
phases of the original MAP
std::vector< std::pair< std::size_t, std::size_t > > pairs
0-based (i,j), i-major, V(i,j) > 0
std::vector< T > done
completion rate D1(i,j)/V(i,j)
bool single
c = 1: carried phase, no pairs
CTMC bookkeeping of a heterogeneous-server station, written by ctmc::ctmc_pools on the solver's priva...
std::vector< std::vector< std::size_t > > len
offset and length of block k of class r
std::vector< std::vector< std::vector< T > > > alpha
entry vector of block k of class r
std::vector< std::vector< std::vector< T > > > exit
exit rates of block k of class r
std::vector< std::vector< bool > > compat
compat[t-1][r-1]
std::vector< double > count
servers per pool
std::vector< std::size_t > alfsorder
pools by ascending class count (ALFS)
std::vector< std::vector< Matrix< T > > > D0
D0 of block k of class r.
std::vector< std::vector< std::size_t > > pools
pools[r-1]: compatible pools of r, ascending
lang::HeteroSchedPolicy policy
bool rotate
ALIS/FAIRNESS order is part of the state.
std::vector< std::vector< std::size_t > > off
std::size_t perm_index(const std::vector< std::size_t > &order) const
1-based index of order in perms, 0 when absent.
std::vector< std::vector< double > > fsfrate
fsfrate[t-1][r-1] = 1/mean (FSF)
std::vector< std::vector< std::size_t > > perms
every pool order, identity first
bool has_map() const
True when the type carries a (D0,D1) pair of its own.
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:863
std::vector< Matrix< T > > Dmark
MMAP per-class D1 blocks / BMAP per-batch-size blocks; empty otherwise.
Definition lang_types.h:873
static Distrib disabled_dist()
Definition lang_types.h:997
Matrix< T > D1
Definition lang_types.h:863
static Distrib immediate()
The point mass at zero.
Definition lang_types.h:986
sn.nodeparam{j}.fj: what a Join node needs to fire on identity.
std::vector< std::size_t > origclasses
std::map< std::size_t, std::vector< std::vector< std::size_t > > > auxmatrix
std::map< std::size_t, std::vector< std::size_t > > required
One fork firing synchronization: sn.fjsync{k}.
std::size_t fork
1-based Fork node
std::vector< std::size_t > weightlink
Per-branch tasksPerLink, EMPTY when every branch carries weight.
std::size_t weight
tasksPerLink: siblings emitted per branch
std::vector< std::size_t > branchheads
1-based node per branch
std::size_t join
1-based Join node that closes it
std::size_t cls
1-based ORIGINAL class being forked
std::vector< std::vector< std::size_t > > auxall
(B x T) every auxiliary class of this (fork, class), for the tag scan.
std::vector< std::size_t > auxclasses
the tag's auxiliary class per branch
std::size_t tag
1-based tag this entry allocates
Variable forking levels, the twin of MATLAB sn.nodeparam{f}.fanOutLink / .fanOutProb / ....
std::vector< std::vector< lang::Distrib< T > > > fan_out_dist
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:771
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double Zero
Definition lang_types.h:767
static constexpr double CoarseTol
Definition lang_types.h:766
One job class of the network.
std::size_t refstat
1-based reference station
bool completes
Whether passage through the reference station is a COMPLETION.
double deadline
sn.classdeadline(r): the soft deadline EDD and EDF order by, and the tardiness JMT reports.
int attr_kind
LayeredNetworkElement of the element it stands for.
std::size_t attr_idx
index of that element
double population
infinite for an open class
bool immfeed
Class-level immediate feedback, ORed with the station's own setting into sn.immfeed.
std::size_t spawn
sn.classspawn(r): the 1-based class injected at the SAME station on every completion of this class,...
bool self_looping
A SelfLoopingClass: a closed class that perpetually cycles at its reference station.
bool is_ref_class
marks the chain's reference class
std::function< T(const std::vector< std::size_t > &)> svc_rate_fun
std::vector< std::vector< bool > > swap_graph
The polling controller of a POLLING station, keyed by station index.
std::size_t pk
the K of K-LIMITED
std::vector< lang::Distrib< T > > switchover
Matrix< T > lincon_A
optional linear constraint A n <= b
std::vector< std::vector< double > > cap
(nstations x nclasses+1), -1 = unbounded
std::string name
The region's declared name, as the wire carries it; a generated one otherwise.
std::vector< double > maxmem
per member station, -1 = unbounded
std::vector< DropStrategy > rule
per class
std::vector< T > size
per class; size is the memory footprint
std::vector< bool > members
membership, independent of the caps
std::function< T(const std::vector< T > &)> fn
std::string kind
The DECLARATIVE form the reward was built from, when it was: the template name (QLen,...
A Logger node's trace configuration, MATLAB's Logger properties and sn.nodeparam{ind}...
std::string file_name
base name, no directory
std::string file_path
directory, MATLAB's model.getLogPath
A node of the network.
std::vector< int > routing_param
The scalar parameter of a parameterized dispatcher, per class: the d of a power-of-d (SQ) choice.
std::vector< std::map< std::size_t, double > > routing_weights
The per-destination weights of a WRROBIN dispatcher, per class: a map from 1-based destination NODE i...
bool queue_object
Declared as a Queue although its INF discipline makes nodetype Delay (Network::add_queue).
std::vector< RoutingStrategy > routing
sn.routing, per class.
double tasks_per_link
Fork.output.tasksPerLink == MATLAB sn.nodeparam{f}.fanOut: how many tasks a fork emits per outgoing l...
The parameters of a retrial station: MATLAB sn.retrialProc and friends.
std::vector< int > max_attempts
0 = unbounded
std::vector< T > retrial_rate
mu_r, the per-class orbit retry rate
std::vector< lang::Distrib< T > > retrial_proc
retrial_proc[r] is the class-r retrial process; empty = not a retrial class.
std::vector< int > max_orbit
MATLAB sn.orbitMaxJobs: -1 = unbounded.
std::vector< int > policy
MATLAB sn.retrialPolicy: 1 = LINEAR (per-job clocks), 2 = CONSTANT (one clock per orbit).
Setup and delay-off of a station that powers down when it falls idle.
bool last(lang::Distrib< T > &su, lang::Distrib< T > &doff) const
The pair the solvers use: the last class that declares one.
std::vector< lang::Distrib< T > > setup
per class, disabled = not declared
std::vector< lang::Distrib< T > > delayoff
per class
Per class; strategy == NONE is a class that declares no balking.
lang::BalkingStrategy strategy
std::vector< BalkingThreshold > thresholds
One balking threshold: with min_jobs <= n <= max_jobs at the station, an arriving job of the class re...
A heterogeneous server pool: count servers that serve only compatible classes, each with its own serv...
std::vector< bool > compatible
per class; empty = every class
std::vector< Distrib< T > > service
per class
One station of the network.
std::vector< Distrib< T > > orbit_impatience
Queue.setOrbitImpatience(class, dist): abandonment from the RETRIAL ORBIT, which is a different popul...
double cap
Station capacity in Kendall's K, as setCapacity sets it.
std::vector< T > jdscalingpeak
sn.jdscalingpeak for this station: the declared peak joint-dependent scaling per class.
std::vector< BalkingParam > balking
std::function< T(const std::vector< std::size_t > &)> svc_rate_fun
sn.nodeparam{ind}.svcRateFun for a PAS / OI station: the TOTAL service rate as a function of the orde...
std::vector< T > batch_reject
Queue.setBatchRejectProbability: per-class rejection of a whole batch.
std::vector< Distrib< T > > patience
Queue.setPatience(class, dist): the abandonment timer of a WAITING job, with impatience[r] naming whi...
std::vector< std::size_t > server_parallelism
Queue.setServerParallelism(class, n): the servers a job seizes for the whole of its service,...
std::vector< int > droprule
Per-class blocking rule as an INT, with 0 meaning "not set".
SchedStrategy sched
std::vector< std::vector< Distrib< T > > > switchover_pair
Queue.setSwitchover(fromClass, toClass, distrib): the walk the server takes when it turns from servin...
double nservers
may be infinite (a Delay, or an inf-scheduled task)
std::vector< lang::PollingType > polling_type
Polling parameters for a POLLING station, MATLAB's pollingType, switchoverTime and pollingPar on the ...
Matrix< T > swap_graph
sn.nodeparam{ind}.swapGraph: which class a departing job promotes the jobs behind it into.
CdScaling< T > jdscaling
sn.jdscaling for this station: MATLAB's Station.ljdScaling, the JOINT dependence map eta_i(n),...
std::vector< T > cdscalingpeak
sn.cdscalingpeak for this station: the DECLARED peak rate scaling per class, empty when the station i...
std::vector< std::size_t > marked_classes
Source.markedClasses: the 1-based class of each mark of an MMAP arrival.
std::vector< T > lldscaling
sn.lldscaling for this station: the multiplier at population 1, 2, ... Empty when the station is not ...
std::vector< lang::ImpatienceType > impatience
std::vector< T > schedparam
sn.schedparam, per class: the DPS / GPS weight, or the SEPT / LEPT rank.
lang::HeteroSchedPolicy hetero_policy
CdScaling< T > cdscaling
sn.cdscaling for this station: the class-dependence map, empty when unset.
std::vector< double > classcap
Per-class buffer from setChainCapacity; infinite where unset.
std::vector< lang::DepartureDiscipline > departure_discipline
Place.departureDiscipline, per class.
std::vector< Distrib< T > > arrival_batch
Source.setArrivalBatch(class, dist): the batch-size law released at each arrival epoch.
std::vector< Distrib< T > > switchover
bool fluid
Place.setFluid (MATLAB sn.fluid): the place holds a continuous LEVEL rather than tokens,...
std::vector< bool > immfeed
Node-level immediate feedback, per class; empty when the station sets none.
std::vector< ServerType > server_types
std::size_t attr_idx
LQN element this station stands for.
One fluid arc of a mode: node is the 1-based fluid place, value a rate or an impulse.
A fluid rate dependence: the firing rate is multiplied by fun(level(node)).
std::function< double(double)> fun
A marking-dependent fluid flow (setFluidFlowDependence): the flow of the mode on fluid place node is ...
std::function< double(const std::vector< double > &)> fun
A fluid guard: the mode is enabled only while level(node) >= thr (sgn = +1) or <= thr (sgn = -1).
The parameters of a Transition node: MATLAB sn.nodeparam{ind} for an SPN transition,...
const std::vector< FluidArc > & fluid_rates(std::size_t m) const
static std::vector< T > inhibit_total(const std::vector< Matrix< T > > &a, std::size_t m)
The inhibiting THRESHOLD of one mode per place, class blind.
std::vector< std::vector< FluidDep > > fluiddep
static std::vector< T > arc_total(const std::vector< Matrix< T > > &a, std::size_t m)
The arcs of one mode summed over classes, for a consumer that is class blind BECAUSE THE NET IS SINGL...
bool is_multiclass() const
True when any mode's arcs touch more than one class.
std::vector< double > firingprio
firing priority per mode
std::vector< std::vector< FluidGuard > > fluidguard
std::vector< std::vector< FluidFlowDep > > fluidflowdep
std::vector< lang::TimingStrategy > timing
immediate or timed
std::vector< std::string > modenames
std::vector< std::vector< FluidArc > > fluidimpulse
std::vector< lang::Distrib< T > > firingproc
firing distribution per mode
bool has_fluid_arcs() const
Transition.hasFluidArcs: any mode declares a fluid rate, guard, impulse, rate or flow dependence.
std::vector< double > nmodeservers
servers per mode, may be infinite
const std::vector< FluidArc > & fluid_impulses(std::size_t m) const
std::vector< T > fireweight
weight among simultaneously enabled modes
std::size_t single_class() const
The one class every arc of every mode touches, 1-based; 0 when none does.
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.
const std::vector< FluidDep > & fluid_deps(std::size_t m) const
const std::vector< FluidFlowDep > & fluid_flow_deps(std::size_t m) const
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).
const std::vector< FluidGuard > & fluid_guards(std::size_t m) const
std::vector< std::vector< FluidArc > > fluidrate
FSPN arcs per mode, MATLAB sn.transparam{ind}.fluidrate/fluidguard/fluidimpulse/fluiddep.
std::vector< std::size_t > firingphases
phase count per mode, 0 when non-Markovian