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