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