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 std::size_t ntot = 0;
1046 for (std::size_t r = 0; r < n.size(); ++r) ntot += n[r];
1047
1048 // The dispatch pointers, one column per class that routes round-robin, in
1049 // CLASS ORDER and immediately after the modulating phases: that is where
1050 // `rr_var_slot` reads them and where the reference appends them.
1051 std::size_t rrw = 0;
1052 if (ind <= sn.nvars.size())
1053 for (std::size_t r = 0; r < R && R + r < sn.nvars[ind - 1].size(); ++r)
1054 rrw += sn.nvars[ind - 1][R + r];
1055 std::vector<std::vector<T>> ptrsets(1, std::vector<T>());
1056 for (std::size_t r = 1; r <= R; ++r) {
1057 if (sn.rr_var_slot(ind, r) == 0) continue;
1058 std::vector<T> vals;
1059 if (sn.nodes[ind - 1].routing[r - 1] == RoutingStrategy::RROBIN) {
1060 const std::vector<std::size_t> ol = sn.rr_outlinks(ind, r);
1061 for (std::size_t d = 0; d < ol.size(); ++d)
1062 vals.push_back(num_traits<T>::from_int(static_cast<long>(ol[d])));
1063 } else {
1064 const std::vector<std::size_t> cy = sn.rr_weighted_outlinks(ind, r);
1065 for (std::size_t d = 0; d < cy.size(); ++d)
1066 vals.push_back(num_traits<T>::from_int(static_cast<long>(d + 1)));
1067 }
1068 if (vals.empty()) vals.push_back(zero);
1069 std::vector<std::vector<T>> grown;
1070 for (std::size_t g = 0; g < ptrsets.size(); ++g)
1071 for (std::size_t v = 0; v < vals.size(); ++v) {
1072 std::vector<T> row = ptrsets[g];
1073 row.push_back(vals[v]);
1074 grown.push_back(row);
1075 }
1076 ptrsets.swap(grown);
1077 }
1078
1079 std::vector<std::vector<T>> out;
1080 for (std::size_t i = 0; i < rows.size(); ++i) {
1081 // The controller depends on WHICH class holds the service facility and
1082 // on how many jobs wait, so recover both from the row just built.
1083 std::vector<std::size_t> nbuf(R, 0);
1084 std::size_t srvclass = 0;
1085 if (pw) {
1086 std::size_t srvw = 0;
1087 for (std::size_t r = 0; r < R; ++r) srvw += phases[r];
1088 const std::size_t srv0 = rows[i].size() - srvw;
1089 std::size_t off = 0;
1090 for (std::size_t r = 0; r < R; ++r) {
1091 std::size_t c = 0;
1092 for (std::size_t k = 0; k < phases[r]; ++k)
1093 c += static_cast<std::size_t>(
1094 num_traits<T>::to_double(rows[i][srv0 + off + k]));
1095 off += phases[r];
1096 if (c > 0 && srvclass == 0) srvclass = r + 1;
1097 // The per-class-count buffer POLLING uses puts the waiting
1098 // count in column r, ahead of the server block.
1099 if (srv0 >= R) nbuf[r] = static_cast<std::size_t>(
1100 num_traits<T>::to_double(rows[i][r]));
1101 }
1102 }
1103 const std::vector<std::vector<T>> blocks =
1104 pw ? polling_blocks(pi, srvclass, nbuf)
1105 : std::vector<std::vector<T>>(1, std::vector<T>());
1106 // No admissible controller configuration means the (buffer, server)
1107 // split itself is unoccupiable, so the row is DROPPED rather than
1108 // emitted without a controller.
1109 // ORDER IS THE ENCODING: nvars is [modulation | routing | node block |
1110 // reply], so the controller sits between the routing pointers and the
1111 // reply counters, not at the end. `polling_info` computes its offset
1112 // the same way, and the two must agree or every read is shifted.
1113 const std::size_t head = pi.valid ? pi.off : width - pw;
1114 for (std::size_t b = 0; b < blocks.size(); ++b) {
1115 for (std::size_t g = 0; g < ptrsets.size(); ++g) {
1116 std::vector<T> row = rows[i];
1117 row.insert(row.end(), head - rrw, zero); // modulating phases
1118 row.insert(row.end(), ptrsets[g].begin(), ptrsets[g].end()); // dispatch pointers
1119 row.insert(row.end(), blocks[b].begin(), blocks[b].end());
1120 row.insert(row.end(), width - head - pw, zero); // reply counters
1121 if (!bas) {
1122 out.push_back(row);
1123 continue;
1124 }
1125 // The marker is the LAST column, which is where the departure
1126 // handler and the generator's become-blocked edge both read it.
1127 // `refresh_bas_blocking` refuses BAS together with a reply block for
1128 // exactly this reason: the reply counters would trail it.
1129 for (std::size_t v = 0; v <= (ntot > 0 ? 1u : 0u); ++v) {
1130 std::vector<T> r2 = row;
1131 r2.back() = num_traits<T>::from_int(static_cast<long>(v));
1132 out.push_back(r2);
1133 }
1134 }
1135 }
1136 }
1137 return out;
1138}
1139
1140template <class T>
1141std::vector<std::vector<T>> from_marginal(const NetworkStruct<T>& sn, std::size_t ist,
1142 const std::vector<std::size_t>& n,
1143 const std::vector<std::size_t>& phases) {
1144 const std::size_t jnd0 = sn.node_of_station(ist);
1145 // A Join of an FJ-augmented struct: its state is the per-class count vector
1146 // itself, DETERMINED by the marginal rather than enumerated from it. There is
1147 // no buffer order to choose and no phase to be in, so exactly one row exists.
1148 if (sn.isfjaugmented && jnd0 != 0 && sn.nodes[jnd0 - 1].nodetype == NodeType::Join) {
1149 std::vector<T> row(sn.nclasses, num_traits<T>::from_int(0));
1150 for (std::size_t r = 0; r < sn.nclasses && r < n.size(); ++r)
1151 row[r] = num_traits<T>::from_int(static_cast<long>(n[r]));
1152 return std::vector<std::vector<T>>(1, row);
1153 }
1154 // SYNCHRONOUS CALL (REPLY signal), `State.fromMarginal:44-90`. A node with a
1155 // reply block holds one server per job that has left for its callee and is
1156 // waiting for the reply, and THAT COUNT IS NOT DERIVABLE FROM THE MARGINAL:
1157 // the job is at the callee, not here. So the held counts are ENUMERATED and
1158 // the rest of the state is built with the REMAINING servers -- with b held,
1159 // only S-b jobs can be in service, a configuration the plain enumeration
1160 // never produces. Without this the counter column exists but is zero in
1161 // every row, the departure that would hold a server has no successor in the
1162 // space, and the chain dead-ends: measured on the `test_ctmc_reply` model,
1163 // 11 states with the whole population frozen at the caller and every rate
1164 // zero, against 21 enumerated and X = 0.47059 in MATLAB, the JAR and Python.
1165 if (jnd0 != 0 && sn.replyblock.size() >= jnd0) {
1166 std::vector<std::size_t> rclasses;
1167 for (std::size_t r = 0; r < sn.replyblock[jnd0 - 1].size(); ++r)
1168 if (sn.replyblock[jnd0 - 1][r]) rclasses.push_back(r + 1);
1169 if (!rclasses.empty()) {
1170 const double Sd = sn.stations[ist - 1].nservers;
1171 const std::size_t S =
1172 std::isfinite(Sd) ? static_cast<std::size_t>(Sd) : static_cast<std::size_t>(0);
1173 // The recursion runs on a struct with the block CLEARED, which is
1174 // what terminates it: the copy takes this branch no further.
1175 NetworkStruct<T> snb = sn;
1176 snb.replyblock[jnd0 - 1].assign(snb.replyblock[jnd0 - 1].size(), false);
1177 if (snb.nvars.size() >= jnd0)
1178 for (std::size_t r = 1; r <= sn.nclasses && 2 * sn.nclasses + r < snb.nvars[jnd0 - 1].size();
1179 ++r)
1180 snb.nvars[jnd0 - 1][2 * sn.nclasses + r] = 0;
1181 std::vector<std::vector<std::size_t>> bspace(1, std::vector<std::size_t>());
1182 for (std::size_t i = 0; i < rclasses.size(); ++i) {
1183 std::vector<std::vector<std::size_t>> next;
1184 for (std::size_t j = 0; j < bspace.size(); ++j)
1185 for (std::size_t v = 0; v <= S; ++v) {
1186 std::vector<std::size_t> row = bspace[j];
1187 row.push_back(v);
1188 next.push_back(row);
1189 }
1190 bspace.swap(next);
1191 }
1192 std::vector<std::vector<std::vector<T>>> subs(bspace.size());
1193 std::size_t maxw = 0;
1194 for (std::size_t bi = 0; bi < bspace.size(); ++bi) {
1195 std::size_t tot = 0;
1196 for (std::size_t i = 0; i < bspace[bi].size(); ++i) tot += bspace[bi][i];
1197 if (tot > S) continue;
1198 snb.stations[ist - 1].nservers = static_cast<double>(S - tot);
1199 subs[bi] = from_marginal(snb, ist, n, phases);
1200 for (std::size_t i = 0; i < subs[bi].size(); ++i)
1201 maxw = std::max(maxw, subs[bi][i].size());
1202 }
1203 // A held server pushes a job into the buffer, so the sub-spaces come
1204 // out at DIFFERENT buffer widths. The buffer is RIGHT-aligned, empty
1205 // slots padding the left, so widen the narrow rows on the left.
1206 std::vector<std::vector<T>> out;
1207 for (std::size_t bi = 0; bi < bspace.size(); ++bi) {
1208 for (std::size_t i = 0; i < subs[bi].size(); ++i) {
1209 std::vector<T> row;
1210 row.reserve(maxw + bspace[bi].size());
1211 if (subs[bi][i].size() < maxw)
1212 row.assign(maxw - subs[bi][i].size(), num_traits<T>::from_int(0));
1213 row.insert(row.end(), subs[bi][i].begin(), subs[bi][i].end());
1214 for (std::size_t j = 0; j < bspace[bi].size(); ++j)
1215 row.push_back(num_traits<T>::from_int(static_cast<long>(bspace[bi][j])));
1216 out.push_back(row);
1217 }
1218 }
1219 std::sort(out.begin(), out.end(), [](const std::vector<T>& a, const std::vector<T>& b) {
1220 for (std::size_t i = 0; i < a.size() && i < b.size(); ++i) {
1221 const double av = num_traits<T>::to_double(a[i]);
1222 const double bv = num_traits<T>::to_double(b[i]);
1223 if (av != bv) return av < bv;
1224 }
1225 return a.size() < b.size();
1226 });
1227 out.erase(std::unique(out.begin(), out.end(),
1228 [](const std::vector<T>& a, const std::vector<T>& b) {
1229 if (a.size() != b.size()) return false;
1230 for (std::size_t i = 0; i < a.size(); ++i)
1231 if (num_traits<T>::to_double(a[i]) !=
1232 num_traits<T>::to_double(b[i]))
1233 return false;
1234 return true;
1235 }),
1236 out.end());
1237 return out;
1238 }
1239 }
1240 return append_local_vars(sn, ist, from_marginal_core(sn, ist, n, phases), n, phases);
1241}
1242
1243/**
1244 * Port of `State.fromMarginalAndStarted`: ONE state realizing both a per-class
1245 * occupancy `n` and a per-class STARTED count `s`.
1246 *
1247 * HOW IT DIFFERS FROM `from_marginal_core`, which is the reason it is a separate
1248 * builder rather than a filter over it. `from_marginal` ENUMERATES every
1249 * (buffer, server) split consistent with a marginal, because the marginal alone
1250 * does not determine which jobs hold the servers. Here the split is GIVEN: `s`
1251 * says how many jobs of each class are in service, so exactly one split is
1252 * meant and the buffer contents follow as `n - s`. Filtering the enumeration
1253 * would be both quadratic and wrong at an empty station, where the reference
1254 * emits a row of a prescribed width rather than selecting one.
1255 *
1256 * EVERY STARTED JOB IS PLACED IN PHASE ONE, which is what makes the result an
1257 * INITIAL state rather than a member of the stationary space: a job that has
1258 * just started service has not advanced through its phase-type law yet.
1259 *
1260 * ONE BUFFER ORDERING IS EMITTED under the ordered-buffer disciplines, the
1261 * descending-sorted one. The reference enumerates every permutation and then
1262 * keeps the lexicographic maximum through its trailing unique/flip, so building
1263 * that row directly is the same answer without the factorial.
1264 *
1265 * A SHARED SERVER IGNORES `s` ENTIRELY. Under INF/PS/DPS/GPS/LPS and their
1266 * priority variants every job present is in a server, so the state holds `n`,
1267 * not `s`; writing `s` there would lose the queued jobs. That is not a
1268 * simplification but the reference's own branch, and it was a real defect in
1269 * the Python twin until 2026-08-09.
1270 *
1271 * PAS/OI ignores `s` for a different reason: its local state is the ordered
1272 * class-index list of the jobs present, which carries no service split at all,
1273 * so this delegates to the marginal builder there.
1274 *
1275 * @param sn the network struct
1276 * @param ist station index (1-based)
1277 * @param n per-class occupancy
1278 * @param s per-class started count; must satisfy s[r] <= n[r]
1279 * @param phases per-class phase counts
1280 */
1281template <class T>
1282std::vector<std::vector<T>> from_marginal_and_started_core(const NetworkStruct<T>& sn,
1283 std::size_t ist,
1284 const std::vector<std::size_t>& n,
1285 const std::vector<std::size_t>& s,
1286 const std::vector<std::size_t>& phases) {
1287 const std::size_t R = sn.nclasses;
1288 if (ist == 0 || ist > sn.stations.size())
1289 throw InputError("from_marginal_and_started: station index " + std::to_string(ist) +
1290 " is out of range");
1291 if (n.size() != R || s.size() != R || phases.size() != R)
1292 throw InputError("from_marginal_and_started: n, s and phases must have one entry per class");
1293
1294 const Station<T>& st = sn.stations[ist - 1];
1295 const SchedStrategy sched = st.sched;
1296 std::vector<std::vector<T>> out;
1297
1298 std::size_t ntot = 0, stot = 0;
1299 for (std::size_t r = 0; r < R; ++r) {
1300 if (s[r] > n[r]) return out; // more started than present: no such state
1301 ntot += n[r];
1302 stot += s[r];
1303 }
1304
1305 // The reference's two pre-switch guards. Both are "no such state" rather
1306 // than an error, since the caller may be sweeping a lattice.
1307 if (ist <= sn.classcap.size())
1308 for (std::size_t r = 0; r < R && r < sn.classcap[ist - 1].size(); ++r)
1309 if (static_cast<double>(n[r]) > sn.classcap[ist - 1][r]) return out;
1310 const double S = st.nservers;
1311 if (S > 0.0 && std::isfinite(S) && static_cast<double>(stot) > S) return out;
1312
1313 // PAS/OI: the started counts are immaterial to the list encoding.
1314 if (sched == SchedStrategy::PAS || sched == SchedStrategy::OI)
1315 return from_marginal_core(sn, ist, n, phases);
1316
1317 // A Source generates rather than holds: its phase block IS the arrival
1318 // process, seeded in phase one for every class it serves, and the leading
1319 // column is the infinite population the reference marks with Inf.
1320 if (sched == SchedStrategy::EXT || st.nodetype == NodeType::Source) {
1321 // ONE BLOCK PER CLASS, INCLUDING A CLASS THE SOURCE DOES NOT GENERATE:
1322 // `phases` is the width vector, so a disabled arrival contributes one
1323 // always-zero column rather than nothing. That is the reference's row
1324 // (`[Inf 1 0 0 0 0 0]` for a Source generating the first of six
1325 // classes), and a narrower one cannot be decoded by anyone who built it
1326 // from `sn.phasessz` -- see `NetworkStruct::phasessz_of`.
1327 std::vector<std::vector<T>> srv;
1328 for (std::size_t r = 0; r < R; ++r) {
1329 if (phases[r] == 0) continue;
1330 std::vector<T> init(phases[r], num_traits<T>::from_int(0));
1331 if (r < sn.classes.size() && std::isinf(sn.classes[r].population) &&
1332 !sn.disabled[ist - 1][r])
1333 init[0] = num_traits<T>::from_int(1);
1334 srv = cartesian(srv, std::vector<std::vector<T>>(1, init));
1335 }
1336 if (srv.empty()) srv.push_back(std::vector<T>());
1337 for (std::size_t i = 0; i < srv.size(); ++i) {
1338 std::vector<T> row(1, num_traits<T>::from_double(
1339 std::numeric_limits<double>::infinity()));
1340 row.insert(row.end(), srv[i].begin(), srv[i].end());
1341 out.push_back(row);
1342 }
1343 return out;
1344 }
1345
1346 /** The server block: `cnt[r]` jobs of class r, all of them in phase one. */
1347 const auto phase_one_block = [&](const std::vector<std::size_t>& cnt,
1348 bool* ok) -> std::vector<T> {
1349 std::vector<T> blk;
1350 *ok = true;
1351 for (std::size_t r = 0; r < R; ++r) {
1352 // No service process, yet jobs demanded there: unreachable. An
1353 // empty factor must annihilate the fold, not be absorbed by it.
1354 // The COLUMN is still emitted -- `phases` is the width vector and a
1355 // disabled class holds an always-zero column, as the reference does.
1356 if (sn.phases_of(ist, r + 1) == 0 && cnt[r] > 0) *ok = false;
1357 if (phases[r] == 0) continue;
1358 blk.push_back(num_traits<T>::from_int(static_cast<long>(cnt[r])));
1359 blk.insert(blk.end(), phases[r] - 1, num_traits<T>::from_int(0));
1360 }
1361 return blk;
1362 };
1363 std::size_t srvw = 0;
1364 for (std::size_t r = 0; r < R; ++r) srvw += phases[r];
1365
1366 if (state_detail::buffer_is_per_class_count(sched)) {
1367 // UNORDERED buffer: one waiting COUNT per class ahead of the server
1368 // block. Below the server count every job is in service and the buffer
1369 // is empty, which is the reference's own special case.
1370 const bool all_in_service = std::isfinite(S) && static_cast<double>(ntot) <= S;
1371 std::vector<std::size_t> insrv(R, 0), wait(R, 0);
1372 for (std::size_t r = 0; r < R; ++r) {
1373 insrv[r] = all_in_service ? n[r] : s[r];
1374 wait[r] = n[r] - insrv[r];
1375 }
1376 bool ok = false;
1377 const std::vector<T> blk = phase_one_block(insrv, &ok);
1378 if (!ok) return out;
1379 std::vector<T> row;
1380 for (std::size_t r = 0; r < R; ++r)
1381 row.push_back(num_traits<T>::from_int(static_cast<long>(wait[r])));
1382 row.insert(row.end(), blk.begin(), blk.end());
1383 out.push_back(row);
1384 return out;
1385 }
1386
1387 const bool paired = state_detail::buffer_is_tag_phase_pairs(sched);
1388 if (state_detail::buffer_is_class_tag(sched) || paired) {
1389 // ORDERED buffer: the waiting positions carry class tags, and under the
1390 // preempt family a [class, phase] PAIR per position. The empty buffer
1391 // keeps the width the decoder expects -- one column, or two when
1392 // paired, since an odd-width paired buffer is half a pair and
1393 // `to_marginal` would read every slot shifted.
1394 const std::size_t bw = paired ? 2u : 1u;
1395 if (ntot == 0) {
1396 std::vector<T> row(bw + srvw, num_traits<T>::from_int(0));
1397 out.push_back(row);
1398 return out;
1399 }
1400 std::vector<std::size_t> inbuf;
1401 for (std::size_t r = 0; r < R; ++r)
1402 for (std::size_t j = 0; j < n[r] - s[r]; ++j) inbuf.push_back(r + 1);
1403 // Descending, which is the lexicographic maximum the reference's
1404 // trailing unique/flip leaves standing.
1405 std::sort(inbuf.begin(), inbuf.end(),
1406 [](std::size_t a, std::size_t b) { return a > b; });
1407
1408 bool ok = false;
1409 const std::vector<T> blk = phase_one_block(s, &ok);
1410 if (!ok) return out;
1411
1412 std::vector<T> row;
1413 if (inbuf.empty()) {
1414 row.assign(bw, num_traits<T>::from_int(0));
1415 } else {
1416 for (std::size_t j = 0; j < inbuf.size(); ++j) {
1417 row.push_back(num_traits<T>::from_int(static_cast<long>(inbuf[j])));
1418 // A preempted job is recorded in its LAST service phase, as the
1419 // reference builds the lexicographic maximum.
1420 if (paired)
1421 row.push_back(num_traits<T>::from_int(
1422 static_cast<long>(phases[inbuf[j] - 1])));
1423 }
1424 }
1425 row.insert(row.end(), blk.begin(), blk.end());
1426 out.push_back(row);
1427 return out;
1428 }
1429
1430 // SHARED SERVER (INF/PS/DPS/GPS/LPS and the priority variants): no buffer,
1431 // and every job present is in a server, so the block holds n and not s.
1432 bool ok = false;
1433 const std::vector<T> blk = phase_one_block(n, &ok);
1434 if (!ok) return out;
1435 out.push_back(blk);
1436 return out;
1437}
1438
1439/**
1440 * `State.fromMarginalAndStarted` with the trailing local-variable block
1441 * appended, i.e. the counterpart of `from_marginal` for a prescribed service
1442 * split. A Join of an FJ-augmented struct carries its per-class counts and no
1443 * split, exactly as in `from_marginal`.
1444 */
1445template <class T>
1446std::vector<std::vector<T>> from_marginal_and_started(const NetworkStruct<T>& sn, std::size_t ist,
1447 const std::vector<std::size_t>& n,
1448 const std::vector<std::size_t>& s,
1449 const std::vector<std::size_t>& phases) {
1450 const std::size_t jnd0 = sn.node_of_station(ist);
1451 if (sn.isfjaugmented && jnd0 != 0 && sn.nodes[jnd0 - 1].nodetype == NodeType::Join) {
1452 std::vector<T> row(sn.nclasses, num_traits<T>::from_int(0));
1453 for (std::size_t r = 0; r < sn.nclasses && r < n.size(); ++r)
1454 row[r] = num_traits<T>::from_int(static_cast<long>(n[r]));
1455 return std::vector<std::vector<T>>(1, row);
1456 }
1457 return append_local_vars(sn, ist, from_marginal_and_started_core(sn, ist, n, s, phases), n,
1458 phases);
1459}
1460
1461
1462/**
1463 * Port of `State.fromMarginal` at its OWN signature: the reference indexes by
1464 * NODE, not by station, and derives the station internally. That distinction is
1465 * load-bearing, not cosmetic -- a Transition is stateful but is NOT a station,
1466 * so it carries no station index and the station-indexed overload can never
1467 * reach it. Routing every caller through here is what makes the Transition
1468 * branch below live rather than dead code.
1469 *
1470 * A node that is not stateful holds no jobs and contributes no local state.
1471 */
1472template <class T>
1473std::vector<std::vector<T>> from_marginal_node(const NetworkStruct<T>& sn, std::size_t ind,
1474 const std::vector<std::size_t>& n,
1475 const std::vector<std::size_t>& phases) {
1476 if (ind == 0 || ind > sn.nodes.size())
1477 throw InputError("from_marginal_node: node index " + std::to_string(ind) +
1478 " is out of range");
1479 const NodeDef& nd = sn.nodes[ind - 1];
1480 if (nd.station != 0) return from_marginal(sn, nd.station, n, phases);
1481 if (!nd.stateful) return std::vector<std::vector<T>>();
1482
1483 const std::size_t R = sn.nclasses;
1484 if (n.size() != R)
1485 throw InputError("from_marginal_node: n must have one entry per class");
1486
1487 if (nd.nodetype == NodeType::Transition) {
1488 // PER-MODE state, not per-class: [free servers | firing phases | fired].
1489 // The reference emits the all-idle row -- every mode's servers free, no
1490 // firing in progress -- and lets the event handlers walk from there, so
1491 // the marginal n plays no part. An infinite server count is clamped to
1492 // MaxInt because the row is a COUNT vector and Inf is not a count.
1493 const typename std::map<std::size_t, TransitionParam<T> >::const_iterator it =
1494 sn.transparam.find(ind);
1495 if (it == sn.transparam.end())
1496 throw InputError(
1497 "from_marginal_node: transition node has no TransitionParam; build it with "
1498 "add_transition");
1499 const TransitionParam<T>& tp = it->second;
1500 std::vector<T> row;
1501 for (std::size_t mm = 0; mm < tp.nmodes; ++mm) {
1502 const double sv = mm < tp.nmodeservers.size() ? tp.nmodeservers[mm] : 1.0;
1503 row.push_back(num_traits<T>::from_double(
1504 std::isfinite(sv) ? sv : static_cast<double>(GlobalConstants::MaxInt)));
1505 }
1506 std::size_t fph = 0;
1507 for (std::size_t mm = 0; mm < tp.nmodes; ++mm)
1508 fph += mm < tp.firingphases.size() && tp.firingphases[mm] > 0 ? tp.firingphases[mm] : 1;
1509 row.insert(row.end(), fph, num_traits<T>::from_int(0));
1510 row.insert(row.end(), tp.nmodes, num_traits<T>::from_int(0));
1511 return std::vector<std::vector<T> >{row};
1512 }
1513
1514 // A CACHE HOLDS ONE READING JOB, which is what makes the read a decision
1515 // rather than a queue: `spaceGeneratorNodes` caps every class there at 1 and
1516 // the node at 1 job in total. Without the cap the lattice admits a cache
1517 // holding two reads, and since `after_event_cache` fires a READ only on a
1518 // node holding exactly one job, that state is ABSORBING -- on an open model
1519 // the whole stationary mass ends up in it and every cache rate reads zero.
1520 //
1521 // A completing fetch is the one exception: it releases every merged
1522 // secondary request into the hit class in ONE immediate transition, so the
1523 // hit classes momentarily hold up to `max_pending_retrieval` extra jobs.
1524 if (nd.nodetype == NodeType::Cache) {
1525 const typename std::map<std::size_t, CacheParam<T> >::const_iterator cc =
1526 sn.nodeparam.find(ind);
1527 if (cc != sn.nodeparam.end()) {
1528 const std::size_t pend =
1529 cc->second.retrieval_capacity > 0 && cc->second.max_pending_retrieval > 0
1530 ? static_cast<std::size_t>(cc->second.max_pending_retrieval)
1531 : 0;
1532 std::size_t tot = 0;
1533 for (std::size_t r = 0; r < R; ++r) {
1534 bool is_hit = false;
1535 for (std::size_t u = 0; u < cc->second.hitclass.size(); ++u)
1536 if (cc->second.hitclass[u] == r + 1) is_hit = true;
1537 if (n[r] > (is_hit ? 1 + pend : 1)) return std::vector<std::vector<T> >();
1538 tot += n[r];
1539 }
1540 if (tot > 1 + pend) return std::vector<std::vector<T> >();
1541 }
1542 }
1543
1544 // Any other stateful non-station (a Cache, a stateful ClassSwitch): the
1545 // reference folds `spaceClosedSingle(1, n(r))` per class, i.e. one column
1546 // per class holding its count, with no phase and no buffer.
1547 std::vector<std::vector<T> > acc;
1548 for (std::size_t r = 0; r < R; ++r) {
1549 const std::vector<std::vector<T> > sr = space_closed_single<T>(1, n[r]);
1550 if (sr.empty() && n[r] > 0) return std::vector<std::vector<T> >();
1551 acc = cartesian(acc, sr);
1552 }
1553
1554 // A DISPATCHER CARRIES ITS POINTER IN THE STATE, for the same reason a cache
1555 // carries its contents: which link the next job takes is not derivable from
1556 // the marginal. RROBIN enumerates its outlinks and WRROBIN the positions of
1557 // its weighted cycle, exactly as `sub_routevars` does, and the columns sit
1558 // in CLASS ORDER right after the per-class counts, where `rr_var_slot` reads
1559 // them. Emitting the node one column narrower than `nvars_of` declares makes
1560 // every successor a state the space does not contain, so the node becomes
1561 // ABSORBING and the whole chain stalls with the jobs sitting in it.
1562 {
1563 std::vector<std::vector<T> > ptrsets(1, std::vector<T>());
1564 bool any = false;
1565 for (std::size_t r = 1; r <= R; ++r) {
1566 if (sn.rr_var_slot(ind, r) == 0) continue;
1567 any = true;
1568 std::vector<T> vals;
1569 if (sn.nodes[ind - 1].routing[r - 1] == RoutingStrategy::RROBIN) {
1570 const std::vector<std::size_t> ol = sn.rr_outlinks(ind, r);
1571 for (std::size_t u = 0; u < ol.size(); ++u)
1572 vals.push_back(num_traits<T>::from_int(static_cast<long>(ol[u])));
1573 } else {
1574 const std::vector<std::size_t> cy = sn.rr_weighted_outlinks(ind, r);
1575 for (std::size_t u = 0; u < cy.size(); ++u)
1576 vals.push_back(num_traits<T>::from_int(static_cast<long>(u + 1)));
1577 }
1578 if (vals.empty()) vals.push_back(num_traits<T>::from_int(0));
1579 std::vector<std::vector<T> > grown;
1580 for (std::size_t g = 0; g < ptrsets.size(); ++g)
1581 for (std::size_t v = 0; v < vals.size(); ++v) {
1582 std::vector<T> row = ptrsets[g];
1583 row.push_back(vals[v]);
1584 grown.push_back(row);
1585 }
1586 ptrsets.swap(grown);
1587 }
1588 if (any) acc = cartesian(acc, ptrsets);
1589 }
1590
1591 // A CACHE CARRIES ITS CONTENTS IN THE STATE, and they are not derivable
1592 // from any marginal: the row is [per-class counts | contents | occupancy],
1593 // exactly as `after_event_cache` slices it. Enumerating only the counts
1594 // left the contents region absent, so every READ landed on a state one
1595 // block wider than anything enumerated and the space came out EMPTY.
1596 //
1597 // A contents slot holds an ITEM INDEX and an item sits in at most one slot --
1598 // a cache holding two copies of one item is not a state the replacement
1599 // policies can reach or leave.
1600 //
1601 // THE ENUMERATED CACHE IS FULL, which is what `State.spaceCache` builds:
1602 // every ordered placement of `tcc` DISTINCT items, with no empty slot. Under
1603 // every replacement policy a cache that has once been filled never empties
1604 // again, so a partly empty configuration is transient and enumerating it
1605 // only spreads stationary mass over states the model leaves for good. It is
1606 // load-bearing beyond tidiness for a retrieval cache: block A may be set
1607 // only for an item that is NOT cached, and the count of such items is the
1608 // retrieval-system capacity `nitems - tcc`, which is a number only if the
1609 // cached set is full. A cache with more slots than items can never fill, so
1610 // the empty slot survives there.
1611 if (nd.nodetype == NodeType::Cache) {
1612 const typename std::map<std::size_t, CacheParam<T> >::const_iterator ci =
1613 sn.nodeparam.find(ind);
1614 if (ci == sn.nodeparam.end()) return acc;
1615 const CacheParam<T>& cp = ci->second;
1616 std::size_t tcc = 0;
1617 for (std::size_t u = 0; u < cp.itemcap.size(); ++u)
1618 if (cp.itemcap[u] > 0) tcc += static_cast<std::size_t>(cp.itemcap[u]);
1619
1620 const std::size_t first_item = tcc <= cp.nitems ? 1 : 0;
1621 std::vector<std::vector<T> > contents(1, std::vector<T>());
1622 for (std::size_t slot = 0; slot < tcc; ++slot) {
1623 std::vector<std::vector<T> > grown;
1624 for (std::size_t c = 0; c < contents.size(); ++c)
1625 for (std::size_t item = first_item; item <= cp.nitems; ++item) {
1626 bool dup = false;
1627 if (item > 0)
1628 for (std::size_t j = 0; j < contents[c].size(); ++j)
1629 if (num_traits<T>::to_double(contents[c][j]) ==
1630 static_cast<double>(item)) { dup = true; break; }
1631 if (dup) continue;
1632 std::vector<T> row = contents[c];
1633 row.push_back(num_traits<T>::from_int(static_cast<long>(item)));
1634 grown.push_back(row);
1635 }
1636 contents.swap(grown);
1637 }
1638 // Block A, the delayed-hit occupancy bitmap: one bit per item, set while
1639 // a retrieval for it is in flight.
1640 if (cp.retrieval_capacity > 0)
1641 for (std::size_t i = 0; i < cp.nitems; ++i) {
1642 std::vector<std::vector<T> > grown;
1643 for (std::size_t c = 0; c < contents.size(); ++c)
1644 for (int b = 0; b <= 1; ++b) {
1645 std::vector<T> row = contents[c];
1646 row.push_back(num_traits<T>::from_int(b));
1647 grown.push_back(row);
1648 }
1649 contents.swap(grown);
1650 }
1651 // Block B, the merged secondary requests: one count per retrieval class,
1652 // nonzero only where that class's item is in flight (block A is set) and
1653 // summing to at most the declared truncation level. `State.spaceCache`
1654 // enumerates the same compositions; -1 means the caller is walking a
1655 // sample path rather than enumerating, so no pending state is generated.
1656 if (cp.retrieval_capacity > 0) {
1657 std::vector<std::size_t> rcl, rci, rco;
1658 cache_retrieval_class_map(cp, rcl, rci, rco);
1659 if (!rcl.empty()) {
1660 const long maxpend =
1662 std::vector<std::vector<T> > grown;
1663 for (std::size_t c = 0; c < contents.size(); ++c) {
1664 // which slots may carry a count in THIS row: the ones whose
1665 // item is currently being fetched
1666 std::vector<std::size_t> active;
1667 for (std::size_t j = 0; j < rcl.size(); ++j)
1668 if (num_traits<T>::to_double(contents[c][tcc + rci[j] - 1]) != 0)
1669 active.push_back(j);
1670 std::vector<std::vector<long> > pend(1, std::vector<long>(rcl.size(), 0));
1671 for (std::size_t a = 0; a < active.size(); ++a) {
1672 std::vector<std::vector<long> > next;
1673 for (std::size_t q = 0; q < pend.size(); ++q) {
1674 long used = 0;
1675 for (std::size_t j = 0; j < rcl.size(); ++j) used += pend[q][j];
1676 for (long v = 0; v + used <= maxpend; ++v) {
1677 std::vector<long> row = pend[q];
1678 row[active[a]] = v;
1679 next.push_back(row);
1680 }
1681 }
1682 pend.swap(next);
1683 }
1684 for (std::size_t q = 0; q < pend.size(); ++q) {
1685 std::vector<T> row = contents[c];
1686 for (std::size_t j = 0; j < rcl.size(); ++j)
1687 row.push_back(num_traits<T>::from_int(pend[q][j]));
1688 grown.push_back(row);
1689 }
1690 }
1691 contents.swap(grown);
1692 }
1693 }
1694 std::vector<std::vector<T> > joint = cartesian(acc, contents);
1695 if (cp.retrieval_capacity <= 0) return joint;
1696
1697 // THE LOCALLY INVALID ROWS OF A RETRIEVAL CACHE, `spaceGeneratorNodes.m`
1698 // lines 206-280. The cartesian product above enumerates combinations the
1699 // dynamics can never reach, and they are not harmless: a row holding a
1700 // returning retrieval job BESIDE another job disables the READ (which
1701 // needs the node to hold exactly one) while leaving the departure
1702 // enabled, so the fetch bounces back to its queue without completing.
1703 // That inflated the retrieval flow by 20% on retrieval_simple while the
1704 // hit and miss shares, being ratios, stayed exactly right -- the kind of
1705 // defect only a flow identity catches.
1706 std::vector<bool> is_hit(R, false), is_miss(R, false);
1707 for (std::size_t u = 0; u < cp.hitclass.size(); ++u)
1708 if (cp.hitclass[u] >= 1 && cp.hitclass[u] <= R) is_hit[cp.hitclass[u] - 1] = true;
1709 for (std::size_t u = 0; u < cp.missclass.size(); ++u)
1710 if (cp.missclass[u] >= 1 && cp.missclass[u] <= R) is_miss[cp.missclass[u] - 1] = true;
1711 const long maxpend = cp.max_pending_retrieval > 0 ? cp.max_pending_retrieval : 0;
1712 std::vector<std::vector<T> > kept;
1713 for (std::size_t row = 0; row < joint.size(); ++row) {
1714 const std::vector<T>& st = joint[row];
1715 double tot = 0, other = 0, miss = 0;
1716 for (std::size_t r = 0; r < R; ++r) {
1717 const double v = num_traits<T>::to_double(st[r]);
1718 tot += v;
1719 if (is_miss[r]) miss += v;
1720 else if (!is_hit[r]) other += v;
1721 }
1722 // ONE JOB READS AT A TIME. The single exception is the state a
1723 // completing fetch lands in: the miss-class job that was the fetch,
1724 // together with the delayed hits it released in the same transition.
1725 if (!(tot <= 1 || (other == 0 && miss <= 1 && tot <= 1 + maxpend))) continue;
1726 bool ok = true;
1727 long nbits = 0;
1728 for (std::size_t i = 0; i < cp.nitems && ok; ++i) {
1729 if (num_traits<T>::to_double(st[R + tcc + i]) == 0) continue;
1730 ++nbits;
1731 // An item cannot be cached and in flight at the same time.
1732 for (std::size_t c = 0; c < tcc; ++c)
1733 if (static_cast<std::size_t>(num_traits<T>::to_double(st[R + c])) == i + 1)
1734 ok = false;
1735 }
1736 if (!ok || nbits > cp.retrieval_capacity) continue;
1737 kept.push_back(st);
1738 }
1739 return kept;
1740 }
1741 return acc;
1742}
1743
1744/**
1745 * Port of `State.fromMarginalAndStarted` at its OWN signature, which indexes by
1746 * NODE rather than by station, mirroring `from_marginal_node`.
1747 *
1748 * A Petri-net element is REFUSED BY NAME, as the reference refuses it: a
1749 * Transition's local state is per MODE, not per class, and a Place holds
1750 * tokens, so neither has a notion of a started job to prescribe. A node that is
1751 * not stateful holds no jobs and contributes no local state.
1752 */
1753template <class T>
1754std::vector<std::vector<T>> from_marginal_node_and_started(
1755 const NetworkStruct<T>& sn, std::size_t ind, const std::vector<std::size_t>& n,
1756 const std::vector<std::size_t>& s, const std::vector<std::size_t>& phases) {
1757 if (ind == 0 || ind > sn.nodes.size())
1758 throw InputError("from_marginal_node_and_started: node index " + std::to_string(ind) +
1759 " is out of range");
1760 const NodeDef& nd = sn.nodes[ind - 1];
1761 if (nd.nodetype == NodeType::Transition || nd.nodetype == NodeType::Place)
1762 throw UnsupportedError(
1763 "from_marginal_node_and_started cannot be used on Petri net elements");
1764 if (nd.station != 0) return from_marginal_and_started(sn, nd.station, n, s, phases);
1765 if (!nd.stateful) return std::vector<std::vector<T>>();
1766 // Every other stateful non-station carries no service split, so the
1767 // marginal builder answers it: a Cache holds its reading job and its
1768 // contents, a stateful ClassSwitch one count per class.
1769 return from_marginal_node(sn, ind, n, phases);
1770}
1771
1772/**
1773 * Port of `State.fromMarg`: the state space with a given TOTAL queue length.
1774 *
1775 * This is the class-summed counterpart of `from_marginal_node`. Where that one
1776 * fixes how many jobs of EACH class the node holds, this one fixes only how
1777 * many it holds ALTOGETHER and returns the union over every class split of
1778 * `ntot` the node can hold.
1779 *
1780 * A class DISABLED at the station has `classcap` 0 and is excluded from the
1781 * split enumeration up front rather than after the fact. Asking
1782 * `from_marginal_node` for a job of such a class yields an EMPTY local space,
1783 * and `space_closed_single(0,1)` returning no rows empties the whole cartesian
1784 * product, so the job would silently disappear -- the trap documented at
1785 * `space_closed_single` above.
1786 *
1787 * `ntot == 0` has the single empty split, which `from_marginal_node` answers
1788 * with the per-discipline empty state; the width is NOT re-derived here.
1789 *
1790 * The buffer is RIGHT-aligned, so sub-spaces of different width are padded on
1791 * the LEFT before they are stacked, exactly as the reference does for the
1792 * reply-block sub-spaces. Rows are then uniqued and reversed, which puts the
1793 * empty state first and the states with jobs in phase 1 earlier.
1794 *
1795 * The twin over BOTH totals is `from_marg_node_started` below.
1796 */
1797template <class T>
1798std::vector<std::vector<T>> from_marg_node(const NetworkStruct<T>& sn, std::size_t ind,
1799 std::size_t ntot,
1800 const std::vector<std::size_t>& phases) {
1801 if (ind == 0 || ind > sn.nodes.size())
1802 throw InputError("from_marg_node: node index " + std::to_string(ind) + " is out of range");
1803 const NodeDef& nd = sn.nodes[ind - 1];
1804 const std::size_t R = sn.nclasses;
1805
1806 // Per-class capacity of the node, unbounded when it is not a station.
1807 std::vector<double> ccap(R, std::numeric_limits<double>::infinity());
1808 if (nd.station != 0 && sn.classcap.size() >= nd.station) {
1809 const std::vector<double>& row = sn.classcap[nd.station - 1];
1810 for (std::size_t r = 0; r < R && r < row.size(); ++r) ccap[r] = row[r];
1811 }
1812
1813 std::vector<std::vector<int>> nset;
1814 if (ntot == 0) {
1815 nset.push_back(std::vector<int>(R, 0));
1816 } else {
1817 const std::vector<std::vector<int>> all =
1818 pfqn::multichoose_rows(static_cast<int>(R), static_cast<int>(ntot));
1819 for (std::size_t j = 0; j < all.size(); ++j) {
1820 bool ok = true;
1821 for (std::size_t r = 0; r < R && ok; ++r)
1822 if (static_cast<double>(all[j][r]) > ccap[r]) ok = false;
1823 if (ok) nset.push_back(all[j]);
1824 }
1825 }
1826
1827 std::vector<std::vector<T>> space;
1828 std::size_t maxw = 0;
1829 std::vector<std::vector<std::vector<T>>> subspaces;
1830 for (std::size_t j = 0; j < nset.size(); ++j) {
1831 std::vector<std::size_t> nj(R, 0);
1832 for (std::size_t r = 0; r < R; ++r) nj[r] = static_cast<std::size_t>(nset[j][r]);
1833 const std::vector<std::vector<T>> sj = from_marginal_node(sn, ind, nj, phases);
1834 if (sj.empty()) continue;
1835 for (std::size_t a = 0; a < sj.size(); ++a) maxw = std::max(maxw, sj[a].size());
1836 subspaces.push_back(sj);
1837 }
1838 for (std::size_t j = 0; j < subspaces.size(); ++j)
1839 for (std::size_t a = 0; a < subspaces[j].size(); ++a) {
1840 std::vector<T> row = subspaces[j][a];
1841 if (row.size() < maxw)
1842 row.insert(row.begin(), maxw - row.size(), num_traits<T>::from_int(0));
1843 space.push_back(row);
1844 }
1845 if (space.empty()) return space;
1846
1847 std::sort(space.begin(), space.end());
1848 space.erase(std::unique(space.begin(), space.end()), space.end());
1849 std::reverse(space.begin(), space.end());
1850 return space;
1851}
1852
1853namespace state_detail {
1854
1855/**
1856 * Port of `multichoosecon.m`: the ways to draw `S` units from the availability
1857 * vector `n`.
1858 *
1859 * Unlike `multichoose_rows`, the count drawn from category r is capped by
1860 * `n[r]`, so the enumeration never proposes a job of a class the station does
1861 * not hold -- which is what keeps `from_marg_node_started` from asking the
1862 * per-class builder for a state that cannot exist.
1863 */
1864inline std::vector<std::vector<std::size_t> > multichoosecon(const std::vector<std::size_t>& n,
1865 std::size_t S) {
1866 const std::size_t R = n.size();
1867 std::vector<std::vector<std::size_t> > out;
1868 if (R == 0) return out;
1869 if (S == 0) {
1870 out.push_back(std::vector<std::size_t>(R, 0));
1871 return out;
1872 }
1873 if (S == 1) {
1874 for (std::size_t i = 0; i < R; ++i)
1875 if (n[i] > 0) {
1876 std::vector<std::size_t> row(R, 0);
1877 row[i] = 1;
1878 out.push_back(row);
1879 }
1880 return out;
1881 }
1882 for (std::size_t i = 0; i < R; ++i) {
1883 if (n[i] == 0) continue;
1884 std::vector<std::size_t> n1 = n;
1885 --n1[i];
1886 const std::vector<std::vector<std::size_t> > tail = multichoosecon(n1, S - 1);
1887 for (std::size_t k = 0; k < tail.size(); ++k) {
1888 std::vector<std::size_t> row = tail[k];
1889 ++row[i];
1890 out.push_back(row);
1891 }
1892 }
1893 return out;
1894}
1895
1896} // namespace state_detail
1897
1898/**
1899 * Port of `State.fromMargAndStarted`: the states with a given TOTAL queue
1900 * length AND a given TOTAL number of started jobs.
1901 *
1902 * Where `from_marginal_node_and_started` takes one per-class occupancy and one
1903 * per-class started vector and builds ONE row, this takes only the two totals
1904 * and returns the union of that row over every pair consistent with them:
1905 * `sum(n) == ntot`, `sum(s) == stot`, and `s <= n` elementwise.
1906 *
1907 * The started counts are drawn with `multichoosecon` from the jobs actually
1908 * present rather than from `multichoose_rows` over all classes, so a split is
1909 * never proposed that puts more of a class in service than the station holds.
1910 * Classes disabled at the station are excluded through `classcap` for the
1911 * reason documented on `from_marg_node`: an empty local space is ABSORBED by
1912 * the cartesian product instead of annihilating it, so the job would silently
1913 * disappear rather than the state being rejected.
1914 *
1915 * Widths are unified on the LEFT and the rows uniqued and reversed, exactly as
1916 * `from_marg_node` does, since the buffer is right-aligned.
1917 */
1918template <class T>
1919std::vector<std::vector<T>> from_marg_node_started(const NetworkStruct<T>& sn, std::size_t ind,
1920 std::size_t ntot, std::size_t stot,
1921 const std::vector<std::size_t>& phases) {
1922 if (ind == 0 || ind > sn.nodes.size())
1923 throw InputError("from_marg_node_started: node index " + std::to_string(ind) +
1924 " is out of range");
1925 std::vector<std::vector<T>> space;
1926 if (stot > ntot) return space;
1927
1928 const NodeDef& nd = sn.nodes[ind - 1];
1929 const std::size_t R = sn.nclasses;
1930
1931 std::vector<double> ccap(R, std::numeric_limits<double>::infinity());
1932 if (nd.station != 0 && sn.classcap.size() >= nd.station) {
1933 const std::vector<double>& row = sn.classcap[nd.station - 1];
1934 for (std::size_t r = 0; r < R && r < row.size(); ++r) ccap[r] = row[r];
1935 }
1936
1937 std::vector<std::vector<int>> nset;
1938 if (ntot == 0) {
1939 nset.push_back(std::vector<int>(R, 0));
1940 } else {
1941 const std::vector<std::vector<int>> all =
1942 pfqn::multichoose_rows(static_cast<int>(R), static_cast<int>(ntot));
1943 for (std::size_t j = 0; j < all.size(); ++j) {
1944 bool ok = true;
1945 for (std::size_t r = 0; r < R && ok; ++r)
1946 if (static_cast<double>(all[j][r]) > ccap[r]) ok = false;
1947 if (ok) nset.push_back(all[j]);
1948 }
1949 }
1950
1951 std::size_t maxw = 0;
1952 std::vector<std::vector<std::vector<T>>> subspaces;
1953 for (std::size_t j = 0; j < nset.size(); ++j) {
1954 std::vector<std::size_t> nj(R, 0);
1955 for (std::size_t r = 0; r < R; ++r) nj[r] = static_cast<std::size_t>(nset[j][r]);
1956 const std::vector<std::vector<std::size_t> > sset =
1957 state_detail::multichoosecon(nj, stot);
1958 for (std::size_t k = 0; k < sset.size(); ++k) {
1959 const std::vector<std::vector<T>> sjk =
1960 from_marginal_node_and_started(sn, ind, nj, sset[k], phases);
1961 if (sjk.empty()) continue;
1962 for (std::size_t a = 0; a < sjk.size(); ++a) maxw = std::max(maxw, sjk[a].size());
1963 subspaces.push_back(sjk);
1964 }
1965 }
1966 for (std::size_t j = 0; j < subspaces.size(); ++j)
1967 for (std::size_t a = 0; a < subspaces[j].size(); ++a) {
1968 std::vector<T> row = subspaces[j][a];
1969 if (row.size() < maxw)
1970 row.insert(row.begin(), maxw - row.size(), num_traits<T>::from_int(0));
1971 space.push_back(row);
1972 }
1973 if (space.empty()) return space;
1974
1975 std::sort(space.begin(), space.end());
1976 space.erase(std::unique(space.begin(), space.end()), space.end());
1977 std::reverse(space.begin(), space.end());
1978 return space;
1979}
1980
1981/**
1982 * The FIRST row `from_marginal_node` emits, BUILT rather than enumerated.
1983 *
1984 * Every caller that seeds a sample path takes row 0 and discards the rest, and
1985 * for a Cache the rest is the ordered placement of distinct items into its
1986 * slots -- (nitems+1)*nitems*(nitems-1)*... rows. Materializing that to read one
1987 * row's contents is what took SolverSSA on tut06_cache_lru_zipf (1000 items, 50
1988 * slots) to 12 GB and an OOM kill of the whole host; the peak is invariant in
1989 * `samples` precisely because the walk itself is lazy and only the SEED costs
1990 * this. A simulator must not pay an exact solver's enumeration to start.
1991 *
1992 * Row 0 holds items 1..tcc in slot order, by construction and not by choice:
1993 * `cartesian` emits `a[0] ++ b[0]`, the contents loop offers the lowest
1994 * admissible item first at every slot and its duplicate test rejects the ones
1995 * already placed, and the delayed-hit occupancy bits start clear. So the row is
1996 * the per-class counts, the first `tcc` items, then zeros -- bit for bit what
1997 * the enumeration returned, which is what lets the CTMC find its declared
1998 * initial state in the space. A cache with more slots than items cannot fill
1999 * and starts empty, as the enumeration also has it. Every other node kind
2000 * delegates, its first row already being cheap.
2001 *
2002 * @return false when the node admits no state at all, as an empty return does
2003 */
2004template <class T>
2005bool from_marginal_node_first(const NetworkStruct<T>& sn, std::size_t ind,
2006 const std::vector<std::size_t>& n,
2007 const std::vector<std::size_t>& phases, std::vector<T>& out) {
2008 if (ind == 0 || ind > sn.nodes.size())
2009 throw InputError("from_marginal_node_first: node index " + std::to_string(ind) +
2010 " is out of range");
2011 const NodeDef& nd = sn.nodes[ind - 1];
2012 if (nd.station != 0 || !nd.stateful || nd.nodetype != NodeType::Cache) {
2013 const std::vector<std::vector<T> > rows = from_marginal_node(sn, ind, n, phases);
2014 if (rows.empty()) return false;
2015 out = rows[0];
2016 return true;
2017 }
2018
2019 const std::size_t R = sn.nclasses;
2020 if (n.size() != R)
2021 throw InputError("from_marginal_node_first: n must have one entry per class");
2022 out.clear();
2023 for (std::size_t r = 0; r < R; ++r) {
2024 const std::vector<std::vector<T> > sr = space_closed_single<T>(1, n[r]);
2025 // an absent placement drops the class's column, exactly as the fold does
2026 if (sr.empty()) {
2027 if (n[r] > 0) return false;
2028 continue;
2029 }
2030 out.insert(out.end(), sr[0].begin(), sr[0].end());
2031 }
2032
2033 const typename std::map<std::size_t, CacheParam<T> >::const_iterator ci =
2034 sn.nodeparam.find(ind);
2035 if (ci == sn.nodeparam.end()) return true;
2036 const CacheParam<T>& cp = ci->second;
2037 std::size_t tcc = 0;
2038 for (std::size_t u = 0; u < cp.itemcap.size(); ++u)
2039 if (cp.itemcap[u] > 0) tcc += static_cast<std::size_t>(cp.itemcap[u]);
2040 for (std::size_t slot = 0; slot < tcc; ++slot)
2041 out.push_back(num_traits<T>::from_int(
2042 tcc <= cp.nitems ? static_cast<long>(slot + 1) : 0L));
2043 if (cp.retrieval_capacity > 0) {
2044 out.insert(out.end(), cp.nitems, num_traits<T>::from_int(0));
2045 std::vector<std::size_t> rcl, rci, rco;
2046 cache_retrieval_class_map(cp, rcl, rci, rco);
2047 out.insert(out.end(), rcl.size(), num_traits<T>::from_int(0));
2048 }
2049 return true;
2050}
2051
2052/**
2053 * Port of `State.spaceClosedMulti`: how N[r] class-r jobs distribute over M
2054 * stateful nodes, for every class, as the cartesian fold of the per-class
2055 * placements. Column block r holds class r's counts, M columns wide.
2056 */
2057template <class T>
2058std::vector<std::vector<T>> space_closed_multi(
2059 std::size_t M, const std::vector<std::size_t>& N,
2060 const std::vector<std::vector<long>>& caps = std::vector<std::vector<long>>()) {
2061 std::vector<std::vector<T>> ss;
2062 for (std::size_t r = 0; r < N.size(); ++r) {
2063 const std::vector<std::vector<T>> sr =
2064 r < caps.size() && !caps[r].empty() ? space_closed_single_capped<T>(M, N[r], caps[r])
2065 : space_closed_single<T>(M, N[r]);
2066 if (sr.empty()) return std::vector<std::vector<T>>();
2067 ss = r == 0 ? sr : cartesian(ss, sr);
2068 }
2069 return ss;
2070}
2071
2072/**
2073 * Port of `State.spaceClosedMultiCS`: the same, but a CHAIN's population is
2074 * shared among its classes, so the split between them is itself enumerated.
2075 *
2076 * Class switching moves a job between classes of one chain, so only the CHAIN
2077 * total is invariant. Enumerating over per-class populations alone would fix a
2078 * split the model does not fix, and drop every state reachable by a switch.
2079 *
2080 * @param M number of stateful nodes (sources excluded by the caller)
2081 * @param N per-class populations
2082 * @param chains chains[c][r] true when class r belongs to chain c
2083 */
2084template <class T>
2085std::vector<std::vector<T>> space_closed_multi_cs(
2086 std::size_t M, const std::vector<std::size_t>& N,
2087 const std::vector<std::vector<bool>>& chains,
2088 const std::vector<std::vector<long>>& caps = std::vector<std::vector<long>>()) {
2089 const std::size_t C = chains.size();
2090 const std::size_t R = N.size();
2091 // Per chain, the ways its total splits across the classes it contains.
2092 std::vector<std::vector<std::vector<int>>> chainInitPos(C);
2093 std::vector<std::size_t> inchain_sz(C, 0);
2094 for (std::size_t c = 0; c < C; ++c) {
2095 std::size_t tot = 0, k = 0;
2096 for (std::size_t r = 0; r < R; ++r)
2097 if (chains[c][r]) { tot += N[r]; ++k; }
2098 inchain_sz[c] = k;
2099 chainInitPos[c] = k == 0 ? std::vector<std::vector<int>>{std::vector<int>()}
2100 : pfqn::multichoose_rows(static_cast<int>(k), static_cast<int>(tot));
2101 }
2102 std::vector<std::vector<T>> ss;
2103 std::vector<std::size_t> v(C, 0);
2104 for (;;) {
2105 std::vector<std::size_t> subN;
2106 subN.reserve(R);
2107 for (std::size_t c = 0; c < C; ++c) {
2108 const std::vector<int>& row = chainInitPos[c][v[c]];
2109 for (std::size_t j = 0; j < row.size(); ++j)
2110 subN.push_back(static_cast<std::size_t>(row[j]));
2111 }
2112 // THE SPLIT IS SCATTERED BACK ONTO GLOBAL CLASS POSITIONS before the
2113 // per-slot caps are applied: `subN` is built chain by chain, so its
2114 // order is the concatenation of the chains, not the class order `caps`
2115 // is indexed by.
2116 std::vector<std::size_t> subNg(R, 0);
2117 {
2118 std::size_t k = 0;
2119 for (std::size_t c = 0; c < C; ++c)
2120 for (std::size_t r = 0; r < R; ++r)
2121 if (chains[c][r]) subNg[r] = subN[k++];
2122 }
2123 const std::vector<std::vector<T>> blk =
2124 caps.empty() ? space_closed_multi<T>(M, subN)
2125 : space_closed_multi<T>(M, subNg, caps);
2126 ss.insert(ss.end(), blk.begin(), blk.end());
2127 // odometer over the per-chain splits
2128 std::size_t c = 0;
2129 for (; c < C; ++c) {
2130 if (++v[c] < chainInitPos[c].size()) break;
2131 v[c] = 0;
2132 }
2133 if (c == C) break;
2134 }
2135 return ss;
2136}
2137
2138/** One network state: the per-stateful-node local rows it is composed of. */
2139template <class T>
2140struct NetState {
2141 std::vector<std::vector<T>> local; ///< local[isf] is that node's state row
2142};
2143
2144/**
2145 * Port of `State.initialOccupancy`: the class-r jobs node `ind` holds in the
2146 * DECLARED initial state, or 0 when there is none.
2147 *
2148 * Only a Place answers nonzero. Every other node type encodes its local state
2149 * differently, so column r there is not an occupancy -- and a station whose
2150 * visit ratio is zero is genuinely never entered, while a Place with no input
2151 * arc is a TRANSIENT state of the chain rather than an absent one.
2152 */
2153template <class T>
2154std::size_t state_initial_occupancy(const NetworkStruct<T>& sn, std::size_t ind, std::size_t r) {
2155 if (ind == 0 || ind > sn.nodes.size()) return 0;
2156 if (sn.nodes[ind - 1].nodetype != NodeType::Place) return 0;
2157 const typename std::map<std::size_t, std::vector<T>>::const_iterator it =
2158 sn.initmarking.find(ind);
2159 if (it == sn.initmarking.end() || r >= it->second.size()) return 0;
2160 const double v = num_traits<T>::to_double(it->second[r]);
2161 return v > 0 ? static_cast<std::size_t>(v) : 0;
2162}
2163
2164/**
2165 * Port of the `capacityc` table of `State.spaceGeneratorNodes`: the largest
2166 * class-r marginal node `ind` may hold in the enumerated space.
2167 *
2168 * WITHOUT THIS TABLE THE LATTICE IS PLACED BLIND. `space_closed_multi_cs`
2169 * spreads a class's population over EVERY stateful node, including the ones the
2170 * class never visits, so a mixed model enumerates open jobs at a closed-only
2171 * station and closed jobs at an open-only one. Those states are unreachable, but
2172 * they are not free: they enlarge the generator, and on `mqn_multiserver_fcfs`
2173 * (1904 states against the reference's 1304) they moved the Source's departure
2174 * rate from 0.24763 to 0.26040 -- an arrival rate the model never offers.
2175 *
2176 * The bound is the reference's, in its order: a class that does not visit the
2177 * node is capped at its DECLARED initial occupancy (nonzero only for an SPN
2178 * Place, which may hold tokens at time zero without ever being re-entered), a
2179 * disabled service at 0, an open class at its cutoff and a closed one at its
2180 * chain population, both cut down by the station's per-class buffer and then by
2181 * any finite-capacity region the station belongs to.
2182 *
2183 * @param sn the network struct
2184 * @param cutoff per-class population bound for open classes
2185 * @param cutoff_mat optional (nstations x nclasses) bound, the reference's
2186 * matrix `options.cutoff`; overrides `cutoff` per station
2187 * @return (nnodes x nclasses) capacities, 0-based in both indices
2188 */
2189template <class T>
2190std::vector<std::vector<std::size_t>> space_capacity_c(
2191 const NetworkStruct<T>& sn, const std::vector<std::size_t>& cutoff,
2192 const std::vector<std::vector<std::size_t>>& cutoff_mat =
2193 std::vector<std::vector<std::size_t>>()) {
2194 const std::size_t R = sn.nclasses, N = sn.nodes.size();
2195 std::vector<std::vector<std::size_t>> cap(N, std::vector<std::size_t>(R, 0));
2196 const std::vector<double> njobs = sn.njobs();
2197
2198 // maxPending mirrors `State.spaceGeneratorNodes`: a completing fetch
2199 // releases every merged secondary request in one immediate transition.
2200 std::size_t maxpend = 0;
2201 for (std::size_t r = 0; r < R; ++r)
2202 if (!std::isfinite(njobs[r])) maxpend = std::max(maxpend, cutoff[r]);
2203 if (maxpend > 0) --maxpend;
2204
2205 for (std::size_t ind = 1; ind <= N; ++ind) {
2206 const NodeDef& nd = sn.nodes[ind - 1];
2207 if (!nd.stateful) continue;
2208 const std::size_t ist = nd.station;
2209 const std::size_t isf = sn.stateful_index(ind);
2210 if (ist != 0 && nd.nodetype != NodeType::Source) {
2211 for (std::size_t r = 0; r < R; ++r) {
2212 std::size_t c = R; // r's chain, or R when the struct carries none
2213 for (std::size_t cc = 0; cc < sn.chains.size(); ++cc)
2214 if (r < sn.chains[cc].size() && sn.chains[cc][r]) { c = cc; break; }
2215 const bool novisit = c < sn.visits.size() && sn.visits[c].rows() != 0 &&
2216 isf != 0 && isf - 1 < sn.visits[c].rows() &&
2217 num_traits<T>::to_double(sn.visits[c](isf - 1, r)) == 0;
2218 if (novisit) {
2219 cap[ind - 1][r] = state_initial_occupancy(sn, ind, r);
2220 continue;
2221 }
2222 if (nd.nodetype != NodeType::Place && ist - 1 < sn.disabled.size() &&
2223 r < sn.disabled[ist - 1].size() && sn.disabled[ist - 1][r]) {
2224 cap[ind - 1][r] = 0;
2225 continue;
2226 }
2227 double b;
2228 if (!std::isfinite(njobs[r])) {
2229 // `capacityc(ind,r) = min(cutoff(ist,r), classcap(ist,r))`:
2230 // the reference's cutoff is a (station x class) MATRIX
2231 // wherever a model needs a different truncation per station,
2232 // and only the uniform case collapses to one number.
2233 b = static_cast<double>(
2234 (!cutoff_mat.empty() && ist - 1 < cutoff_mat.size() &&
2235 r < cutoff_mat[ist - 1].size())
2236 ? cutoff_mat[ist - 1][r]
2237 : cutoff[r]);
2238 } else {
2239 b = 0.0; // the whole CHAIN's population may sit here
2240 for (std::size_t k = 0; k < R; ++k)
2241 if (c < sn.chains.size() && k < sn.chains[c].size() && sn.chains[c][k] &&
2242 std::isfinite(njobs[k]))
2243 b += njobs[k];
2244 }
2245 if (ist - 1 < sn.classcap.size() && r < sn.classcap[ist - 1].size())
2246 b = std::min(b, sn.classcap[ist - 1][r]);
2247 if (b > 0)
2248 for (std::size_t f = 0; f < sn.regions.size(); ++f) {
2249 const typename NetworkStruct<T>::Region& rg = sn.regions[f];
2250 if (ist - 1 >= rg.cap.size()) continue;
2251 if (r < rg.cap[ist - 1].size() && rg.cap[ist - 1][r] >= 0)
2252 b = std::min(b, rg.cap[ist - 1][r]);
2253 if (R < rg.cap[ist - 1].size() && rg.cap[ist - 1][R] >= 0)
2254 b = std::min(b, rg.cap[ist - 1][R]);
2255 }
2256 if (!(b > 0)) b = 0;
2257 cap[ind - 1][r] = std::isfinite(b) ? static_cast<std::size_t>(b)
2258 : static_cast<std::size_t>(-1);
2259 }
2260 continue;
2261 }
2262 switch (nd.nodetype) {
2263 case NodeType::Cache: {
2264 for (std::size_t r = 0; r < R; ++r) cap[ind - 1][r] = 1;
2265 const typename std::map<std::size_t, CacheParam<T>>::const_iterator ci =
2266 sn.nodeparam.find(ind);
2267 if (ci != sn.nodeparam.end() && ci->second.retrieval_capacity > 0)
2268 for (std::size_t r = 0; r < R; ++r)
2269 for (std::size_t k = 0; k < ci->second.hitclass.size(); ++k)
2270 if (ci->second.hitclass[k] == r + 1) cap[ind - 1][r] = 1 + maxpend;
2271 break;
2272 }
2273 case NodeType::Router:
2274 for (std::size_t r = 0; r < R; ++r) {
2275 std::size_t c = R;
2276 for (std::size_t cc = 0; cc < sn.chains.size(); ++cc)
2277 if (r < sn.chains[cc].size() && sn.chains[cc][r]) { c = cc; break; }
2278 cap[ind - 1][r] =
2279 (c < sn.nodevisits.size() && sn.nodevisits[c].rows() != 0 &&
2280 ind - 1 < sn.nodevisits[c].rows() &&
2281 num_traits<T>::to_double(sn.nodevisits[c](ind - 1, r)) > 0)
2282 ? 1
2283 : 0;
2284 }
2285 break;
2286 default:
2287 // A Transition holds no class-indexed job, and a Source is an
2288 // infinite reservoir the lattice never places into.
2289 for (std::size_t r = 0; r < R; ++r)
2290 cap[ind - 1][r] = static_cast<std::size_t>(-1);
2291 break;
2292 }
2293 }
2294 return cap;
2295}
2296
2297/**
2298 * Port of `State.spaceGenerator`: every network state, reachable or not.
2299 *
2300 * The population lattice is walked with `space_closed_multi_cs`, and each
2301 * lattice row is expanded per node through `from_marginal`; the network states
2302 * are the cartesian product of those local spaces. An open class has no finite
2303 * population, so `cutoff` bounds it -- WITHOUT a cutoff the lattice is infinite
2304 * and the reference errors rather than truncating silently.
2305 *
2306 * @param sn the network struct
2307 * @param cutoff per-class population bound for open classes
2308 * @param maxst refuse beyond this many states (the reference's ctmc_max_states)
2309 * @param cutoff_mat optional (nstations x nclasses) bound, the reference's
2310 * matrix `options.cutoff`
2311 */
2312template <class T>
2313std::vector<NetState<T>> space_generator(
2314 const NetworkStruct<T>& sn, const std::vector<std::size_t>& cutoff,
2315 std::size_t maxst = 3000000,
2316 const std::vector<std::vector<std::size_t>>& cutoff_mat =
2317 std::vector<std::vector<std::size_t>>()) {
2318 const std::size_t R = sn.nclasses;
2319 if (cutoff.size() != R)
2320 throw InputError("space_generator: cutoff must have one entry per class");
2321
2322 // Closed classes carry their own population; open ones are bounded by the
2323 // cutoff. A missing cutoff on an open class is an error in the reference,
2324 // because a silently truncated lattice yields a state space that looks
2325 // complete and is not.
2326 // `njobs()` is a population COUNT, always a plain double; it is not carried
2327 // in T, so reading it as a vector<T> fails to compile on every backend but
2328 // double -- which is how this survived until SolverCTMC was instantiated at
2329 // higher precision.
2330 const std::vector<double> njobs = sn.njobs();
2331 std::vector<std::size_t> Np(R, 0);
2332 for (std::size_t r = 0; r < R; ++r) {
2333 const double nj = njobs[r];
2334 if (std::isfinite(nj)) {
2335 Np[r] = static_cast<std::size_t>(nj);
2336 } else {
2337 if (cutoff[r] == 0)
2338 throw InputError(
2339 "space_generator: class " + std::to_string(r) +
2340 " is open, so its population is unbounded; supply a cutoff for it");
2341 Np[r] = cutoff[r];
2342 }
2343 }
2344
2345 // The per-node bound the reference places the lattice under. An open class's
2346 // lattice height is then the LARGEST capacity any node grants it, not the raw
2347 // cutoff: levels above that produce only rows every node refuses.
2348 const std::vector<std::vector<std::size_t>> capc = space_capacity_c(sn, cutoff, cutoff_mat);
2349 const std::size_t unbounded = static_cast<std::size_t>(-1);
2350 for (std::size_t r = 0; r < R; ++r) {
2351 if (std::isfinite(njobs[r])) continue;
2352 std::size_t hi = 0;
2353 bool any_unbounded = false;
2354 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
2355 if (!sn.nodes[ind - 1].stateful || sn.nodes[ind - 1].nodetype == NodeType::Source)
2356 continue;
2357 if (capc[ind - 1][r] == unbounded) any_unbounded = true;
2358 else hi = std::max(hi, capc[ind - 1][r]);
2359 }
2360 if (!any_unbounded) Np[r] = std::min(Np[r], hi);
2361 }
2362
2363 std::vector<std::vector<bool>> chains = sn.chains;
2364 if (chains.empty()) { // no class switching: each class is its own chain
2365 chains.assign(R, std::vector<bool>(R, false));
2366 for (std::size_t r = 0; r < R; ++r) chains[r][r] = true;
2367 }
2368
2369 // The lattice is over the STATEFUL NODES, not the stations: a Transition or
2370 // a Cache is stateful without being a station, and iterating stations drops
2371 // its local block entirely. Sources hold no jobs, so they take no column.
2372 const std::vector<std::size_t>& sfn = sn.stateful_nodes;
2373 const std::size_t NF = sfn.size();
2374 std::vector<std::size_t> lat_col(NF, static_cast<std::size_t>(-1));
2375 std::size_t Mp = 0;
2376 for (std::size_t k = 0; k < NF; ++k)
2377 if (sn.nodes[sfn[k] - 1].nodetype != NodeType::Source) lat_col[k] = Mp++;
2378
2379 // The population LATTICE, not just its top row. An open class has no fixed
2380 // population, so every level from 0 to its cutoff is a distinct set of
2381 // states; only a CLOSED class is pinned to its own N. Enumerating the top
2382 // row alone yields a chain that can never empty -- for an M/M/1/K it left
2383 // exactly one state, and a one-state generator is trivially valid, which is
2384 // why the closed-model test could not detect this.
2385 std::vector<bool> is_open(R, false);
2386 for (std::size_t r = 0; r < R; ++r)
2387 is_open[r] = !std::isfinite(njobs[r]);
2388
2389 // THE LATTICE IS CAPACITY-BOUND, not just the per-node local rows. `capc`
2390 // already zeroes a (node, class) pair the class never visits, and the
2391 // composition below drops the rows that breach it -- but it drops them ONE
2392 // AT A TIME, after `space_closed_multi_cs` has spread every class over every
2393 // slot. On the class-switching chain Source->Q1(A)->Q2(B)->Q3(C) that is
2394 // (cutoff+1)^(3*3) candidates for (cutoff+1)^3 reachable states, and the
2395 // rejection is not free: each candidate costs a `from_marginal`.
2396 // `lat_caps[class][slot]` is `capc` in the SLOT order the lattice row uses.
2397 std::vector<std::vector<long>> lat_caps(R, std::vector<long>(Mp, -1));
2398 {
2399 bool ok = Mp > 0;
2400 for (std::size_t k = 0; k < NF && ok; ++k) {
2401 if (lat_col[k] == static_cast<std::size_t>(-1)) continue;
2402 const std::size_t ind = sfn[k];
2403 for (std::size_t r = 0; r < R; ++r) {
2404 const std::size_t cap = capc[ind - 1][r];
2405 lat_caps[r][lat_col[k]] =
2406 cap == unbounded ? -1 : static_cast<long>(cap);
2407 }
2408 }
2409 if (!ok) lat_caps.clear();
2410 }
2411 std::vector<std::vector<T>> pos;
2412 std::vector<std::size_t> nlev(R, 0);
2413 for (;;) {
2414 bool admissible = true;
2415 for (std::size_t r = 0; r < R; ++r)
2416 if (!is_open[r] && nlev[r] != Np[r]) { admissible = false; break; }
2417 if (admissible) {
2418 const std::vector<std::vector<T>> part =
2419 space_closed_multi_cs<T>(Mp, nlev, chains, lat_caps);
2420 pos.insert(pos.end(), part.begin(), part.end());
2421 if (pos.size() > maxst)
2422 throw UnsupportedError(
2423 "space_generator: the population lattice exceeds the cap of " +
2424 std::to_string(maxst) + " states; raise it or use another solver");
2425 }
2426 // Mixed-radix increment over the per-class levels, the reference's pprod.
2427 std::size_t r = 0;
2428 for (; r < R; ++r) {
2429 if (nlev[r] < Np[r]) { ++nlev[r]; break; }
2430 nlev[r] = 0;
2431 }
2432 if (r == R) break;
2433 }
2434 // Distinct levels can yield the same lattice row when a class is absent
2435 // from it, so drop the duplicates the reference removes with `unique`.
2436 std::sort(pos.begin(), pos.end());
2437 pos.erase(std::unique(pos.begin(), pos.end()), pos.end());
2438 // Per node and per lattice row, the local rows -- collected BEFORE any
2439 // composition because their width is not uniform. A marginal of 3 jobs at
2440 // an FCFS station needs two buffer columns where a marginal of 2 needs one,
2441 // and the reference unifies the two by RIGHT-ALIGNING the narrower row into
2442 // the widest (`fromMarginalBounds`), padding zeros on the left. Composing
2443 // before padding leaves an arrival unable to match its own successor,
2444 // because the successor is a wider vector and no index lookup can find it.
2445 std::vector<std::vector<std::vector<std::vector<T>>>> allper(pos.size());
2446 std::vector<bool> row_ok(pos.size(), true);
2447 std::vector<std::size_t> maxw(NF, 0);
2448 std::vector<NetState<T>> out;
2449 for (std::size_t j = 0; j < pos.size(); ++j) {
2450 std::vector<std::vector<std::vector<T>>> per(NF);
2451 bool ok = true;
2452 for (std::size_t k = 0; k < NF && ok; ++k) {
2453 const std::size_t ind = sfn[k];
2454 const std::size_t ist = sn.nodes[ind - 1].station;
2455 std::vector<std::size_t> ph(R, 1), nmarg(R, 0);
2456 // phases_of takes a 1-BASED class index, as elsewhere in NetworkStruct.
2457 if (ist != 0)
2458 for (std::size_t r = 0; r < R; ++r) ph[r] = sn.phasessz_of(ist, r + 1);
2459 if (lat_col[k] == static_cast<std::size_t>(-1)) {
2460 // A Source is an infinite reservoir: one job per class in
2461 // service, no marginal of its own -- but ONLY for the classes it
2462 // actually generates. A class the Source does not generate has a
2463 // disabled arrival, so phases_of is 0, and demanding one job of
2464 // it makes space_closed_single(0, 1) return nothing: the Source
2465 // yields no local row and the WHOLE state space comes out empty.
2466 // Invisible on a single-class open model, because its one class
2467 // is generated; a Source-Cache-Sink model, whose Hit and Miss
2468 // classes exist only downstream, hits it immediately.
2469 for (std::size_t r = 0; r < R; ++r)
2470 if (!sn.disabled[ist - 1][r]) nmarg[r] = 1;
2471 } else {
2472 for (std::size_t r = 0; r < R; ++r)
2473 nmarg[r] = static_cast<std::size_t>(
2474 num_traits<T>::to_double(pos[j][r * Mp + lat_col[k]]));
2475 // `any(stateMarg_i > capacityc(ind,:))` of the reference: the node
2476 // yields NO row, so the whole lattice row is dropped by the
2477 // cartesian product below.
2478 for (std::size_t r = 0; r < R && ok; ++r)
2479 if (capc[ind - 1][r] != static_cast<std::size_t>(-1) &&
2480 nmarg[r] > capc[ind - 1][r])
2481 ok = false;
2482 if (!ok) break;
2483 }
2484 per[k] = from_marginal_node(sn, ind, nmarg, ph);
2485 if (per[k].empty()) ok = false;
2486 for (std::size_t b = 0; b < per[k].size(); ++b)
2487 maxw[k] = std::max(maxw[k], per[k][b].size());
2488 }
2489 row_ok[j] = ok;
2490 allper[j].swap(per);
2491 }
2492
2493 for (std::size_t j = 0; j < pos.size(); ++j) {
2494 if (!row_ok[j]) continue;
2495 std::vector<std::vector<std::vector<T>>>& per = allper[j];
2496 // Right-align every row into the node's widest, as the reference does.
2497 for (std::size_t k = 0; k < NF; ++k)
2498 for (std::size_t b = 0; b < per[k].size(); ++b)
2499 if (per[k][b].size() < maxw[k])
2500 per[k][b].insert(per[k][b].begin(), maxw[k] - per[k][b].size(),
2502 // Cartesian product across nodes, carrying the per-node rows.
2503 std::vector<NetState<T>> acc(1);
2504 for (std::size_t i = 0; i < NF; ++i) {
2505 std::vector<NetState<T>> next;
2506 for (std::size_t a = 0; a < acc.size(); ++a)
2507 for (std::size_t b = 0; b < per[i].size(); ++b) {
2508 NetState<T> ns = acc[a];
2509 ns.local.push_back(per[i][b]);
2510 next.push_back(ns);
2511 }
2512 acc.swap(next);
2513 if (out.size() + acc.size() > maxst)
2514 throw UnsupportedError(
2515 "space_generator: the state space exceeds the cap of " +
2516 std::to_string(maxst) + " states; raise it or use another solver");
2517 }
2518 out.insert(out.end(), acc.begin(), acc.end());
2519 }
2520 // The same network state can be produced by two lattice rows once the
2521 // widths are unified, so drop duplicates as the reference's `unique` does.
2522 std::vector<std::vector<std::vector<T>>> seen;
2523 std::vector<NetState<T>> uniq;
2524 for (std::size_t i = 0; i < out.size(); ++i) {
2525 bool dup = false;
2526 for (std::size_t p = 0; p < seen.size() && !dup; ++p)
2527 if (seen[p] == out[i].local) dup = true;
2528 if (dup) continue;
2529 seen.push_back(out[i].local);
2530 uniq.push_back(out[i]);
2531 }
2532 return uniq;
2533}
2534
2535} // namespace qn
2536} // namespace line
2537
2538#endif // LINE_LANG_QN_STATE_H
InputError(const std::string &what)
Definition error.h:39
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:373
@ EXHAUSTIVE
serve until the queue empties
Definition lang_types.h:372
@ GATED
serve exactly the jobs present at the polling instant
Definition lang_types.h:371
@ DECREMENTING
serve until the queue is one shorter than at arrival
Definition lang_types.h:374
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:483
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:1141
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:2313
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:1473
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:2005
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:2154
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:1798
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:1919
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:1282
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:1754
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:2190
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:1446
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:2085
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:2058
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
static constexpr double MaxInt
Stand-in for an unbounded COUNT, MATLAB GlobalConstants.MaxInt.
Definition lang_types.h:679
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:2140
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2141
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