LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_waitq.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_SOLVERS_CTMC_SOLVER_CTMC_WAITQ_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_WAITQ_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_ctmc_fcr_waitq.m`: the reachability-built generator of a model
12 * whose finite capacity region applies WAITQ.
13 *
14 * WHY THIS IS A SEPARATE GENERATOR AND NOT A FILTER. Under DROP a refused job is
15 * destroyed, so the chain never occupies a forbidden state and censoring the
16 * enumerated space IS the censored chain -- that is `solver_ctmc_fcr.h`. Under
17 * WAITQ the refused job LEAVES its upstream station and parks in a per-region
18 * FIFO of (class, destination) tokens that sits outside every station. That FIFO
19 * is state, it is owned by the region rather than by any node, and no filter on
20 * the per-node state space can represent it. The state here is therefore
21 * augmented as [per-node states, buf_1, ..., buf_F] and the transition relation
22 * is rebuilt around it.
23 *
24 * THE FOUR RULES THAT MAKE WAITQ WHAT IT IS, all from the JMT reference:
25 *
26 * 1. RELEASE IS STRICTLY HEAD-OF-LINE. After every transition that frees
27 * capacity, tokens leave in FIFO order and a head that still does not fit
28 * blocks the whole queue behind it, even where a token further back would
29 * fit. Releasing the first token that fits instead would be a different
30 * discipline with a different mean.
31 * 2. A FRESH ARRIVAL IS NOT QUEUED BEHIND THE FIFO. It is admitted whenever the
32 * constraints permit, so it overtakes a head stuck on a different
33 * constraint. The FIFO gates only the jobs already in it.
34 * 3. THE RELEASE CASCADE IS PART OF THE SAME TRANSITION. Freeing one slot can
35 * release a token whose admission frees another slot, and so on; the chain
36 * jumps straight to the settled state. Splitting the cascade into separate
37 * transitions would invent intermediate states with a residence time the
38 * model does not have.
39 * 4. PARKED JOBS ARE IN NO STATION AND IN NO REGION. Station queue lengths
40 * exclude them, which is the JMT report convention; they are visible only
41 * through `ctmc_waitq_parked`, and any population accounting has to add
42 * them back by hand.
43 *
44 * A CLASS SWITCH INSIDE ONE REGION IS THE SUBTLE CASE. Such a hop leaves the
45 * region occupancy unchanged in total but moves one job between the per-class
46 * counts, so it can violate a per-class cap that the pre-transition state
47 * satisfied. It cannot be tested before the transition either, because the
48 * departure frees the old class's slot first. It is therefore deferred: the
49 * re-entry is resolved after the release cascade has settled, and parked at the
50 * TAIL if it still does not fit, since it was refused after every token already
51 * in the queue.
52 */
53
54#include <algorithm>
55#include <cmath>
56#include <cstddef>
57#include <deque>
58#include <map>
59#include <string>
60#include <utility>
61#include <vector>
62
67#include "line/lang/qn/state.h"
72#include "line/util/error.h"
73#include "line/util/matrix.h"
74
75namespace line {
76namespace ctmc {
77
78/**
79 * One augmented state: the network state, plus the token FIFO of every region.
80 *
81 * A token is `(dest_node - 1) * K + cls`, both 1-based, which is the reference's
82 * encoding: a parked job knows the class it will enter in and the node it was on
83 * its way to when it was refused.
84 */
85template <class T>
86struct WaitqState {
88 std::vector<std::vector<std::size_t>> buf;
89};
90
91/** The chain the WAITQ walk produces, alongside the FIFOs its states carry. */
92template <class T>
94 CtmcResult<T> chain; ///< Q, the net halves, rates, filt
95 std::vector<std::vector<std::vector<std::size_t>>> buf; ///< per state, per region
96};
97
98namespace waitq_detail {
99
100/**
101 * The admission constraints of one region, flattened out of the Region struct
102 * once so the inner loop is not re-deriving them at every candidate state.
103 *
104 * The predicate this feeds is the one `ctmc_region_admissible` applies to a
105 * whole state; it is restated per region because WAITQ has to ask about ONE
106 * region at a time, on a candidate population vector that no state realizes yet.
107 */
108template <class T>
109struct RegionCaps {
110 std::vector<bool> member; ///< per station, 0-based
111 std::vector<double> ccap; ///< per class, -1 = unbounded
112 double gcap = -1.0;
113 double memcap = -1.0;
114 std::vector<double> size; ///< per class memory footprint
115 Matrix<T> A; ///< the linear constraint A x <= b
116 std::vector<T> b;
117 std::vector<bool> iswaitq; ///< per class; false = this class is DROP here
118 std::size_t lmax = 0; ///< FIFO length bound
119 std::vector<std::size_t> tokbound; ///< per class (0-based) FIFO bound: chain population, or the open-class cutoff
120};
121
122/**
123 * True if the region FIFO BUF already holds the per-class bound of class CLS (1-based) tokens. The bound is per class:
124 * a total-length test lets an open class fill the slots reserved for the closed chains, whose parking arcs were then dropped.
125 */
126template <class T>
127bool tok_full(const RegionCaps<T>& cap, const std::vector<std::size_t>& buf, std::size_t cls, std::size_t K) {
128 std::size_t n = 0;
129 for (std::size_t t : buf)
130 if ((t - 1) % K + 1 == cls) ++n;
131 return n >= (cls - 1 < cap.tokbound.size() ? cap.tokbound[cls - 1] : 0);
132}
133
134template <class T>
135std::vector<RegionCaps<T>> extract_caps(const NetworkStruct<T>& sn) {
136 const std::size_t M = sn.stations.size(), K = sn.nclasses;
137 std::vector<RegionCaps<T>> out;
138 out.reserve(sn.regions.size());
139 for (std::size_t f = 0; f < sn.regions.size(); ++f) {
140 const typename NetworkStruct<T>::Region& rg = sn.regions[f];
141 RegionCaps<T> c;
142 c.member.assign(M, false);
143 c.ccap.assign(K, -1.0);
144 c.size.assign(K, 1.0);
145 c.iswaitq.assign(K, false);
146 for (std::size_t i = 0; i < M; ++i) {
147 if (i >= rg.members.size() || !rg.members[i]) continue;
148 c.member[i] = true;
149 // The caps are replicated on every member row, so taking the
150 // tightest is both correct and safe against a hand-built struct.
151 for (std::size_t k = 0; k < K && k < rg.cap[i].size(); ++k)
152 if (rg.cap[i][k] != -1.0)
153 c.ccap[k] = c.ccap[k] == -1.0 ? rg.cap[i][k] : std::min(c.ccap[k], rg.cap[i][k]);
154 if (K < rg.cap[i].size() && rg.cap[i][K] != -1.0)
155 c.gcap = c.gcap == -1.0 ? rg.cap[i][K] : std::min(c.gcap, rg.cap[i][K]);
156 if (i < rg.maxmem.size() && rg.maxmem[i] != -1.0)
157 c.memcap = c.memcap == -1.0 ? rg.maxmem[i] : std::min(c.memcap, rg.maxmem[i]);
158 }
159 for (std::size_t k = 0; k < K; ++k) {
160 if (k < rg.size.size()) c.size[k] = num_traits<T>::to_double(rg.size[k]);
161 c.iswaitq[k] = k < rg.rule.size() && rg.rule[k] != DropStrategy::DROP;
162 }
163 c.A = rg.lincon_A;
164 c.b = rg.lincon_b;
165 out.push_back(c);
166 }
167 return out;
168}
169
170/** The reference's `violates`: does this per-class population break region f. */
171template <class T>
172bool violates(const RegionCaps<T>& c, const std::vector<double>& x) {
173 double total = 0, memory = 0;
174 for (std::size_t k = 0; k < x.size(); ++k) {
175 total += x[k];
176 memory += x[k] * c.size[k];
177 if (c.ccap[k] != -1.0 && x[k] > c.ccap[k] + 1e-9) return true;
178 }
179 if (c.gcap != -1.0 && total > c.gcap + 1e-9) return true;
180 if (c.memcap != -1.0 && memory > c.memcap + 1e-9) return true;
181 for (std::size_t row = 0; row < c.A.rows() && row < c.b.size(); ++row) {
182 double lhs = 0;
183 for (std::size_t k = 0; k < x.size() && k < c.A.cols(); ++k)
184 lhs += num_traits<T>::to_double(c.A(row, k)) * x[k];
185 if (lhs > num_traits<T>::to_double(c.b[row]) + 1e-9) return true;
186 }
187 return false;
188}
189
190/**
191 * The reference's `regionAggr`: the per-class population a region currently
192 * holds, summed over its member stations.
193 *
194 * `to_marginal_aggr` rather than `to_marginal`, for the reason the reference's
195 * arrival branch uses it: it leaves a preempted job out of the count, and
196 * counting one twice would refuse an admission the region has room for.
197 */
198template <class T>
199std::vector<double> region_aggr(const NetworkStruct<T>& sn, const RegionCaps<T>& c,
200 const NetState<T>& net) {
201 const std::size_t K = sn.nclasses;
202 std::vector<double> x(K, 0.0);
203 for (std::size_t i = 0; i < c.member.size(); ++i) {
204 if (!c.member[i]) continue;
205 const std::size_t ind = sn.node_of_station(i + 1);
206 const std::size_t isf = sn.stateful_index(ind);
207 if (isf == 0) continue;
208 const std::pair<T, std::vector<T>> mg = qn::to_marginal_aggr(sn, ind, net.local[isf - 1]);
209 for (std::size_t k = 0; k < K && k < mg.second.size(); ++k) {
210 const double v = num_traits<T>::to_double(mg.second[k]);
211 // A Source reports an infinite reservoir, which describes the
212 // encoding and not an occupancy; adding it would make every region
213 // permanently full.
214 if (std::isfinite(v)) x[k] += v;
215 }
216 }
217 return x;
218}
219
220/** The flattened key of an augmented state, for exact index lookup. */
221template <class T>
222std::vector<double> waitq_key(const WaitqState<T>& ws) {
223 std::vector<double> key = ctmc_detail::state_key(ws.net);
224 for (std::size_t f = 0; f < ws.buf.size(); ++f) {
225 key.push_back(-3.0); // a separator no node block and no token emits
226 for (std::size_t j = 0; j < ws.buf[f].size(); ++j)
227 key.push_back(static_cast<double>(ws.buf[f][j]));
228 }
229 return key;
230}
231
232/**
233 * The FIFO length bound of each region: at most every job that could be parked
234 * at once.
235 *
236 * A CLOSED CLASS IS BOUNDED BY ITS WHOLE CHAIN, not by its own population, since
237 * a job may switch into the class before being refused. An open class has no
238 * population to bound it and takes the state-space cutoff instead, which is what
239 * makes the FIFO finite at all -- and what makes the walk TRUNCATE rather than
240 * refuse when the bound is reached, exactly as the reference does.
241 */
242template <class T>
243void resolve_lmax(const NetworkStruct<T>& sn, const std::vector<std::size_t>& cutoff,
244 std::vector<RegionCaps<T>>& caps) {
245 const std::size_t K = sn.nclasses;
246 const std::vector<double> N = sn.njobs();
247 std::vector<std::size_t> tokbound(K, 0);
248 for (std::size_t r = 0; r < K; ++r) {
249 bool any = false;
250 for (std::size_t f = 0; f < caps.size(); ++f)
251 if (caps[f].iswaitq[r]) any = true;
252 if (!any) continue;
253 double chainpop = 0;
254 bool finite = true;
255 for (std::size_t c = 0; c < sn.nchains; ++c) {
256 bool holds = false;
257 for (std::size_t a = 0; a < sn.inchain[c].size(); ++a)
258 if (sn.inchain[c][a] == r + 1) holds = true;
259 if (!holds) continue;
260 for (std::size_t a = 0; a < sn.inchain[c].size(); ++a) {
261 const double nj = N[sn.inchain[c][a] - 1];
262 if (std::isfinite(nj))
263 chainpop += nj;
264 else
265 finite = false;
266 }
267 }
268 tokbound[r] = finite ? static_cast<std::size_t>(chainpop)
269 : (r < cutoff.size() ? cutoff[r] : 0);
270 }
271 for (std::size_t f = 0; f < caps.size(); ++f) {
272 std::size_t s = 0;
273 for (std::size_t r = 0; r < K; ++r)
274 if (caps[f].iswaitq[r]) s += tokbound[r];
275 caps[f].lmax = s;
276 caps[f].tokbound = tokbound;
277 }
278}
279
280/**
281 * The widest local row stateful node `ind` admits, which is the width every
282 * state of that node carries in the ENUMERATED space.
283 *
284 * WHY A WALK NEEDS THIS AND THE ENUMERATION DOES NOT. `space_generator` builds
285 * each node's rows for every marginal and RIGHT-ALIGNS the narrow ones into the
286 * widest it saw (the reference's `fromMarginalBounds`), so an FCFS station whose
287 * buffer is one slot wide when empty carries the buffer of its FULLEST marginal
288 * in every state. A walk seeded from `default_init_state` gets the row at the
289 * NATURAL width of the initial marginal instead -- for an empty FCFS station a
290 * one-slot buffer -- and nothing ever widens it: the arrival branch here places a
291 * job in an existing empty slot and refuses when there is none, where MATLAB's
292 * `afterEventStation` prepends a column first. The station then holds nservers+1
293 * jobs and no more; for a CLOSED class the refusal is a BLOCK rather than a loss,
294 * so no successor is emitted at all and the upstream departure never fires.
295 *
296 * MEASURED on Think -> Q1 -> Q2 with three jobs and an UNBOUNDED region, which
297 * must reproduce the region-less solution exactly: X came out 0.935049 against
298 * 0.962806. That is not a perturbation, it is the exact answer for a different
299 * model -- the same network truncated to two jobs per queue, which is what a
300 * one-slot buffer in front of one server means.
301 *
302 * The width of a node's row is a property of that node alone: for an ordered
303 * (class-tag) buffer it grows with the TOTAL jobs held and not with how they
304 * split across classes, and for every other encoding it is constant. So one call
305 * to `from_marginal_node` at a maximal admissible marginal settles it. The
306 * descent to a smaller total is not defensive padding: `from_marginal_node`
307 * returns NO rows for a marginal the station cannot hold, and the joint bound
308 * (`cap` against the sum of the per-class bounds) can be met by a marginal that
309 * some other constraint inside the handler still rejects.
310 *
311 * `ssa::serial_detail::max_row_width` is this same computation, which the serial
312 * simulator needs for this same reason -- it walks the encoding too. The two are
313 * stated twice because a CTMC generator has no business including the
314 * simulator's header, and they must be changed together; the one place that
315 * would hold a single copy is `State` itself.
316 */
317template <class T>
318std::size_t max_row_width(const NetworkStruct<T>& sn, std::size_t ind,
319 const std::vector<std::size_t>& cutoff) {
320 const std::size_t R = sn.nclasses;
321 const std::size_t ist = sn.nodes[ind - 1].station;
322 std::vector<std::size_t> ph(R, 1);
323 if (ist != 0)
324 for (std::size_t r = 0; r < R; ++r) ph[r] = sn.phasessz_of(ist, r + 1);
325
326 // A Source and a stateful non-station (a Cache, a Join) hold a per-class
327 // count and their local variables, both of fixed width, so the empty
328 // marginal already gives the final width.
329 if (ist == 0 || sn.stations[ist - 1].nodetype == NodeType::Source) {
330 std::vector<T> row;
331 if (!qn::from_marginal_node_first(sn, ind, std::vector<std::size_t>(R, 0), ph, row))
332 throw UnsupportedError("SolverCTMC: node '" + sn.nodes[ind - 1].name +
333 "' admits no state at all, so the WAITQ walk cannot start");
334 return row.size();
335 }
336
337 // The per-class bound: the class population when closed, the state-space
338 // cutoff when open, never above the station's own per-class capacity.
339 const std::vector<double> N = sn.njobs();
340 std::vector<std::size_t> bound(R, 0);
341 for (std::size_t r = 0; r < R; ++r) {
342 double b = std::isfinite(N[r]) ? N[r]
343 : static_cast<double>(r < cutoff.size() ? cutoff[r] : 0);
344 const double cc = sn.classcap[ist - 1][r];
345 if (cc < b) b = cc;
346 bound[r] = b > 0 ? static_cast<std::size_t>(b) : 0;
347 }
348 std::size_t total = 0;
349 for (std::size_t r = 0; r < R; ++r) total += bound[r];
350 const double tcap = sn.cap[ist - 1];
351 if (std::isfinite(tcap) && tcap < static_cast<double>(total))
352 total = tcap > 0 ? static_cast<std::size_t>(tcap) : 0;
353
354 for (std::size_t t = total + 1; t-- > 0;) {
355 std::vector<std::size_t> n(R, 0);
356 std::size_t left = t;
357 for (std::size_t r = 0; r < R && left > 0; ++r) {
358 n[r] = std::min(left, bound[r]);
359 left -= n[r];
360 }
361 if (left > 0) continue; // this total does not fit the per-class bounds
362 const std::vector<std::vector<T>> rows = qn::from_marginal_node(sn, ind, n, ph);
363 if (!rows.empty()) return rows[0].size();
364 }
365 throw UnsupportedError("SolverCTMC: station '" + sn.stations[ist - 1].name +
366 "' admits no state at all, so the WAITQ walk cannot start");
367}
368
369/**
370 * The initial network state, padded to the encoding width of every node.
371 *
372 * The LEFT pad is not a convention chosen here: it is what `space_generator`
373 * does to the narrow rows, and every slicer in `state_events.h` measures its
374 * blocks from the RIGHT-hand end, so a right pad would shift the server block
375 * and silently decode the wrong queue.
376 */
377template <class T>
378NetState<T> wide_init_state(const NetworkStruct<T>& sn,
379 const std::vector<std::size_t>& cutoff) {
380 NetState<T> init;
381 if (!analyzer_detail::default_init_state(sn, init))
382 throw UnsupportedError(
383 "SolverCTMC: the model's initial state admits no state at some node; check the class "
384 "populations against their reference stations");
385 for (std::size_t f = 0; f < sn.stateful_nodes.size() && f < init.local.size(); ++f) {
386 const std::size_t w = max_row_width(sn, sn.stateful_nodes[f], cutoff);
387 if (init.local[f].size() >= w) continue;
388 init.local[f].insert(init.local[f].begin(), w - init.local[f].size(),
389 num_traits<T>::from_int(0));
390 }
391 return init;
392}
393
394/**
395 * One tentative successor, before the release cascade has run on it.
396 *
397 * `arv` and `dep_isf` carry the per-class rate bookkeeping through the cascade
398 * rather than letting the caller add it: the cascade can DROP a branch (a full
399 * FIFO), and a rate charged before that happens would count a transition that
400 * never fired.
401 */
402template <class T>
403struct Emission {
404 NetState<T> net;
405 std::vector<std::vector<std::size_t>> buf;
406 T w = num_traits<T>::from_int(0);
407 std::vector<std::pair<std::size_t, std::size_t>> arv; ///< (stateful index, class), 1-based
408 std::size_t dep_isf = 0, dep_cls = 0; ///< 0 when the active half is not a DEP
409 std::size_t pf = 0; ///< deferred re-entry region, 1-based
410 std::size_t pcls = 0, pdest = 0;
411 bool pwaitq = false;
412};
413
414/** One item of the release cascade: a tentative state and how it got there. */
415template <class T>
416struct Work {
417 NetState<T> net;
418 std::vector<std::vector<std::size_t>> buf;
419 T prob;
420 std::vector<std::pair<std::size_t, std::size_t>> arv;
421 std::size_t dep_isf = 0, dep_cls = 0;
422 std::size_t pf = 0, pcls = 0, pdest = 0;
423 bool pwaitq = false;
424};
425
426/**
427 * One SETTLED successor of an augmented state: where the transition goes, what
428 * it weighs, and the rate statistics it carries.
429 *
430 * "Settled" means the release cascade has run to its fixed point, so a caller
431 * never sees an intermediate state with a token that could still have left. The
432 * weight is the rate times the probability of this particular leaf of the
433 * cascade tree, which is why one synchronization can appear several times.
434 */
435template <class T>
436struct Successor {
437 std::size_t sync = 0; ///< index into the `sync` list the enumeration was given
438 WaitqState<T> next;
439 T w = num_traits<T>::from_int(0);
440 std::vector<std::pair<std::size_t, std::size_t>> arv; ///< (stateful, class), 1-based
441 std::size_t dep_isf = 0, dep_cls = 0; ///< 0 when nothing departed
442};
443
444/**
445 * Every transition enabled in one augmented state, with its settled successor.
446 *
447 * THIS IS THE WAITQ TRANSITION RELATION, and it is written once. The chain
448 * builder walks it breadth-first to fill a generator; the SSA serial engine
449 * draws one of the returned moves at each step. Two copies of the four rules in
450 * this file's header would be two chances for the region discipline to differ
451 * between the exact solver and the simulator while both looked correct.
452 *
453 * `sync` and `caps` are passed in rather than rebuilt because both are loop
454 * invariants of either caller, and `resolve_lmax` has to have run on `caps`
455 * already -- an unbounded FIFO admits every token and the walk would not
456 * terminate.
457 */
458template <class T>
459void waitq_successors(const NetworkStruct<T>& sn, const std::vector<Sync<T>>& sync,
460 const std::vector<RegionCaps<T>>& caps, const WaitqState<T>& ws,
461 std::vector<Successor<T>>& out) {
462 const std::size_t K = sn.nclasses;
463 const std::size_t F = caps.size();
464 const std::size_t local = sn.nodes.size() + 1;
465 const NetState<T>& net = ws.net;
466 const std::vector<std::vector<std::size_t>>& buf = ws.buf;
467 out.clear();
468
469 std::vector<std::vector<double>> xf(F);
470 for (std::size_t f = 0; f < F; ++f) xf[f] = region_aggr(sn, caps[f], net);
471
472 // State-dependent routing, evaluated once at the state the transitions leave
473 // from, exactly as the ordinary generator does. A region and an SDR
474 // subnetwork are independent constructs, so this relation must carry eq. (10)
475 // too -- reading the uniform placeholder in `sn.rt` here would answer a
476 // uniformly-routed model under an SDR name at every model that has both.
477 Matrix<T> rt_now;
478 const bool sdr = sn.has_sdr_routing();
479 if (sdr) rt_now = qn::rt_state(sn, net.local);
480
481 /*
482 * The reference's `emit`: run the release cascade to a fixed point, then
483 * record one successor per settled leaf.
484 *
485 * A WORK LIST and not a recursion because a release can have several
486 * outcomes -- an arrival that splits over the phase it starts service in --
487 * so the cascade is a tree, every leaf is a distinct successor of the SAME
488 * transition, and each carries its own share of the probability.
489 */
490 const auto emit = [&](std::size_t a, const Emission<T>& e) {
491 if (num_traits<T>::to_double(e.w) <= 0) return;
492 std::deque<Work<T>> work;
493 Work<T> w0;
494 w0.net = e.net;
495 w0.buf = e.buf;
496 w0.prob = num_traits<T>::from_int(1);
497 w0.arv = e.arv;
498 w0.dep_isf = e.dep_isf;
499 w0.dep_cls = e.dep_cls;
500 w0.pf = e.pf;
501 w0.pcls = e.pcls;
502 w0.pdest = e.pdest;
503 w0.pwaitq = e.pwaitq;
504 work.push_back(w0);
505
506 while (!work.empty()) {
507 const Work<T> it = work.front();
508 work.pop_front();
509
510 bool progressed = false;
511 for (std::size_t f = 0; f < F && !progressed; ++f) {
512 if (it.buf[f].empty()) continue;
513 const std::size_t tok = it.buf[f][0];
514 const std::size_t dest = (tok - 1) / K + 1;
515 const std::size_t r = (tok - 1) % K + 1;
516 std::vector<double> x = region_aggr(sn, caps[f], it.net);
517 x[r - 1] += 1.0;
518 // Head of line: this region's FIFO stays blocked even where a
519 // token behind the head would fit.
520 if (violates(caps[f], x)) continue;
521 const std::size_t isf_d = sn.stateful_index(dest);
522 if (isf_d == 0) continue;
523 const qn::EventOutcome<T> od =
524 qn::after_event(sn, dest, it.net.local[isf_d - 1], EventType::ARV, r);
525 if (od.space.empty()) continue;
526 for (std::size_t id = 0; id < od.space.size(); ++id) {
527 if (num_traits<T>::to_double(od.prob[id]) <= 0) continue;
528 Work<T> nx = it;
529 nx.net.local[isf_d - 1] = od.space[id];
530 nx.buf[f].erase(nx.buf[f].begin());
531 nx.prob = T(it.prob * od.prob[id]);
532 nx.arv.push_back(std::make_pair(isf_d, r));
533 work.push_back(nx);
534 }
535 progressed = true;
536 }
537 if (progressed) continue;
538
539 if (it.pf != 0) {
540 // The cascade has settled, so the deferred class switch can be
541 // tested against a region occupancy that already reflects the
542 // departure which freed the old class's slot.
543 const std::size_t f = it.pf - 1;
544 std::vector<double> x = region_aggr(sn, caps[f], it.net);
545 x[it.pcls - 1] += 1.0;
546 Work<T> nx = it;
547 nx.pf = 0;
548 if (violates(caps[f], x)) {
549 // A DROP class loses the switching job outright; a WAITQ one
550 // joins the tail.
551 if (it.pwaitq) {
552 if (tok_full(caps[f], nx.buf[f], it.pcls, K)) continue;
553 nx.buf[f].push_back((it.pdest - 1) * K + it.pcls);
554 }
555 work.push_back(nx);
556 continue;
557 }
558 const std::size_t isf_d = sn.stateful_index(it.pdest);
559 bool admitted = false;
560 if (isf_d != 0) {
561 const qn::EventOutcome<T> od = qn::after_event(
562 sn, it.pdest, it.net.local[isf_d - 1], EventType::ARV, it.pcls);
563 for (std::size_t id = 0; id < od.space.size(); ++id) {
564 if (num_traits<T>::to_double(od.prob[id]) <= 0) continue;
565 Work<T> ny = nx;
566 ny.net.local[isf_d - 1] = od.space[id];
567 ny.prob = T(it.prob * od.prob[id]);
568 ny.arv.push_back(std::make_pair(isf_d, it.pcls));
569 work.push_back(ny);
570 admitted = true;
571 }
572 }
573 if (!admitted) {
574 // The region had room but the destination's own state does
575 // not admit the job -- its station capacity, say -- so it
576 // parks rather than vanishing.
577 if (tok_full(caps[f], nx.buf[f], it.pcls, K)) continue;
578 nx.buf[f].push_back((it.pdest - 1) * K + it.pcls);
579 work.push_back(nx);
580 }
581 continue;
582 }
583
584 const T contrib = T(e.w * it.prob);
585 if (num_traits<T>::to_double(contrib) <= 0) continue;
586 Successor<T> s;
587 s.sync = a;
588 s.next.net = it.net;
589 s.next.buf = it.buf;
590 s.w = contrib;
591 s.arv = it.arv;
592 s.dep_isf = it.dep_isf;
593 s.dep_cls = it.dep_cls;
594 out.push_back(s);
595 }
596 };
597
598 for (std::size_t a = 0; a < sync.size(); ++a) {
599 const Sync<T>& sy = sync[a];
600 const std::size_t node_a = sy.active.node;
601 const std::size_t isf_a = sn.stateful_index(node_a);
602 if (isf_a == 0) continue;
603 const std::size_t cls_a = sy.active.cls;
604 const std::size_t stat_a = sn.nodes[node_a - 1].station;
605
606 // Immediate feedback (sn.immfeed): the departure half of a self-loop must
607 // run with no_promote or the fed-back job re-queues. See
608 // qn::immfeed_self_loop.
609 const qn::EventOutcome<T> oa =
610 qn::after_event(sn, node_a, net.local[isf_a - 1], sy.active.event, cls_a,
611 qn::immfeed_self_loop(sn, sy));
612 // A round-robin dispatcher decides the destination from its own pointer, as in solver_ctmc.h.
613 const bool rr_here = sy.active.event == EventType::DEP && sn.rr_var_slot(node_a, cls_a) != 0;
614 for (std::size_t ia = 0; ia < oa.space.size(); ++ia) {
615 // The active half's own share counts, as in solver_ctmc.h: SIRO's random promotion returns it below 1.
616 const T rate = T(oa.rate[ia] * oa.prob[ia]);
617 if (num_traits<T>::to_double(rate) <= 0) continue;
618
619 const std::size_t node_p = sy.passive.node;
620 if (node_p == local) {
621 Emission<T> e;
622 e.net = net;
623 e.net.local[isf_a - 1] = oa.space[ia];
624 e.buf = buf;
625 e.w = rate;
626 emit(a, e);
627 continue;
628 }
629 const std::size_t isf_p = sn.stateful_index(node_p);
630 if (isf_p == 0) continue;
631 const std::size_t cls_p = sy.passive.cls;
632 const std::size_t stat_p = sn.nodes[node_p - 1].station;
633 // The route is decided at departure and kept by a refused job: SDR reads the state the job leaves from,
634 // round-robin the pointer the active row carries once its departure advanced it. Neither reads the passive
635 // row, so the same weight applies whether the region admits, parks or drops the job, as in LDES.
636 T proute = sy.passive.statedep ? rt_now(sy.passive.rt_row, sy.passive.rt_col) : sy.passive.prob;
637 if (rr_here) {
638 const std::size_t nv = sn.nvars_of(node_a);
639 const std::vector<T>& arow = oa.space[ia];
640 std::size_t dest = 0;
641 if (arow.size() >= nv) {
642 const std::vector<T> var(arow.end() - nv, arow.end());
643 dest = sn.rr_dest(node_a, cls_a, var);
644 }
645 proute = (dest == node_p && sy.passive.cls == cls_a) ? num_traits<T>::from_int(1)
646 : num_traits<T>::from_int(0);
647 }
648 const T w = T(rate * proute);
649 if (num_traits<T>::to_double(w) <= 0) continue;
650 const std::size_t dep_isf = sy.active.event == EventType::DEP ? isf_a : 0;
651
652 // REGION ENTRY is the only place the caps are tested on the way
653 // in: the passive station is inside the region and the active
654 // node is not, so this transition raises the region's occupancy.
655 std::size_t blockedf = 0, droppedf = 0;
656 if (sy.passive.event == EventType::ARV && stat_p != 0) {
657 for (std::size_t f = 0; f < F; ++f) {
658 if (!caps[f].member[stat_p - 1]) continue;
659 if (stat_a != 0 && caps[f].member[stat_a - 1]) continue;
660 std::vector<double> xn = xf[f];
661 xn[cls_p - 1] += 1.0;
662 if (!violates(caps[f], xn)) continue;
663 if (caps[f].iswaitq[cls_p - 1])
664 blockedf = f + 1;
665 else
666 droppedf = f + 1;
667 break;
668 }
669 }
670
671 if (droppedf != 0) {
672 // DROP: the refused job is destroyed, so only the active
673 // half applies. It still departed, which is why the loss
674 // shows up downstream as ArvR - Tput.
675 Emission<T> e;
676 e.net = net;
677 e.net.local[isf_a - 1] = oa.space[ia];
678 e.buf = buf;
679 e.w = w;
680 e.dep_isf = dep_isf;
681 e.dep_cls = cls_a;
682 emit(a, e);
683 continue;
684 }
685
686 // A class switch between two members of the same region: see the
687 // file header. Deferred, not tested here.
688 std::size_t switchf = 0;
689 if (blockedf == 0 && sy.passive.event == EventType::ARV && cls_p != cls_a &&
690 stat_a != 0 && stat_p != 0)
691 for (std::size_t f = 0; f < F; ++f)
692 if (caps[f].member[stat_a - 1] && caps[f].member[stat_p - 1]) {
693 switchf = f + 1;
694 break;
695 }
696
697 if (switchf != 0) {
698 Emission<T> e;
699 e.net = net;
700 e.net.local[isf_a - 1] = oa.space[ia];
701 e.buf = buf;
702 e.w = w;
703 e.dep_isf = dep_isf;
704 e.dep_cls = cls_a;
705 e.pf = switchf;
706 e.pcls = cls_p;
707 e.pdest = node_p;
708 e.pwaitq = caps[switchf - 1].iswaitq[cls_p - 1];
709 emit(a, e);
710 continue;
711 }
712
713 if (blockedf != 0) {
714 // The FIFO bound is a TRUNCATION of an open class's
715 // unbounded queue, not an error: the reference drops the
716 // transition, and so does this. It is the same kind of
717 // approximation the state-space cutoff already is.
718 if (tok_full(caps[blockedf - 1], buf[blockedf - 1], cls_p, K)) continue;
719 Emission<T> e;
720 e.net = net;
721 e.net.local[isf_a - 1] = oa.space[ia];
722 e.buf = buf;
723 e.buf[blockedf - 1].push_back((node_p - 1) * K + cls_p);
724 e.w = w;
725 e.dep_isf = dep_isf;
726 e.dep_cls = cls_a;
727 emit(a, e);
728 continue;
729 }
730
731 const std::vector<T>& psrc = node_p == node_a ? oa.space[ia] : net.local[isf_p - 1];
732 const qn::EventOutcome<T> op = qn::after_event(sn, node_p, psrc, sy.passive.event, cls_p);
733 // No successor at the passive node is a BLOCKED arrival, the
734 // reference's true-BAS branch. Every model that declares true
735 // blocking has already been refused by name, so what remains is
736 // a destination refusing the job outside any region, and the
737 // transition simply does not fire.
738 for (std::size_t ip = 0; ip < op.space.size(); ++ip) {
739 if (num_traits<T>::to_double(op.prob[ip]) <= 0) continue;
740 Emission<T> e;
741 e.net = net;
742 e.net.local[isf_a - 1] = oa.space[ia];
743 e.net.local[isf_p - 1] = op.space[ip];
744 e.buf = buf;
745 e.w = T(w * op.prob[ip]);
746 e.dep_isf = dep_isf;
747 e.dep_cls = cls_a;
748 if (sy.passive.event == EventType::ARV)
749 e.arv.push_back(std::make_pair(isf_p, cls_p));
750 emit(a, e);
751 }
752 }
753 }
754}
755
756} // namespace waitq_detail
757
758/** True when the model declares a region that applies anything other than DROP. */
759template <class T>
761 for (std::size_t f = 0; f < sn.regions.size(); ++f)
762 for (std::size_t r = 0; r < sn.regions[f].rule.size(); ++r)
763 if (sn.regions[f].rule[r] != DropStrategy::DROP) return true;
764 return false;
765}
766
767/**
768 * The combinations the reference gates, plus the two this port cannot represent.
769 *
770 * The first two are the reference's own: each needs a semantics for what a
771 * parked token means that the reference declines to define. The routing check
772 * and the last two are this port's, and all three are about state that does not
773 * exist here rather than about semantics (JSQ and SQ never reach this point:
774 * `refresh_routing` refuses them when the struct is built).
775 */
776template <class T>
778 if (!sn.transparam.empty())
779 throw UnsupportedError(
780 "SolverCTMC: a WAITQ finite capacity region is not supported together with stochastic "
781 "Petri net transitions; a firing is atomic across its arcs and has no single "
782 "destination to park a refused token against");
783 if (sn.has_fork() || !sn.fj.empty())
784 throw UnsupportedError(
785 "SolverCTMC: a WAITQ finite capacity region is not supported together with fork-join; "
786 "a forked task refused entry would park without its siblings, and the join has no rule "
787 "for a sibling that never arrived");
788 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
789 const qn::NodeDef& nd = sn.nodes[i];
790 for (std::size_t r = 0; r < nd.routing.size(); ++r) {
791 const lang::RoutingStrategy rs = nd.routing[r];
792 // The route is drawn at departure and a refused job keeps it (LDES), so the strategies the ordinary generator
793 // evaluates, the round-robin pointer and Krzesinski SDR, carry over to the token FIFO unchanged.
797 continue;
798 throw UnsupportedError(
799 "SolverCTMC: a WAITQ finite capacity region is not supported together with the "
800 "routing strategy at node '" +
801 nd.name +
802 "'; the generator evaluates PROB, RAND, RROBIN, WRROBIN and SDR routing only");
803 }
804 }
805 for (std::size_t f = 0; f < sn.regions.size(); ++f)
806 for (std::size_t r = 0; r < sn.regions[f].rule.size(); ++r) {
807 const DropStrategy d = sn.regions[f].rule[r];
808 if (d == DropStrategy::DROP || d == DropStrategy::WAITQ) continue;
809 throw UnsupportedError(
810 "SolverCTMC: finite capacity region " + std::to_string(f + 1) +
811 " applies a blocking rule other than DROP or WAITQ to class " +
812 std::to_string(r + 1) +
813 "; BAS, BBS and RSRD hold the job AT ITS SERVER, which needs a blocked-server "
814 "marker column in the node state that this port does not allocate");
815 }
816 // TRUE BAS BLOCKING IS ORTHOGONAL TO THE REGION RULE and is refused on its
817 // own terms. The reference holds the completing job at its server by setting
818 // the last column of the station's state row; `refresh_local_vars` here
819 // reserves that shared column for a Cache, a breakdown or a polling
820 // controller and never for a BAS marker, so there is no column to set and no
821 // handler that would release it. Voiding the departure instead is NOT
822 // equivalent: it frees the server to re-serve the same job, which for
823 // exponential service redraws the completion and understates throughput.
824 for (std::size_t i = 0; i < sn.droprule.size() && i < sn.stations.size(); ++i)
825 for (std::size_t r = 0; r < sn.droprule[i].size(); ++r) {
826 const DropStrategy d = sn.droprule[i][r];
827 if (d != DropStrategy::BAS && d != DropStrategy::BBS && d != DropStrategy::RSRD)
828 continue;
829 throw UnsupportedError(
830 "SolverCTMC: station '" + sn.stations[i].name +
831 "' applies true blocking (BAS/BBS/RSRD) to class " + std::to_string(r + 1) +
832 ", which holds the completing job at its server; this port allocates no "
833 "blocked-server marker in the node state, and voiding the departure instead would "
834 "let the server re-serve the same job and understate throughput");
835 }
836}
837
838/**
839 * Port of the reachability walk of `solver_ctmc_fcr_waitq.m`.
840 *
841 * The space is WALKED and not enumerated, for the reason an SPN is: no
842 * population marginal produces a state whose region FIFO is non-empty, so a
843 * lattice enumeration would emit only the empty-buffer states and every blocking
844 * transition would land outside the space.
845 */
846template <class T>
849
850 const std::size_t K = sn.nclasses;
851 const std::size_t NF = sn.stateful_nodes.size();
852 const T zero = num_traits<T>::from_int(0);
853
854 const std::vector<Sync<T>> sync = refresh_sync(sn);
855 std::vector<waitq_detail::RegionCaps<T>> caps = waitq_detail::extract_caps(sn);
856 const std::size_t F = caps.size();
857 const std::vector<std::size_t> cutoff = analyzer_detail::resolve_cutoff(sn, opt);
858 waitq_detail::resolve_lmax(sn, cutoff, caps);
859
860 // AT THE ENCODING WIDTH, not at the width of the initial marginal: the rows
861 // never grow during the walk, so a station seeded with the narrow buffer of
862 // its empty state can never hold more than nservers+1 jobs. See
863 // `max_row_width`.
864 WaitqState<T> init;
865 init.net = waitq_detail::wide_init_state(sn, cutoff);
866 init.buf.assign(F, std::vector<std::size_t>());
867 for (std::size_t f = 0; f < F; ++f)
868 if (waitq_detail::violates(caps[f], waitq_detail::region_aggr(sn, caps[f], init.net)))
869 throw InputError(
870 "SolverCTMC: the initial state violates the constraints of finite capacity region " +
871 std::to_string(f + 1) + "; the region cannot hold the model's initial population");
872
873 std::vector<WaitqState<T>> states;
874 states.push_back(init);
875 std::map<std::vector<double>, std::size_t> index;
876 index[waitq_detail::waitq_key(init)] = 0;
877 std::deque<std::size_t> frontier;
878 frontier.push_back(0);
879
880 // Triplets rather than a matrix, because the state count is not known until
881 // the walk has finished and a growing dense matrix would be recopied at
882 // every doubling.
883 std::vector<std::size_t> ta, ti, tj;
884 std::vector<T> tv;
885 std::vector<std::vector<std::vector<T>>> arv, dep;
886 arv.push_back(std::vector<std::vector<T>>(NF, std::vector<T>(K, zero)));
887 dep.push_back(std::vector<std::vector<T>>(NF, std::vector<T>(K, zero)));
888
889 // Index an augmented state, appending it and its rate blocks when new.
890 const auto state_index = [&](const WaitqState<T>& ws) -> std::size_t {
891 const std::vector<double> key = waitq_detail::waitq_key(ws);
892 const typename std::map<std::vector<double>, std::size_t>::const_iterator it =
893 index.find(key);
894 if (it != index.end()) return it->second;
895 if (states.size() >= opt.state_max)
896 throw UnsupportedError(
897 "SolverCTMC: the WAITQ state space exceeds the cap of " +
898 std::to_string(opt.state_max) +
899 " states; the region FIFO multiplies the space by its own occupancy");
900 states.push_back(ws);
901 arv.push_back(std::vector<std::vector<T>>(NF, std::vector<T>(K, zero)));
902 dep.push_back(std::vector<std::vector<T>>(NF, std::vector<T>(K, zero)));
903 index[key] = states.size() - 1;
904 frontier.push_back(states.size() - 1);
905 return states.size() - 1;
906 };
907
908 // The walk. `frontier` is consumed from the front, so states are expanded in
909 // discovery order and the numbering matches the reference's own
910 // breadth-first construction. The transition relation itself is
911 // `waitq_successors`, shared with the SSA serial engine so that the region
912 // discipline cannot differ between the exact solver and the simulator.
913 std::vector<waitq_detail::Successor<T>> succ;
914 while (!frontier.empty()) {
915 const std::size_t s = frontier.front();
916 frontier.pop_front();
917 const WaitqState<T> ws = states[s]; // by value: `states` grows below
918 waitq_detail::waitq_successors(sn, sync, caps, ws, succ);
919 for (std::size_t e = 0; e < succ.size(); ++e) {
920 const waitq_detail::Successor<T>& su = succ[e];
921 const std::size_t dst = state_index(su.next);
922 ta.push_back(su.sync);
923 ti.push_back(s);
924 tj.push_back(dst);
925 tv.push_back(su.w);
926 if (su.dep_isf != 0) dep[s][su.dep_isf - 1][su.dep_cls - 1] += su.w;
927 for (std::size_t q = 0; q < su.arv.size(); ++q)
928 arv[s][su.arv[q].first - 1][su.arv[q].second - 1] += su.w;
929 }
930 }
931
932 const std::size_t n = states.size();
933 WaitqResult<T> res;
934 res.chain.Q = Matrix<T>(n, n, zero);
935 res.chain.space.reserve(n);
936 res.buf.reserve(n);
937 for (std::size_t s = 0; s < n; ++s) {
938 res.chain.space.push_back(states[s].net);
939 res.buf.push_back(states[s].buf);
940 }
941 res.chain.arv_rates = arv;
942 res.chain.dep_rates = dep;
943 if (opt.keep_filtration) res.chain.filt.assign(sync.size(), Matrix<T>(n, n, zero));
944 for (std::size_t e = 0; e < tv.size(); ++e) {
945 res.chain.Q(ti[e], tj[e]) += tv[e];
946 if (opt.keep_filtration) res.chain.filt[ta[e]](ti[e], tj[e]) += tv[e];
947 }
948 // A refused arrival that leaves every node unchanged is a self-loop, and the
949 // diagonal has to cancel it exactly as it does in the default generator.
950 make_infgen(res.chain.Q);
951 return res;
952}
953
954/**
955 * The mean number of parked jobs per class, over a stationary law.
956 *
957 * IT IS IN NO QLen. A parked job is in no station and in no region, so
958 * `solver_ctmc_avg_from_pi` cannot see it and the model's population is
959 * conserved only once this is added back. That is the JMT report convention,
960 * not an omission.
961 */
962template <class T>
963std::vector<T> ctmc_waitq_parked(const NetworkStruct<T>& sn, const WaitqResult<T>& r,
964 const std::vector<T>& pi) {
965 const std::size_t K = sn.nclasses;
966 std::vector<T> out(K, num_traits<T>::from_int(0));
967 for (std::size_t s = 0; s < r.buf.size() && s < pi.size(); ++s)
968 for (std::size_t f = 0; f < r.buf[s].size(); ++f)
969 for (std::size_t j = 0; j < r.buf[s][f].size(); ++j) {
970 const std::size_t cls = (r.buf[s][f][j] - 1) % K + 1;
971 out[cls - 1] += pi[s];
972 }
973 return out;
974}
975
976/** A solved WAITQ model: the usual CTMC solution, plus what the FIFOs hold. */
977template <class T>
980 std::vector<std::vector<std::vector<std::size_t>>> buf;
981 std::vector<T> parked; ///< mean parked jobs per class
982};
983
984/**
985 * Build the WAITQ chain, solve it, and map it onto the same means every other
986 * CTMC path reports.
987 *
988 * NO WEAKLY-CONNECTED-COMPONENT STEP, unlike `solver_ctmc_analyzer`. That step
989 * exists because the lattice enumeration emits states the dynamics cannot reach;
990 * this walk starts at the initial state and applies the same handlers the
991 * generator does, so every state it holds is reachable by construction and
992 * restricting to a component could only remove states the model does occupy.
993 */
994template <class T>
996 check_method(opt.method);
999 out.sol.cutoff = analyzer_detail::resolve_cutoff(sn, opt);
1000 // Seeded from the initial state, which the walk stores as row 0: a reducible chain (a synchronous-call deadlock,
1001 // say) has several closed classes, and only the one the model starts in is its answer, as in MATLAB ctmc_stationary.
1002 out.sol.pi = ctmc_stationary(r.chain.Q, 0).pi;
1003 out.sol.avg = solver_ctmc_avg_from_pi(sn, r.chain, out.sol.pi);
1004 out.sol.chain = r.chain;
1005 out.sol.actualmethod = opt.method;
1006 out.buf = r.buf;
1007 out.parked = ctmc_waitq_parked(sn, r, out.sol.pi);
1008 return out;
1009}
1010
1011/** A CTMC solve routed to whichever path the model's region rules require. */
1012template <class T>
1015 std::vector<std::vector<std::vector<std::size_t>>> buf; ///< empty off the WAITQ path
1016 std::vector<T> parked; ///< mean parked jobs per class; empty off the WAITQ path
1017 bool waitq = false;
1018};
1019
1020/**
1021 * The entry point a caller who does not know which path a model needs should
1022 * use: pick the WAITQ walk when a region asks for anything other than DROP, and
1023 * the lattice analyzer otherwise.
1024 *
1025 * IT IS A SEPARATE ENTRY POINT AND NOT A CHANGE TO `solver_ctmc_run_analyzer`. That
1026 * function is the DEFAULT path, and a WAITQ region reaching it is a defect it
1027 * must keep reporting: its generator carries no token buffer, so it would solve
1028 * the region as DROP. The dispatch is therefore placed here, where both paths
1029 * are in scope, and the default path keeps refusing by name.
1030 *
1031 * `ctmc_check_support` runs on both branches. `ctmc_check_waitq_support` covers
1032 * fork-join and true blocking on its own and needs no entry for class or joint
1033 * dependence: this generator never scales a rate itself, it walks on whatever
1034 * `EventOutcome::rate` the shared `qn::after_event` -> `after_event_station`
1035 * dispatcher hands back (state_events.h:1591, :1675), and `cd_factor` is
1036 * folded into that rate INSIDE the dispatcher (state_events.h:831-832, :1298)
1037 * for every caller alike. `solver_ctmc.h`'s default generator calls the same
1038 * `qn::after_event`, so the two paths scale identically by construction, not
1039 * by coincidence. GREPPING THIS FILE FOR `cd_factor` FINDS NOTHING BY DESIGN --
1040 * that is not a missing call, it is this generator having no rate arithmetic
1041 * of its own to put one in. Trace the dispatcher, not the grep, before
1042 * concluding otherwise. MATLAB has the identical shape for the identical
1043 * reason: `solver_ctmc_fcr_waitq.m` computes every rate through
1044 * `State.afterEventHashed`, which calls the same `State.afterEvent` the
1045 * default generator uses, so it too carries no cdscaling/jdscaling text of
1046 * its own.
1047 */
1048template <class T>
1051 // The WAITQ branch short-circuits below and would otherwise never see the
1052 // gate that solver_ctmc_analyzer applies on the default path.
1053 qn::feature_gate("SolverCTMC", qn::ctmc_feature_set(opt.method), sn);
1055 if (!ctmc_has_waitq_region(sn)) {
1057 return out;
1058 }
1060 out.sol = w.sol;
1061 out.buf = w.buf;
1062 out.parked = w.parked;
1063 out.waitq = true;
1064 return out;
1065}
1066
1067/** Solve on whichever path applies and format, mirroring `solver_ctmc_run_analyzer`. */
1068template <class T>
1072
1073} // namespace ctmc
1074} // namespace line
1075
1076#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_WAITQ_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
Steady-state distribution of a continuous-time Markov chain.
Port of matlab/src/solvers/CTMC/ctmc_stationary.m: the single entry point for the stationary distribu...
The exception types the port throws.
Dense matrix and non-owning view.
void check_method(const std::string &method)
Port of runAnalyzerChecks' method gate.
void ctmc_check_support(const NetworkStruct< T > &sn)
Refuse the constructs this port generates a chain for but does not MODEL.
void make_infgen(Matrix< T > &Q)
Port of ctmc_makeinfgen: turn an off-diagonal rate matrix into a generator.
mva::AvgResult< T > solver_ctmc_avg_table(const NetworkStruct< T > &sn, const CtmcSolution< T > &d, const std::string &method)
Port of @@SolverCTMC/runAnalyzer.m's result assembly: solve, then apply the metric filter @@NetworkSo...
CtmcAvg< T > solver_ctmc_avg_from_pi(const NetworkStruct< T > &sn, const CtmcResult< T > &r, const std::vector< T > &pivec, bool stationary=true)
Port of solver_ctmc_avg_from_pi: map a state distribution to mean metrics.
CtmcSolution< T > solver_ctmc_analyzer(const NetworkStruct< T > &sn_in, const CtmcOptions &opt)
Port of solver_ctmc_analyzer.m plus the fork-join wrapper of @@SolverCTMC/runAnalyzer....
CtmcAnySolution< T > solver_ctmc_analyzer_any(const NetworkStruct< T > &sn, const CtmcOptions &opt)
The entry point a caller who does not know which path a model needs should use: pick the WAITQ walk w...
CtmcStationaryResult< T > ctmc_stationary(const Matrix< T > &Q, std::size_t init_index=static_cast< std::size_t >(-1))
std::vector< T > ctmc_waitq_parked(const NetworkStruct< T > &sn, const WaitqResult< T > &r, const std::vector< T > &pi)
The mean number of parked jobs per class, over a stationary law.
WaitqSolution< T > solver_ctmc_waitq_analyzer(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Build the WAITQ chain, solve it, and map it onto the same means every other CTMC path reports.
WaitqResult< T > solver_ctmc_waitq(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Port of the reachability walk of solver_ctmc_fcr_waitq.m.
bool ctmc_has_waitq_region(const NetworkStruct< T > &sn)
True when the model declares a region that applies anything other than DROP.
mva::AvgResult< T > solver_ctmc_run_analyzer_any(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Solve on whichever path applies and format, mirroring solver_ctmc_run_analyzer.
void ctmc_check_waitq_support(const NetworkStruct< T > &sn)
The combinations the reference gates, plus the two this port cannot represent.
DropStrategy
Blocking and loss rules, with the values of MATLAB DropStrategy.
Definition lang_types.h:426
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
@ SDR
Krzesinski (1987) product-form state-dependent routing.
Definition lang_types.h:400
std::vector< std::vector< T > > from_marginal_node(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases)
Port of State.fromMarginal at its OWN signature: the reference indexes by NODE, not by station,...
Definition state.h:1680
bool from_marginal_node_first(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases, std::vector< T > &out)
The FIRST row from_marginal_node emits, BUILT rather than enumerated.
Definition state.h:2212
bool immfeed_self_loop(const NetworkStruct< T > &sn, const Sync< T > &sy)
True when a synchronization is an IMMEDIATE-FEEDBACK SELF-LOOP: a departure whose passive half is an ...
FeatureSet ctmc_feature_set(const std::string &method)
SolverCTMC.getFeatureSet, the reference's 104 MATLAB names in full.
EventOutcome< T > after_event(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls, bool no_promote=false, const T &aux_rate=num_traits< T >::from_int(0))
Port of State.afterEvent: the successors of one event at one NODE.
void feature_gate(const std::string &solver, const FeatureSet &declared, const NetworkStruct< T > &sn, const std::string &requested_method="", const std::string &resolved_method="")
runAnalyzerChecks: refuse a model the solver does not declare, by name.
std::pair< T, std::vector< T > > to_marginal_aggr(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &state_i)
Port of State.toMarginalAggr: the job counts of one node's state row, without the per-phase detail to...
Matrix< T > rt_state(const NetworkStruct< T > &sn, const std::vector< std::vector< T > > &local)
Port of sn.rtfun: the routing over the stateful nodes AT ONE STATE.
Definition state.h:375
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Port of solver_ctmc.m: the infinitesimal generator of a queueing network, assembled from the enumerat...
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
Finite Capacity Regions in SolverCTMC: the DROP rule, as a filter on the enumerated state space,...
The DECLARED side of the gate: one feature set per solver.
Port of the MATLAB +State package: the encoding that turns a station's state row into marginal job co...
Port of the event half of MATLAB's +State package: the successor states an event produces at one node...
A CTMC solve routed to whichever path the model's region rules require.
std::vector< T > parked
mean parked jobs per class; empty off the WAITQ path
std::vector< std::vector< std::vector< std::size_t > > > buf
empty off the WAITQ path
The SolverCTMC knobs this port honours.
The generator, the state space it is indexed by, and the event rates.
Definition solver_ctmc.h:57
Everything one CTMC solve produces.
The chain the WAITQ walk produces, alongside the FIFOs its states carry.
std::vector< std::vector< std::vector< std::size_t > > > buf
per state, per region
CtmcResult< T > chain
Q, the net halves, rates, filt.
A solved WAITQ model: the usual CTMC solution, plus what the FIFOs hold.
std::vector< std::vector< std::vector< std::size_t > > > buf
std::vector< T > parked
mean parked jobs per class
One augmented state: the network state, plus the token FIFO of every region.
std::vector< std::vector< std::size_t > > buf
The metrics getAvg returns, after filtering.
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2347
A node of the network.
std::vector< RoutingStrategy > routing
sn.routing, per class.