LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
state.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_STATE_H
6#define LINE_LANG_QN_STATE_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * Port of the MATLAB `+State` package: the encoding that turns a station's
12 * state row into marginal job counts.
13 *
14 * WHAT A STATE ROW IS. For a station the row is [buffer | server | vars]:
15 * `nvars(ind,:)` local-variable slots at the end, `sum(phasesz)` server slots
16 * before them (one per class-phase pair, laid out by `phaseshift`), and
17 * whatever remains at the front is the buffer. The buffer encoding is NOT
18 * uniform across disciplines -- FCFS/LCFS store a CLASS TAG per waiting
19 * position, so class-r jobs are counted by matching the tag, while SIRO,
20 * POLLING, SEPT, LEPT and SRPT store a per-class COUNT in column r. Reading
21 * one as the other silently produces a plausible number, which is why the
22 * discipline switch below is transcribed case by case rather than collapsed.
23 *
24 * THE EXT SENTINEL. `to_marginal` returns nir = +Inf for a Source, and that is
25 * deliberate: a Source is an infinite reservoir and the value describes the
26 * ENCODING, not a queue length. A caller that treats it as a queue length gets
27 * Inf, which is exactly the defect that reached SolverCTMC's averagers in
28 * MATLAB (see `_kb/07-cross-language-parity.md`, the Source-row section). Any
29 * consumer must test `is_source` before using nir, not clamp the Inf away.
30 */
31
32#include <algorithm>
33#include <map>
34#include <cmath>
35#include <cstddef>
36#include <limits>
37#include <vector>
38
43#include "line/util/error.h"
44#include "line/util/matrix.h"
45
46namespace line {
47namespace qn {
48
49/** What `State.toMarginal` returns for one station and one state row. */
50template <class T>
51struct Marginal {
52 T ni = T(); ///< total jobs in the station
53 std::vector<T> nir; ///< jobs per class
54 std::vector<T> sir; ///< jobs in service per class
55 std::vector<std::vector<T>> kir; ///< jobs in service per class and phase
56};
57
58namespace state_detail {
59
60/** True when the discipline stores a per-class COUNT in buffer column r. */
61inline bool buffer_is_per_class_count(SchedStrategy s) {
62 switch (s) {
63 case SchedStrategy::SIRO:
64 case SchedStrategy::POLLING:
65 case SchedStrategy::SEPT:
66 case SchedStrategy::LEPT:
67 case SchedStrategy::SRPT:
68 case SchedStrategy::SRPTPRIO:
69 return true;
70 default:
71 return false;
72 }
73}
74
75/**
76 * True when the discipline stores a CLASS TAG per waiting position. HOL is
77 * MATLAB's FCFSPRIO, so the two are the same enumerator and appear once.
78 */
79inline bool buffer_is_class_tag(SchedStrategy s) {
80 switch (s) {
81 case SchedStrategy::FCFS:
82 case SchedStrategy::HOL:
83 case SchedStrategy::LCFS:
84 case SchedStrategy::LCFSPRIO:
85 return true;
86 default:
87 return false;
88 }
89}
90
91/**
92 * True when the buffer interleaves [class, phase] pairs, so only the even
93 * positions carry class tags. The preemptive families do this to remember the
94 * phase every preempted job was interrupted in.
95 */
96inline bool buffer_is_tag_phase_pairs(SchedStrategy s) {
97 switch (s) {
98 // The whole preempt-resume / preempt-independent family, exactly as
99 // `toMarginal` groups it. PI restarts a preempted job from its entry
100 // phase and PR resumes it in place, but BOTH must record the phase per
101 // waiting job, so they share the paired encoding.
102 case SchedStrategy::FCFSPI:
103 case SchedStrategy::FCFSPIPRIO:
104 case SchedStrategy::FCFSPR:
105 case SchedStrategy::FCFSPRPRIO:
106 case SchedStrategy::LCFSPI:
107 case SchedStrategy::LCFSPIPRIO:
108 case SchedStrategy::LCFSPR:
109 case SchedStrategy::LCFSPRPRIO:
110 return true;
111 default:
112 return false;
113 }
114}
115
116} // namespace state_detail
117
118/**
119 * Port of `State.toMarginal` for a STATION, one state row at a time.
120 *
121 * @param sn the network struct
122 * @param ist station index (1-based, as elsewhere in NetworkStruct)
123 * @param state_i the station's state row
124 * @param phasesz per-class phase counts
125 * @param phaseshift per-class offset into the server block
126 * @param nvar width of the trailing local-variable block, 0 when absent
127 * @return the marginal counts; nir is +Inf for every class at a Source
128 */
129template <class T>
131 const std::vector<T>& state_i, const std::vector<std::size_t>& phasesz,
132 const std::vector<std::size_t>& phaseshift, std::size_t nvar = 0) {
133 const std::size_t R = sn.nclasses;
134 if (ist == 0 || ist > sn.stations.size())
135 throw InputError("to_marginal: station index " + std::to_string(ist) + " is out of range");
136
137 const T zero = num_traits<T>::from_int(0);
138 Marginal<T> m;
139 m.nir.assign(R, zero);
140 m.sir.assign(R, zero);
141 std::size_t maxph = 1;
142 for (std::size_t r = 0; r < R; ++r) maxph = std::max(maxph, phasesz[r]);
143 m.kir.assign(R, std::vector<T>(maxph, zero));
144
145 // A Join of an FJ-augmented struct: the row is a bare per-class count of jobs
146 // WAITING to synchronize. Nothing is in service and nothing is in a phase,
147 // which is why `sir` and `kir` stay zero rather than mirroring `nir`.
148 const std::size_t jnd = sn.node_of_station(ist);
149 if (sn.isfjaugmented && jnd != 0 && sn.nodes[jnd - 1].nodetype == NodeType::Join &&
150 state_i.size() >= R) {
151 for (std::size_t r = 0; r < R; ++r) m.nir[r] = state_i[state_i.size() - R + r];
152 return m;
153 }
154
155 // AN ORDER-INDEPENDENT ROW HAS NO SERVER BLOCK. PAS and OI encode the
156 // station as the ORDERED LIST of the class indices present, one slot per
157 // job the buffer can hold, so its width is the capacity and not
158 // sum(phasesz). Slicing a server block off it read the last list positions
159 // as phase occupancies, and where the capacity is SMALLER than the class
160 // count the width test below fired outright: `to_marginal: state row is
161 // narrower than the server block it declares` on pas_compatibility_5class
162 // (five classes, capacity three). The arm further down decodes the list; it
163 // needs neither `srv0` nor the per-phase sum.
164 const SchedStrategy sched = sn.stations[ist - 1].sched;
165 const bool ordered_list = (sched == SchedStrategy::PAS || sched == SchedStrategy::OI);
166
167 // [buffer | server | vars]: slice from the RIGHT, since only the buffer
168 // width varies with the discipline.
169 std::size_t srvw = 0;
170 for (std::size_t r = 0; r < R; ++r) srvw += phasesz[r];
171 if (!ordered_list && state_i.size() < nvar + srvw)
172 throw InputError("to_marginal: state row is narrower than the server block it declares");
173 const std::size_t srv0 = ordered_list ? 0 : state_i.size() - nvar - srvw;
174 const std::size_t bufw = srv0;
175
176 if (!ordered_list) {
177 for (std::size_t r = 0; r < R; ++r) {
178 for (std::size_t k = 0; k < phasesz[r]; ++k) {
179 const T v = state_i[srv0 + phaseshift[r] + k];
180 m.kir[r][k] = v;
181 m.sir[r] += v;
182 }
183 }
184 }
185
186 if (sched == SchedStrategy::EXT) {
187 // Infinite reservoir: a statement about the encoding, not a queue
188 // length. Consumers must branch on the station being a Source.
189 //
190 // AN EXACT TYPE HAS NO INFINITY. Rational is a field of quotients of
191 // integers, so building one from a double Inf throws outright ("Cannot
192 // convert a non-finite number to an integer") -- and the sentinel is
193 // built for every Source row whether or not anybody reads it, which
194 // killed the whole exact run inside `after_event_station_dep`
195 // (state_events.h): it calls this on the Source before its own EXT
196 // branch ever looks at sir/kir. Clamp to MaxInt there, the same clamp
197 // `from_marginal_node` applies to an infinite server count for the same
198 // reason -- still absurd as a queue length, so a consumer that forgot
199 // its `is_source` test is still visibly wrong rather than plausibly
200 // wrong, but representable. Types that DO carry an infinity keep it, so
201 // the double path is untouched.
202 const T ext = num_traits<T>::is_exact
205 std::numeric_limits<double>::infinity());
206 for (std::size_t r = 0; r < R; ++r) m.nir[r] = ext;
207 } else if (sched == SchedStrategy::PAS || sched == SchedStrategy::OI) {
208 // `State.toMarginal`'s PAS arm (toMarginal.m:105-130), which this port
209 // did not have: the row is the ORDERED LIST of class indices and there
210 // is no server block to slice at all, so the generic sum above read the
211 // last list POSITION as a phase occupancy. A station holding jobs then
212 // reported a queue length of about zero in every consumer of this
213 // decode -- `ctmc_state_space_aggr` and with it SolverCTMC's averages,
214 // SolverSSA's serial analyzer, and the reward and transient analyzers.
215 //
216 // IN SERVICE IS NOT "IN THE SERVER" HERE but "receiving a positive rate
217 // increment": Delta mu over the prefix ending at that position, exactly
218 // as the reference computes it. That is what makes an order-independent
219 // station's utilization a quantity rather than a slot count.
220 for (std::size_t r = 0; r < R; ++r) {
221 m.nir[r] = zero;
222 m.sir[r] = zero;
223 for (std::size_t k = 0; k < maxph; ++k) m.kir[r][k] = zero;
224 }
225 const std::size_t w = state_i.size() > nvar ? state_i.size() - nvar : 0;
226 std::vector<std::size_t> clist;
227 for (std::size_t b = 0; b < w; ++b) {
228 const long tag = static_cast<long>(num_traits<T>::to_double(state_i[b]) + 0.5);
229 if (tag >= 1 && static_cast<std::size_t>(tag) <= R) {
230 m.nir[tag - 1] += num_traits<T>::from_int(1);
231 clist.push_back(static_cast<std::size_t>(tag));
232 }
233 }
234 const typename std::map<std::size_t,
235 typename NetworkStruct<T>::PasParam>::const_iterator pit =
236 sn.pasparam.find(ist);
237 if (pit != sn.pasparam.end() && pit->second.svc_rate_fun) {
238 T muprev = zero;
239 for (std::size_t p = 0; p < clist.size(); ++p) {
240 const std::vector<std::size_t> prefix(clist.begin(), clist.begin() + p + 1);
241 const T mucur = pit->second.svc_rate_fun(prefix);
242 if (num_traits<T>::to_double(T(mucur - muprev)) > 0) {
243 m.sir[clist[p] - 1] += num_traits<T>::from_int(1);
244 m.kir[clist[p] - 1][0] += num_traits<T>::from_int(1);
245 }
246 muprev = mucur;
247 }
248 } else {
249 // No rate function declared: the reference falls back to "every job
250 // present is in service", which is the OI reading of an empty swap
251 // graph.
252 m.sir = m.nir;
253 for (std::size_t r = 0; r < R; ++r) m.kir[r][0] = m.nir[r];
254 }
255 } else if (state_detail::buffer_is_class_tag(sched)) {
256 for (std::size_t r = 0; r < R; ++r) {
257 T waiting = zero;
258 for (std::size_t b = 0; b < bufw; ++b)
259 if (state_i[b] == num_traits<T>::from_int(static_cast<long>(r + 1))) waiting += num_traits<T>::from_int(1);
260 m.nir[r] = T(m.sir[r] + waiting);
261 }
262 } else if (state_detail::buffer_is_tag_phase_pairs(sched)) {
263 if (bufw > 1) {
264 for (std::size_t r = 0; r < R; ++r) {
265 T waiting = zero;
266 for (std::size_t b = 0; b < bufw; b += 2) // even positions are the class tags
267 if (state_i[b] == num_traits<T>::from_int(static_cast<long>(r + 1))) waiting += num_traits<T>::from_int(1);
268 m.nir[r] = T(m.sir[r] + waiting);
269 }
270 } else {
271 m.nir = m.sir;
272 }
273 } else if (state_detail::buffer_is_per_class_count(sched)) {
274 for (std::size_t r = 0; r < R; ++r)
275 m.nir[r] = bufw >= R ? T(m.sir[r] + state_i[r]) : m.sir[r];
276 } else {
277 // INF, PS, DPS, GPS and the rest: everything present is in service.
278 m.nir = m.sir;
279 }
280
281 // A Place is a token container: the buffer/server split in its encoding is
282 // an artifact of a transition FIRE relocating surviving tokens, so fold the
283 // buffer slot back in for the INF-family disciplines it uses. Without this
284 // the token count collapses to the oscillating server slot while the
285 // dynamics stay correct -- the measurement moves, the model does not.
286 if (sn.stations[ist - 1].nodetype == NodeType::Place) {
287 switch (sched) {
288 case SchedStrategy::INF:
289 case SchedStrategy::PS:
290 case SchedStrategy::PSPRIO:
291 case SchedStrategy::DPS:
292 case SchedStrategy::DPSPRIO:
293 case SchedStrategy::GPS:
294 case SchedStrategy::GPSPRIO:
295 case SchedStrategy::LPS:
296 if (bufw >= R)
297 for (std::size_t r = 0; r < R; ++r) m.nir[r] = T(m.sir[r] + state_i[r]);
298 break;
299 default:
300 break;
301 }
302 for (std::size_t r = 0; r < R; ++r)
303 if (sn.disabled[ist - 1][r]) {
304 for (std::size_t k = 0; k < phasesz[r]; ++k) m.kir[r][k] = zero;
305 m.sir[r] = zero;
306 }
307 } else {
308 for (std::size_t r = 0; r < R; ++r)
309 if (sn.disabled[ist - 1][r]) {
310 m.nir[r] = zero;
311 for (std::size_t k = 0; k < phasesz[r]; ++k) m.kir[r][k] = zero;
312 m.sir[r] = zero;
313 }
314 }
315
316 m.ni = zero;
317 for (std::size_t r = 0; r < R; ++r) m.ni += m.nir[r];
318 return m;
319}
320
321/**
322 * Total jobs held by every STATION at one network state, indexed by station.
323 *
324 * A Source is an infinite reservoir whose `to_marginal` reports +Inf, which
325 * describes the encoding and not a queue length, so it is reported as zero here
326 * exactly as `ctmc_state_space_aggr` does. A station with no stateful node
327 * holds nothing.
328 */
329template <class T>
330std::vector<double> station_populations(const NetworkStruct<T>& sn,
331 const std::vector<std::vector<T>>& local) {
332 const std::size_t M = sn.stations.size(), K = sn.nclasses;
333 std::vector<double> n(M, 0.0);
334 for (std::size_t ist = 1; ist <= M; ++ist) {
335 const std::size_t isf = sn.stateful_of_station(ist);
336 if (isf == 0 || isf > local.size()) continue;
337 if (sn.stations[ist - 1].nodetype == NodeType::Source) continue;
338 std::vector<std::size_t> ph(K, 1), shift(K, 0);
339 std::size_t w = 0;
340 for (std::size_t k = 0; k < K; ++k) {
341 ph[k] = sn.phasessz_of(ist, k + 1);
342 shift[k] = w;
343 w += ph[k];
344 }
345 const Marginal<T> m =
346 to_marginal(sn, ist, local[isf - 1], ph, shift, sn.nvars_of(sn.node_of_station(ist)));
347 n[ist - 1] = num_traits<T>::to_double(m.ni);
348 }
349 return n;
350}
351
352/**
353 * Port of `sn.rtfun`: the routing over the stateful nodes AT ONE STATE.
354 *
355 * `rt` is a constant matrix because almost every routing strategy is; SDR is
356 * not, and eq. (10) of Krzesinski (1987) makes the split out of the entry
357 * centre a function of the branch and subnetwork populations. This rebuilds the
358 * node-level table with the SDR rows re-evaluated at `local` and eliminates the
359 * stateless nodes through the SAME stochastic complement `rt` is built with, so
360 * a model with a Router between the centres is complemented identically either
361 * way.
362 *
363 * The residual mass returns the customer to the departure centre -- the busy
364 * form of waiting of Sec. 2.5 -- and a branch closed by its own population bound
365 * simply receives nothing, because `pfqn_sdrprob` returns zero there.
366 *
367 * `sub_sdr` returns zero off the class diagonal: SDR does not switch class, so
368 * only the (r, r) block of the entry row carries mass.
369 *
370 * @param sn the refreshed network struct
371 * @param local per-stateful-node state rows, the `NetState::local` of the state
372 * @return the (nstateful * nclasses) square routing table at that state
373 */
374template <class T>
375Matrix<T> rt_state(const NetworkStruct<T>& sn, const std::vector<std::vector<T>>& local) {
376 const T zero = num_traits<T>::from_int(0);
377 const std::size_t K = sn.nclasses, I = sn.nodes.size();
378 Matrix<T> full = sn.rtnodes;
379 if (full.rows() != I * K)
380 throw InputError("rt_state: the node-level routing has not been refreshed");
381 if (sn.sdr.empty()) return sn.stoch_comp_stateful(full, K);
382
383 const pfqn::SdrCoeff co = pfqn::pfqn_sdrcoeff(sn.sdr);
384 const std::vector<double> Pb = pfqn::pfqn_sdrprob(co, station_populations(sn, local));
385 const double Ped = pfqn::pfqn_sdrped(Pb);
386
387 // The destination split out of the entry centre, by NODE: a branch is
388 // entered at its own entry centre, and the denied customer goes to d.
389 std::vector<double> p(I, 0.0);
390 for (std::size_t b = 1; b < sn.sdr_nodes.branch.size(); ++b)
391 p[sn.sdr_nodes.entryOf[b]] += Pb[b];
392 p[sn.sdr_nodes.departure] += Ped;
393
394 for (std::size_t ind = 1; ind <= I; ++ind) {
395 const NodeDef& nd = sn.nodes[ind - 1];
396 for (std::size_t r = 1; r <= K && r <= nd.routing.size(); ++r) {
397 if (nd.routing[r - 1] != RoutingStrategy::SDR) continue;
398 for (std::size_t jnd = 1; jnd <= I; ++jnd)
399 for (std::size_t s = 1; s <= K; ++s)
400 full((ind - 1) * K + (r - 1), (jnd - 1) * K + (s - 1)) =
401 s == r ? num_traits<T>::from_double(p[jnd - 1]) : zero;
402 }
403 }
404 return sn.stoch_comp_stateful(full, K);
405}
406
407/**
408 * Port of `State.cartesian`: pair every row of `a` with every row of `b`.
409 *
410 * An empty `a` is the identity, as in the reference, so a fold over classes can
411 * start from nothing. Row order is a's outer, b's inner -- the same order
412 * `fromMarginal` relies on when it appends the server block after the buffer.
413 */
414template <class T>
415std::vector<std::vector<T>> cartesian(const std::vector<std::vector<T>>& a,
416 const std::vector<std::vector<T>>& b) {
417 if (a.empty()) return b;
418 if (b.empty()) return a;
419 std::vector<std::vector<T>> out;
420 out.reserve(a.size() * b.size());
421 for (std::size_t i = 0; i < a.size(); ++i)
422 for (std::size_t j = 0; j < b.size(); ++j) {
423 std::vector<T> row = a[i];
424 row.insert(row.end(), b[j].begin(), b[j].end());
425 out.push_back(row);
426 }
427 return out;
428}
429
430/**
431 * Port of `State.spaceClosedSingle`: the ways to place `n` jobs over `m`
432 * phases. `m == 0` yields NO rows, not one empty row -- a class with no service
433 * process contributes nothing, and returning an empty row instead would let it
434 * multiply the product by one and silently survive the fold.
435 */
436template <class T>
437std::vector<std::vector<T>> space_closed_single(std::size_t m, std::size_t n) {
438 std::vector<std::vector<T>> out;
439 if (m == 0) return out;
440 const std::vector<std::vector<int>> rows =
441 pfqn::multichoose_rows(static_cast<int>(m), static_cast<int>(n));
442 out.reserve(rows.size());
443 for (std::size_t i = 0; i < rows.size(); ++i) {
444 std::vector<T> r;
445 r.reserve(rows[i].size());
446 for (std::size_t j = 0; j < rows[i].size(); ++j)
447 r.push_back(num_traits<T>::from_int(rows[i][j]));
448 out.push_back(r);
449 }
450 return out;
451}
452
453/**
454 * `space_closed_single` with a PER-SLOT bound, the reference's
455 * `spaceClosedSingle(M, N, caps)`.
456 *
457 * The bound is applied BEFORE recursing, which is the whole point: a slot of
458 * capacity 0 takes only the zero item, so a class that can occupy one node out
459 * of M enumerates M rows rather than binomial(n+M-1, M-1). The unbounded form
460 * generates the full lattice and leaves the caller to reject the impossible
461 * rows one at a time, at one `from_marginal` call each.
462 *
463 * `caps` is one entry per slot; an entry of `-1` means unbounded.
464 */
465template <class T>
466void space_closed_single_capped_rec(std::size_t m, long n, const std::vector<long>& caps,
467 std::size_t off, std::vector<T>& row,
468 std::vector<std::vector<T>>& out) {
469 if (m == 0) {
470 if (n == 0) out.push_back(row);
471 return;
472 }
473 long room = 0;
474 bool unbounded = false;
475 for (std::size_t k = off; k < off + m && k < caps.size(); ++k) {
476 if (caps[k] < 0) { unbounded = true; break; }
477 room += caps[k];
478 }
479 if (!unbounded && n > room) return;
480 const long here = off < caps.size() ? caps[off] : -1;
481 const long hi = here < 0 ? n : std::min<long>(n, here);
482 for (long i = 0; i <= hi; ++i) {
483 row.push_back(num_traits<T>::from_int(i));
484 space_closed_single_capped_rec(m - 1, n - i, caps, off + 1, row, out);
485 row.pop_back();
486 }
487}
488
489/** @see space_closed_single_capped_rec */
490template <class T>
491std::vector<std::vector<T>> space_closed_single_capped(std::size_t m, std::size_t n,
492 const std::vector<long>& caps) {
493 std::vector<std::vector<T>> out;
494 if (m == 0) return out;
495 std::vector<T> row;
496 row.reserve(m);
497 space_closed_single_capped_rec<T>(m, static_cast<long>(n), caps, 0, row, out);
498 return out;
499}
500
501/**
502 * Port of `matlab/util/multiset_perms.m` on an ASCENDING multiset, ROW ORDER
503 * INCLUDED. (Until 2026-08-19 the reference was the vendored `uniqueperms.m`,
504 * removed for want of a license grant; the replacement reproduces its listing
505 * exactly, so this port is unchanged.)
506 *
507 * The row order is not an aesthetic choice: row 0 is what `default_init_state`
508 * takes as the initial state, so it decides which communicating class a chain
509 * made reducible by the swap graph settles in. The reference's own ordering is
510 * inconsistent between its two branches and is mirrored here rather than
511 * normalised: an all-distinct multiset goes through `perms`, which is REVERSE
512 * lexicographic (descending first), while a multiset with a repeat recurses
513 * over its unique values in ASCENDING order. Both are reproduced.
514 *
515 * The caller supplies `vec` sorted ascending, which is what the class-major
516 * build in the PAS arm of `from_marginal_core` produces; the recursion erases
517 * one element and preserves that order.
518 */
519inline std::vector<std::vector<std::size_t> > pas_multiset_perms(
520 const std::vector<std::size_t>& vec) {
521 std::vector<std::vector<std::size_t> > pu;
522 if (vec.empty()) return pu;
523 std::vector<std::size_t> uvec = vec;
524 std::sort(uvec.begin(), uvec.end());
525 uvec.erase(std::unique(uvec.begin(), uvec.end()), uvec.end());
526 if (uvec.size() == 1) {
527 pu.push_back(vec);
528 return pu;
529 }
530 if (uvec.size() == vec.size()) {
531 std::vector<std::size_t> p(vec.rbegin(), vec.rend());
532 do {
533 pu.push_back(p);
534 } while (std::prev_permutation(p.begin(), p.end()));
535 return pu;
536 }
537 for (std::size_t i = 0; i < uvec.size(); ++i) {
538 std::vector<std::size_t> v = vec;
539 for (std::size_t j = 0; j < v.size(); ++j)
540 if (v[j] == uvec[i]) {
541 v.erase(v.begin() + static_cast<std::ptrdiff_t>(j));
542 break;
543 }
544 const std::vector<std::vector<std::size_t> > tmp = pas_multiset_perms(v);
545 for (std::size_t t = 0; t < tmp.size(); ++t) {
546 std::vector<std::size_t> row(1, uvec[i]);
547 row.insert(row.end(), tmp[t].begin(), tmp[t].end());
548 pu.push_back(row);
549 }
550 }
551 return pu;
552}
553
554/**
555 * Port of `State.fromMarginal` for the station families CTMC enumerates:
556 * every local state in which station `ist` holds exactly `n[r]` class-r jobs.
557 *
558 * COVERED: Queue, Delay, Source and Place under the disciplines whose buffer is
559 * either absent (INF/PS/DPS/GPS) or a per-class count. The row is laid out
560 * [buffer | server] to match `to_marginal`, and the server block is the
561 * cartesian fold of `space_closed_single(phases[r], n[r])` over classes.
562 *
563 * Retrial stations are covered here too, by the (in-service, orbit) split, and
564 * PAS/OI by the ordered class-index list that `to_marginal` already decoded.
565 * Transition nodes are NOT: a Transition is stateful but is not a station, so
566 * it never carries a station index -- `from_marginal_node` handles it, which is
567 * also where the reference puts it.
568 *
569 * REFUSED BY NAME, because a wrong guess is indistinguishable from a correct
570 * one downstream: a station carrying a MAP/MMPP2 arrival process under a
571 * discipline whose encoding does not carry the modulating phase.
572 */
573template <class T>
574std::vector<std::vector<T>> from_marginal_core(const NetworkStruct<T>& sn, std::size_t ist,
575 const std::vector<std::size_t>& n,
576 const std::vector<std::size_t>& phases) {
577 const std::size_t R = sn.nclasses;
578 if (ist == 0 || ist > sn.stations.size())
579 throw InputError("from_marginal: station index " + std::to_string(ist) + " is out of range");
580 if (n.size() != R || phases.size() != R)
581 throw InputError("from_marginal: n and phases must have one entry per class");
582
583 const Station<T>& st = sn.stations[ist - 1];
584 const SchedStrategy sched = st.sched;
585
586 // The reference's MAP guard. A MAP/MMPP2 arrival process modulates between
587 // phases, and only the FCFS encoding carries that phase in the state; under
588 // any other discipline the enumeration would drop the modulating chain and
589 // return states that cannot represent the process. A Source is exempt: its
590 // phase block IS the modulating chain, which the EXT branch builds.
591 for (std::size_t r = 0; r < R; ++r) {
592 const ProcessType pt = sn.procid(ist, r + 1);
593 if ((pt == ProcessType::MAP || pt == ProcessType::MMPP2) &&
594 sched != SchedStrategy::FCFS && st.nodetype != NodeType::Source)
595 throw UnsupportedError(
596 "from_marginal: a MAP/MMPP2 process at a non-FCFS station is not supported; "
597 "only the FCFS encoding carries the modulating phase");
598 }
599
600 // RETRIAL: no waiting line. An arrival that finds every server busy joins an
601 // ORBIT and re-attempts at the retrial rate, so the state is the
602 // (in-service, orbit) SPLIT and every admissible split is a distinct state
603 // -- including the idle-server ones, which an ordinary queue cannot occupy
604 // while jobs wait. A completion does NOT promote from the orbit.
605 // The reference's own test is `any(~cellfun(@@isempty, sn.retrialProc(ist,:)))`:
606 // an entry exists only once some class actually has a retrial process, so a
607 // present-but-empty record must NOT switch the encoding.
608 bool is_retrial = false;
609 {
610 const typename std::map<std::size_t, RetrialParam<T> >::const_iterator rit =
611 sn.retrialparam.find(ist);
612 if (rit != sn.retrialparam.end())
613 for (std::size_t r = 0; r < rit->second.retrial_proc.size(); ++r)
614 if (!rit->second.retrial_proc[r].disabled) { is_retrial = true; break; }
615 }
616 if (is_retrial) {
617 std::size_t rr = R; // first class actually present
618 for (std::size_t r = 0; r < R; ++r)
619 if (n[r] > 0) { rr = r; break; }
620 if (rr == R) { // empty station: one idle row, no orbit slots
621 std::size_t w = 0;
622 for (std::size_t r = 0; r < R; ++r) w += phases[r];
623 return std::vector<std::vector<T>>{std::vector<T>(w, num_traits<T>::from_int(0))};
624 }
625 const double S = st.nservers;
626 const std::size_t maxsrv =
627 std::isfinite(S) ? std::min(n[rr], static_cast<std::size_t>(S)) : n[rr];
628 const std::size_t maxorbit = n[rr];
629 std::vector<std::vector<T>> res3;
630 for (std::size_t csrv = 0; csrv <= maxsrv; ++csrv) {
631 const std::size_t orbit = n[rr] - csrv;
632 std::vector<T> buf(maxorbit - orbit, num_traits<T>::from_int(0));
633 buf.insert(buf.end(), orbit, num_traits<T>::from_int(static_cast<long>(rr + 1)));
634 std::vector<std::vector<T>> srv3;
635 bool ok3 = true;
636 for (std::size_t cls = 0; cls < R; ++cls) {
637 const std::size_t want = cls == rr ? csrv : 0;
638 const std::vector<std::vector<T>> sc = space_closed_single<T>(phases[cls], want);
639 if (sc.empty() && want > 0) { ok3 = false; break; }
640 srv3 = cartesian(srv3, sc);
641 }
642 if (!ok3) continue;
643 for (std::size_t i3 = 0; i3 < srv3.size(); ++i3) {
644 std::vector<T> row = buf;
645 row.insert(row.end(), srv3[i3].begin(), srv3[i3].end());
646 res3.push_back(row);
647 }
648 }
649 return res3;
650 }
651
652 // The capacity gate of the reference: a Source has no finite buffer, every
653 // other station refuses a marginal it cannot hold, returning NO rows rather
654 // than an unreachable one.
655 std::vector<std::vector<T>> out;
656 if (sched != SchedStrategy::EXT)
657 for (std::size_t r = 0; r < R; ++r)
658 // Compare in DOUBLE: classcap is +Inf for an uncapped class, and
659 // casting Inf to size_t is undefined behaviour -- it made this gate
660 // reject every marginal and from_marginal returned no states at all.
661 if (sn.classcap.size() >= ist && r < sn.classcap[ist - 1].size() &&
662 static_cast<double>(n[r]) > sn.classcap[ist - 1][r])
663 return out;
664
665 if (sched == SchedStrategy::PAS || sched == SchedStrategy::OI) {
666 // `fromMarginal.m:457-475`. THE PAS/OI LOCAL STATE IS AN ORDERED LIST,
667 // not a count: the row is the class index of the job in each of the
668 // `sn.cap(ist)` positions, left-aligned and zero-padded. There is no
669 // buffer/server split, no phase block, and no server count, which is
670 // why this arm returns before the cartesian fold below.
671 //
672 // Without it a PAS station fell through to the count-shaped default,
673 // one column per class, while `to_marginal`'s PAS arm decoded the same
674 // row as a list -- the two encodings disagreed and nothing errored. A
675 // count of 2 in column 1 decoded as one class-1 job in position 0 and
676 // whatever class index 2 names in position 1, so queue length at a PAS
677 // station read near zero in every consumer of the decode: SolverSSA's
678 // serial analyzer AND SolverCTMC's averages, which share this walk.
679 //
680 // REFUSE AN INFINITE CAPACITY, as the reference does. The list encoding
681 // has no width without one and substituting a default would silently
682 // truncate the state space instead of reporting that the model is
683 // underspecified.
684 const double W = ist <= sn.cap.size() ? sn.cap[ist - 1]
685 : std::numeric_limits<double>::infinity();
686 if (!std::isfinite(W))
687 throw InputError(
688 "from_marginal: PAS stations require finite capacity for state-space generation");
689 const std::size_t w = static_cast<std::size_t>(W);
690 std::size_t tot = 0;
691 for (std::size_t r = 0; r < R; ++r) tot += n[r];
692 if (tot == 0)
693 return std::vector<std::vector<T> >(
694 1, std::vector<T>(w, num_traits<T>::from_int(0)));
695 if (tot > w) return out; // infeasible: exceeds the station's total capacity
696 std::vector<std::size_t> vi;
697 for (std::size_t r = 0; r < R; ++r)
698 for (std::size_t j = 0; j < n[r]; ++j) vi.push_back(r + 1);
699 const std::vector<std::vector<std::size_t> > mi = pas_multiset_perms(vi);
700 out.reserve(mi.size());
701 for (std::size_t i = 0; i < mi.size(); ++i) {
702 std::vector<T> row;
703 row.reserve(w);
704 for (std::size_t j = 0; j < mi[i].size(); ++j)
705 row.push_back(num_traits<T>::from_int(static_cast<long>(mi[i][j])));
706 row.resize(w, num_traits<T>::from_int(0));
707 out.push_back(row);
708 }
709 return out;
710 }
711
712 // `space_closed_single` is empty exactly when the class has no phase, so
713 // this is the unreachability test the eager fold below used to perform,
714 // separated from the fold itself. THE FOLD IS DEFERRED to its only reader,
715 // the INF/PS arm: it places every job of every class in a phase, which is
716 // C(n+K-1, K-1) rows for K phases, and the ordered-buffer arm right below
717 // never reads it -- it takes si[r] from the permutation tail. Building it
718 // there anyway cost 8.5e8 discarded rows on an M/ME/1 with an order-11
719 // service and cutoff 30 (test_cme_distribution.py::test_ctmc_mg1_is_exact),
720 // which reads as a hang rather than as an error.
721 //
722 // THE TEST IS ON THE CONTENT, NOT ON THE WIDTH. `phases` is the WIDTH
723 // vector (`phasessz_of`, one column per class even where the class has no
724 // process), so a disabled class still occupies a column and the emptiness
725 // has to be asked of `phases_of` directly.
726 for (std::size_t r = 0; r < R; ++r)
727 if (sn.phases_of(ist, r + 1) == 0 && n[r] > 0)
728 return out; // no service process, yet jobs demanded: unreachable
729
730 if (state_detail::buffer_is_class_tag(sched) || state_detail::buffer_is_tag_phase_pairs(sched)) {
731 // ORDERED BUFFER, as `fromMarginal`'s FCFS/LCFS branch builds it: the
732 // waiting positions record WHICH class occupies each slot, so the state
733 // is a permutation of the multiset {r repeated n[r] times}, not a count.
734 // The last S entries of each permutation are the jobs in service and
735 // the rest is the buffer, which is why the marginal alone does not
736 // determine the state and a count-shaped buffer would be wrong.
737 // The preempt family stores [class, phase] PAIRS, so one permutation of
738 // the waiting classes yields one row per assignment of an interruption
739 // phase to each waiting job, exactly as `fromMarginal` interleaves
740 // `mi_buf` with `bkstate` (fromMarginal.m:310-325).
741 const bool paired = state_detail::buffer_is_tag_phase_pairs(sched);
742 std::vector<std::size_t> vi;
743 for (std::size_t r = 0; r < R; ++r)
744 for (std::size_t j = 0; j < n[r]; ++j) vi.push_back(r + 1);
745 const double S = st.nservers;
746 const std::size_t nsrv =
747 std::isfinite(S) ? static_cast<std::size_t>(S) : vi.size();
748 if (vi.empty()) {
749 // Empty station: one state, all-zero buffer and idle servers. The
750 // preempt-resume buffer is [class, phase] pairs, so its empty width
751 // is even -- a one-column buffer there would misalign to_marginal.
752 const std::size_t bw = state_detail::buffer_is_tag_phase_pairs(sched) ? 2u : 1u;
753 std::vector<T> row(bw, num_traits<T>::from_int(0));
754 std::size_t srvw2 = 0;
755 for (std::size_t r = 0; r < R; ++r) srvw2 += phases[r];
756 row.insert(row.end(), srvw2, num_traits<T>::from_int(0));
757 return std::vector<std::vector<T>>{row};
758 }
759 // Descending first, walked down with prev_permutation: the SAME set of
760 // permutations as an ascending next_permutation walk, but row 0 is the
761 // descending buffer, which is the row MATLAB State.fromMarginalAndStarted
762 // returns. Anything taking rows[0] as the initial state (default_init_state)
763 // then agrees with the reference, and on a chain made reducible by
764 // non-overtaking routing that is what picks the closed communicating class.
765 std::sort(vi.begin(), vi.end(), [](std::size_t a, std::size_t b) { return a > b; });
766 std::vector<std::vector<T>> res2;
767 do {
768 // Split the permutation: the tail is in service, the head waits.
769 const std::size_t insrv = std::min(nsrv, vi.size());
770 std::vector<std::size_t> si(R, 0);
771 for (std::size_t j = vi.size() - insrv; j < vi.size(); ++j) si[vi[j] - 1] += 1;
772 std::vector<std::vector<T>> kst;
773 bool ok2 = true;
774 for (std::size_t r = 0; r < R; ++r) {
775 const std::vector<std::vector<T>> sr = space_closed_single<T>(phases[r], si[r]);
776 if (sr.empty() && si[r] > 0) { ok2 = false; break; }
777 kst = cartesian(kst, sr);
778 }
779 if (!ok2) continue;
780 std::vector<std::size_t> wait;
781 for (std::size_t j = 0; j + insrv < vi.size(); ++j) wait.push_back(vi[j]);
782 std::vector<std::vector<T>> bufs;
783 if (!paired) {
784 std::vector<T> b;
785 for (std::size_t j = 0; j < wait.size(); ++j)
786 b.push_back(num_traits<T>::from_int(static_cast<long>(wait[j])));
787 if (b.empty()) b.push_back(num_traits<T>::from_int(0));
788 bufs.push_back(b);
789 } else if (wait.empty()) {
790 // An empty paired buffer is still two columns wide, so that a
791 // narrower row left-padded to the widest one keeps its parity.
792 bufs.push_back(std::vector<T>(2, num_traits<T>::from_int(0)));
793 } else {
794 bufs.push_back(std::vector<T>());
795 for (std::size_t j = 0; j < wait.size(); ++j) {
796 std::vector<std::vector<T>> next;
797 for (std::size_t b = 0; b < bufs.size(); ++b)
798 for (std::size_t p = 0; p < phases[wait[j] - 1]; ++p) {
799 std::vector<T> row = bufs[b];
800 row.push_back(num_traits<T>::from_int(static_cast<long>(wait[j])));
801 row.push_back(num_traits<T>::from_int(static_cast<long>(p + 1)));
802 next.push_back(row);
803 }
804 bufs.swap(next);
805 }
806 }
807 for (std::size_t bi = 0; bi < bufs.size(); ++bi)
808 for (std::size_t i2 = 0; i2 < kst.size(); ++i2) {
809 std::vector<T> row = bufs[bi];
810 row.insert(row.end(), kst[i2].begin(), kst[i2].end());
811 res2.push_back(row);
812 }
813 } while (std::prev_permutation(vi.begin(), vi.end()));
814 return res2;
815 }
816
817 if (!state_detail::buffer_is_per_class_count(sched)) {
818 // The INF/PS families track only jobs in the servers, so every job of
819 // every class carries a phase and this fold IS the local state space.
820 std::vector<std::vector<T>> srv;
821 for (std::size_t r = 0; r < R; ++r)
822 srv = cartesian(srv, space_closed_single<T>(phases[r], n[r]));
823 return srv;
824 }
825
826 // Per-class-count buffer: everything beyond the servers waits. The split
827 // between in-service and waiting is NOT determined by the marginal, so it is
828 // enumerated here.
829 //
830 // THE SPLIT MUST DRIVE THE SERVER BLOCK, NOT BE RECOVERED FROM IT. The `srv`
831 // fold above places all n[r] jobs of every class, because that is what the
832 // INF and PS families need. An earlier version of this branch built the
833 // server block the same way and then tried to read the in-service count back
834 // out of it -- but that count is identically n[r], the very number just
835 // placed there, so the buffer came out identically zero and every marginal
836 // whose total exceeded the server count was discarded. A POLLING station
837 // then held at most `nservers` jobs and never queued one: a two-class model
838 // with lambda 0.2/0.3 and mu 2 reported the throughput of an M/M/1/1,
839 // 0.16/0.24 against the correct 0.20/0.30, with QLen equal to Util in every
840 // state because nothing ever waited. The buffered states were not merely
841 // improbable, they were absent, so the arrival handler's successors had
842 // nowhere to land and the generator dropped those edges.
843 //
844 // The other two branches of this function already do it this way -- the
845 // class-tag branch takes si[r] from the permutation tail, the retrial branch
846 // takes `want` from the in-service count -- and they are the two that worked.
847 //
848 // HOW MANY JOBS ARE IN SERVICE IS A PROPERTY OF THE DISCIPLINE. The
849 // reference splits this family in two, and the split is exactly the
850 // work-conservation of the station:
851 //
852 // POLLING has its own `fromMarginal` case, and it emits the EMPTY-facility
853 // row -- all n waiting, nothing served -- alongside the one-job-in-service
854 // rows. A polling server may sit idle with a backlog because it is walking
855 // between buffers or parked, so that row is a state it genuinely occupies.
856 // Its single facility also means at most one job is ever in service.
857 //
858 // SIRO, SEPT, LEPT and the SRPT pair are work conserving, and the
859 // reference enumerates EXACTLY min(S, sum n) in service (`multichoosecon`).
860 // Emitting the partially-idle rows for them would widen the space past
861 // MATLAB's with states no transition enters.
862 //
863 // So the bound is `sum s == maxsrv`, relaxed to `sum s >= 0` for POLLING.
864 const double S = st.nservers;
865 if (sched == SchedStrategy::POLLING && S != 1.0)
866 throw UnsupportedError(
867 "from_marginal: a polling station must have exactly one server; the controller "
868 "encoding pins the visit to a single class at a time, so a multi-server polling "
869 "station cannot be represented (the reference refuses it the same way)");
870 const std::size_t nsrv =
871 std::isfinite(S) ? static_cast<std::size_t>(S) : static_cast<std::size_t>(-1);
872 std::size_t ntot = 0;
873 for (std::size_t r = 0; r < R; ++r) ntot += n[r];
874 const std::size_t maxsrv = std::min(nsrv, ntot);
875 const std::size_t minsrv = sched == SchedStrategy::POLLING ? 0 : maxsrv;
876
877 std::vector<std::vector<T>> res;
878 std::vector<std::size_t> sv(R, 0);
879 for (;;) {
880 std::size_t stot = 0;
881 for (std::size_t r = 0; r < R; ++r) stot += sv[r];
882 if (stot >= minsrv && stot <= maxsrv) {
883 std::vector<std::vector<T>> srv_s;
884 bool ok = true;
885 for (std::size_t r = 0; r < R && ok; ++r) {
886 const std::vector<std::vector<T>> sr = space_closed_single<T>(phases[r], sv[r]);
887 if (sr.empty() && sv[r] > 0) ok = false;
888 else srv_s = cartesian(srv_s, sr);
889 }
890 if (ok) {
891 std::vector<T> buf(R, num_traits<T>::from_int(0));
892 for (std::size_t r = 0; r < R; ++r)
893 buf[r] = num_traits<T>::from_int(static_cast<long>(n[r] - sv[r]));
894 for (std::size_t i = 0; i < srv_s.size(); ++i) {
895 std::vector<T> row = buf;
896 row.insert(row.end(), srv_s[i].begin(), srv_s[i].end());
897 res.push_back(row);
898 }
899 }
900 }
901 // Mixed-radix increment of the per-class in-service counts.
902 std::size_t r = 0;
903 for (; r < R; ++r) {
904 if (sv[r] < n[r]) { ++sv[r]; break; }
905 sv[r] = 0;
906 }
907 if (r == R) break;
908 }
909 return res;
910}
911
912/**
913 * Port of `State.pollingBlocks` + `State.pollingProject`: every controller
914 * configuration compatible with one (buffer, server) row.
915 *
916 * ENUMERATING PER ROW rather than taking a blind cartesian product is what
917 * keeps the space tight and the chain irreducible. pos is PINNED to the class
918 * in service; a switchover excludes a busy facility; and a park excludes a
919 * non-empty station, because with only immediate switchovers the server would
920 * have reached the waiting work in zero time. A cartesian product would admit
921 * "serving buffer 1 while a class-2 job holds the server", which no transition
922 * can enter or leave consistently with the marginals.
923 *
924 * @param srvclass 1-based class holding the single service facility, 0 if idle
925 * @param nbuf per-class waiting counts
926 * @param pi the polling configuration of the station
927 * @return one row per configuration, PROJECTED onto the columns `pinfo`
928 * materializes; the elided ones are reconstructible from the rest of
929 * the state (`polling_get`), so keeping them would split each state
930 * into copies no observation can tell apart
931 */
932template <class T>
933std::vector<std::vector<T>> polling_blocks(const PollingInfo<T>& pi, std::size_t srvclass,
934 const std::vector<std::size_t>& nbuf) {
935 const std::size_t R = pi.polled.size();
936 std::vector<std::vector<long>> trips; // full [pos, swk, ctr]
937 if (srvclass > 0) {
938 // A buffer outside the cyclic order can hold no job at all.
939 if (!pi.polled[srvclass - 1]) return std::vector<std::vector<T>>();
940 std::vector<long> ctrset;
941 switch (pi.ptype) {
943 ctrset.push_back(0);
944 break;
946 // ctr counts the jobs admitted at the polling instant that have
947 // not completed, the one in service included, so ctr >= 1 and
948 // the ctr-1 still uncompleted ones all wait in the buffer.
949 for (long c = 1; c <= static_cast<long>(nbuf[srvclass - 1]) + 1; ++c)
950 ctrset.push_back(c);
951 break;
953 for (long c = 1; c <= static_cast<long>(pi.pk); ++c) ctrset.push_back(c);
954 break;
956 // ctr is the population the visit is driving the class down to.
957 for (long c = 0; c <= static_cast<long>(nbuf[srvclass - 1]); ++c)
958 ctrset.push_back(c);
959 break;
960 }
961 for (std::size_t i = 0; i < ctrset.size(); ++i)
962 trips.push_back(std::vector<long>{static_cast<long>(srvclass), 0, ctrset[i]});
963 } else {
964 bool anysw = false;
965 for (std::size_t q = 1; q <= R; ++q) {
966 if (!pi.has_sw[q - 1]) continue;
967 anysw = true;
968 // Every phase of a non-immediate switchover is dwelt in, and jobs
969 // may wait meanwhile: this is exactly what makes a polling station
970 // non-work-conserving.
971 for (std::size_t k = 1; k <= pi.ksw[q - 1]; ++k)
972 trips.push_back(std::vector<long>{static_cast<long>(q), static_cast<long>(k), 0});
973 }
974 std::size_t total = 0;
975 for (std::size_t r = 0; r < nbuf.size(); ++r) total += nbuf[r];
976 if (!anysw && total == 0) {
977 std::size_t first = 0;
978 for (std::size_t r = 1; r <= R; ++r)
979 if (pi.polled[r - 1]) { first = r; break; }
980 trips.push_back(std::vector<long>{static_cast<long>(first), 0, 0});
981 }
982 }
983
984 const std::size_t npos = static_cast<std::size_t>(-1);
985 std::vector<std::vector<T>> out;
986 for (std::size_t i = 0; i < trips.size(); ++i) {
987 std::vector<T> row;
988 if (pi.ipos != npos) row.push_back(num_traits<T>::from_int(trips[i][0]));
989 if (pi.iswk != npos) row.push_back(num_traits<T>::from_int(trips[i][1]));
990 if (pi.ictr != npos) row.push_back(num_traits<T>::from_int(trips[i][2]));
991 out.push_back(row);
992 }
993 return out;
994}
995
996/**
997 * Append the trailing local-variable block to every row the core builders
998 * produce, which is what makes a state row as wide as `nvars_of` declares.
999 *
1000 * Shared by `from_marginal` and `from_marginal_and_started`: the two differ in
1001 * how the (buffer, server) split is chosen, never in what follows it, so the
1002 * controller, the BAS marker and the reply counters are appended once here.
1003 *
1004 * WITHOUT THIS THE ROWS ARE ONE BLOCK TOO NARROW and every slicer that takes
1005 * `nvar` clear of the right-hand end reads the server block from the wrong
1006 * offset. Measured on a two-class EXHAUSTIVE polling station: the controller
1007 * was absent from the enumerated space entirely and the station behaved as a
1008 * capacity-one queue, reporting Tput 0.16/0.24 against MATLAB's 0.20/0.30.
1009 *
1010 * The polling controller is enumerated per row (`polling_blocks`), and so is the
1011 * ROUND-ROBIN POINTER: which link the next job takes is a coordinate of the
1012 * state, so a space that emitted it at zero would carry one configuration of a
1013 * dispatcher that has several, and the chain could never leave it. RROBIN
1014 * enumerates its outlinks and WRROBIN the positions of its weighted cycle,
1015 * exactly as `State.fromMarginal`'s `sub_routevars` does.
1016 *
1017 * The modulating phase and the REPLY blocked-server counters are still emitted
1018 * at zero: exact for a model that declares none of them, and width-correct
1019 * rather than enumerated for one that does; see `14-cpp-multiprecision.md`.
1020 */
1021template <class T>
1022std::vector<std::vector<T>> append_local_vars(const NetworkStruct<T>& sn, std::size_t ist,
1023 std::vector<std::vector<T>> rows,
1024 const std::vector<std::size_t>& n,
1025 const std::vector<std::size_t>& phases) {
1026 if (rows.empty()) return rows;
1027 const std::size_t ind = sn.node_of_station(ist);
1028 // No node means no local-variable block to append; a Layer submodel carries
1029 // stations without a node map at all.
1030 if (ind == 0) return rows;
1031 const std::size_t width = sn.nvars_of(ind);
1032 if (width == 0) return rows;
1033
1034 const std::size_t R = sn.nclasses;
1035 const PollingInfo<T> pi = polling_info(sn, ind);
1036 const std::size_t pw = pi.valid ? pi.width : 0;
1037 const T zero = num_traits<T>::from_int(0);
1038
1039 // The true-BAS blocked marker takes the shared node-block column, so it is
1040 // MUTUALLY EXCLUSIVE with the polling controller and is enumerated instead of
1041 // it. Only a NON-EMPTY station can hold a blocked job -- the marker says "the
1042 // front job here has completed and is waiting for room" -- so an empty
1043 // marginal carries the single value 0 rather than both.
1044 const bool bas = ind <= sn.isbasblocking.size() && sn.isbasblocking[ind - 1];
1045 // The server status of a breakdown station takes the same trailing column
1046 // and, unlike the BAS marker, is enumerated for EVERY marginal including the
1047 // empty one: an idle server still fails. UP (1) is emitted first, so the
1048 // default initial state (the first row) has the server up, which is the row
1049 // the reference's descending sort puts first.
1050 const bool brk = sn.has_breakdown_node(ind);
1051 std::size_t ntot = 0;
1052 for (std::size_t r = 0; r < n.size(); ++r) ntot += n[r];
1053
1054 // The dispatch pointers, one column per class that routes round-robin, in
1055 // CLASS ORDER and immediately after the modulating phases: that is where
1056 // `rr_var_slot` reads them and where the reference appends them.
1057 std::size_t rrw = 0;
1058 if (ind <= sn.nvars.size())
1059 for (std::size_t r = 0; r < R && R + r < sn.nvars[ind - 1].size(); ++r)
1060 rrw += sn.nvars[ind - 1][R + r];
1061 std::vector<std::vector<T>> ptrsets(1, std::vector<T>());
1062 for (std::size_t r = 1; r <= R; ++r) {
1063 if (sn.rr_var_slot(ind, r) == 0) continue;
1064 std::vector<T> vals;
1065 if (sn.nodes[ind - 1].routing[r - 1] == RoutingStrategy::RROBIN) {
1066 const std::vector<std::size_t> ol = sn.rr_outlinks(ind, r);
1067 for (std::size_t d = 0; d < ol.size(); ++d)
1068 vals.push_back(num_traits<T>::from_int(static_cast<long>(ol[d])));
1069 } else {
1070 const std::vector<std::size_t> cy = sn.rr_weighted_outlinks(ind, r);
1071 for (std::size_t d = 0; d < cy.size(); ++d)
1072 vals.push_back(num_traits<T>::from_int(static_cast<long>(d + 1)));
1073 }
1074 if (vals.empty()) vals.push_back(zero);
1075 std::vector<std::vector<T>> grown;
1076 for (std::size_t g = 0; g < ptrsets.size(); ++g)
1077 for (std::size_t v = 0; v < vals.size(); ++v) {
1078 std::vector<T> row = ptrsets[g];
1079 row.push_back(vals[v]);
1080 grown.push_back(row);
1081 }
1082 ptrsets.swap(grown);
1083 }
1084
1085 std::vector<std::vector<T>> out;
1086 for (std::size_t i = 0; i < rows.size(); ++i) {
1087 // The controller depends on WHICH class holds the service facility and
1088 // on how many jobs wait, so recover both from the row just built.
1089 std::vector<std::size_t> nbuf(R, 0);
1090 std::size_t srvclass = 0;
1091 if (pw) {
1092 std::size_t srvw = 0;
1093 for (std::size_t r = 0; r < R; ++r) srvw += phases[r];
1094 const std::size_t srv0 = rows[i].size() - srvw;
1095 std::size_t off = 0;
1096 for (std::size_t r = 0; r < R; ++r) {
1097 std::size_t c = 0;
1098 for (std::size_t k = 0; k < phases[r]; ++k)
1099 c += static_cast<std::size_t>(
1100 num_traits<T>::to_double(rows[i][srv0 + off + k]));
1101 off += phases[r];
1102 if (c > 0 && srvclass == 0) srvclass = r + 1;
1103 // The per-class-count buffer POLLING uses puts the waiting
1104 // count in column r, ahead of the server block.
1105 if (srv0 >= R) nbuf[r] = static_cast<std::size_t>(
1106 num_traits<T>::to_double(rows[i][r]));
1107 }
1108 }
1109 const std::vector<std::vector<T>> blocks =
1110 pw ? polling_blocks(pi, srvclass, nbuf)
1111 : std::vector<std::vector<T>>(1, std::vector<T>());
1112 // No admissible controller configuration means the (buffer, server)
1113 // split itself is unoccupiable, so the row is DROPPED rather than
1114 // emitted without a controller.
1115 // ORDER IS THE ENCODING: nvars is [modulation | routing | node block |
1116 // reply], so the controller sits between the routing pointers and the
1117 // reply counters, not at the end. `polling_info` computes its offset
1118 // the same way, and the two must agree or every read is shifted.
1119 const std::size_t head = pi.valid ? pi.off : width - pw;
1120 for (std::size_t b = 0; b < blocks.size(); ++b) {
1121 for (std::size_t g = 0; g < ptrsets.size(); ++g) {
1122 std::vector<T> row = rows[i];
1123 row.insert(row.end(), head - rrw, zero); // modulating phases
1124 row.insert(row.end(), ptrsets[g].begin(), ptrsets[g].end()); // dispatch pointers
1125 row.insert(row.end(), blocks[b].begin(), blocks[b].end());
1126 row.insert(row.end(), width - head - pw, zero); // reply counters
1127 if (brk) {
1128 for (long v = 1; v >= 0; --v) {
1129 std::vector<T> r2 = row;
1130 r2.back() = num_traits<T>::from_int(v);
1131 out.push_back(r2);
1132 }
1133 continue;
1134 }
1135 if (!bas) {
1136 out.push_back(row);
1137 continue;
1138 }
1139 // The marker is the LAST column, which is where the departure
1140 // handler and the generator's become-blocked edge both read it.
1141 // `refresh_bas_blocking` refuses BAS together with a reply block for
1142 // exactly this reason: the reply counters would trail it.
1143 for (std::size_t v = 0; v <= (ntot > 0 ? 1u : 0u); ++v) {
1144 std::vector<T> r2 = row;
1145 r2.back() = num_traits<T>::from_int(static_cast<long>(v));
1146 out.push_back(r2);
1147 }
1148 }
1149 }
1150 }
1151 // A MAP service in CTMC pair form (`ctmc_mapc`) carries the landing phase of its last
1152 // started draw, 1..p: every row is enumerated over it, value 1 first for the default state.
1153 // A single server's memory is the phase of the class-r job in service when there is one,
1154 // as in `State.fromMarginal`, and 1..p otherwise.
1155 std::size_t srvw = 0;
1156 for (std::size_t r = 0; r < R && r < phases.size(); ++r) srvw += phases[r];
1157 for (std::size_t r = 1; r <= R; ++r) {
1158 const CtmcMapc<T>* mc = sn.mapc_of(ist, r);
1159 if (mc == nullptr || sn.nvars[ind - 1][r - 1] == 0) continue;
1160 const std::size_t slot = sn.map_var_slot(ind, r);
1161 std::size_t off = 0;
1162 for (std::size_t j = 0; j + 1 < r; ++j) off += phases[j];
1163 std::vector<std::vector<T>> grown;
1164 grown.reserve(out.size() * mc->p);
1165 for (std::size_t i = 0; i < out.size(); ++i) {
1166 const std::size_t srv0 = out[i].size() - width - srvw;
1167 std::size_t busy = 0;
1168 for (std::size_t k = 0; mc->single && k < phases[r - 1] && busy == 0; ++k)
1169 if (num_traits<T>::to_double(out[i][srv0 + off + k]) > 0) busy = k + 1;
1170 for (std::size_t h = 1; h <= mc->p; ++h) {
1171 if (busy != 0 && h != busy) continue;
1172 std::vector<T> row = out[i];
1173 row[row.size() - width + slot] = num_traits<T>::from_int(static_cast<long>(h));
1174 grown.push_back(row);
1175 }
1176 }
1177 out.swap(grown);
1178 }
1179 return out;
1180}
1181
1182namespace state_detail {
1183
1184/**
1185 * The in-service allocations of `State.fromMarginalPool`: x[r][k] class-r jobs on
1186 * the k-th compatible pool of r, within the pool sizes, such that no class with a
1187 * waiting job finds a free compatible server.
1188 */
1189template <class T>
1190void pool_allocations(const CtmcPool<T>& pool, const std::vector<std::size_t>& n, std::size_t r,
1191 std::size_t k, std::vector<std::vector<std::size_t>>& x,
1192 std::vector<double>& load,
1193 std::vector<std::vector<std::vector<std::size_t>>>& out) {
1194 const std::size_t R = n.size();
1195 if (r == R) {
1196 for (std::size_t s = 0; s < R; ++s) {
1197 if (pool.pools[s].empty()) continue;
1198 std::size_t ins = 0;
1199 for (std::size_t j = 0; j < x[s].size(); ++j) ins += x[s][j];
1200 if (n[s] <= ins) continue;
1201 for (std::size_t j = 0; j < pool.pools[s].size(); ++j) {
1202 const std::size_t t = pool.pools[s][j];
1203 if (load[t - 1] < pool.count[t - 1]) return; // a waiting job has a free server
1204 }
1205 }
1206 out.push_back(x);
1207 return;
1208 }
1209 if (pool.pools[r].empty() || k == pool.pools[r].size()) {
1210 pool_allocations(pool, n, r + 1, 0, x, load, out);
1211 return;
1212 }
1213 const std::size_t t = pool.pools[r][k];
1214 std::size_t used = 0;
1215 for (std::size_t j = 0; j < k; ++j) used += x[r][j];
1216 const double room = pool.count[t - 1] - load[t - 1];
1217 const std::size_t cmax = std::min<std::size_t>(
1218 n[r] - used, room > 0 ? static_cast<std::size_t>(room + 0.5) : 0);
1219 for (std::size_t c = 0; c <= cmax; ++c) {
1220 x[r][k] = c;
1221 load[t - 1] += static_cast<double>(c);
1222 pool_allocations(pool, n, r, k + 1, x, load, out);
1223 load[t - 1] -= static_cast<double>(c);
1224 }
1225 x[r][k] = 0;
1226}
1227
1228} // namespace state_detail
1229
1230/**
1231 * Port of `State.fromMarginalPool`: the local states with marginal `n` at a station
1232 * rewritten by `ctmc::ctmc_pools`. A row is [buffer | servers | pool order]: every
1233 * split of the jobs into in-service ones (per class and pool, within the pool sizes)
1234 * and waiting ones such that no waiting job has a free compatible server, every phase
1235 * assignment of the jobs in service, every order of the waiting jobs (per-class counts
1236 * under SIRO) and, under a rotating ALIS/FAIRNESS order, every order index. The ordered
1237 * buffer is max(1, min(sum(n), cap)) wide; rows come out in descending order.
1238 */
1239template <class T>
1240std::vector<std::vector<T>> from_marginal_pool(const NetworkStruct<T>& sn, std::size_t ist,
1241 const std::vector<std::size_t>& n) {
1242 const std::size_t R = sn.nclasses;
1243 const std::size_t ind = sn.node_of_station(ist);
1244 const CtmcPool<T>& pool = sn.ctmcpool.at(ind);
1245 const T zero = num_traits<T>::from_int(0);
1246 std::vector<std::vector<T>> space;
1247 if (sn.nvars_of(ind) != (pool.rotate ? 1u : 0u))
1248 throw UnsupportedError("Station '" + sn.nodes[ind - 1].name +
1249 "' carries local variables that a heterogeneous server pool "
1250 "cannot hold.");
1251 std::size_t ntot = 0;
1252 for (std::size_t r = 0; r < R; ++r) {
1253 ntot += n[r];
1254 if (sn.classcap.size() >= ist && r < sn.classcap[ist - 1].size() &&
1255 static_cast<double>(n[r]) > sn.classcap[ist - 1][r])
1256 return space;
1257 if (pool.pools[r].empty() && n[r] > 0) return space;
1258 }
1259 const double cap = sn.cap[ist - 1];
1260 if (static_cast<double>(ntot) > cap) return space;
1261 const bool siro = sn.stations[ist - 1].sched == SchedStrategy::SIRO;
1262 const std::size_t W =
1263 siro ? R
1264 : std::max<std::size_t>(1, std::isfinite(cap) ? std::min<std::size_t>(
1265 ntot, static_cast<std::size_t>(cap))
1266 : ntot);
1267 std::vector<std::vector<std::size_t>> x(R);
1268 for (std::size_t r = 0; r < R; ++r) x[r].assign(pool.pools[r].size(), 0);
1269 std::vector<double> load(pool.ntypes, 0.0);
1270 std::vector<std::vector<std::vector<std::size_t>>> alloc;
1271 state_detail::pool_allocations(pool, n, 0, 0, x, load, alloc);
1272 for (std::size_t a = 0; a < alloc.size(); ++a) {
1273 std::vector<std::size_t> w = n;
1274 for (std::size_t r = 0; r < R; ++r)
1275 for (std::size_t k = 0; k < alloc[a][r].size(); ++k) w[r] -= alloc[a][r][k];
1276 // server part: the phases of the jobs of each class-pool block
1277 std::vector<std::vector<T>> srvset;
1278 bool ok = true;
1279 for (std::size_t r = 0; r < R && ok; ++r) {
1280 const std::size_t Kr = sn.phasessz_of(ist, r + 1);
1281 std::vector<std::vector<T>> blk;
1282 if (pool.pools[r].empty()) {
1283 blk.push_back(std::vector<T>(Kr, zero));
1284 } else {
1285 for (std::size_t k = 0; k < pool.pools[r].size(); ++k) {
1286 const std::vector<std::vector<T>> sc =
1287 space_closed_single<T>(pool.len[r][k], alloc[a][r][k]);
1288 if (sc.empty()) { ok = false; break; }
1289 blk = cartesian(blk, sc);
1290 }
1291 for (std::size_t b = 0; b < blk.size(); ++b)
1292 if (blk[b].size() < Kr) blk[b].resize(Kr, zero);
1293 }
1294 srvset = cartesian(srvset, blk);
1295 }
1296 if (!ok) continue;
1297 // buffer part
1298 std::vector<std::vector<T>> bufset;
1299 if (siro) {
1300 std::vector<T> b(R, zero);
1301 for (std::size_t r = 0; r < R; ++r) b[r] = num_traits<T>::from_int(static_cast<long>(w[r]));
1302 bufset.push_back(b);
1303 } else {
1304 std::vector<std::size_t> vi;
1305 for (std::size_t r = 0; r < R; ++r) vi.insert(vi.end(), w[r], r + 1);
1306 if (vi.size() > W) continue;
1307 do {
1308 std::vector<T> b(W - vi.size(), zero);
1309 for (std::size_t j = 0; j < vi.size(); ++j)
1310 b.push_back(num_traits<T>::from_int(static_cast<long>(vi[j])));
1311 bufset.push_back(b);
1312 } while (std::next_permutation(vi.begin(), vi.end()));
1313 }
1314 const std::vector<std::vector<T>> part = cartesian(bufset, srvset);
1315 space.insert(space.end(), part.begin(), part.end());
1316 }
1317 if (pool.rotate) {
1318 std::vector<std::vector<T>> ords;
1319 for (std::size_t p = 1; p <= pool.perms.size(); ++p)
1320 ords.push_back(std::vector<T>(1, num_traits<T>::from_int(static_cast<long>(p))));
1321 space = cartesian(space, ords);
1322 }
1323 const auto less = [](const std::vector<T>& a, const std::vector<T>& b) {
1324 for (std::size_t i = 0; i < a.size() && i < b.size(); ++i) {
1325 const double av = num_traits<T>::to_double(a[i]), bv = num_traits<T>::to_double(b[i]);
1326 if (av != bv) return av > bv; // descending, as the reference's space(end:-1:1,:)
1327 }
1328 return a.size() < b.size();
1329 };
1330 const auto same = [](const std::vector<T>& a, const std::vector<T>& b) {
1331 if (a.size() != b.size()) return false;
1332 for (std::size_t i = 0; i < a.size(); ++i)
1333 if (num_traits<T>::to_double(a[i]) != num_traits<T>::to_double(b[i])) return false;
1334 return true;
1335 };
1336 std::sort(space.begin(), space.end(), less);
1337 space.erase(std::unique(space.begin(), space.end(), same), space.end());
1338 return space;
1339}
1340
1341template <class T>
1342std::vector<std::vector<T>> from_marginal(const NetworkStruct<T>& sn, std::size_t ist,
1343 const std::vector<std::size_t>& n,
1344 const std::vector<std::size_t>& phases) {
1345 const std::size_t jnd0 = sn.node_of_station(ist);
1346 // heterogeneous server pools rewritten for the CTMC enumerate in their own layout
1347 if (jnd0 != 0 && !sn.ctmcpool.empty() && sn.ctmcpool.count(jnd0))
1348 return from_marginal_pool(sn, ist, n);
1349 // A Join of an FJ-augmented struct: its state is the per-class count vector
1350 // itself, DETERMINED by the marginal rather than enumerated from it. There is
1351 // no buffer order to choose and no phase to be in, so exactly one row exists.
1352 if (sn.isfjaugmented && jnd0 != 0 && sn.nodes[jnd0 - 1].nodetype == NodeType::Join) {
1353 std::vector<T> row(sn.nclasses, num_traits<T>::from_int(0));
1354 for (std::size_t r = 0; r < sn.nclasses && r < n.size(); ++r)
1355 row[r] = num_traits<T>::from_int(static_cast<long>(n[r]));
1356 return std::vector<std::vector<T>>(1, row);
1357 }
1358 // SYNCHRONOUS CALL (REPLY signal), `State.fromMarginal:44-90`. A node with a
1359 // reply block holds one server per job that has left for its callee and is
1360 // waiting for the reply, and THAT COUNT IS NOT DERIVABLE FROM THE MARGINAL:
1361 // the job is at the callee, not here. So the held counts are ENUMERATED and
1362 // the rest of the state is built with the REMAINING servers -- with b held,
1363 // only S-b jobs can be in service, a configuration the plain enumeration
1364 // never produces. Without this the counter column exists but is zero in
1365 // every row, the departure that would hold a server has no successor in the
1366 // space, and the chain dead-ends: measured on the `test_ctmc_reply` model,
1367 // 11 states with the whole population frozen at the caller and every rate
1368 // zero, against 21 enumerated and X = 0.47059 in MATLAB, the JAR and Python.
1369 if (jnd0 != 0 && sn.replyblock.size() >= jnd0) {
1370 std::vector<std::size_t> rclasses;
1371 for (std::size_t r = 0; r < sn.replyblock[jnd0 - 1].size(); ++r)
1372 if (sn.replyblock[jnd0 - 1][r]) rclasses.push_back(r + 1);
1373 if (!rclasses.empty()) {
1374 const double Sd = sn.stations[ist - 1].nservers;
1375 const std::size_t S =
1376 std::isfinite(Sd) ? static_cast<std::size_t>(Sd) : static_cast<std::size_t>(0);
1377 // The recursion runs on a struct with the block CLEARED, which is
1378 // what terminates it: the copy takes this branch no further.
1379 NetworkStruct<T> snb = sn;
1380 snb.replyblock[jnd0 - 1].assign(snb.replyblock[jnd0 - 1].size(), false);
1381 if (snb.nvars.size() >= jnd0)
1382 for (std::size_t r = 1; r <= sn.nclasses && 2 * sn.nclasses + r < snb.nvars[jnd0 - 1].size();
1383 ++r)
1384 snb.nvars[jnd0 - 1][2 * sn.nclasses + r] = 0;
1385 std::vector<std::vector<std::size_t>> bspace(1, std::vector<std::size_t>());
1386 for (std::size_t i = 0; i < rclasses.size(); ++i) {
1387 std::vector<std::vector<std::size_t>> next;
1388 for (std::size_t j = 0; j < bspace.size(); ++j)
1389 for (std::size_t v = 0; v <= S; ++v) {
1390 std::vector<std::size_t> row = bspace[j];
1391 row.push_back(v);
1392 next.push_back(row);
1393 }
1394 bspace.swap(next);
1395 }
1396 std::vector<std::vector<std::vector<T>>> subs(bspace.size());
1397 std::size_t maxw = 0;
1398 for (std::size_t bi = 0; bi < bspace.size(); ++bi) {
1399 std::size_t tot = 0;
1400 for (std::size_t i = 0; i < bspace[bi].size(); ++i) tot += bspace[bi][i];
1401 if (tot > S) continue;
1402 snb.stations[ist - 1].nservers = static_cast<double>(S - tot);
1403 subs[bi] = from_marginal(snb, ist, n, phases);
1404 for (std::size_t i = 0; i < subs[bi].size(); ++i)
1405 maxw = std::max(maxw, subs[bi][i].size());
1406 }
1407 // A held server pushes a job into the buffer, so the sub-spaces come
1408 // out at DIFFERENT buffer widths. The buffer is RIGHT-aligned, empty
1409 // slots padding the left, so widen the narrow rows on the left.
1410 std::vector<std::vector<T>> out;
1411 for (std::size_t bi = 0; bi < bspace.size(); ++bi) {
1412 for (std::size_t i = 0; i < subs[bi].size(); ++i) {
1413 std::vector<T> row;
1414 row.reserve(maxw + bspace[bi].size());
1415 if (subs[bi][i].size() < maxw)
1416 row.assign(maxw - subs[bi][i].size(), num_traits<T>::from_int(0));
1417 row.insert(row.end(), subs[bi][i].begin(), subs[bi][i].end());
1418 for (std::size_t j = 0; j < bspace[bi].size(); ++j)
1419 row.push_back(num_traits<T>::from_int(static_cast<long>(bspace[bi][j])));
1420 out.push_back(row);
1421 }
1422 }
1423 std::sort(out.begin(), out.end(), [](const std::vector<T>& a, const std::vector<T>& b) {
1424 for (std::size_t i = 0; i < a.size() && i < b.size(); ++i) {
1425 const double av = num_traits<T>::to_double(a[i]);
1426 const double bv = num_traits<T>::to_double(b[i]);
1427 if (av != bv) return av < bv;
1428 }
1429 return a.size() < b.size();
1430 });
1431 out.erase(std::unique(out.begin(), out.end(),
1432 [](const std::vector<T>& a, const std::vector<T>& b) {
1433 if (a.size() != b.size()) return false;
1434 for (std::size_t i = 0; i < a.size(); ++i)
1435 if (num_traits<T>::to_double(a[i]) !=
1436 num_traits<T>::to_double(b[i]))
1437 return false;
1438 return true;
1439 }),
1440 out.end());
1441 return out;
1442 }
1443 }
1444 return append_local_vars(sn, ist, from_marginal_core(sn, ist, n, phases), n, phases);
1445}
1446
1447/**
1448 * Port of `State.fromMarginalAndStarted`: ONE state realizing both a per-class
1449 * occupancy `n` and a per-class STARTED count `s`.
1450 *
1451 * HOW IT DIFFERS FROM `from_marginal_core`, which is the reason it is a separate
1452 * builder rather than a filter over it. `from_marginal` ENUMERATES every
1453 * (buffer, server) split consistent with a marginal, because the marginal alone
1454 * does not determine which jobs hold the servers. Here the split is GIVEN: `s`
1455 * says how many jobs of each class are in service, so exactly one split is
1456 * meant and the buffer contents follow as `n - s`. Filtering the enumeration
1457 * would be both quadratic and wrong at an empty station, where the reference
1458 * emits a row of a prescribed width rather than selecting one.
1459 *
1460 * EVERY STARTED JOB IS PLACED IN PHASE ONE, which is what makes the result an
1461 * INITIAL state rather than a member of the stationary space: a job that has
1462 * just started service has not advanced through its phase-type law yet.
1463 *
1464 * ONE BUFFER ORDERING IS EMITTED under the ordered-buffer disciplines, the
1465 * descending-sorted one. The reference enumerates every permutation and then
1466 * keeps the lexicographic maximum through its trailing unique/flip, so building
1467 * that row directly is the same answer without the factorial.
1468 *
1469 * A SHARED SERVER IGNORES `s` ENTIRELY. Under INF/PS/DPS/GPS/LPS and their
1470 * priority variants every job present is in a server, so the state holds `n`,
1471 * not `s`; writing `s` there would lose the queued jobs. That is not a
1472 * simplification but the reference's own branch, and it was a real defect in
1473 * the Python twin until 2026-08-09.
1474 *
1475 * PAS/OI ignores `s` for a different reason: its local state is the ordered
1476 * class-index list of the jobs present, which carries no service split at all,
1477 * so this delegates to the marginal builder there.
1478 *
1479 * @param sn the network struct
1480 * @param ist station index (1-based)
1481 * @param n per-class occupancy
1482 * @param s per-class started count; must satisfy s[r] <= n[r]
1483 * @param phases per-class phase counts
1484 */
1485template <class T>
1486std::vector<std::vector<T>> from_marginal_and_started_core(const NetworkStruct<T>& sn,
1487 std::size_t ist,
1488 const std::vector<std::size_t>& n,
1489 const std::vector<std::size_t>& s,
1490 const std::vector<std::size_t>& phases) {
1491 const std::size_t R = sn.nclasses;
1492 if (ist == 0 || ist > sn.stations.size())
1493 throw InputError("from_marginal_and_started: station index " + std::to_string(ist) +
1494 " is out of range");
1495 if (n.size() != R || s.size() != R || phases.size() != R)
1496 throw InputError("from_marginal_and_started: n, s and phases must have one entry per class");
1497
1498 const Station<T>& st = sn.stations[ist - 1];
1499 const SchedStrategy sched = st.sched;
1500 std::vector<std::vector<T>> out;
1501
1502 std::size_t ntot = 0, stot = 0;
1503 for (std::size_t r = 0; r < R; ++r) {
1504 if (s[r] > n[r]) return out; // more started than present: no such state
1505 ntot += n[r];
1506 stot += s[r];
1507 }
1508
1509 // The reference's two pre-switch guards. Both are "no such state" rather
1510 // than an error, since the caller may be sweeping a lattice.
1511 if (ist <= sn.classcap.size())
1512 for (std::size_t r = 0; r < R && r < sn.classcap[ist - 1].size(); ++r)
1513 if (static_cast<double>(n[r]) > sn.classcap[ist - 1][r]) return out;
1514 const double S = st.nservers;
1515 if (S > 0.0 && std::isfinite(S) && static_cast<double>(stot) > S) return out;
1516
1517 // PAS/OI: the started counts are immaterial to the list encoding.
1518 if (sched == SchedStrategy::PAS || sched == SchedStrategy::OI)
1519 return from_marginal_core(sn, ist, n, phases);
1520
1521 // A Source generates rather than holds: its phase block IS the arrival
1522 // process, seeded in phase one for every class it serves, and the leading
1523 // column is the infinite population the reference marks with Inf.
1524 if (sched == SchedStrategy::EXT || st.nodetype == NodeType::Source) {
1525 // ONE BLOCK PER CLASS, INCLUDING A CLASS THE SOURCE DOES NOT GENERATE:
1526 // `phases` is the width vector, so a disabled arrival contributes one
1527 // always-zero column rather than nothing. That is the reference's row
1528 // (`[Inf 1 0 0 0 0 0]` for a Source generating the first of six
1529 // classes), and a narrower one cannot be decoded by anyone who built it
1530 // from `sn.phasessz` -- see `NetworkStruct::phasessz_of`.
1531 std::vector<std::vector<T>> srv;
1532 for (std::size_t r = 0; r < R; ++r) {
1533 if (phases[r] == 0) continue;
1534 std::vector<T> init(phases[r], num_traits<T>::from_int(0));
1535 // A NON-CARRIER MARK holds an ALL-ZERO column: its arrivals are
1536 // driven by the chain in the carrier's block, so seeding a token
1537 // here would claim a second chain the process does not have.
1538 if (r < sn.classes.size() && std::isinf(sn.classes[r].population) &&
1539 !sn.disabled[ist - 1][r] && sn.markidx_of(ist, r + 1) <= 1)
1540 init[0] = num_traits<T>::from_int(1);
1541 srv = cartesian(srv, std::vector<std::vector<T>>(1, init));
1542 }
1543 if (srv.empty()) srv.push_back(std::vector<T>());
1544 for (std::size_t i = 0; i < srv.size(); ++i) {
1545 std::vector<T> row(1, num_traits<T>::from_double(
1546 std::numeric_limits<double>::infinity()));
1547 row.insert(row.end(), srv[i].begin(), srv[i].end());
1548 out.push_back(row);
1549 }
1550 return out;
1551 }
1552
1553 /** The server block: `cnt[r]` jobs of class r, all of them in phase one. */
1554 const auto phase_one_block = [&](const std::vector<std::size_t>& cnt,
1555 bool* ok) -> std::vector<T> {
1556 std::vector<T> blk;
1557 *ok = true;
1558 for (std::size_t r = 0; r < R; ++r) {
1559 // No service process, yet jobs demanded there: unreachable. An
1560 // empty factor must annihilate the fold, not be absorbed by it.
1561 // The COLUMN is still emitted -- `phases` is the width vector and a
1562 // disabled class holds an always-zero column, as the reference does.
1563 if (sn.phases_of(ist, r + 1) == 0 && cnt[r] > 0) *ok = false;
1564 if (phases[r] == 0) continue;
1565 blk.push_back(num_traits<T>::from_int(static_cast<long>(cnt[r])));
1566 blk.insert(blk.end(), phases[r] - 1, num_traits<T>::from_int(0));
1567 }
1568 return blk;
1569 };
1570 std::size_t srvw = 0;
1571 for (std::size_t r = 0; r < R; ++r) srvw += phases[r];
1572
1573 if (state_detail::buffer_is_per_class_count(sched)) {
1574 // UNORDERED buffer: one waiting COUNT per class ahead of the server
1575 // block. Below the server count every job is in service and the buffer
1576 // is empty, which is the reference's own special case.
1577 const bool all_in_service = std::isfinite(S) && static_cast<double>(ntot) <= S;
1578 std::vector<std::size_t> insrv(R, 0), wait(R, 0);
1579 for (std::size_t r = 0; r < R; ++r) {
1580 insrv[r] = all_in_service ? n[r] : s[r];
1581 wait[r] = n[r] - insrv[r];
1582 }
1583 bool ok = false;
1584 const std::vector<T> blk = phase_one_block(insrv, &ok);
1585 if (!ok) return out;
1586 std::vector<T> row;
1587 for (std::size_t r = 0; r < R; ++r)
1588 row.push_back(num_traits<T>::from_int(static_cast<long>(wait[r])));
1589 row.insert(row.end(), blk.begin(), blk.end());
1590 out.push_back(row);
1591 return out;
1592 }
1593
1594 const bool paired = state_detail::buffer_is_tag_phase_pairs(sched);
1595 if (state_detail::buffer_is_class_tag(sched) || paired) {
1596 // ORDERED buffer: the waiting positions carry class tags, and under the
1597 // preempt family a [class, phase] PAIR per position. The empty buffer
1598 // keeps the width the decoder expects -- one column, or two when
1599 // paired, since an odd-width paired buffer is half a pair and
1600 // `to_marginal` would read every slot shifted.
1601 const std::size_t bw = paired ? 2u : 1u;
1602 if (ntot == 0) {
1603 std::vector<T> row(bw + srvw, num_traits<T>::from_int(0));
1604 out.push_back(row);
1605 return out;
1606 }
1607 std::vector<std::size_t> inbuf;
1608 for (std::size_t r = 0; r < R; ++r)
1609 for (std::size_t j = 0; j < n[r] - s[r]; ++j) inbuf.push_back(r + 1);
1610 // Descending, which is the lexicographic maximum the reference's
1611 // trailing unique/flip leaves standing.
1612 std::sort(inbuf.begin(), inbuf.end(),
1613 [](std::size_t a, std::size_t b) { return a > b; });
1614
1615 bool ok = false;
1616 const std::vector<T> blk = phase_one_block(s, &ok);
1617 if (!ok) return out;
1618
1619 std::vector<T> row;
1620 if (inbuf.empty()) {
1621 row.assign(bw, num_traits<T>::from_int(0));
1622 } else {
1623 for (std::size_t j = 0; j < inbuf.size(); ++j) {
1624 row.push_back(num_traits<T>::from_int(static_cast<long>(inbuf[j])));
1625 // A preempted job is recorded in its LAST service phase, as the
1626 // reference builds the lexicographic maximum.
1627 if (paired)
1628 row.push_back(num_traits<T>::from_int(
1629 static_cast<long>(phases[inbuf[j] - 1])));
1630 }
1631 }
1632 row.insert(row.end(), blk.begin(), blk.end());
1633 out.push_back(row);
1634 return out;
1635 }
1636
1637 // SHARED SERVER (INF/PS/DPS/GPS/LPS and the priority variants): no buffer,
1638 // and every job present is in a server, so the block holds n and not s.
1639 bool ok = false;
1640 const std::vector<T> blk = phase_one_block(n, &ok);
1641 if (!ok) return out;
1642 out.push_back(blk);
1643 return out;
1644}
1645
1646/**
1647 * `State.fromMarginalAndStarted` with the trailing local-variable block
1648 * appended, i.e. the counterpart of `from_marginal` for a prescribed service
1649 * split. A Join of an FJ-augmented struct carries its per-class counts and no
1650 * split, exactly as in `from_marginal`.
1651 */
1652template <class T>
1653std::vector<std::vector<T>> from_marginal_and_started(const NetworkStruct<T>& sn, std::size_t ist,
1654 const std::vector<std::size_t>& n,
1655 const std::vector<std::size_t>& s,
1656 const std::vector<std::size_t>& phases) {
1657 const std::size_t jnd0 = sn.node_of_station(ist);
1658 if (sn.isfjaugmented && jnd0 != 0 && sn.nodes[jnd0 - 1].nodetype == NodeType::Join) {
1659 std::vector<T> row(sn.nclasses, num_traits<T>::from_int(0));
1660 for (std::size_t r = 0; r < sn.nclasses && r < n.size(); ++r)
1661 row[r] = num_traits<T>::from_int(static_cast<long>(n[r]));
1662 return std::vector<std::vector<T>>(1, row);
1663 }
1664 return append_local_vars(sn, ist, from_marginal_and_started_core(sn, ist, n, s, phases), n,
1665 phases);
1666}
1667
1668
1669/**
1670 * Port of `State.fromMarginal` at its OWN signature: the reference indexes by
1671 * NODE, not by station, and derives the station internally. That distinction is
1672 * load-bearing, not cosmetic -- a Transition is stateful but is NOT a station,
1673 * so it carries no station index and the station-indexed overload can never
1674 * reach it. Routing every caller through here is what makes the Transition
1675 * branch below live rather than dead code.
1676 *
1677 * A node that is not stateful holds no jobs and contributes no local state.
1678 */
1679template <class T>
1680std::vector<std::vector<T>> from_marginal_node(const NetworkStruct<T>& sn, std::size_t ind,
1681 const std::vector<std::size_t>& n,
1682 const std::vector<std::size_t>& phases) {
1683 if (ind == 0 || ind > sn.nodes.size())
1684 throw InputError("from_marginal_node: node index " + std::to_string(ind) +
1685 " is out of range");
1686 const NodeDef& nd = sn.nodes[ind - 1];
1687 if (nd.station != 0) return from_marginal(sn, nd.station, n, phases);
1688 if (!nd.stateful) return std::vector<std::vector<T>>();
1689
1690 const std::size_t R = sn.nclasses;
1691 if (n.size() != R)
1692 throw InputError("from_marginal_node: n must have one entry per class");
1693
1694 if (nd.nodetype == NodeType::Transition) {
1695 // PER-MODE state, not per-class: [free servers | firing phases | fired].
1696 // The reference emits the all-idle row -- every mode's servers free, no
1697 // firing in progress -- and lets the event handlers walk from there, so
1698 // the marginal n plays no part. An infinite server count is clamped to
1699 // MaxInt because the row is a COUNT vector and Inf is not a count.
1700 const typename std::map<std::size_t, TransitionParam<T> >::const_iterator it =
1701 sn.transparam.find(ind);
1702 if (it == sn.transparam.end())
1703 throw InputError(
1704 "from_marginal_node: transition node has no TransitionParam; build it with "
1705 "add_transition");
1706 const TransitionParam<T>& tp = it->second;
1707 std::vector<T> row;
1708 for (std::size_t mm = 0; mm < tp.nmodes; ++mm) {
1709 const double sv = mm < tp.nmodeservers.size() ? tp.nmodeservers[mm] : 1.0;
1710 row.push_back(num_traits<T>::from_double(
1711 std::isfinite(sv) ? sv : static_cast<double>(GlobalConstants::MaxInt)));
1712 }
1713 std::size_t fph = 0;
1714 for (std::size_t mm = 0; mm < tp.nmodes; ++mm)
1715 fph += mm < tp.firingphases.size() && tp.firingphases[mm] > 0 ? tp.firingphases[mm] : 1;
1716 row.insert(row.end(), fph, num_traits<T>::from_int(0));
1717 row.insert(row.end(), tp.nmodes, num_traits<T>::from_int(0));
1718 return std::vector<std::vector<T> >{row};
1719 }
1720
1721 // A CACHE HOLDS ONE READING JOB, which is what makes the read a decision
1722 // rather than a queue: `spaceGeneratorNodes` caps every class there at 1 and
1723 // the node at 1 job in total. Without the cap the lattice admits a cache
1724 // holding two reads, and since `after_event_cache` fires a READ only on a
1725 // node holding exactly one job, that state is ABSORBING -- on an open model
1726 // the whole stationary mass ends up in it and every cache rate reads zero.
1727 //
1728 // A completing fetch is the one exception: it releases every merged
1729 // secondary request into the hit class in ONE immediate transition, so the
1730 // hit classes momentarily hold up to `max_pending_retrieval` extra jobs.
1731 if (nd.nodetype == NodeType::Cache) {
1732 const typename std::map<std::size_t, CacheParam<T> >::const_iterator cc =
1733 sn.nodeparam.find(ind);
1734 if (cc != sn.nodeparam.end()) {
1735 const std::size_t pend =
1736 cc->second.retrieval_capacity > 0 && cc->second.max_pending_retrieval > 0
1737 ? static_cast<std::size_t>(cc->second.max_pending_retrieval)
1738 : 0;
1739 std::size_t tot = 0;
1740 for (std::size_t r = 0; r < R; ++r) {
1741 bool is_hit = false;
1742 for (std::size_t u = 0; u < cc->second.hitclass.size(); ++u)
1743 if (cc->second.hitclass[u] == r + 1) is_hit = true;
1744 if (n[r] > (is_hit ? 1 + pend : 1)) return std::vector<std::vector<T> >();
1745 tot += n[r];
1746 }
1747 if (tot > 1 + pend) return std::vector<std::vector<T> >();
1748 }
1749 }
1750
1751 // Any other stateful non-station (a Cache, a stateful ClassSwitch): the
1752 // reference folds `spaceClosedSingle(1, n(r))` per class, i.e. one column
1753 // per class holding its count, with no phase and no buffer.
1754 std::vector<std::vector<T> > acc;
1755 for (std::size_t r = 0; r < R; ++r) {
1756 const std::vector<std::vector<T> > sr = space_closed_single<T>(1, n[r]);
1757 if (sr.empty() && n[r] > 0) return std::vector<std::vector<T> >();
1758 acc = cartesian(acc, sr);
1759 }
1760
1761 // A DISPATCHER CARRIES ITS POINTER IN THE STATE, for the same reason a cache
1762 // carries its contents: which link the next job takes is not derivable from
1763 // the marginal. RROBIN enumerates its outlinks and WRROBIN the positions of
1764 // its weighted cycle, exactly as `sub_routevars` does, and the columns sit
1765 // in CLASS ORDER right after the per-class counts, where `rr_var_slot` reads
1766 // them. Emitting the node one column narrower than `nvars_of` declares makes
1767 // every successor a state the space does not contain, so the node becomes
1768 // ABSORBING and the whole chain stalls with the jobs sitting in it.
1769 {
1770 std::vector<std::vector<T> > ptrsets(1, std::vector<T>());
1771 bool any = false;
1772 for (std::size_t r = 1; r <= R; ++r) {
1773 if (sn.rr_var_slot(ind, r) == 0) continue;
1774 any = true;
1775 std::vector<T> vals;
1776 if (sn.nodes[ind - 1].routing[r - 1] == RoutingStrategy::RROBIN) {
1777 const std::vector<std::size_t> ol = sn.rr_outlinks(ind, r);
1778 for (std::size_t u = 0; u < ol.size(); ++u)
1779 vals.push_back(num_traits<T>::from_int(static_cast<long>(ol[u])));
1780 } else {
1781 const std::vector<std::size_t> cy = sn.rr_weighted_outlinks(ind, r);
1782 for (std::size_t u = 0; u < cy.size(); ++u)
1783 vals.push_back(num_traits<T>::from_int(static_cast<long>(u + 1)));
1784 }
1785 if (vals.empty()) vals.push_back(num_traits<T>::from_int(0));
1786 std::vector<std::vector<T> > grown;
1787 for (std::size_t g = 0; g < ptrsets.size(); ++g)
1788 for (std::size_t v = 0; v < vals.size(); ++v) {
1789 std::vector<T> row = ptrsets[g];
1790 row.push_back(vals[v]);
1791 grown.push_back(row);
1792 }
1793 ptrsets.swap(grown);
1794 }
1795 if (any) acc = cartesian(acc, ptrsets);
1796 }
1797
1798 // A CACHE CARRIES ITS CONTENTS IN THE STATE, and they are not derivable
1799 // from any marginal: the row is [per-class counts | contents | occupancy],
1800 // exactly as `after_event_cache` slices it. Enumerating only the counts
1801 // left the contents region absent, so every READ landed on a state one
1802 // block wider than anything enumerated and the space came out EMPTY.
1803 //
1804 // A contents slot holds an ITEM INDEX and an item sits in at most one slot --
1805 // a cache holding two copies of one item is not a state the replacement
1806 // policies can reach or leave.
1807 //
1808 // THE ENUMERATED CACHE IS FULL, which is what `State.spaceCache` builds:
1809 // every ordered placement of `tcc` DISTINCT items, with no empty slot. Under
1810 // every replacement policy a cache that has once been filled never empties
1811 // again, so a partly empty configuration is transient and enumerating it
1812 // only spreads stationary mass over states the model leaves for good. It is
1813 // load-bearing beyond tidiness for a retrieval cache: block A may be set
1814 // only for an item that is NOT cached, and the count of such items is the
1815 // retrieval-system capacity `nitems - tcc`, which is a number only if the
1816 // cached set is full. A cache with more slots than items can never fill, so
1817 // the empty slot survives there.
1818 if (nd.nodetype == NodeType::Cache) {
1819 const typename std::map<std::size_t, CacheParam<T> >::const_iterator ci =
1820 sn.nodeparam.find(ind);
1821 if (ci == sn.nodeparam.end()) return acc;
1822 const CacheParam<T>& cp = ci->second;
1823 std::size_t tcc = 0;
1824 for (std::size_t u = 0; u < cp.itemcap.size(); ++u)
1825 if (cp.itemcap[u] > 0) tcc += static_cast<std::size_t>(cp.itemcap[u]);
1826
1827 const std::size_t first_item = tcc <= cp.nitems ? 1 : 0;
1828 std::vector<std::vector<T> > contents(1, std::vector<T>());
1829 for (std::size_t slot = 0; slot < tcc; ++slot) {
1830 std::vector<std::vector<T> > grown;
1831 for (std::size_t c = 0; c < contents.size(); ++c)
1832 for (std::size_t item = first_item; item <= cp.nitems; ++item) {
1833 bool dup = false;
1834 if (item > 0)
1835 for (std::size_t j = 0; j < contents[c].size(); ++j)
1836 if (num_traits<T>::to_double(contents[c][j]) ==
1837 static_cast<double>(item)) { dup = true; break; }
1838 if (dup) continue;
1839 std::vector<T> row = contents[c];
1840 row.push_back(num_traits<T>::from_int(static_cast<long>(item)));
1841 grown.push_back(row);
1842 }
1843 contents.swap(grown);
1844 }
1845 // Block A, the delayed-hit occupancy bitmap: one bit per item, set while
1846 // a retrieval for it is in flight.
1847 if (cp.retrieval_capacity > 0)
1848 for (std::size_t i = 0; i < cp.nitems; ++i) {
1849 std::vector<std::vector<T> > grown;
1850 for (std::size_t c = 0; c < contents.size(); ++c)
1851 for (int b = 0; b <= 1; ++b) {
1852 std::vector<T> row = contents[c];
1853 row.push_back(num_traits<T>::from_int(b));
1854 grown.push_back(row);
1855 }
1856 contents.swap(grown);
1857 }
1858 // Block B, the merged secondary requests: one count per retrieval class,
1859 // nonzero only where that class's item is in flight (block A is set) and
1860 // summing to at most the declared truncation level. `State.spaceCache`
1861 // enumerates the same compositions; -1 means the caller is walking a
1862 // sample path rather than enumerating, so no pending state is generated.
1863 if (cp.retrieval_capacity > 0) {
1864 std::vector<std::size_t> rcl, rci, rco;
1865 cache_retrieval_class_map(cp, rcl, rci, rco);
1866 if (!rcl.empty()) {
1867 const long maxpend =
1869 std::vector<std::vector<T> > grown;
1870 for (std::size_t c = 0; c < contents.size(); ++c) {
1871 // which slots may carry a count in THIS row: the ones whose
1872 // item is currently being fetched
1873 std::vector<std::size_t> active;
1874 for (std::size_t j = 0; j < rcl.size(); ++j)
1875 if (num_traits<T>::to_double(contents[c][tcc + rci[j] - 1]) != 0)
1876 active.push_back(j);
1877 std::vector<std::vector<long> > pend(1, std::vector<long>(rcl.size(), 0));
1878 for (std::size_t a = 0; a < active.size(); ++a) {
1879 std::vector<std::vector<long> > next;
1880 for (std::size_t q = 0; q < pend.size(); ++q) {
1881 long used = 0;
1882 for (std::size_t j = 0; j < rcl.size(); ++j) used += pend[q][j];
1883 for (long v = 0; v + used <= maxpend; ++v) {
1884 std::vector<long> row = pend[q];
1885 row[active[a]] = v;
1886 next.push_back(row);
1887 }
1888 }
1889 pend.swap(next);
1890 }
1891 for (std::size_t q = 0; q < pend.size(); ++q) {
1892 std::vector<T> row = contents[c];
1893 for (std::size_t j = 0; j < rcl.size(); ++j)
1894 row.push_back(num_traits<T>::from_int(pend[q][j]));
1895 grown.push_back(row);
1896 }
1897 }
1898 contents.swap(grown);
1899 }
1900 }
1901 std::vector<std::vector<T> > joint = cartesian(acc, contents);
1902 if (cp.retrieval_capacity <= 0) return joint;
1903
1904 // THE LOCALLY INVALID ROWS OF A RETRIEVAL CACHE, `spaceGeneratorNodes.m`
1905 // lines 206-280. The cartesian product above enumerates combinations the
1906 // dynamics can never reach, and they are not harmless: a row holding a
1907 // returning retrieval job BESIDE another job disables the READ (which
1908 // needs the node to hold exactly one) while leaving the departure
1909 // enabled, so the fetch bounces back to its queue without completing.
1910 // That inflated the retrieval flow by 20% on retrieval_simple while the
1911 // hit and miss shares, being ratios, stayed exactly right -- the kind of
1912 // defect only a flow identity catches.
1913 std::vector<bool> is_hit(R, false), is_miss(R, false);
1914 for (std::size_t u = 0; u < cp.hitclass.size(); ++u)
1915 if (cp.hitclass[u] >= 1 && cp.hitclass[u] <= R) is_hit[cp.hitclass[u] - 1] = true;
1916 for (std::size_t u = 0; u < cp.missclass.size(); ++u)
1917 if (cp.missclass[u] >= 1 && cp.missclass[u] <= R) is_miss[cp.missclass[u] - 1] = true;
1918 const long maxpend = cp.max_pending_retrieval > 0 ? cp.max_pending_retrieval : 0;
1919 std::vector<std::vector<T> > kept;
1920 for (std::size_t row = 0; row < joint.size(); ++row) {
1921 const std::vector<T>& st = joint[row];
1922 double tot = 0, other = 0, miss = 0;
1923 for (std::size_t r = 0; r < R; ++r) {
1924 const double v = num_traits<T>::to_double(st[r]);
1925 tot += v;
1926 if (is_miss[r]) miss += v;
1927 else if (!is_hit[r]) other += v;
1928 }
1929 // ONE JOB READS AT A TIME. The single exception is the state a
1930 // completing fetch lands in: the miss-class job that was the fetch,
1931 // together with the delayed hits it released in the same transition.
1932 if (!(tot <= 1 || (other == 0 && miss <= 1 && tot <= 1 + maxpend))) continue;
1933 bool ok = true;
1934 long nbits = 0;
1935 for (std::size_t i = 0; i < cp.nitems && ok; ++i) {
1936 if (num_traits<T>::to_double(st[R + tcc + i]) == 0) continue;
1937 ++nbits;
1938 // An item cannot be cached and in flight at the same time.
1939 for (std::size_t c = 0; c < tcc; ++c)
1940 if (static_cast<std::size_t>(num_traits<T>::to_double(st[R + c])) == i + 1)
1941 ok = false;
1942 }
1943 if (!ok || nbits > cp.retrieval_capacity) continue;
1944 kept.push_back(st);
1945 }
1946 return kept;
1947 }
1948 return acc;
1949}
1950
1951/**
1952 * Port of `State.fromMarginalAndStarted` at its OWN signature, which indexes by
1953 * NODE rather than by station, mirroring `from_marginal_node`.
1954 *
1955 * A Petri-net element is REFUSED BY NAME, as the reference refuses it: a
1956 * Transition's local state is per MODE, not per class, and a Place holds
1957 * tokens, so neither has a notion of a started job to prescribe. A node that is
1958 * not stateful holds no jobs and contributes no local state.
1959 */
1960template <class T>
1961std::vector<std::vector<T>> from_marginal_node_and_started(
1962 const NetworkStruct<T>& sn, std::size_t ind, const std::vector<std::size_t>& n,
1963 const std::vector<std::size_t>& s, const std::vector<std::size_t>& phases) {
1964 if (ind == 0 || ind > sn.nodes.size())
1965 throw InputError("from_marginal_node_and_started: node index " + std::to_string(ind) +
1966 " is out of range");
1967 const NodeDef& nd = sn.nodes[ind - 1];
1968 if (nd.nodetype == NodeType::Transition || nd.nodetype == NodeType::Place)
1969 throw UnsupportedError(
1970 "from_marginal_node_and_started cannot be used on Petri net elements");
1971 if (nd.station != 0) return from_marginal_and_started(sn, nd.station, n, s, phases);
1972 if (!nd.stateful) return std::vector<std::vector<T>>();
1973 // Every other stateful non-station carries no service split, so the
1974 // marginal builder answers it: a Cache holds its reading job and its
1975 // contents, a stateful ClassSwitch one count per class.
1976 return from_marginal_node(sn, ind, n, phases);
1977}
1978
1979/**
1980 * Port of `State.fromMarg`: the state space with a given TOTAL queue length.
1981 *
1982 * This is the class-summed counterpart of `from_marginal_node`. Where that one
1983 * fixes how many jobs of EACH class the node holds, this one fixes only how
1984 * many it holds ALTOGETHER and returns the union over every class split of
1985 * `ntot` the node can hold.
1986 *
1987 * A class DISABLED at the station has `classcap` 0 and is excluded from the
1988 * split enumeration up front rather than after the fact. Asking
1989 * `from_marginal_node` for a job of such a class yields an EMPTY local space,
1990 * and `space_closed_single(0,1)` returning no rows empties the whole cartesian
1991 * product, so the job would silently disappear -- the trap documented at
1992 * `space_closed_single` above.
1993 *
1994 * `ntot == 0` has the single empty split, which `from_marginal_node` answers
1995 * with the per-discipline empty state; the width is NOT re-derived here.
1996 *
1997 * The buffer is RIGHT-aligned, so sub-spaces of different width are padded on
1998 * the LEFT before they are stacked, exactly as the reference does for the
1999 * reply-block sub-spaces. Rows are then uniqued and reversed, which puts the
2000 * empty state first and the states with jobs in phase 1 earlier.
2001 *
2002 * The twin over BOTH totals is `from_marg_node_started` below.
2003 */
2004template <class T>
2005std::vector<std::vector<T>> from_marg_node(const NetworkStruct<T>& sn, std::size_t ind,
2006 std::size_t ntot,
2007 const std::vector<std::size_t>& phases) {
2008 if (ind == 0 || ind > sn.nodes.size())
2009 throw InputError("from_marg_node: node index " + std::to_string(ind) + " is out of range");
2010 const NodeDef& nd = sn.nodes[ind - 1];
2011 const std::size_t R = sn.nclasses;
2012
2013 // Per-class capacity of the node, unbounded when it is not a station.
2014 std::vector<double> ccap(R, std::numeric_limits<double>::infinity());
2015 if (nd.station != 0 && sn.classcap.size() >= nd.station) {
2016 const std::vector<double>& row = sn.classcap[nd.station - 1];
2017 for (std::size_t r = 0; r < R && r < row.size(); ++r) ccap[r] = row[r];
2018 }
2019
2020 std::vector<std::vector<int>> nset;
2021 if (ntot == 0) {
2022 nset.push_back(std::vector<int>(R, 0));
2023 } else {
2024 const std::vector<std::vector<int>> all =
2025 pfqn::multichoose_rows(static_cast<int>(R), static_cast<int>(ntot));
2026 for (std::size_t j = 0; j < all.size(); ++j) {
2027 bool ok = true;
2028 for (std::size_t r = 0; r < R && ok; ++r)
2029 if (static_cast<double>(all[j][r]) > ccap[r]) ok = false;
2030 if (ok) nset.push_back(all[j]);
2031 }
2032 }
2033
2034 std::vector<std::vector<T>> space;
2035 std::size_t maxw = 0;
2036 std::vector<std::vector<std::vector<T>>> subspaces;
2037 for (std::size_t j = 0; j < nset.size(); ++j) {
2038 std::vector<std::size_t> nj(R, 0);
2039 for (std::size_t r = 0; r < R; ++r) nj[r] = static_cast<std::size_t>(nset[j][r]);
2040 const std::vector<std::vector<T>> sj = from_marginal_node(sn, ind, nj, phases);
2041 if (sj.empty()) continue;
2042 for (std::size_t a = 0; a < sj.size(); ++a) maxw = std::max(maxw, sj[a].size());
2043 subspaces.push_back(sj);
2044 }
2045 for (std::size_t j = 0; j < subspaces.size(); ++j)
2046 for (std::size_t a = 0; a < subspaces[j].size(); ++a) {
2047 std::vector<T> row = subspaces[j][a];
2048 if (row.size() < maxw)
2049 row.insert(row.begin(), maxw - row.size(), num_traits<T>::from_int(0));
2050 space.push_back(row);
2051 }
2052 if (space.empty()) return space;
2053
2054 std::sort(space.begin(), space.end());
2055 space.erase(std::unique(space.begin(), space.end()), space.end());
2056 std::reverse(space.begin(), space.end());
2057 return space;
2058}
2059
2060namespace state_detail {
2061
2062/**
2063 * Port of `multichoosecon.m`: the ways to draw `S` units from the availability
2064 * vector `n`.
2065 *
2066 * Unlike `multichoose_rows`, the count drawn from category r is capped by
2067 * `n[r]`, so the enumeration never proposes a job of a class the station does
2068 * not hold -- which is what keeps `from_marg_node_started` from asking the
2069 * per-class builder for a state that cannot exist.
2070 */
2071inline std::vector<std::vector<std::size_t> > multichoosecon(const std::vector<std::size_t>& n,
2072 std::size_t S) {
2073 const std::size_t R = n.size();
2074 std::vector<std::vector<std::size_t> > out;
2075 if (R == 0) return out;
2076 if (S == 0) {
2077 out.push_back(std::vector<std::size_t>(R, 0));
2078 return out;
2079 }
2080 if (S == 1) {
2081 for (std::size_t i = 0; i < R; ++i)
2082 if (n[i] > 0) {
2083 std::vector<std::size_t> row(R, 0);
2084 row[i] = 1;
2085 out.push_back(row);
2086 }
2087 return out;
2088 }
2089 for (std::size_t i = 0; i < R; ++i) {
2090 if (n[i] == 0) continue;
2091 std::vector<std::size_t> n1 = n;
2092 --n1[i];
2093 const std::vector<std::vector<std::size_t> > tail = multichoosecon(n1, S - 1);
2094 for (std::size_t k = 0; k < tail.size(); ++k) {
2095 std::vector<std::size_t> row = tail[k];
2096 ++row[i];
2097 out.push_back(row);
2098 }
2099 }
2100 return out;
2101}
2102
2103} // namespace state_detail
2104
2105/**
2106 * Port of `State.fromMargAndStarted`: the states with a given TOTAL queue
2107 * length AND a given TOTAL number of started jobs.
2108 *
2109 * Where `from_marginal_node_and_started` takes one per-class occupancy and one
2110 * per-class started vector and builds ONE row, this takes only the two totals
2111 * and returns the union of that row over every pair consistent with them:
2112 * `sum(n) == ntot`, `sum(s) == stot`, and `s <= n` elementwise.
2113 *
2114 * The started counts are drawn with `multichoosecon` from the jobs actually
2115 * present rather than from `multichoose_rows` over all classes, so a split is
2116 * never proposed that puts more of a class in service than the station holds.
2117 * Classes disabled at the station are excluded through `classcap` for the
2118 * reason documented on `from_marg_node`: an empty local space is ABSORBED by
2119 * the cartesian product instead of annihilating it, so the job would silently
2120 * disappear rather than the state being rejected.
2121 *
2122 * Widths are unified on the LEFT and the rows uniqued and reversed, exactly as
2123 * `from_marg_node` does, since the buffer is right-aligned.
2124 */
2125template <class T>
2126std::vector<std::vector<T>> from_marg_node_started(const NetworkStruct<T>& sn, std::size_t ind,
2127 std::size_t ntot, std::size_t stot,
2128 const std::vector<std::size_t>& phases) {
2129 if (ind == 0 || ind > sn.nodes.size())
2130 throw InputError("from_marg_node_started: node index " + std::to_string(ind) +
2131 " is out of range");
2132 std::vector<std::vector<T>> space;
2133 if (stot > ntot) return space;
2134
2135 const NodeDef& nd = sn.nodes[ind - 1];
2136 const std::size_t R = sn.nclasses;
2137
2138 std::vector<double> ccap(R, std::numeric_limits<double>::infinity());
2139 if (nd.station != 0 && sn.classcap.size() >= nd.station) {
2140 const std::vector<double>& row = sn.classcap[nd.station - 1];
2141 for (std::size_t r = 0; r < R && r < row.size(); ++r) ccap[r] = row[r];
2142 }
2143
2144 std::vector<std::vector<int>> nset;
2145 if (ntot == 0) {
2146 nset.push_back(std::vector<int>(R, 0));
2147 } else {
2148 const std::vector<std::vector<int>> all =
2149 pfqn::multichoose_rows(static_cast<int>(R), static_cast<int>(ntot));
2150 for (std::size_t j = 0; j < all.size(); ++j) {
2151 bool ok = true;
2152 for (std::size_t r = 0; r < R && ok; ++r)
2153 if (static_cast<double>(all[j][r]) > ccap[r]) ok = false;
2154 if (ok) nset.push_back(all[j]);
2155 }
2156 }
2157
2158 std::size_t maxw = 0;
2159 std::vector<std::vector<std::vector<T>>> subspaces;
2160 for (std::size_t j = 0; j < nset.size(); ++j) {
2161 std::vector<std::size_t> nj(R, 0);
2162 for (std::size_t r = 0; r < R; ++r) nj[r] = static_cast<std::size_t>(nset[j][r]);
2163 const std::vector<std::vector<std::size_t> > sset =
2164 state_detail::multichoosecon(nj, stot);
2165 for (std::size_t k = 0; k < sset.size(); ++k) {
2166 const std::vector<std::vector<T>> sjk =
2167 from_marginal_node_and_started(sn, ind, nj, sset[k], phases);
2168 if (sjk.empty()) continue;
2169 for (std::size_t a = 0; a < sjk.size(); ++a) maxw = std::max(maxw, sjk[a].size());
2170 subspaces.push_back(sjk);
2171 }
2172 }
2173 for (std::size_t j = 0; j < subspaces.size(); ++j)
2174 for (std::size_t a = 0; a < subspaces[j].size(); ++a) {
2175 std::vector<T> row = subspaces[j][a];
2176 if (row.size() < maxw)
2177 row.insert(row.begin(), maxw - row.size(), num_traits<T>::from_int(0));
2178 space.push_back(row);
2179 }
2180 if (space.empty()) return space;
2181
2182 std::sort(space.begin(), space.end());
2183 space.erase(std::unique(space.begin(), space.end()), space.end());
2184 std::reverse(space.begin(), space.end());
2185 return space;
2186}
2187
2188/**
2189 * The FIRST row `from_marginal_node` emits, BUILT rather than enumerated.
2190 *
2191 * Every caller that seeds a sample path takes row 0 and discards the rest, and
2192 * for a Cache the rest is the ordered placement of distinct items into its
2193 * slots -- (nitems+1)*nitems*(nitems-1)*... rows. Materializing that to read one
2194 * row's contents is what took SolverSSA on tut06_cache_lru_zipf (1000 items, 50
2195 * slots) to 12 GB and an OOM kill of the whole host; the peak is invariant in
2196 * `samples` precisely because the walk itself is lazy and only the SEED costs
2197 * this. A simulator must not pay an exact solver's enumeration to start.
2198 *
2199 * Row 0 holds items 1..tcc in slot order, by construction and not by choice:
2200 * `cartesian` emits `a[0] ++ b[0]`, the contents loop offers the lowest
2201 * admissible item first at every slot and its duplicate test rejects the ones
2202 * already placed, and the delayed-hit occupancy bits start clear. So the row is
2203 * the per-class counts, the first `tcc` items, then zeros -- bit for bit what
2204 * the enumeration returned, which is what lets the CTMC find its declared
2205 * initial state in the space. A cache with more slots than items cannot fill
2206 * and starts empty, as the enumeration also has it. Every other node kind
2207 * delegates, its first row already being cheap.
2208 *
2209 * @return false when the node admits no state at all, as an empty return does
2210 */
2211template <class T>
2212bool from_marginal_node_first(const NetworkStruct<T>& sn, std::size_t ind,
2213 const std::vector<std::size_t>& n,
2214 const std::vector<std::size_t>& phases, std::vector<T>& out) {
2215 if (ind == 0 || ind > sn.nodes.size())
2216 throw InputError("from_marginal_node_first: node index " + std::to_string(ind) +
2217 " is out of range");
2218 const NodeDef& nd = sn.nodes[ind - 1];
2219 if (nd.station != 0 || !nd.stateful || nd.nodetype != NodeType::Cache) {
2220 const std::vector<std::vector<T> > rows = from_marginal_node(sn, ind, n, phases);
2221 if (rows.empty()) return false;
2222 out = rows[0];
2223 return true;
2224 }
2225
2226 const std::size_t R = sn.nclasses;
2227 if (n.size() != R)
2228 throw InputError("from_marginal_node_first: n must have one entry per class");
2229 out.clear();
2230 for (std::size_t r = 0; r < R; ++r) {
2231 const std::vector<std::vector<T> > sr = space_closed_single<T>(1, n[r]);
2232 // an absent placement drops the class's column, exactly as the fold does
2233 if (sr.empty()) {
2234 if (n[r] > 0) return false;
2235 continue;
2236 }
2237 out.insert(out.end(), sr[0].begin(), sr[0].end());
2238 }
2239
2240 const typename std::map<std::size_t, CacheParam<T> >::const_iterator ci =
2241 sn.nodeparam.find(ind);
2242 if (ci == sn.nodeparam.end()) return true;
2243 const CacheParam<T>& cp = ci->second;
2244 std::size_t tcc = 0;
2245 for (std::size_t u = 0; u < cp.itemcap.size(); ++u)
2246 if (cp.itemcap[u] > 0) tcc += static_cast<std::size_t>(cp.itemcap[u]);
2247 for (std::size_t slot = 0; slot < tcc; ++slot)
2248 out.push_back(num_traits<T>::from_int(
2249 tcc <= cp.nitems ? static_cast<long>(slot + 1) : 0L));
2250 if (cp.retrieval_capacity > 0) {
2251 out.insert(out.end(), cp.nitems, num_traits<T>::from_int(0));
2252 std::vector<std::size_t> rcl, rci, rco;
2253 cache_retrieval_class_map(cp, rcl, rci, rco);
2254 out.insert(out.end(), rcl.size(), num_traits<T>::from_int(0));
2255 }
2256 return true;
2257}
2258
2259/**
2260 * Port of `State.spaceClosedMulti`: how N[r] class-r jobs distribute over M
2261 * stateful nodes, for every class, as the cartesian fold of the per-class
2262 * placements. Column block r holds class r's counts, M columns wide.
2263 */
2264template <class T>
2265std::vector<std::vector<T>> space_closed_multi(
2266 std::size_t M, const std::vector<std::size_t>& N,
2267 const std::vector<std::vector<long>>& caps = std::vector<std::vector<long>>()) {
2268 std::vector<std::vector<T>> ss;
2269 for (std::size_t r = 0; r < N.size(); ++r) {
2270 const std::vector<std::vector<T>> sr =
2271 r < caps.size() && !caps[r].empty() ? space_closed_single_capped<T>(M, N[r], caps[r])
2272 : space_closed_single<T>(M, N[r]);
2273 if (sr.empty()) return std::vector<std::vector<T>>();
2274 ss = r == 0 ? sr : cartesian(ss, sr);
2275 }
2276 return ss;
2277}
2278
2279/**
2280 * Port of `State.spaceClosedMultiCS`: the same, but a CHAIN's population is
2281 * shared among its classes, so the split between them is itself enumerated.
2282 *
2283 * Class switching moves a job between classes of one chain, so only the CHAIN
2284 * total is invariant. Enumerating over per-class populations alone would fix a
2285 * split the model does not fix, and drop every state reachable by a switch.
2286 *
2287 * @param M number of stateful nodes (sources excluded by the caller)
2288 * @param N per-class populations
2289 * @param chains chains[c][r] true when class r belongs to chain c
2290 */
2291template <class T>
2292std::vector<std::vector<T>> space_closed_multi_cs(
2293 std::size_t M, const std::vector<std::size_t>& N,
2294 const std::vector<std::vector<bool>>& chains,
2295 const std::vector<std::vector<long>>& caps = std::vector<std::vector<long>>()) {
2296 const std::size_t C = chains.size();
2297 const std::size_t R = N.size();
2298 // Per chain, the ways its total splits across the classes it contains.
2299 std::vector<std::vector<std::vector<int>>> chainInitPos(C);
2300 std::vector<std::size_t> inchain_sz(C, 0);
2301 for (std::size_t c = 0; c < C; ++c) {
2302 std::size_t tot = 0, k = 0;
2303 for (std::size_t r = 0; r < R; ++r)
2304 if (chains[c][r]) { tot += N[r]; ++k; }
2305 inchain_sz[c] = k;
2306 chainInitPos[c] = k == 0 ? std::vector<std::vector<int>>{std::vector<int>()}
2307 : pfqn::multichoose_rows(static_cast<int>(k), static_cast<int>(tot));
2308 }
2309 std::vector<std::vector<T>> ss;
2310 std::vector<std::size_t> v(C, 0);
2311 for (;;) {
2312 std::vector<std::size_t> subN;
2313 subN.reserve(R);
2314 for (std::size_t c = 0; c < C; ++c) {
2315 const std::vector<int>& row = chainInitPos[c][v[c]];
2316 for (std::size_t j = 0; j < row.size(); ++j)
2317 subN.push_back(static_cast<std::size_t>(row[j]));
2318 }
2319 // THE SPLIT IS SCATTERED BACK ONTO GLOBAL CLASS POSITIONS before the
2320 // per-slot caps are applied: `subN` is built chain by chain, so its
2321 // order is the concatenation of the chains, not the class order `caps`
2322 // is indexed by.
2323 std::vector<std::size_t> subNg(R, 0);
2324 {
2325 std::size_t k = 0;
2326 for (std::size_t c = 0; c < C; ++c)
2327 for (std::size_t r = 0; r < R; ++r)
2328 if (chains[c][r]) subNg[r] = subN[k++];
2329 }
2330 const std::vector<std::vector<T>> blk =
2331 caps.empty() ? space_closed_multi<T>(M, subN)
2332 : space_closed_multi<T>(M, subNg, caps);
2333 ss.insert(ss.end(), blk.begin(), blk.end());
2334 // odometer over the per-chain splits
2335 std::size_t c = 0;
2336 for (; c < C; ++c) {
2337 if (++v[c] < chainInitPos[c].size()) break;
2338 v[c] = 0;
2339 }
2340 if (c == C) break;
2341 }
2342 return ss;
2343}
2344
2345/** One network state: the per-stateful-node local rows it is composed of. */
2346template <class T>
2347struct NetState {
2348 std::vector<std::vector<T>> local; ///< local[isf] is that node's state row
2349};
2350
2351/**
2352 * Port of `State.initialOccupancy`: the class-r jobs node `ind` holds in the
2353 * DECLARED initial state, or 0 when there is none.
2354 *
2355 * Only a Place answers nonzero. Every other node type encodes its local state
2356 * differently, so column r there is not an occupancy -- and a station whose
2357 * visit ratio is zero is genuinely never entered, while a Place with no input
2358 * arc is a TRANSIENT state of the chain rather than an absent one.
2359 */
2360template <class T>
2361std::size_t state_initial_occupancy(const NetworkStruct<T>& sn, std::size_t ind, std::size_t r) {
2362 if (ind == 0 || ind > sn.nodes.size()) return 0;
2363 if (sn.nodes[ind - 1].nodetype != NodeType::Place) return 0;
2364 const typename std::map<std::size_t, std::vector<T>>::const_iterator it =
2365 sn.initmarking.find(ind);
2366 if (it == sn.initmarking.end() || r >= it->second.size()) return 0;
2367 const double v = num_traits<T>::to_double(it->second[r]);
2368 return v > 0 ? static_cast<std::size_t>(v) : 0;
2369}
2370
2371/**
2372 * Port of the `capacityc` table of `State.spaceGeneratorNodes`: the largest
2373 * class-r marginal node `ind` may hold in the enumerated space.
2374 *
2375 * WITHOUT THIS TABLE THE LATTICE IS PLACED BLIND. `space_closed_multi_cs`
2376 * spreads a class's population over EVERY stateful node, including the ones the
2377 * class never visits, so a mixed model enumerates open jobs at a closed-only
2378 * station and closed jobs at an open-only one. Those states are unreachable, but
2379 * they are not free: they enlarge the generator, and on `mqn_multiserver_fcfs`
2380 * (1904 states against the reference's 1304) they moved the Source's departure
2381 * rate from 0.24763 to 0.26040 -- an arrival rate the model never offers.
2382 *
2383 * The bound is the reference's, in its order: a class that does not visit the
2384 * node is capped at its DECLARED initial occupancy (nonzero only for an SPN
2385 * Place, which may hold tokens at time zero without ever being re-entered), a
2386 * disabled service at 0, an open class at its cutoff and a closed one at its
2387 * chain population, both cut down by the station's per-class buffer and then by
2388 * any finite-capacity region the station belongs to.
2389 *
2390 * @param sn the network struct
2391 * @param cutoff per-class population bound for open classes
2392 * @param cutoff_mat optional (nstations x nclasses) bound, the reference's
2393 * matrix `options.cutoff`; overrides `cutoff` per station
2394 * @return (nnodes x nclasses) capacities, 0-based in both indices
2395 */
2396template <class T>
2397std::vector<std::vector<std::size_t>> space_capacity_c(
2398 const NetworkStruct<T>& sn, const std::vector<std::size_t>& cutoff,
2399 const std::vector<std::vector<std::size_t>>& cutoff_mat =
2400 std::vector<std::vector<std::size_t>>()) {
2401 const std::size_t R = sn.nclasses, N = sn.nodes.size();
2402 std::vector<std::vector<std::size_t>> cap(N, std::vector<std::size_t>(R, 0));
2403 const std::vector<double> njobs = sn.njobs();
2404
2405 // maxPending mirrors `State.spaceGeneratorNodes`: a completing fetch
2406 // releases every merged secondary request in one immediate transition.
2407 std::size_t maxpend = 0;
2408 for (std::size_t r = 0; r < R; ++r)
2409 if (!std::isfinite(njobs[r])) maxpend = std::max(maxpend, cutoff[r]);
2410 if (maxpend > 0) --maxpend;
2411
2412 for (std::size_t ind = 1; ind <= N; ++ind) {
2413 const NodeDef& nd = sn.nodes[ind - 1];
2414 if (!nd.stateful) continue;
2415 const std::size_t ist = nd.station;
2416 const std::size_t isf = sn.stateful_index(ind);
2417 if (ist != 0 && nd.nodetype != NodeType::Source) {
2418 for (std::size_t r = 0; r < R; ++r) {
2419 std::size_t c = R; // r's chain, or R when the struct carries none
2420 for (std::size_t cc = 0; cc < sn.chains.size(); ++cc)
2421 if (r < sn.chains[cc].size() && sn.chains[cc][r]) { c = cc; break; }
2422 const bool novisit = c < sn.visits.size() && sn.visits[c].rows() != 0 &&
2423 isf != 0 && isf - 1 < sn.visits[c].rows() &&
2424 num_traits<T>::to_double(sn.visits[c](isf - 1, r)) == 0;
2425 if (novisit) {
2426 cap[ind - 1][r] = state_initial_occupancy(sn, ind, r);
2427 continue;
2428 }
2429 if (nd.nodetype != NodeType::Place && ist - 1 < sn.disabled.size() &&
2430 r < sn.disabled[ist - 1].size() && sn.disabled[ist - 1][r]) {
2431 cap[ind - 1][r] = 0;
2432 continue;
2433 }
2434 double b;
2435 if (!std::isfinite(njobs[r])) {
2436 // `capacityc(ind,r) = min(cutoff(ist,r), classcap(ist,r))`:
2437 // the reference's cutoff is a (station x class) MATRIX
2438 // wherever a model needs a different truncation per station,
2439 // and only the uniform case collapses to one number.
2440 b = static_cast<double>(
2441 (!cutoff_mat.empty() && ist - 1 < cutoff_mat.size() &&
2442 r < cutoff_mat[ist - 1].size())
2443 ? cutoff_mat[ist - 1][r]
2444 : cutoff[r]);
2445 } else {
2446 b = 0.0; // the whole CHAIN's population may sit here
2447 for (std::size_t k = 0; k < R; ++k)
2448 if (c < sn.chains.size() && k < sn.chains[c].size() && sn.chains[c][k] &&
2449 std::isfinite(njobs[k]))
2450 b += njobs[k];
2451 }
2452 if (ist - 1 < sn.classcap.size() && r < sn.classcap[ist - 1].size())
2453 b = std::min(b, sn.classcap[ist - 1][r]);
2454 if (b > 0)
2455 for (std::size_t f = 0; f < sn.regions.size(); ++f) {
2456 const typename NetworkStruct<T>::Region& rg = sn.regions[f];
2457 if (ist - 1 >= rg.cap.size()) continue;
2458 if (r < rg.cap[ist - 1].size() && rg.cap[ist - 1][r] >= 0)
2459 b = std::min(b, rg.cap[ist - 1][r]);
2460 if (R < rg.cap[ist - 1].size() && rg.cap[ist - 1][R] >= 0)
2461 b = std::min(b, rg.cap[ist - 1][R]);
2462 }
2463 if (!(b > 0)) b = 0;
2464 cap[ind - 1][r] = std::isfinite(b) ? static_cast<std::size_t>(b)
2465 : static_cast<std::size_t>(-1);
2466 }
2467 continue;
2468 }
2469 switch (nd.nodetype) {
2470 case NodeType::Cache: {
2471 for (std::size_t r = 0; r < R; ++r) cap[ind - 1][r] = 1;
2472 const typename std::map<std::size_t, CacheParam<T>>::const_iterator ci =
2473 sn.nodeparam.find(ind);
2474 if (ci != sn.nodeparam.end() && ci->second.retrieval_capacity > 0)
2475 for (std::size_t r = 0; r < R; ++r)
2476 for (std::size_t k = 0; k < ci->second.hitclass.size(); ++k)
2477 if (ci->second.hitclass[k] == r + 1) cap[ind - 1][r] = 1 + maxpend;
2478 break;
2479 }
2480 case NodeType::Router:
2481 for (std::size_t r = 0; r < R; ++r) {
2482 std::size_t c = R;
2483 for (std::size_t cc = 0; cc < sn.chains.size(); ++cc)
2484 if (r < sn.chains[cc].size() && sn.chains[cc][r]) { c = cc; break; }
2485 cap[ind - 1][r] =
2486 (c < sn.nodevisits.size() && sn.nodevisits[c].rows() != 0 &&
2487 ind - 1 < sn.nodevisits[c].rows() &&
2488 num_traits<T>::to_double(sn.nodevisits[c](ind - 1, r)) > 0)
2489 ? 1
2490 : 0;
2491 }
2492 break;
2493 default:
2494 // A Transition holds no class-indexed job, and a Source is an
2495 // infinite reservoir the lattice never places into.
2496 for (std::size_t r = 0; r < R; ++r)
2497 cap[ind - 1][r] = static_cast<std::size_t>(-1);
2498 break;
2499 }
2500 }
2501 return cap;
2502}
2503
2504/**
2505 * Port of `State.spaceGenerator`: every network state, reachable or not.
2506 *
2507 * The population lattice is walked with `space_closed_multi_cs`, and each
2508 * lattice row is expanded per node through `from_marginal`; the network states
2509 * are the cartesian product of those local spaces. An open class has no finite
2510 * population, so `cutoff` bounds it -- WITHOUT a cutoff the lattice is infinite
2511 * and the reference errors rather than truncating silently.
2512 *
2513 * @param sn the network struct
2514 * @param cutoff per-class population bound for open classes
2515 * @param maxst refuse beyond this many states (the reference's ctmc_max_states)
2516 * @param cutoff_mat optional (nstations x nclasses) bound, the reference's
2517 * matrix `options.cutoff`
2518 */
2519template <class T>
2520std::vector<NetState<T>> space_generator(
2521 const NetworkStruct<T>& sn, const std::vector<std::size_t>& cutoff,
2522 std::size_t maxst = 3000000,
2523 const std::vector<std::vector<std::size_t>>& cutoff_mat =
2524 std::vector<std::vector<std::size_t>>()) {
2525 const std::size_t R = sn.nclasses;
2526 if (cutoff.size() != R)
2527 throw InputError("space_generator: cutoff must have one entry per class");
2528
2529 // Closed classes carry their own population; open ones are bounded by the
2530 // cutoff. A missing cutoff on an open class is an error in the reference,
2531 // because a silently truncated lattice yields a state space that looks
2532 // complete and is not.
2533 // `njobs()` is a population COUNT, always a plain double; it is not carried
2534 // in T, so reading it as a vector<T> fails to compile on every backend but
2535 // double -- which is how this survived until SolverCTMC was instantiated at
2536 // higher precision.
2537 const std::vector<double> njobs = sn.njobs();
2538 std::vector<std::size_t> Np(R, 0);
2539 for (std::size_t r = 0; r < R; ++r) {
2540 const double nj = njobs[r];
2541 if (std::isfinite(nj)) {
2542 Np[r] = static_cast<std::size_t>(nj);
2543 } else {
2544 if (cutoff[r] == 0)
2545 throw InputError(
2546 "space_generator: class " + std::to_string(r) +
2547 " is open, so its population is unbounded; supply a cutoff for it");
2548 Np[r] = cutoff[r];
2549 }
2550 }
2551
2552 // The per-node bound the reference places the lattice under. An open class's
2553 // lattice height is then the LARGEST capacity any node grants it, not the raw
2554 // cutoff: levels above that produce only rows every node refuses.
2555 const std::vector<std::vector<std::size_t>> capc = space_capacity_c(sn, cutoff, cutoff_mat);
2556 const std::size_t unbounded = static_cast<std::size_t>(-1);
2557 for (std::size_t r = 0; r < R; ++r) {
2558 if (std::isfinite(njobs[r])) continue;
2559 std::size_t hi = 0;
2560 bool any_unbounded = false;
2561 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
2562 if (!sn.nodes[ind - 1].stateful || sn.nodes[ind - 1].nodetype == NodeType::Source)
2563 continue;
2564 if (capc[ind - 1][r] == unbounded) any_unbounded = true;
2565 else hi = std::max(hi, capc[ind - 1][r]);
2566 }
2567 if (!any_unbounded) Np[r] = std::min(Np[r], hi);
2568 }
2569
2570 std::vector<std::vector<bool>> chains = sn.chains;
2571 if (chains.empty()) { // no class switching: each class is its own chain
2572 chains.assign(R, std::vector<bool>(R, false));
2573 for (std::size_t r = 0; r < R; ++r) chains[r][r] = true;
2574 }
2575
2576 // The lattice is over the STATEFUL NODES, not the stations: a Transition or
2577 // a Cache is stateful without being a station, and iterating stations drops
2578 // its local block entirely. Sources hold no jobs, so they take no column.
2579 const std::vector<std::size_t>& sfn = sn.stateful_nodes;
2580 const std::size_t NF = sfn.size();
2581 std::vector<std::size_t> lat_col(NF, static_cast<std::size_t>(-1));
2582 std::size_t Mp = 0;
2583 for (std::size_t k = 0; k < NF; ++k)
2584 if (sn.nodes[sfn[k] - 1].nodetype != NodeType::Source) lat_col[k] = Mp++;
2585
2586 // The population LATTICE, not just its top row. An open class has no fixed
2587 // population, so every level from 0 to its cutoff is a distinct set of
2588 // states; only a CLOSED class is pinned to its own N. Enumerating the top
2589 // row alone yields a chain that can never empty -- for an M/M/1/K it left
2590 // exactly one state, and a one-state generator is trivially valid, which is
2591 // why the closed-model test could not detect this.
2592 std::vector<bool> is_open(R, false);
2593 for (std::size_t r = 0; r < R; ++r)
2594 is_open[r] = !std::isfinite(njobs[r]);
2595
2596 // THE LATTICE IS CAPACITY-BOUND, not just the per-node local rows. `capc`
2597 // already zeroes a (node, class) pair the class never visits, and the
2598 // composition below drops the rows that breach it -- but it drops them ONE
2599 // AT A TIME, after `space_closed_multi_cs` has spread every class over every
2600 // slot. On the class-switching chain Source->Q1(A)->Q2(B)->Q3(C) that is
2601 // (cutoff+1)^(3*3) candidates for (cutoff+1)^3 reachable states, and the
2602 // rejection is not free: each candidate costs a `from_marginal`.
2603 // `lat_caps[class][slot]` is `capc` in the SLOT order the lattice row uses.
2604 std::vector<std::vector<long>> lat_caps(R, std::vector<long>(Mp, -1));
2605 {
2606 bool ok = Mp > 0;
2607 for (std::size_t k = 0; k < NF && ok; ++k) {
2608 if (lat_col[k] == static_cast<std::size_t>(-1)) continue;
2609 const std::size_t ind = sfn[k];
2610 for (std::size_t r = 0; r < R; ++r) {
2611 const std::size_t cap = capc[ind - 1][r];
2612 lat_caps[r][lat_col[k]] =
2613 cap == unbounded ? -1 : static_cast<long>(cap);
2614 }
2615 }
2616 if (!ok) lat_caps.clear();
2617 }
2618 std::vector<std::vector<T>> pos;
2619 std::vector<std::size_t> nlev(R, 0);
2620 for (;;) {
2621 bool admissible = true;
2622 for (std::size_t r = 0; r < R; ++r)
2623 if (!is_open[r] && nlev[r] != Np[r]) { admissible = false; break; }
2624 if (admissible) {
2625 const std::vector<std::vector<T>> part =
2626 space_closed_multi_cs<T>(Mp, nlev, chains, lat_caps);
2627 pos.insert(pos.end(), part.begin(), part.end());
2628 if (pos.size() > maxst)
2629 throw UnsupportedError(
2630 "space_generator: the population lattice exceeds the cap of " +
2631 std::to_string(maxst) + " states; raise it or use another solver");
2632 }
2633 // Mixed-radix increment over the per-class levels, the reference's pprod.
2634 std::size_t r = 0;
2635 for (; r < R; ++r) {
2636 if (nlev[r] < Np[r]) { ++nlev[r]; break; }
2637 nlev[r] = 0;
2638 }
2639 if (r == R) break;
2640 }
2641 // Distinct levels can yield the same lattice row when a class is absent
2642 // from it, so drop the duplicates the reference removes with `unique`.
2643 std::sort(pos.begin(), pos.end());
2644 pos.erase(std::unique(pos.begin(), pos.end()), pos.end());
2645 // Per node and per lattice row, the local rows -- collected BEFORE any
2646 // composition because their width is not uniform. A marginal of 3 jobs at
2647 // an FCFS station needs two buffer columns where a marginal of 2 needs one,
2648 // and the reference unifies the two by RIGHT-ALIGNING the narrower row into
2649 // the widest (`fromMarginalBounds`), padding zeros on the left. Composing
2650 // before padding leaves an arrival unable to match its own successor,
2651 // because the successor is a wider vector and no index lookup can find it.
2652 std::vector<std::vector<std::vector<std::vector<T>>>> allper(pos.size());
2653 std::vector<bool> row_ok(pos.size(), true);
2654 std::vector<std::size_t> maxw(NF, 0);
2655 std::vector<NetState<T>> out;
2656 for (std::size_t j = 0; j < pos.size(); ++j) {
2657 std::vector<std::vector<std::vector<T>>> per(NF);
2658 bool ok = true;
2659 for (std::size_t k = 0; k < NF && ok; ++k) {
2660 const std::size_t ind = sfn[k];
2661 const std::size_t ist = sn.nodes[ind - 1].station;
2662 std::vector<std::size_t> ph(R, 1), nmarg(R, 0);
2663 // phases_of takes a 1-BASED class index, as elsewhere in NetworkStruct.
2664 if (ist != 0)
2665 for (std::size_t r = 0; r < R; ++r) ph[r] = sn.phasessz_of(ist, r + 1);
2666 if (lat_col[k] == static_cast<std::size_t>(-1)) {
2667 // A Source is an infinite reservoir: one job per class in
2668 // service, no marginal of its own -- but ONLY for the classes it
2669 // actually generates. A class the Source does not generate has a
2670 // disabled arrival, so phases_of is 0, and demanding one job of
2671 // it makes space_closed_single(0, 1) return nothing: the Source
2672 // yields no local row and the WHOLE state space comes out empty.
2673 // Invisible on a single-class open model, because its one class
2674 // is generated; a Source-Cache-Sink model, whose Hit and Miss
2675 // classes exist only downstream, hits it immediately.
2676 for (std::size_t r = 0; r < R; ++r)
2677 if (!sn.disabled[ist - 1][r]) nmarg[r] = 1;
2678 } else {
2679 for (std::size_t r = 0; r < R; ++r)
2680 nmarg[r] = static_cast<std::size_t>(
2681 num_traits<T>::to_double(pos[j][r * Mp + lat_col[k]]));
2682 // `any(stateMarg_i > capacityc(ind,:))` of the reference: the node
2683 // yields NO row, so the whole lattice row is dropped by the
2684 // cartesian product below.
2685 for (std::size_t r = 0; r < R && ok; ++r)
2686 if (capc[ind - 1][r] != static_cast<std::size_t>(-1) &&
2687 nmarg[r] > capc[ind - 1][r])
2688 ok = false;
2689 if (!ok) break;
2690 }
2691 per[k] = from_marginal_node(sn, ind, nmarg, ph);
2692 if (per[k].empty()) ok = false;
2693 for (std::size_t b = 0; b < per[k].size(); ++b)
2694 maxw[k] = std::max(maxw[k], per[k][b].size());
2695 }
2696 row_ok[j] = ok;
2697 allper[j].swap(per);
2698 }
2699
2700 for (std::size_t j = 0; j < pos.size(); ++j) {
2701 if (!row_ok[j]) continue;
2702 std::vector<std::vector<std::vector<T>>>& per = allper[j];
2703 // Right-align every row into the node's widest, as the reference does.
2704 for (std::size_t k = 0; k < NF; ++k)
2705 for (std::size_t b = 0; b < per[k].size(); ++b)
2706 if (per[k][b].size() < maxw[k])
2707 per[k][b].insert(per[k][b].begin(), maxw[k] - per[k][b].size(),
2709 // Cartesian product across nodes, carrying the per-node rows.
2710 std::vector<NetState<T>> acc(1);
2711 for (std::size_t i = 0; i < NF; ++i) {
2712 std::vector<NetState<T>> next;
2713 for (std::size_t a = 0; a < acc.size(); ++a)
2714 for (std::size_t b = 0; b < per[i].size(); ++b) {
2715 NetState<T> ns = acc[a];
2716 ns.local.push_back(per[i][b]);
2717 next.push_back(ns);
2718 }
2719 acc.swap(next);
2720 if (out.size() + acc.size() > maxst)
2721 throw UnsupportedError(
2722 "space_generator: the state space exceeds the cap of " +
2723 std::to_string(maxst) + " states; raise it or use another solver");
2724 }
2725 out.insert(out.end(), acc.begin(), acc.end());
2726 }
2727 // The same network state can be produced by two lattice rows once the
2728 // widths are unified, so drop duplicates as the reference's `unique` does.
2729 std::vector<std::vector<std::vector<T>>> seen;
2730 std::vector<NetState<T>> uniq;
2731 for (std::size_t i = 0; i < out.size(); ++i) {
2732 bool dup = false;
2733 for (std::size_t p = 0; p < seen.size() && !dup; ++p)
2734 if (seen[p] == out[i].local) dup = true;
2735 if (dup) continue;
2736 seen.push_back(out[i].local);
2737 uniq.push_back(out[i]);
2738 }
2739 return uniq;
2740}
2741
2742} // namespace qn
2743} // namespace line
2744
2745#endif // LINE_LANG_QN_STATE_H
InputError(const std::string &what)
Definition error.h:39
std::size_t size() const
Definition matrix.h:91
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::vector< std::vector< bool > > replyblock
sn.replyblock (nnodes x nclasses) and sn.syncreply (nclasses).
std::vector< std::vector< std::size_t > > nvars
sn.nvars, (nnodes x 3R+1): the LOCAL VARIABLE columns each node appends to its state,...
std::vector< Station< T > > stations
stations[k-1] is the k-th station
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense matrix and non-owning view.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
@ KLIMITED
serve at most K per visit (K in pollingPar)
Definition lang_types.h:375
@ EXHAUSTIVE
serve until the queue empties
Definition lang_types.h:374
@ GATED
serve exactly the jobs present at the polling instant
Definition lang_types.h:373
@ DECREMENTING
serve until the queue is one shorter than at arrival
Definition lang_types.h:376
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:485
std::vector< double > pfqn_sdrprob(const SdrCoeff &c, const std::vector< double > &n)
SDR routing probabilities of eq.
Definition pfqn_sdr.h:207
std::vector< std::vector< int > > multichoose_rows(int n, int k)
All n-vectors of nonnegative integers summing to k, in MATLAB multichoose(n,k) order.
SdrCoeff pfqn_sdrcoeff(const SdrStruct &sdr)
Validates an SDR structure and returns its derived coefficients.
Definition pfqn_sdr.h:105
double pfqn_sdrped(const std::vector< double > &P)
Probability of being denied entry and routed straight to the departure centre.
Definition pfqn_sdr.h:239
std::vector< std::vector< T > > from_marginal(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases)
Definition state.h:1342
std::vector< std::vector< T > > polling_blocks(const PollingInfo< T > &pi, std::size_t srvclass, const std::vector< std::size_t > &nbuf)
Port of State.pollingBlocks + State.pollingProject: every controller configuration compatible with on...
Definition state.h:933
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,...
Marginal< T > to_marginal(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< T > &state_i, const std::vector< std::size_t > &phasesz, const std::vector< std::size_t > &phaseshift, std::size_t nvar=0)
Port of State.toMarginal for a STATION, one state row at a time.
Definition state.h:130
std::vector< double > station_populations(const NetworkStruct< T > &sn, const std::vector< std::vector< T > > &local)
Total jobs held by every STATION at one network state, indexed by station.
Definition state.h:330
std::vector< NetState< T > > space_generator(const NetworkStruct< T > &sn, const std::vector< std::size_t > &cutoff, std::size_t maxst=3000000, const std::vector< std::vector< std::size_t > > &cutoff_mat=std::vector< std::vector< std::size_t > >())
Port of State.spaceGenerator: every network state, reachable or not.
Definition state.h:2520
std::vector< std::vector< T > > from_marginal_core(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases)
Port of State.fromMarginal for the station families CTMC enumerates: every local state in which stati...
Definition state.h:574
std::vector< std::vector< T > > from_marginal_node(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases)
Port of State.fromMarginal at its OWN signature: the reference indexes by NODE, not by station,...
Definition state.h:1680
bool from_marginal_node_first(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases, std::vector< T > &out)
The FIRST row from_marginal_node emits, BUILT rather than enumerated.
Definition state.h:2212
std::size_t state_initial_occupancy(const NetworkStruct< T > &sn, std::size_t ind, std::size_t r)
Port of State.initialOccupancy: the class-r jobs node ind holds in the DECLARED initial state,...
Definition state.h:2361
std::vector< std::vector< T > > from_marg_node(const NetworkStruct< T > &sn, std::size_t ind, std::size_t ntot, const std::vector< std::size_t > &phases)
Port of State.fromMarg: the state space with a given TOTAL queue length.
Definition state.h:2005
void space_closed_single_capped_rec(std::size_t m, long n, const std::vector< long > &caps, std::size_t off, std::vector< T > &row, std::vector< std::vector< T > > &out)
space_closed_single with a PER-SLOT bound, the reference's spaceClosedSingle(M, N,...
Definition state.h:466
std::vector< std::vector< T > > space_closed_single(std::size_t m, std::size_t n)
Port of State.spaceClosedSingle: the ways to place n jobs over m phases.
Definition state.h:437
std::vector< std::vector< T > > from_marg_node_started(const NetworkStruct< T > &sn, std::size_t ind, std::size_t ntot, std::size_t stot, const std::vector< std::size_t > &phases)
Port of State.fromMargAndStarted: the states with a given TOTAL queue length AND a given TOTAL number...
Definition state.h:2126
std::vector< std::vector< T > > from_marginal_and_started_core(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< std::size_t > &n, const std::vector< std::size_t > &s, const std::vector< std::size_t > &phases)
Port of State.fromMarginalAndStarted: ONE state realizing both a per-class occupancy n and a per-clas...
Definition state.h:1486
std::vector< std::vector< T > > cartesian(const std::vector< std::vector< T > > &a, const std::vector< std::vector< T > > &b)
Port of State.cartesian: pair every row of a with every row of b.
Definition state.h:415
std::vector< std::vector< T > > from_marginal_node_and_started(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< std::size_t > &n, const std::vector< std::size_t > &s, const std::vector< std::size_t > &phases)
Port of State.fromMarginalAndStarted at its OWN signature, which indexes by NODE rather than by stati...
Definition state.h:1961
std::vector< std::vector< std::size_t > > space_capacity_c(const NetworkStruct< T > &sn, const std::vector< std::size_t > &cutoff, const std::vector< std::vector< std::size_t > > &cutoff_mat=std::vector< std::vector< std::size_t > >())
Port of the capacityc table of State.spaceGeneratorNodes: the largest class-r marginal node ind may h...
Definition state.h:2397
PollingInfo< T > polling_info(const NetworkStruct< T > &sn, std::size_t ind)
Matrix< T > rt_state(const NetworkStruct< T > &sn, const std::vector< std::vector< T > > &local)
Port of sn.rtfun: the routing over the stateful nodes AT ONE STATE.
Definition state.h:375
std::vector< std::vector< std::size_t > > pas_multiset_perms(const std::vector< std::size_t > &vec)
Port of matlab/util/multiset_perms.m on an ASCENDING multiset, ROW ORDER INCLUDED.
Definition state.h:519
std::vector< std::vector< T > > from_marginal_and_started(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< std::size_t > &n, const std::vector< std::size_t > &s, const std::vector< std::size_t > &phases)
State.fromMarginalAndStarted with the trailing local-variable block appended, i.e.
Definition state.h:1653
std::vector< std::vector< T > > space_closed_multi_cs(std::size_t M, const std::vector< std::size_t > &N, const std::vector< std::vector< bool > > &chains, const std::vector< std::vector< long > > &caps=std::vector< std::vector< long > >())
Port of State.spaceClosedMultiCS: the same, but a CHAIN's population is shared among its classes,...
Definition state.h:2292
std::vector< std::vector< T > > append_local_vars(const NetworkStruct< T > &sn, std::size_t ist, std::vector< std::vector< T > > rows, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases)
Append the trailing local-variable block to every row the core builders produce, which is what makes ...
Definition state.h:1022
std::vector< std::vector< T > > space_closed_single_capped(std::size_t m, std::size_t n, const std::vector< long > &caps)
Definition state.h:491
std::vector< std::vector< T > > space_closed_multi(std::size_t M, const std::vector< std::size_t > &N, const std::vector< std::vector< long > > &caps=std::vector< std::vector< long > >())
Port of State.spaceClosedMulti: how N[r] class-r jobs distribute over M stateful nodes,...
Definition state.h:2265
std::vector< std::vector< T > > from_marginal_pool(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< std::size_t > &n)
Port of State.fromMarginalPool: the local states with marginal n at a station rewritten by ctmc::ctmc...
Definition state.h:1240
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Integer-composition enumeration shared by the CoMoM and MVAC ports.
State.pollingInfo and the controller description it returns.
Derived coefficients of an SDR structure, eqs.
Definition pfqn_sdr.h:81
long max_pending_retrieval
Truncation level of block B: how many secondary requests may be merged onto the in-flight fetches of ...
std::vector< int > itemcap
std::vector< std::size_t > missclass
std::vector< std::size_t > hitclass
CTMC pair form of a MAP service at an FCFS station, written by ctmc::ctmc_mapc on the solver's privat...
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::size_t > > pools
pools[r-1]: compatible pools of r, ascending
bool rotate
ALIS/FAIRNESS order is part of the state.
std::vector< std::vector< std::size_t > > perms
every pool order, identity first
static constexpr double MaxInt
Stand-in for an unbounded COUNT, MATLAB GlobalConstants.MaxInt.
Definition lang_types.h:771
What State.toMarginal returns for one station and one state row.
Definition state.h:51
std::vector< std::vector< T > > kir
jobs in service per class and phase
Definition state.h:55
T ni
total jobs in the station
Definition state.h:52
std::vector< T > nir
jobs per class
Definition state.h:53
std::vector< T > sir
jobs in service per class
Definition state.h:54
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2347
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2348
The G-network signal declaration, per CLASS.
FINITE CAPACITY REGIONS, MATLAB's refreshRegions output.
std::vector< std::vector< double > > cap
(nstations x nclasses+1), -1 = unbounded
A node of the network.
std::vector< RoutingStrategy > routing
sn.routing, per class.
Port of State.pollingInfo: the derived description of a polling controller.
std::vector< bool > has_sw
std::vector< std::size_t > ksw
std::vector< bool > polled
lang::PollingType ptype
One station of the network.
SchedStrategy sched
double nservers
may be infinite (a Delay, or an inf-scheduled task)
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
std::vector< double > nmodeservers
servers per mode, may be infinite
std::vector< std::size_t > firingphases
phase count per mode, 0 when non-Markovian