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