LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc.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_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_ctmc.m`: the infinitesimal generator of a queueing network,
12 * assembled from the enumerated state space and the synchronization list.
13 *
14 * THE ASSEMBLY. Every transition of the chain is one SYNCHRONIZATION: an active
15 * event that sets the rate, and a passive event that says where the job lands.
16 * For each (synchronization, state) pair the active handler is applied at its
17 * node, the passive handler at its node, and the two local successors are
18 * spliced back into a full network state whose index gives the column. The
19 * generator entry is rate * probability, accumulated -- one (s, ns) pair can be
20 * reached by several synchronizations, and each contributes.
21 *
22 * WHY THE DIAGONAL COMES LAST. Self-loops are generated deliberately (a lost
23 * arrival is one), and they must cancel: `ctmc_makeinfgen` drops the diagonal
24 * and then sets it to minus the row sum, so a self-loop contributes nothing to
25 * the balance equations while the event still fired for rate-counting purposes.
26 */
27
28#include <cmath>
29#include <cstddef>
30#include <map>
31#include <vector>
32
34#include "line/lang/qn/state.h"
38#include "line/util/error.h"
40#include "line/util/lu.h"
41#include "line/util/matrix.h"
42
43namespace line {
44namespace ctmc {
45
46using qn::EventOutcome;
47using qn::NetState;
48using qn::NetworkStruct;
49using qn::Sync;
50using lang::EventType;
51using lang::GlobalConstants;
52using lang::NodeType;
54
55/** The generator, the state space it is indexed by, and the event rates. */
56template <class T>
57struct CtmcResult {
58 Matrix<T> Q; ///< (n x n) infinitesimal generator
59 std::vector<NetState<T>> space; ///< row i of Q is space[i]
60 /**
61 * `arvRates` / `depRates`, indexed [state][stateful-1][class-1]: the total
62 * rate of arrivals into, and departures out of, each stateful node in each
63 * class, from each state. They are accumulated per synchronization rather
64 * than read off Q, because Q has already summed the contributions of every
65 * synchronization into one entry and they cannot be separated afterwards.
66 */
67 std::vector<std::vector<std::vector<T>>> arv_rates, dep_rates;
68 /**
69 * `Dfilt`, MATLAB's EVENT FILTRATION: `filt[a]` holds only the rates that
70 * synchronization `a` contributed, so `sum_a filt[a]` is the off-diagonal
71 * part of Q.
72 *
73 * IT CANNOT BE RECOVERED FROM Q, which is why it is carried rather than
74 * recomputed: Q has already summed every synchronization's contribution
75 * into one entry. The response-time CDF is built by splitting the generator
76 * on ONE event -- the tagged job's arrival, then its departure -- into a
77 * MAP (Q - D1, D1), and that split needs the per-event matrix.
78 *
79 * Empty unless `solver_ctmc` was asked for it: it costs one n x n matrix per
80 * synchronization, which on a model with many routing pairs dwarfs Q itself.
81 */
82 std::vector<Matrix<T>> filt;
83 /**
84 * The DERIVED START and PREEMPT filtrations, indexed [station-1][class-1]:
85 * the rate at which a transition starts a class-r service at station i, and
86 * the rate at which it pushes a class-r job in service back into the buffer.
87 *
88 * They are NOT part of `filt`, which pairs one-to-one with the
89 * synchronization list and whose sum is the off-diagonal of Q: a START rides
90 * on the SAME arc as the ARV or DEP that causes it, so adding it there would
91 * double-count the generator. Filled whenever `filt` is.
92 */
93 std::vector<std::vector<Matrix<T>>> start_filt, preempt_filt;
94 /**
95 * `Qimm`, the IMMEDIATE-ONLY part of Q: the arcs contributed by a Router or
96 * Fork pass-through, by a Join firing on an FJ-augmented struct, and by an
97 * SPN ENABLE or an IMMEDIATE-mode firing. Empty when the model has no such
98 * source.
99 *
100 * It exists for the VANISHING-ROW PURGE. A zero-sojourn state is left the
101 * instant it is entered, so a timed arc out of one describes an event that
102 * cannot occur; the purge replaces such a row by its immediate part. Q has
103 * already summed the two together, so the split cannot be recovered from it
104 * afterwards -- the same reason `filt` is carried rather than recomputed.
105 */
107 /**
108 * The parts of `arv_rates` / `dep_rates` contributed by those same immediate
109 * sources. Kept alongside the totals for two reasons, both from
110 * `solver_ctmc.m`: the purge has to restate a vanishing row's rates as its
111 * immediate part, and the RATE COMPLEMENT applies to the immediate part
112 * alone -- an event that fires only from vanishing states would otherwise be
113 * lost when those rows are eliminated. Empty when `Qimm` is.
114 */
115 std::vector<std::vector<std::vector<T>>> arv_rates_imm, dep_rates_imm;
116 /**
117 * The rows the purge restated, i.e. the vanishing states that `Qimm` gave an
118 * immediate exit. `ctmc_eliminate_vanishing` complements exactly these out;
119 * carrying them avoids re-running the predicate, which costs one event
120 * evaluation per (state, source).
121 */
122 std::vector<std::size_t> vanishing;
123};
124
125namespace ctmc_detail {
126
127/** The flattened key of a network state, for exact index lookup. */
128template <class T>
129std::vector<double> state_key(const NetState<T>& ns) {
130 std::vector<double> key;
131 for (std::size_t i = 0; i < ns.local.size(); ++i) {
132 // The separator keeps two different splits of the same concatenation
133 // from colliding, which a plain flatten would allow.
134 key.push_back(-2.0);
135 for (std::size_t j = 0; j < ns.local[i].size(); ++j)
136 key.push_back(num_traits<T>::to_double(ns.local[i][j]));
137 }
138 return key;
139}
140
141/**
142 * The largest per-class count any single stateful node holds in a state.
143 *
144 * The peak and not the sum, because the bound it feeds is per node.
145 *
146 * A TRANSITION IS SKIPPED, not counted. Its row is per MODE -- idle servers,
147 * firing phases, fired counts -- so the leading columns `to_marginal_aggr`
148 * would read as class counts are mode counts, and an infinite-server mode
149 * carries MaxInt there. It holds no jobs; the tokens are in the places.
150 */
151template <class T>
152std::vector<double> state_peak_occupancy(const NetworkStruct<T>& sn, const NetState<T>& st) {
153 std::vector<double> pk(sn.nclasses, 0.0);
154 const std::vector<std::size_t>& sfn = sn.stateful_nodes;
155 for (std::size_t f = 0; f < sfn.size() && f < st.local.size(); ++f) {
156 if (sn.nodes[sfn[f] - 1].nodetype == NodeType::Transition) continue;
157 const std::pair<T, std::vector<T>> mg = qn::to_marginal_aggr(sn, sfn[f], st.local[f]);
158 for (std::size_t r = 0; r < sn.nclasses && r < mg.second.size(); ++r) {
159 const double v = num_traits<T>::to_double(mg.second[r]);
160 if (v > pk[r]) pk[r] = v;
161 }
162 }
163 return pk;
164}
165
166/**
167 * True when no stateful node holds more of an OPEN class than `lim` allows.
168 *
169 * PER NODE AND NOT IN TOTAL, which is the reference's `capacityc(ind,r)` of
170 * `State.spaceGeneratorNodes`: an open class is capped at the cutoff AT EACH
171 * node. A total bound is wrong for an SPN whose firings do not conserve tokens
172 * -- `spn_open_sevenplaces` has a mode consuming one token and producing two --
173 * because the sum then crosses the bound on a firing that no place overflows,
174 * the arc vanishes, and the truncated chain absorbs at the boundary and reports
175 * Tput 0. A per-node bound censors only where a place itself overflows.
176 */
177template <class T>
178bool within_cutoff(const NetworkStruct<T>& sn, const NetState<T>& st,
179 const std::vector<double>& njobs, const std::vector<std::size_t>& lim,
180 const std::vector<std::vector<std::size_t>>& lim_mat =
181 std::vector<std::vector<std::size_t>>()) {
182 const std::vector<std::size_t>& sfn = sn.stateful_nodes;
183 for (std::size_t f = 0; f < sfn.size() && f < st.local.size(); ++f) {
184 if (sn.nodes[sfn[f] - 1].nodetype == NodeType::Transition) continue;
185 // The reference's cutoff may be a (station x class) MATRIX, in which
186 // case the bound at THIS node is its own row rather than the per-class
187 // maximum; a node with no station (Cache, Router) keeps the vector.
188 const std::size_t ist = sn.nodes[sfn[f] - 1].station;
189 const std::pair<T, std::vector<T>> mg = qn::to_marginal_aggr(sn, sfn[f], st.local[f]);
190 for (std::size_t r = 0; r < sn.nclasses && r < njobs.size(); ++r) {
191 if (std::isfinite(njobs[r])) continue;
192 std::size_t bound = r < lim.size() ? lim[r] : 0;
193 if (!lim_mat.empty() && ist != 0 && ist - 1 < lim_mat.size() &&
194 r < lim_mat[ist - 1].size())
195 bound = lim_mat[ist - 1][r];
196 if (bound == 0) continue;
197 if (r < mg.second.size() &&
198 num_traits<T>::to_double(mg.second[r]) > static_cast<double>(bound))
199 return false;
200 }
201 }
202 return true;
203}
204
205/**
206 * Accumulate the START/PREEMPT annotation of one successor row into the derived
207 * filtrations of the station behind NODE. W is the same weight the caller added
208 * to Q and to `filt`, so the filtration integrates rate * count and pi*F*e is a
209 * rate of starts (or of preemptions) per unit time.
210 */
211template <class T, class R>
212inline void add_aux_filt(R& res, const NetworkStruct<T>& sn, std::size_t node, std::size_t s,
213 std::size_t ns, const T& w, const qn::EventOutcome<T>& oc,
214 std::size_t row) {
215 if (num_traits<T>::to_double(w) == 0) return;
216 if (node == 0 || node > sn.nodes.size()) return;
217 const std::size_t ist = sn.nodes[node - 1].station;
218 if (ist == 0 || ist > res.start_filt.size()) return;
219 if (row < oc.start.size())
220 for (std::size_t j = 0; j < oc.start[row].size(); ++j) {
221 const std::size_t cls = oc.start[row][j];
222 if (cls >= 1 && cls <= res.start_filt[ist - 1].size())
223 res.start_filt[ist - 1][cls - 1](s, ns) += w;
224 }
225 if (row < oc.preempt.size())
226 for (std::size_t j = 0; j < oc.preempt[row].size(); ++j) {
227 const std::size_t cls = oc.preempt[row][j];
228 if (cls >= 1 && cls <= res.preempt_filt[ist - 1].size())
229 res.preempt_filt[ist - 1][cls - 1](s, ns) += w;
230 }
231}
232
233/**
234 * Solve (-Q22) X = B, one elimination shared by every column.
235 *
236 * The vanishing machinery needs the SAME solve three times over -- inside the
237 * complement, once per event filtration and once per rate vector -- so the
238 * censored block is factorized once and back-substituted per right-hand side.
239 * `ctmc_stochcomp` performs its own factorization for S; this is the one every
240 * OTHER right-hand side rides on.
241 */
242template <class T>
243Matrix<T> censored_solve(const Matrix<T>& Q22, const Matrix<T>& B) {
244 const std::size_t nd = Q22.rows();
245 if (B.rows() != nd) throw InputError("censored_solve: the right-hand side is misshapen");
246 Matrix<T> A(nd, nd, num_traits<T>::from_int(0));
247 for (std::size_t a = 0; a < nd; ++a)
248 for (std::size_t b = 0; b < nd; ++b) A(a, b) = T(-Q22(a, b));
249 const std::vector<std::size_t> piv = lu_factor(A);
250 Matrix<T> X = B;
251 std::vector<T> rhs(nd);
252 for (std::size_t c = 0; c < B.cols(); ++c) {
253 for (std::size_t d = 0; d < nd; ++d) rhs[d] = B(d, c);
254 lu_solve(A, piv, rhs);
255 for (std::size_t d = 0; d < nd; ++d) X(d, c) = rhs[d];
256 }
257 return X;
258}
259
260/**
261 * Complement ONE filtration onto the tangible states:
262 * Dnew = D(nonimm, nonimm) + Q12 (-Q22)^-1 D(imm, nonimm)
263 *
264 * All right-hand sides of one matrix go through a single elimination, since the
265 * censored block is the same for every filtration in the model.
266 */
267template <class T>
268Matrix<T> complement_one_filt(const Matrix<T>& D, const std::vector<std::size_t>& nonimm,
269 const std::vector<std::size_t>& imm,
270 const mc::StochCompResult<T>& sc) {
271 const T zero = num_traits<T>::from_int(0);
272 const std::size_t nk = nonimm.size(), nd = imm.size();
273 Matrix<T> out(nk, nk, zero);
274 for (std::size_t a = 0; a < nk; ++a)
275 for (std::size_t b = 0; b < nk; ++b) out(a, b) = D(nonimm[a], nonimm[b]);
276 if (nd == 0) return out;
277 Matrix<T> B(nd, nk, zero);
278 bool any = false;
279 for (std::size_t d = 0; d < nd; ++d)
280 for (std::size_t b = 0; b < nk; ++b) {
281 B(d, b) = D(imm[d], nonimm[b]);
282 if (num_traits<T>::to_double(B(d, b)) != 0) any = true;
283 }
284 if (!any) return out;
285 const Matrix<T> X = censored_solve(sc.Q22, B);
286 for (std::size_t a = 0; a < nk; ++a)
287 for (std::size_t b = 0; b < nk; ++b) {
288 T acc = zero;
289 for (std::size_t d = 0; d < nd; ++d) acc = T(acc + sc.Q12(a, d) * X(d, b));
290 out(a, b) = T(out(a, b) + acc);
291 }
292 return out;
293}
294
295/** Apply `complement_one_filt` to the event, START and PREEMPT filtrations. */
296template <class T, class R>
297void complement_filtrations(R& res, const std::vector<std::size_t>& nonimm,
298 const std::vector<std::size_t>& imm,
299 const mc::StochCompResult<T>& sc) {
300 for (std::size_t a = 0; a < res.filt.size(); ++a)
301 res.filt[a] = complement_one_filt(res.filt[a], nonimm, imm, sc);
302 for (std::size_t i = 0; i < res.start_filt.size(); ++i)
303 for (std::size_t r = 0; r < res.start_filt[i].size(); ++r)
304 res.start_filt[i][r] = complement_one_filt(res.start_filt[i][r], nonimm, imm, sc);
305 for (std::size_t i = 0; i < res.preempt_filt.size(); ++i)
306 for (std::size_t r = 0; r < res.preempt_filt[i].size(); ++r)
307 res.preempt_filt[i][r] = complement_one_filt(res.preempt_filt[i][r], nonimm, imm, sc);
308}
309
310} // namespace ctmc_detail
311
312/**
313 * Port of `ctmc_makeinfgen`: turn an off-diagonal rate matrix into a generator.
314 *
315 * The diagonal is discarded first and then set to minus the row sum, so any
316 * self-loop that was accumulated cancels exactly.
317 */
318template <class T>
320 const T zero = num_traits<T>::from_int(0);
321 for (std::size_t i = 0; i < Q.rows(); ++i) Q(i, i) = zero;
322 for (std::size_t i = 0; i < Q.rows(); ++i) {
323 T s = zero;
324 for (std::size_t j = 0; j < Q.cols(); ++j) s += Q(i, j);
325 Q(i, i) = T(-s);
326 }
327}
328
329template <class T>
330Matrix<T> ctmc_state_space_aggr(const NetworkStruct<T>& sn, const std::vector<NetState<T>>& space);
331
332/**
333 * Tabulates the globally state-dependent rate scaling phi(n) declared through
334 * `set_global_dependence`, ONE evaluation per state.
335 *
336 * Returns an (nstates x nstations*nclasses) matrix, row-major in (station,
337 * class), of the scaling applying at each state; empty when the model declares
338 * no global dependence. Evaluating once per state rather than per transition is
339 * the whole point: phi may be expensive (a bandwidth-sharing allocation solves a
340 * convex program per call), and within a state it is a CONSTANT multiplying every
341 * rate there, which is why it factors out of the generator assembly below.
342 */
343template <class T>
344Matrix<T> ctmc_gd_factor(const NetworkStruct<T>& sn, const std::vector<NetState<T>>& space) {
345 const T zero = num_traits<T>::from_int(0);
346 if (!static_cast<bool>(sn.gdscaling)) return Matrix<T>(0, 0, zero);
347 if (!sn.regions.empty())
348 throw InputError(
349 "setGlobalDependence cannot be combined with finite capacity regions: the region "
350 "generator builds its own transitions and would ignore the scaling");
351 const std::size_t M = sn.stations.size(), K = sn.nclasses, n = space.size();
352 const std::size_t max_entries = 30000000u;
353 if (n * M * K > max_entries)
354 throw InputError(
355 "the global dependence table would exceed the state budget; lower the cutoff");
356 const Matrix<T> aggr = ctmc_state_space_aggr(sn, space);
357 Matrix<T> out(n, M * K, num_traits<T>::from_int(1));
358 std::vector<T> npop(M * K, zero);
359 for (std::size_t s = 0; s < n; ++s) {
360 for (std::size_t i = 0; i < M * K; ++i) npop[i] = aggr(s, i);
361 const std::vector<T> v = sn.gdscaling(npop);
362 for (std::size_t i = 0; i < M; ++i)
363 for (std::size_t r = 0; r < K; ++r) {
364 const T f = v.size() == 1 ? v[0] : (v.size() == M ? v[i] : v[i * K + r]);
365 if (!(num_traits<T>::to_double(f) >= 0))
366 throw InputError(
367 "the global dependence handle returned a non-finite or negative scaling");
368 out(s, i * K + r) = f;
369 }
370 }
371 return out;
372}
373
374/**
375 * Port of `ctmc_find_vanishing_states` (`solver_ctmc.m:928`): the indices of the
376 * VANISHING (zero-sojourn) global states.
377 *
378 * A state is vanishing when the model leaves it at the `GlobalConstants`
379 * Immediate scale rather than at a modelled rate, so its sojourn is an artefact
380 * of realising "instantaneous" as a very fast exponential. Four sources, which
381 * are the whole list:
382 *
383 * - a Router or Fork holding a job. Neither performs service: the job is in
384 * transit and leaves on the next event.
385 * - a Join whose sibling set is COMPLETE for some original class, so the
386 * rendezvous can fire. Only on an FJ-augmented struct -- without the tag
387 * classes a Join buffers nothing and its departures are timed elsewhere.
388 * - an SPN marking from which an ENABLE moves the Transition's own row.
389 * - an SPN marking enabling a `TimingStrategy::IMMEDIATE` firing mode.
390 *
391 * The same predicate drives BOTH the vanishing-row purge and the stochastic
392 * complementation, which is why it is one function: a row purged as vanishing
393 * and then left in the chain would carry only its immediate arcs and dominate
394 * the stationary vector with a 1e-8 sojourn, and a row complemented out without
395 * being purged would push its timed arcs into the tangible states.
396 *
397 * @return 0-based row indices, ascending and unique
398 */
399template <class T>
400std::vector<std::size_t> ctmc_find_vanishing_states(
401 const NetworkStruct<T>& sn, const std::vector<NetState<T>>& space,
402 const std::vector<qn::GlobalSync<T>>& gsync, bool isfjaug) {
403 const std::size_t n = space.size();
404 const std::size_t R = sn.nclasses;
405 std::vector<bool> mark(n, false);
406
407 // Router / Fork pass-through occupancy.
408 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
409 const NodeType nt = sn.nodes[ind - 1].nodetype;
410 if (nt != NodeType::Router && nt != NodeType::Fork) continue;
411 if (sn.nodes[ind - 1].station != 0) continue; // stateful and NOT a station
412 const std::size_t isf = sn.stateful_index(ind);
413 if (isf == 0) continue;
414 for (std::size_t s = 0; s < n; ++s) {
415 if (mark[s]) continue;
416 const std::pair<T, std::vector<T>> mg =
417 qn::to_marginal_aggr(sn, ind, space[s].local[isf - 1]);
418 for (std::size_t r = 0; r < R && r < mg.second.size(); ++r) {
419 const double v = num_traits<T>::to_double(mg.second[r]);
420 if (std::isfinite(v) && v > 0.0) {
421 mark[s] = true;
422 break;
423 }
424 }
425 }
426 }
427
428 // Join rendezvous ready to fire.
429 if (isfjaug) {
430 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
431 if (sn.nodes[ind - 1].nodetype != NodeType::Join) continue;
432 const std::size_t isf = sn.stateful_index(ind);
433 if (isf == 0) continue;
434 const typename std::map<std::size_t, qn::FjJoinParam>::const_iterator jit =
435 sn.fjjoinparam.find(ind);
436 if (jit == sn.fjjoinparam.end()) continue;
437 const std::vector<std::size_t>& origcl = jit->second.origclasses;
438 for (std::size_t s = 0; s < n; ++s) {
439 if (mark[s]) continue;
440 for (std::size_t x = 0; x < origcl.size(); ++x) {
442 sn, ind, space[s].local[isf - 1], EventType::DEP, origcl[x]);
443 if (!oj.space.empty()) {
444 mark[s] = true;
445 break;
446 }
447 }
448 }
449 }
450 }
451
452 // SPN: an ENABLE that moves the Transition's row, and an IMMEDIATE firing.
453 for (std::size_t g = 0; g < gsync.size(); ++g) {
454 const qn::ModeEvent<T>& ae = gsync[g].active;
455 const std::size_t isf_t = sn.stateful_index(ae.node);
456 if (isf_t == 0) continue;
457 bool is_enable = ae.event == EventType::ENABLE;
458 bool is_imm_fire = false;
459 if (!is_enable && ae.event == EventType::FIRE) {
460 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
461 sn.transparam.find(ae.node);
462 is_imm_fire = it != sn.transparam.end() && ae.mode >= 1 &&
463 ae.mode <= it->second.timing.size() &&
464 it->second.timing[ae.mode - 1] == lang::TimingStrategy::IMMEDIATE;
465 }
466 if (!is_enable && !is_imm_fire) continue;
467 for (std::size_t s = 0; s < n; ++s) {
468 if (mark[s]) continue;
469 const qn::GlobalOutcome<T> go = qn::after_global_event(sn, space[s], gsync[g]);
470 for (std::size_t io = 0; io < go.space.size(); ++io) {
471 if (num_traits<T>::to_double(go.rate[io]) <= 0) continue;
472 // AN ENABLE THAT LEAVES THE ROW ALONE IS NOT A MOVE. The
473 // reference tests the Transition's OWN row rather than the whole
474 // marking (`solver_ctmc.m:983`): an enabling that finds the mode
475 // already enabled re-emits the same state and takes no time to
476 // do nothing, which is not a vanishing state.
477 if (is_imm_fire || go.space[io].local[isf_t - 1] != space[s].local[isf_t - 1]) {
478 mark[s] = true;
479 break;
480 }
481 }
482 }
483 }
484
485 std::vector<std::size_t> imm;
486 for (std::size_t s = 0; s < n; ++s)
487 if (mark[s]) imm.push_back(s);
488 return imm;
489}
490
491/**
492 * Port of the generator assembly of `solver_ctmc.m`.
493 *
494 * @param sn the network struct
495 * @param space the enumerated state space, from `space_generator`
496 * @param sync the synchronization list, from `refresh_sync`
497 * @param gsync the SPN global synchronizations, from `refresh_gsync`
498 * @param fjsync the fork firing list, from `fj_tag`
499 * @param want_filtration also return the per-synchronisation rate matrices (the filtration), which sampling and reward paths need
500 */
501template <class T>
502CtmcResult<T> solver_ctmc(const NetworkStruct<T>& sn, const std::vector<NetState<T>>& space,
503 const std::vector<Sync<T>>& sync,
504 const std::vector<qn::GlobalSync<T>>& gsync =
505 std::vector<qn::GlobalSync<T>>(),
506 bool want_filtration = false,
507 const std::vector<qn::FjSync<T>>& fjsync =
508 std::vector<qn::FjSync<T>>()) {
509 const std::size_t n = space.size();
510 const std::size_t local = sn.nodes.size() + 1; // the dummy passive node
511 const T zero = num_traits<T>::from_int(0);
512 CtmcResult<T> res;
513 res.space = space;
514 res.Q = Matrix<T>(n, n, zero);
515
516 const std::size_t NF = sn.stateful_nodes.size();
517 const std::size_t R = sn.nclasses;
518 res.arv_rates.assign(n, std::vector<std::vector<T>>(NF, std::vector<T>(R, zero)));
519 res.dep_rates.assign(n, std::vector<std::vector<T>>(NF, std::vector<T>(R, zero)));
520
521 if (want_filtration) {
522 res.filt.assign(sync.size(), Matrix<T>(n, n, zero));
523 res.start_filt.assign(sn.nstations, std::vector<Matrix<T>>(R, Matrix<T>(n, n, zero)));
524 res.preempt_filt.assign(sn.nstations, std::vector<Matrix<T>>(R, Matrix<T>(n, n, zero)));
525 }
526
527 // `immAction` / `immGsync` of `solver_ctmc.m:430-467`: which sources emit at
528 // the GlobalConstants::Immediate scale, and therefore land in `Qimm`. A Join
529 // counts only on an FJ-AUGMENTED struct -- without the tag classes a Join is
530 // an ordinary pass-through whose departures are timed by the siblings.
531 const bool isfjaug = !fjsync.empty();
532 std::vector<bool> imm_action(sync.size(), false);
533 bool has_imm = !fjsync.empty();
534 for (std::size_t a = 0; a < sync.size(); ++a) {
535 const std::size_t na = sync[a].active.node;
536 if (na == 0 || na > sn.nodes.size()) continue;
537 const NodeType nt = sn.nodes[na - 1].nodetype;
538 imm_action[a] = nt == NodeType::Router || nt == NodeType::Fork ||
539 (isfjaug && nt == NodeType::Join);
540 if (imm_action[a]) has_imm = true;
541 }
542 std::vector<bool> imm_gsync(gsync.size(), false);
543 for (std::size_t g = 0; g < gsync.size(); ++g) {
544 const qn::ModeEvent<T>& ae = gsync[g].active;
545 if (ae.event == EventType::ENABLE) {
546 imm_gsync[g] = true;
547 } else if (ae.event == EventType::FIRE) {
548 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
549 sn.transparam.find(ae.node);
550 imm_gsync[g] = it != sn.transparam.end() && ae.mode >= 1 &&
551 ae.mode <= it->second.timing.size() &&
552 it->second.timing[ae.mode - 1] == lang::TimingStrategy::IMMEDIATE;
553 }
554 if (imm_gsync[g]) has_imm = true;
555 }
556 if (has_imm) {
557 res.Qimm = Matrix<T>(n, n, zero);
558 res.arv_rates_imm.assign(n, std::vector<std::vector<T>>(NF, std::vector<T>(R, zero)));
559 res.dep_rates_imm.assign(n, std::vector<std::vector<T>>(NF, std::vector<T>(R, zero)));
560 }
561
562 // The true-BAS become-blocked edges, kept out of Q until the end because they
563 // are not departures: they must reach the generator but not `dep_rates`.
564 bool any_bas = false;
565 for (std::size_t i = 0; i < sn.isbasblocking.size(); ++i)
566 if (sn.isbasblocking[i]) any_bas = true;
567 Matrix<T> bas_block(any_bas ? n : 0, any_bas ? n : 0, zero);
568
569 std::map<std::vector<double>, std::size_t> index;
570 for (std::size_t s = 0; s < n; ++s) index[ctmc_detail::state_key(space[s])] = s;
571
572 // phi(n) is constant within a state, so it factors out of every rate there
573 const Matrix<T> gd = ctmc_gd_factor(sn, space);
574 const bool has_gd = gd.rows() != 0;
575
576 // The state-dependent routing table, one per state. It is tabulated here for
577 // the same reason phi(n) is: the loop below runs synchronization-outer and
578 // state-inner, so evaluating eq. (10) inside it would redo one stochastic
579 // complement per (sync, state) pair rather than one per state.
580 std::vector<Matrix<T>> rt_by_state;
581 if (sn.has_sdr_routing()) {
582 rt_by_state.reserve(n);
583 for (std::size_t s = 0; s < n; ++s) rt_by_state.push_back(qn::rt_state(sn, space[s].local));
584 }
585
586 for (std::size_t a = 0; a < sync.size(); ++a) {
587 const Sync<T>& sy = sync[a];
588 const std::size_t node_a = sy.active.node;
589 const std::size_t isf_a = sn.stateful_index(node_a);
590 if (isf_a == 0) continue; // a stateless node schedules nothing
591 const std::size_t node_p = sy.passive.node;
592 const std::size_t isf_p = node_p == local ? 0 : sn.stateful_index(node_p);
593 if (node_p != local && isf_p == 0) continue;
594
595 // PHASE is scaled too, or phase-type service would advance unscaled
596 const bool gd_here = has_gd && sn.nodes[node_a - 1].station != 0 &&
597 (sy.active.event == EventType::DEP ||
598 sy.active.event == EventType::PHASE);
599 const std::size_t gd_col = gd_here ? (sn.nodes[node_a - 1].station - 1) * sn.nclasses +
600 (sy.active.cls - 1)
601 : 0;
602 // A round-robin dispatcher decides the destination from its own pointer;
603 // see the proute branch below.
604 const bool rr_here = sy.active.event == EventType::DEP &&
605 sn.rr_var_slot(node_a, sy.active.cls) != 0;
606 for (std::size_t s = 0; s < n; ++s) {
607 const NetState<T>& st = space[s];
608 T fired = zero; // the rate this synchronization contributes here
609 const EventOutcome<T> oa =
610 qn::after_event(sn, node_a, st.local[isf_a - 1], sy.active.event, sy.active.cls);
611 for (std::size_t ia = 0; ia < oa.space.size(); ++ia) {
612 const T rate = gd_here ? T(oa.rate[ia] * gd(s, gd_col)) : oa.rate[ia];
613 // A zero-rate successor is a state the reference still emits so
614 // that the event exists; it contributes nothing to the balance
615 // equations, so skip it here rather than adding a zero.
616 if (num_traits<T>::to_double(rate) == 0) continue;
617
618 if (node_p == local) {
619 // A local action moves no job elsewhere: only the active
620 // node's block changes.
621 NetState<T> nsx = st;
622 nsx.local[isf_a - 1] = oa.space[ia];
623 const typename std::map<std::vector<double>, std::size_t>::const_iterator it =
624 index.find(ctmc_detail::state_key(nsx));
625 if (it == index.end()) continue;
626 res.Q(s, it->second) += T(rate * oa.prob[ia]);
627 if (imm_action[a]) res.Qimm(s, it->second) += T(rate * oa.prob[ia]);
628 if (want_filtration) {
629 res.filt[a](s, it->second) += T(rate * oa.prob[ia]);
630 // local action: only the active node can tag
631 ctmc_detail::add_aux_filt(res, sn, node_a, s, it->second,
632 T(rate * oa.prob[ia]), oa, ia);
633 }
634 fired += T(rate * oa.prob[ia]);
635 continue;
636 }
637
638 // A self-loop synchronization reads the passive node's state
639 // AFTER the active half has been applied, since they are the
640 // same node; otherwise the two halves see independent blocks.
641 const std::vector<T>& src =
642 node_p == node_a ? oa.space[ia] : st.local[isf_p - 1];
643 const EventOutcome<T> op =
644 qn::after_event(sn, node_p, src, sy.passive.event, sy.passive.cls);
645 // The routing probability, read at the state the job LEAVES from,
646 // which is what `sub_sdr` reads: the branch it is admitted to is
647 // decided by the populations the departing customer sees.
648 //
649 // ROUND-ROBIN IS THE ONE THAT READS THE STATE AFTER. Its
650 // destination is the pointer the ACTIVE node carries once its own
651 // departure has advanced it, so the probability is the 0/1
652 // indicator of that pointer and not the uniform mask
653 // `refresh_routing` wrote into `rt`. Reading `rt` here instead
654 // would spread the job over every outlink, which is the random
655 // routing the dispatcher exists not to be. This is the
656 // reference's `sub_rr`, which likewise takes `state_after`.
657 T proute = sy.passive.statedep
658 ? rt_by_state[s](sy.passive.rt_row, sy.passive.rt_col)
659 : sy.passive.prob;
660 if (rr_here) {
661 const std::size_t w = sn.nvars_of(node_a);
662 const std::vector<T>& arow = oa.space[ia];
663 std::size_t dest = 0;
664 if (arow.size() >= w) {
665 const std::vector<T> var(arow.end() - w, arow.end());
666 dest = sn.rr_dest(node_a, sy.active.cls, var);
667 }
668 proute = (dest == node_p && sy.passive.cls == sy.active.cls)
670 : zero;
671 }
672 bool placed = false;
673 for (std::size_t ip = 0; ip < op.space.size(); ++ip) {
674 NetState<T> nsx = st;
675 nsx.local[isf_a - 1] = oa.space[ia];
676 nsx.local[isf_p - 1] = op.space[ip];
677 const typename std::map<std::vector<double>, std::size_t>::const_iterator it =
678 index.find(ctmc_detail::state_key(nsx));
679 if (it == index.end()) continue;
680 placed = true;
681 // THE ACTIVE HALF'S OWN PROBABILITY COUNTS TOO. `oa.prob[ia]`
682 // is the share of the completion that leads to THIS successor
683 // -- SIRO's random pick of the next job to promote is the only
684 // branch that returns it below 1 -- and the LOCAL branch above
685 // already multiplies by it. Omitting it here gave every
686 // promotion candidate the FULL service rate, so a SIRO station
687 // with two waiting classes left the state at 2*mu: on
688 // prio_hol_open that is Source Tput 0.052334 against the
689 // reference's 0.052281. The reference folds the same share
690 // into its `outrate` instead (afterEventStation.m, `pick_prob`)
691 // and keeps `outprob` at 1, which is the same product.
692 const T w = T(rate * oa.prob[ia] * proute * op.prob[ip]);
693 res.Q(s, it->second) += w;
694 if (imm_action[a]) res.Qimm(s, it->second) += w;
695 if (want_filtration) {
696 res.filt[a](s, it->second) += w;
697 // Both halves of the synchronization are tagged: a DEP
698 // promotes at the sender while the paired ARV starts or
699 // preempts at the receiver.
700 ctmc_detail::add_aux_filt(res, sn, node_a, s, it->second, w, oa, ia);
701 ctmc_detail::add_aux_filt(res, sn, node_p, s, it->second, w, op, ip);
702 }
703 fired += w;
704 }
705 // TRUE BAS, the become-blocked half. The passive arrival was
706 // refused at every outcome, so the completing job cannot leave.
707 // Only the generator can emit this edge: the event layer sees one
708 // node at a time and cannot know the destination is full.
709 //
710 // The successor is the CURRENT state with the marker set, not the
711 // post-departure one: the job stays in the server it completed in,
712 // which is the whole content of blocking after service. It is
713 // accumulated separately from Q and folded in below because it is
714 // NOT a departure -- counting it in `dep_rates` would inflate
715 // throughput by the blocked transitions.
716 if (!placed && sy.active.event == EventType::DEP &&
717 node_a <= sn.isbasblocking.size() && sn.isbasblocking[node_a - 1] &&
718 !st.local[isf_a - 1].empty() &&
719 num_traits<T>::to_double(st.local[isf_a - 1].back()) == 0) {
720 NetState<T> nsb = st;
721 nsb.local[isf_a - 1].back() = num_traits<T>::from_int(1);
722 const typename std::map<std::vector<double>, std::size_t>::const_iterator ib =
723 index.find(ctmc_detail::state_key(nsb));
724 // THE ROUTING PROBABILITY IS NOT APPLIED, verbatim from
725 // `solver_ctmc.m:361`, which adds `rate_a(ia)` alone. It makes
726 // no difference where the blocked destination is the only one,
727 // and `solver_ctmc_avg_from_pi` declines to shift queue lengths
728 // at a station with several destinations anyway.
729 if (ib != index.end()) bas_block(s, ib->second) += rate;
730 }
731 }
732 // A DEP synchronization is one job LEAVING the active node and
733 // ENTERING the passive one, so the same accumulated rate is both a
734 // departure there and an arrival here. The passive half of a LOCAL
735 // action is the dummy node, which is nobody's arrival.
736 if (sy.active.event == EventType::DEP && num_traits<T>::to_double(fired) != 0) {
737 res.dep_rates[s][isf_a - 1][sy.active.cls - 1] += fired;
738 if (isf_p != 0) res.arv_rates[s][isf_p - 1][sy.passive.cls - 1] += fired;
739 // ONLY AN FJ-AUGMENTED JOIN takes the rate complement, verbatim
740 // from `solver_ctmc.m:841`: a Router or Fork DEP is restricted
741 // to the tangible rows like any timed action, and only a Join
742 // firing -- which exists nowhere else -- is complemented back.
743 if (isfjaug && sn.nodes[node_a - 1].nodetype == NodeType::Join) {
744 res.dep_rates_imm[s][isf_a - 1][sy.active.cls - 1] += fired;
745 if (isf_p != 0)
746 res.arv_rates_imm[s][isf_p - 1][sy.passive.cls - 1] += fired;
747 }
748 }
749 }
750 }
751
752 // SPN global synchronizations. A firing is ATOMIC across all its arcs, so
753 // unlike an ordinary sync it rewrites several nodes in one transition and
754 // cannot be decomposed into per-node halves.
755 //
756 // A PLACE'S FLOW IS COUNTED HERE OR NOWHERE. `refresh_sync` emits no DEP
757 // sync touching a Place -- a token crosses an arc of a firing, not a routing
758 // edge -- so leaving this loop to write only Q left `dep_rates` and
759 // `arv_rates` identically zero at every Place, and the AvgTable reported
760 // Tput 0 (hence RespT 0) for a Place whose QLen and Util were right. The
761 // reference accumulates the same two from `Dfilt_gsync_comp`
762 // (solver_ctmc.m:624-649): the PRE passives of a FIRE are that place's
763 // departures and the POST passives its arrivals.
764 //
765 // ONLY A COMPLETION COUNTS, which is what `GlobalOutcome::completion`
766 // records and what the reference's `is_comp` gates `Dfilt_gsync_comp` on. A
767 // FIRE outcome that merely starts a firing phase moves no token, and an
768 // ENABLE outcome never does, so neither is a flow.
769 for (std::size_t g = 0; g < gsync.size(); ++g) {
770 const bool is_fire = gsync[g].active.event == EventType::FIRE;
771 for (std::size_t s = 0; s < n; ++s) {
772 const qn::GlobalOutcome<T> go = qn::after_global_event(sn, space[s], gsync[g]);
773 T completed = zero;
774 for (std::size_t io = 0; io < go.space.size(); ++io) {
775 const T contrib = T(go.rate[io] * go.prob[io]);
776 if (num_traits<T>::to_double(contrib) == 0) continue;
777 const typename std::map<std::vector<double>, std::size_t>::const_iterator it =
778 index.find(ctmc_detail::state_key(go.space[io]));
779 if (it == index.end()) continue;
780 res.Q(s, it->second) += contrib;
781 if (imm_gsync[g]) res.Qimm(s, it->second) += contrib;
782 if (is_fire && io < go.completion.size() && go.completion[io]) completed += contrib;
783 }
784 if (!is_fire || num_traits<T>::to_double(completed) == 0) continue;
785 for (std::size_t j = 0; j < gsync[g].passive.size(); ++j) {
786 const qn::ModeEvent<T>& pev = gsync[g].passive[j];
787 if (pev.node == 0 || pev.node > sn.nodes.size()) continue;
788 const std::size_t isf_v = sn.stateful_index(pev.node);
789 if (isf_v == 0 || pev.cls == 0 || pev.cls > sn.nclasses) continue;
790 // THE ARC MULTIPLICITY IS PART OF THE FLOW. A place loses (or
791 // gains) `weight` tokens per firing, not one, so a rate counted
792 // per firing is a firing rate and not a job rate. Measured
793 // against the reference: `spn_closed_fourplaces`, whose cycle moves 2
794 // tokens a firing and the unweighted count reported exactly half
795 // its throughput, while `spn_twomodes`, whose two arcs have
796 // multiplicities 4 and 2, was off by exactly those two factors
797 // at its two places. Multiplicity 1 is the common case and
798 // leaves `spn_basic_closed`/`_open` unchanged.
799 const T flow = T(completed * pev.weight);
800 // EVERY FIRE completion takes the rate complement, not only an
801 // immediate one: `solver_ctmc.m:833-855` complements
802 // `Dfilt_gsync_comp{g}` for all of them, and for a timed mode the
803 // vanishing rows contribute nothing so the two agree.
804 if (pev.event == EventType::PRE) {
805 res.dep_rates[s][isf_v - 1][pev.cls - 1] += flow;
806 if (has_imm) res.dep_rates_imm[s][isf_v - 1][pev.cls - 1] += flow;
807 } else if (pev.event == EventType::POST) {
808 res.arv_rates[s][isf_v - 1][pev.cls - 1] += flow;
809 if (has_imm) res.arv_rates_imm[s][isf_v - 1][pev.cls - 1] += flow;
810 }
811 }
812 }
813 }
814
815 // FORK FIRINGS. Like an SPN firing this is atomic across several nodes, so it
816 // takes the whole network state and cannot be decomposed into sync halves.
817 //
818 // The rate statistics are accumulated by hand rather than through the DEP
819 // path: a firing is one DEPARTURE of the parent class at the fork and one
820 // ARRIVAL of the tag's auxiliary class at each branch head, which is B
821 // arrivals for one departure and is exactly what an ordinary sync cannot
822 // express. `refresh_sync` therefore emits no DEP sync for a Fork.
823 for (std::size_t k = 0; k < fjsync.size(); ++k) {
824 const qn::FjSync<T>& e = fjsync[k];
825 const std::size_t isf_f = sn.stateful_index(e.fork);
826 if (isf_f == 0) continue;
827 for (std::size_t s = 0; s < n; ++s) {
828 const qn::GlobalOutcome<T> fo = qn::after_fj_event(sn, e, space[s]);
829 T fired = zero;
830 for (std::size_t io = 0; io < fo.space.size(); ++io) {
831 const T contrib = T(fo.rate[io] * fo.prob[io]);
832 if (num_traits<T>::to_double(contrib) <= 0) continue;
833 const typename std::map<std::vector<double>, std::size_t>::const_iterator it =
834 index.find(ctmc_detail::state_key(fo.space[io]));
835 if (it == index.end()) continue;
836 res.Q(s, it->second) += contrib;
837 res.Qimm(s, it->second) += contrib;
838 fired += contrib;
839 }
840 if (num_traits<T>::to_double(fired) == 0) continue;
841 res.dep_rates[s][isf_f - 1][e.cls - 1] += fired;
842 res.dep_rates_imm[s][isf_f - 1][e.cls - 1] += fired;
843 for (std::size_t b = 0; b < e.branchheads.size(); ++b) {
844 const std::size_t isf_b = sn.stateful_index(e.branchheads[b]);
845 if (isf_b == 0) continue;
846 res.arv_rates[s][isf_b - 1][e.auxclasses[b] - 1] += fired;
847 res.arv_rates_imm[s][isf_b - 1][e.auxclasses[b] - 1] += fired;
848 }
849 }
850 }
851
852 if (any_bas)
853 for (std::size_t i = 0; i < n; ++i)
854 for (std::size_t j = 0; j < n; ++j) res.Q(i, j) += bas_block(i, j);
855
856 // THE VANISHING-ROW PURGE, `solver_ctmc.m:592-620`. A zero-sojourn state is
857 // left the instant it is entered, so a TIMED arc out of one describes an
858 // event that cannot occur -- the state is gone before the clock advances.
859 // Restating such a row as its immediate part is what makes the stochastic
860 // complement below the elimination of the immediate transitions rather than
861 // a censoring of a chain that still contains them.
862 if (has_imm) {
863 res.vanishing = ctmc_find_vanishing_states(sn, space, gsync, isfjaug);
864 // A vanishing state with NO immediate exit is a disagreement between the
865 // predicate and the arc tagging, not a model: purging it would leave an
866 // absorbing row and complementing it out would make the censored block
867 // singular. The reference purges neither but complements it anyway and
868 // lands on a NaN; drop it from both and say so.
869 std::vector<std::size_t> keep_van;
870 std::size_t gap = 0;
871 for (std::size_t x = 0; x < res.vanishing.size(); ++x) {
872 const std::size_t s = res.vanishing[x];
873 T out_imm = zero;
874 for (std::size_t j = 0; j < n; ++j)
875 if (j != s) out_imm += res.Qimm(s, j);
876 if (num_traits<T>::to_double(out_imm) > 0)
877 keep_van.push_back(s);
878 else
879 ++gap;
880 }
881 if (gap > 0)
883 "CTMC: %zu vanishing state(s) have no immediate outgoing arc; the vanishing "
884 "predicate and the immediate-arc tagging disagree, so those rows keep their "
885 "timed arcs",
886 gap);
887 res.vanishing.swap(keep_van);
888 for (std::size_t x = 0; x < res.vanishing.size(); ++x) {
889 const std::size_t s = res.vanishing[x];
890 for (std::size_t j = 0; j < n; ++j) res.Q(s, j) = res.Qimm(s, j);
891 res.arv_rates[s] = res.arv_rates_imm[s];
892 res.dep_rates[s] = res.dep_rates_imm[s];
893 for (std::size_t a = 0; a < res.filt.size(); ++a)
894 if (!imm_action[a])
895 for (std::size_t j = 0; j < n; ++j) res.filt[a](s, j) = zero;
896 }
897 }
898
899 make_infgen(res.Q);
900 return res;
901}
902
903/**
904 * Port of the "now remove immediate transitions" block of `solver_ctmc.m`
905 * (:812-870): eliminate the vanishing states by stochastic complementation.
906 *
907 * WHAT IT CHANGES AND WHAT IT MUST NOT. The chain shrinks to its TANGIBLE states
908 * and every metric read from it is unchanged to the digits printed, because the
909 * vanishing states carry ~1e-8 of the mass -- which is exactly why the omission
910 * was invisible until a caller asked for the chain itself. `-a states` and
911 * `-a gen` are the callers that see it: on `fj_tiny_closed` this takes the six
912 * enumerated states to the four the other three codebases return.
913 *
914 * THE RATE COMPLEMENT IS NOT OPTIONAL. An action that fires ONLY from vanishing
915 * states -- a fork firing, a join rendezvous, an immediate SPN mode -- has no
916 * tangible row to be read off, so restricting `arv_rates` / `dep_rates` to the
917 * tangible states alone would silently zero its flow. The rate observed from a
918 * tangible state is its own plus the expected number of firings along the
919 * vanishing excursion entered from it,
920 *
921 * r = total(nonimm) + Q12 (-Q22)^-1 imm_part(imm),
922 *
923 * which is `solver_ctmc_ratecomplement` applied to the immediate part and added
924 * to the plain restriction of the total. The two agree term by term with the
925 * reference's per-filtration form, since the totals are already the row sums.
926 *
927 * A no-op when the model declared no immediate source, which is the common case.
928 */
929template <class T>
931 const T zero = num_traits<T>::from_int(0);
932 if (res.vanishing.empty()) {
933 res.Qimm = Matrix<T>();
934 res.arv_rates_imm.clear();
935 res.dep_rates_imm.clear();
936 return;
937 }
938 const std::size_t n = res.Q.rows();
939 std::vector<bool> is_van(n, false);
940 for (std::size_t x = 0; x < res.vanishing.size(); ++x) is_van[res.vanishing[x]] = true;
941 std::vector<std::size_t> nonimm;
942 nonimm.reserve(n - res.vanishing.size());
943 for (std::size_t s = 0; s < n; ++s)
944 if (!is_van[s]) nonimm.push_back(s);
945 if (nonimm.empty())
946 throw NumericError(
947 "SolverCTMC: every state is vanishing; the chain has no tangible state to observe");
948
949 const mc::StochCompResult<T> sc = mc::ctmc_stochcomp(res.Q, nonimm);
950 const std::size_t nk = nonimm.size(), nd = res.vanishing.size();
951
952 // The rate complement, batched: one elimination of (-Q22) serves every
953 // (stateful, class) pair of both directions, which is what keeps the cost at
954 // one factorization rather than 2*NF*R of them.
955 const std::size_t NF = res.arv_rates.empty() ? 0 : res.arv_rates[0].size();
956 const std::size_t R = NF == 0 ? 0 : res.arv_rates[0][0].size();
957 const std::size_t ncol = 2 * NF * R;
958 Matrix<T> corr;
959 if (ncol > 0) {
960 Matrix<T> B(nd, ncol, zero);
961 for (std::size_t d = 0; d < nd; ++d) {
962 const std::size_t s = res.vanishing[d];
963 for (std::size_t f = 0; f < NF; ++f)
964 for (std::size_t r = 0; r < R; ++r) {
965 B(d, f * R + r) = res.arv_rates_imm[s][f][r];
966 B(d, NF * R + f * R + r) = res.dep_rates_imm[s][f][r];
967 }
968 }
969 const Matrix<T> X = ctmc_detail::censored_solve(sc.Q22, B);
970 corr = Matrix<T>(nk, ncol, zero);
971 for (std::size_t a = 0; a < nk; ++a)
972 for (std::size_t c = 0; c < ncol; ++c) {
973 T acc = zero;
974 for (std::size_t d = 0; d < nd; ++d) acc = T(acc + sc.Q12(a, d) * X(d, c));
975 corr(a, c) = acc;
976 }
977 }
978
979 std::vector<NetState<T>> space;
980 std::vector<std::vector<std::vector<T>>> arv, dep;
981 space.reserve(nk);
982 arv.reserve(nk);
983 dep.reserve(nk);
984 for (std::size_t a = 0; a < nk; ++a) {
985 space.push_back(res.space[nonimm[a]]);
986 arv.push_back(res.arv_rates[nonimm[a]]);
987 dep.push_back(res.dep_rates[nonimm[a]]);
988 for (std::size_t f = 0; f < NF; ++f)
989 for (std::size_t r = 0; r < R; ++r) {
990 arv[a][f][r] = T(arv[a][f][r] + corr(a, f * R + r));
991 dep[a][f][r] = T(dep[a][f][r] + corr(a, NF * R + f * R + r));
992 }
993 }
994
995 // Each filtration is complemented exactly as Q is: an event that reaches a
996 // tangible state THROUGH a vanishing excursion belongs on the arc it induces,
997 // and dropping the excursion rows would lose it from every CDF built on the
998 // split. The derived START and PREEMPT filtrations take the same treatment --
999 // a service start that lands on a vanishing state would otherwise undercount.
1000 ctmc_detail::complement_filtrations(res, nonimm, res.vanishing, sc);
1001
1002 res.Q = sc.S;
1003 res.space.swap(space);
1004 res.arv_rates.swap(arv);
1005 res.dep_rates.swap(dep);
1006 res.Qimm = Matrix<T>();
1007 res.arv_rates_imm.clear();
1008 res.dep_rates_imm.clear();
1009 res.vanishing.clear();
1010}
1011
1012
1013/**
1014 * Port of `State.reachableSpaceGenerator`: the states reachable from `init`.
1015 *
1016 * `space_generator` enumerates every state the ENCODING admits; this walks the
1017 * ones the DYNAMICS can actually occupy. The two differ whenever the encoding
1018 * is wider than the model -- a retrial station's idle-server states are
1019 * reachable, whereas an ordinary queue's are not, and enumerating the latter
1020 * leaves a generator with absorbing junk that perturbs the stationary vector
1021 * after normalization.
1022 *
1023 * The walk applies exactly the same handlers the generator does, so a state is
1024 * included precisely when some synchronization produces it at a positive rate.
1025 *
1026 * IT IS THE ONLY GENERATOR AN SPN HAS. `from_marginal_node` emits a single row
1027 * for a Transition -- every mode's servers free, nothing firing -- because a
1028 * transition's state is per-MODE and no population marginal determines it. The
1029 * lattice enumeration therefore never produces a state in which a mode is
1030 * firing, and a generator built over that space has every ENABLE landing
1031 * outside it. Walking `gsync` from the idle state is what materializes them,
1032 * which is why the reference forces `state_space_gen='reachable'` for any model
1033 * whose firings break per-chain population conservation.
1034 *
1035 * `cutoff` TRUNCATES AN OPEN CLASS, and without it this walk does not terminate.
1036 * The lattice generator bounds an open class's total population by the cutoff;
1037 * this walk had no such bound, so on ANY open SPN -- `spn_basic_open` at cutoff
1038 * 1, `spn_pareto_service`, `spn_open_sevenplaces` -- the Source kept producing
1039 * tokens and the walk ran to the `maxst` cap instead of answering. Passing the
1040 * same cutoff makes the two paths mean the same thing by "cutoff": a candidate
1041 * whose open-class population would exceed it is not a state of the truncated
1042 * chain, so the arc to it simply does not exist and `make_infgen` re-closes the
1043 * row, which is exactly what the lattice path leaves behind. Empty means
1044 * unbounded, which is right for a closed model and is what every existing
1045 * caller passes.
1046 *
1047 * THE INITIAL MARKING RAISES THE BOUND WHERE IT EXCEEDS IT. A Place may start
1048 * with more tokens than the cutoff -- `spn_open_sevenplaces` puts 2 in P1
1049 * against a default cutoff of 2 -- and a bound below the state the walk starts
1050 * from censors every successor of it, leaving the initial state alone in a
1051 * chain that is not the model's. A state space that cannot contain its own
1052 * initial state is empty by construction, so the floor is the initial marking.
1053 */
1054template <class T>
1055std::vector<NetState<T>> reachable_space_generator(
1056 const NetworkStruct<T>& sn, const NetState<T>& init, const std::vector<Sync<T>>& sync,
1057 const std::vector<qn::GlobalSync<T>>& gsync = std::vector<qn::GlobalSync<T>>(),
1058 std::size_t maxst = 3000000,
1059 const std::vector<qn::FjSync<T>>& fjsync = std::vector<qn::FjSync<T>>(),
1060 const std::vector<std::size_t>& cutoff = std::vector<std::size_t>(),
1061 const std::vector<std::vector<std::size_t>>& cutoff_mat =
1062 std::vector<std::vector<std::size_t>>()) {
1063 const std::size_t local = sn.nodes.size() + 1;
1064 std::vector<NetState<T>> out;
1065 std::map<std::vector<double>, std::size_t> seen;
1066 std::vector<std::size_t> stack;
1067 const std::vector<double> njobs = sn.njobs();
1068 // Only an OPEN class can leave the bound, so a closed model pays nothing.
1069 bool bound = false;
1070 for (std::size_t r = 0; r < sn.nclasses && r < njobs.size(); ++r)
1071 if (!std::isfinite(njobs[r]) && r < cutoff.size() && cutoff[r] > 0) bound = true;
1072 const bool bounded = bound;
1073 std::vector<std::size_t> lim = cutoff;
1074 if (bounded) {
1075 const std::vector<double> n0 = ctmc_detail::state_peak_occupancy(sn, init);
1076 for (std::size_t r = 0; r < lim.size() && r < n0.size(); ++r) {
1077 const std::size_t p0 =
1078 n0[r] > 0 ? static_cast<std::size_t>(std::floor(n0[r] + 0.5)) : 0;
1079 if (p0 > lim[r]) lim[r] = p0;
1080 }
1081 }
1082
1083 seen[ctmc_detail::state_key(init)] = 0;
1084 out.push_back(init);
1085 stack.push_back(0);
1086
1087 while (!stack.empty()) {
1088 const std::size_t si = stack.back();
1089 stack.pop_back();
1090 const NetState<T> st = out[si]; // by value: `out` grows inside the loop
1091
1092 for (std::size_t a = 0; a < sync.size(); ++a) {
1093 const Sync<T>& sy = sync[a];
1094 const std::size_t isf_a = sn.stateful_index(sy.active.node);
1095 if (isf_a == 0) continue;
1096 const std::size_t isf_p =
1097 sy.passive.node == local ? 0 : sn.stateful_index(sy.passive.node);
1098 if (sy.passive.node != local && isf_p == 0) continue;
1099
1100 const EventOutcome<T> oa = qn::after_event(sn, sy.active.node, st.local[isf_a - 1],
1101 sy.active.event, sy.active.cls);
1102 for (std::size_t ia = 0; ia < oa.space.size(); ++ia) {
1103 if (num_traits<T>::to_double(oa.rate[ia]) <= 0) continue;
1104
1105 std::vector<NetState<T>> cand;
1106 if (sy.passive.node == local) {
1107 NetState<T> nsx = st;
1108 nsx.local[isf_a - 1] = oa.space[ia];
1109 cand.push_back(nsx);
1110 } else {
1111 const std::vector<T>& src =
1112 sy.passive.node == sy.active.node ? oa.space[ia] : st.local[isf_p - 1];
1113 const EventOutcome<T> op = qn::after_event(sn, sy.passive.node, src,
1114 sy.passive.event, sy.passive.cls);
1115 for (std::size_t ip = 0; ip < op.space.size(); ++ip) {
1116 if (num_traits<T>::to_double(op.prob[ip]) <= 0) continue;
1117 NetState<T> nsx = st;
1118 nsx.local[isf_a - 1] = oa.space[ia];
1119 nsx.local[isf_p - 1] = op.space[ip];
1120 cand.push_back(nsx);
1121 }
1122 }
1123 for (std::size_t c = 0; c < cand.size(); ++c) {
1124 if (bounded && !ctmc_detail::within_cutoff(sn, cand[c], njobs, lim, cutoff_mat))
1125 continue;
1126 const std::vector<double> key = ctmc_detail::state_key(cand[c]);
1127 if (seen.find(key) != seen.end()) continue;
1128 if (out.size() >= maxst)
1129 throw UnsupportedError(
1130 "reachable_space_generator: the reachable state space exceeds the "
1131 "cap of " + std::to_string(maxst) + " states");
1132 seen[key] = out.size();
1133 out.push_back(cand[c]);
1134 stack.push_back(out.size() - 1);
1135 }
1136 }
1137 }
1138
1139 // The SPN half of the walk. A global synchronization already returns
1140 // WHOLE network states, since a firing is atomic across every arc it
1141 // touches and cannot be decomposed into an active and a passive half.
1142 for (std::size_t g = 0; g < gsync.size(); ++g) {
1143 const qn::GlobalOutcome<T> go = qn::after_global_event(sn, st, gsync[g]);
1144 for (std::size_t io = 0; io < go.space.size(); ++io) {
1145 if (num_traits<T>::to_double(go.rate[io]) <= 0) continue;
1146 if (num_traits<T>::to_double(go.prob[io]) <= 0) continue;
1147 if (bounded && !ctmc_detail::within_cutoff(sn, go.space[io], njobs, lim, cutoff_mat))
1148 continue;
1149 const std::vector<double> key = ctmc_detail::state_key(go.space[io]);
1150 if (seen.find(key) != seen.end()) continue;
1151 if (out.size() >= maxst)
1152 throw UnsupportedError(
1153 "reachable_space_generator: the reachable state space exceeds the cap of " +
1154 std::to_string(maxst) + " states");
1155 seen[key] = out.size();
1156 out.push_back(go.space[io]);
1157 stack.push_back(out.size() - 1);
1158 }
1159 }
1160
1161 // The fork-join half. A firing is atomic in the same sense and returns
1162 // whole network states too; it is walked here rather than folded into the
1163 // sync loop because it has no active/passive decomposition at all.
1164 for (std::size_t k = 0; k < fjsync.size(); ++k) {
1165 const qn::GlobalOutcome<T> fo = qn::after_fj_event(sn, fjsync[k], st);
1166 for (std::size_t io = 0; io < fo.space.size(); ++io) {
1167 if (num_traits<T>::to_double(fo.rate[io]) <= 0) continue;
1168 if (num_traits<T>::to_double(fo.prob[io]) <= 0) continue;
1169 if (bounded && !ctmc_detail::within_cutoff(sn, fo.space[io], njobs, lim, cutoff_mat))
1170 continue;
1171 const std::vector<double> key = ctmc_detail::state_key(fo.space[io]);
1172 if (seen.find(key) != seen.end()) continue;
1173 if (out.size() >= maxst)
1174 throw UnsupportedError(
1175 "reachable_space_generator: the reachable state space exceeds the cap of " +
1176 std::to_string(maxst) + " states");
1177 seen[key] = out.size();
1178 out.push_back(fo.space[io]);
1179 stack.push_back(out.size() - 1);
1180 }
1181 }
1182 }
1183 return out;
1184}
1185
1186/**
1187 * Port of `StateSpaceAggr`: the per-(station, class) job counts of every state,
1188 * as an (nstates x nstations*nclasses) matrix in column block order
1189 * `(ist-1)*K + k`.
1190 *
1191 * It is what `@@SolverCTMC/getStateSpaceAggr` returns and what the transient
1192 * analyzer, the reward analyzer and the BAS shift all index; building it once
1193 * keeps the three from re-deriving the same marginal decode with three chances
1194 * to disagree about the buffer encoding.
1195 *
1196 * A SOURCE ROW IS ZERO, not Inf. `to_marginal` reports an infinite reservoir for
1197 * an EXT station, which describes the encoding rather than a queue length, and
1198 * an Inf here would propagate into every aggregate that sums this matrix.
1199 */
1200template <class T>
1202 const std::vector<NetState<T>>& space) {
1203 const std::size_t M = sn.stations.size(), K = sn.nclasses;
1204 const T zero = num_traits<T>::from_int(0);
1205 Matrix<T> A(space.size(), M * K, zero);
1206 for (std::size_t ist = 1; ist <= M; ++ist) {
1207 const std::size_t isf = sn.stateful_of_station(ist);
1208 const std::size_t ind = sn.node_of_station(ist);
1209 if (isf == 0) continue;
1210 if (sn.stations[ist - 1].nodetype == NodeType::Source) continue;
1211 std::vector<std::size_t> ph(K, 1), shift(K, 0);
1212 std::size_t w = 0;
1213 for (std::size_t k = 0; k < K; ++k) {
1214 ph[k] = sn.phasessz_of(ist, k + 1);
1215 shift[k] = w;
1216 w += ph[k];
1217 }
1218 const std::size_t nvar = sn.nvars_of(ind);
1219 for (std::size_t s = 0; s < space.size(); ++s) {
1220 const qn::Marginal<T> m =
1221 qn::to_marginal(sn, ist, space[s].local[isf - 1], ph, shift, nvar);
1222 for (std::size_t k = 0; k < K; ++k) A(s, (ist - 1) * K + k) = m.nir[k];
1223 }
1224 }
1225 return A;
1226}
1227
1228/**
1229 * Port of `ctmc_signal_lossy`: classes a G-network signal can annihilate here.
1230 *
1231 * Such a job leaves the station WITHOUT a service completion, so the
1232 * arrival-based (offered-load) utilization estimator is invalid for it and only
1233 * the departure-based carried load is meaningful -- the same reasoning as the
1234 * finite-capacity `canDropClass`, reached by a different route.
1235 *
1236 * A signal class is active at this node when its stationary arrival rate there
1237 * is positive. A TARGETED signal removes only its target class; an untargeted
1238 * one is class-agnostic and removes any non-signal class, matching
1239 * `after_event_station_signal`, MAM and LDES.
1240 */
1241template <class T>
1242std::vector<bool> ctmc_signal_lossy(const NetworkStruct<T>& sn, const CtmcResult<T>& r,
1243 const std::vector<T>& p, std::size_t isf) {
1244 const std::size_t K = sn.nclasses;
1245 std::vector<bool> lossy(K, false);
1246 bool any = false;
1247 for (std::size_t k = 0; k < K && k < sn.issignal.size(); ++k) any = any || sn.issignal[k];
1248 if (!any) return lossy;
1249
1250 for (std::size_t r2 = 1; r2 <= K; ++r2) {
1251 if (sn.issignal.size() < r2 || !sn.issignal[r2 - 1]) continue;
1252 T arv = num_traits<T>::from_int(0);
1253 for (std::size_t s = 0; s < r.space.size(); ++s)
1254 arv += T(p[s] * r.arv_rates[s][isf - 1][r2 - 1]);
1255 if (num_traits<T>::to_double(arv) <= 0) continue;
1256 const std::size_t tgt =
1257 sn.signaltarget.size() >= r2 ? sn.signaltarget[r2 - 1] : 0;
1258 if (tgt >= 1 && tgt <= K) {
1259 lossy[tgt - 1] = true;
1260 } else {
1261 for (std::size_t k = 0; k < K; ++k)
1262 if (k >= sn.issignal.size() || !sn.issignal[k]) lossy[k] = true;
1263 }
1264 }
1265 return lossy;
1266}
1267
1268/**
1269 * Port of `ctmc_signal_busy`: the exact per-class busy-server fraction, read
1270 * off the enumerated state space.
1271 *
1272 * WHY THE DEPARTURE ESTIMATOR IS NOT ENOUGH. `T*E[S]/c` is exact for a lossy
1273 * class only under EXPONENTIAL service: a job destroyed mid-service leaves busy
1274 * time behind with no completion to account for it, so with phase-type service
1275 * the carried-rate estimator under-counts. Measured on an M/Er2/1 with
1276 * lambda+ = 0.5 and lambda- = 0.4 it gives 0.34941 against a true 0.37696. The
1277 * in-service occupancy below is exact for any service process.
1278 *
1279 * A PS-like discipline shares the servers among every resident job, so class k
1280 * takes the weighted share n_k w_k / sum_j n_j w_j of the busy servers; every
1281 * other discipline exposes the in-service indicator directly through
1282 * `to_marginal`.
1283 */
1284template <class T>
1285std::vector<T> ctmc_signal_busy(const NetworkStruct<T>& sn, std::size_t ist,
1286 const CtmcResult<T>& r, const std::vector<T>& p,
1287 std::size_t isf) {
1288 const std::size_t K = sn.nclasses;
1289 const T zero = num_traits<T>::from_int(0);
1290 std::vector<T> unb(K, zero);
1291 const SchedStrategy sched = sn.stations[ist - 1].sched;
1292 const bool is_ps = sched == SchedStrategy::PS || sched == SchedStrategy::DPS ||
1293 sched == SchedStrategy::GPS || sched == SchedStrategy::LPS;
1294 const double S = sn.stations[ist - 1].nservers;
1295 const std::size_t ind = sn.node_of_station(ist);
1296
1297 std::vector<std::size_t> ph(K, 1), shift(K, 0);
1298 std::size_t w = 0;
1299 for (std::size_t k = 0; k < K; ++k) {
1300 ph[k] = sn.phasessz_of(ist, k + 1);
1301 shift[k] = w;
1302 w += ph[k];
1303 }
1304 const std::size_t nvar = sn.nvars_of(ind);
1305
1306 for (std::size_t s = 0; s < r.space.size(); ++s) {
1307 if (num_traits<T>::to_double(p[s]) == 0) continue;
1308 const qn::Marginal<T> m =
1309 qn::to_marginal(sn, ist, r.space[s].local[isf - 1], ph, shift, nvar);
1310 double ni = 0;
1311 for (std::size_t k = 0; k < K; ++k) ni += num_traits<T>::to_double(m.nir[k]);
1312 if (ni <= 0) continue;
1313 if (is_ps) {
1314 T wtot = zero;
1315 for (std::size_t k = 0; k < K; ++k)
1316 wtot += T(m.nir[k] * sn.stations[ist - 1].schedparam[k]);
1317 if (num_traits<T>::to_double(wtot) <= 0) continue;
1318 const double busy = std::min(ni, S) / S;
1319 for (std::size_t k = 0; k < K; ++k)
1320 unb[k] += T(p[s] * (m.nir[k] * sn.stations[ist - 1].schedparam[k] / wtot) *
1322 } else {
1323 for (std::size_t k = 0; k < K; ++k)
1324 unb[k] += T(p[s] * m.sir[k] / num_traits<T>::from_double(S));
1325 }
1326 }
1327 return unb;
1328}
1329
1330/**
1331 * MATLAB's `all(sn.isph(:))`, read off the matrices instead of off a flag.
1332 *
1333 * False as soon as one station-class process is a matrix exponential: its D0
1334 * carries negative off-diagonal entries, or its D1 negative entries, neither of
1335 * which a phase-type has. A Source keeps its arrival process in `sn.service`,
1336 * so this one scan covers arrivals and services alike.
1337 */
1338template <class T>
1340 for (std::size_t i = 0; i < sn.service.size(); ++i)
1341 for (std::size_t r = 0; r < sn.service[i].size(); ++r) {
1342 const lang::Distrib<T>& d = sn.service[i][r];
1343 if (d.disabled || d.D0.rows() == 0) continue;
1344 for (std::size_t a = 0; a < d.D0.rows(); ++a)
1345 for (std::size_t b = 0; b < d.D0.cols(); ++b)
1346 if (a != b && num_traits<T>::to_double(d.D0(a, b)) < 0) return false;
1347 for (std::size_t a = 0; a < d.D1.rows(); ++a)
1348 for (std::size_t b = 0; b < d.D1.cols(); ++b)
1349 if (num_traits<T>::to_double(d.D1(a, b)) < 0) return false;
1350 }
1351 return true;
1352}
1353
1354/** The mean performance metrics a stationary vector maps to. */
1355template <class T>
1356struct CtmcAvg {
1357 Matrix<T> QN, UN, RN, TN; ///< (nstations x nclasses)
1358 std::vector<T> XN, CN; ///< (nclasses) system throughput and response time
1359 /**
1360 * The DERIVED rates, (nstations x nclasses): how often per unit time a
1361 * class-r service STARTS at station i, and how often a class-r job in
1362 * service is PUSHED BACK into the buffer there. All zero unless the result
1363 * carried the filtrations they reduce (`solver_ctmc` was asked for them).
1364 *
1365 * At a lossless station with no in-service abandonment
1366 * StartN == TN + PreemptN,
1367 * because every job starts service once per entry into a server and every
1368 * preemption is followed by exactly one later resume or restart.
1369 */
1371};
1372
1373/**
1374 * Port of `solver_ctmc_avg_from_pi`: map a state distribution to mean metrics.
1375 *
1376 * Factored from the analyzer exactly as the reference factors it, so a caller
1377 * holding its own distribution -- a time-averaged transient one, say -- reuses
1378 * the same discipline-aware reduction instead of re-deriving it.
1379 */
1380template <class T>
1382 const std::vector<T>& pivec) {
1383 const std::size_t M = sn.stations.size(), R = sn.nclasses, n = r.space.size();
1384 const T zero = num_traits<T>::from_int(0);
1385 CtmcAvg<T> a;
1386 a.QN = Matrix<T>(M, R, zero);
1387 a.UN = Matrix<T>(M, R, zero);
1388 a.RN = Matrix<T>(M, R, zero);
1389 a.TN = Matrix<T>(M, R, zero);
1390 a.XN.assign(R, zero);
1391 a.CN.assign(R, zero);
1392 a.StartN = Matrix<T>(M, R, zero);
1393 a.PreemptN = Matrix<T>(M, R, zero);
1394
1395 // Renormalize, clamping the numerical dust an eigenvector solve leaves
1396 // below the zero threshold. WITH A MATRIX EXPONENTIAL THE CLAMP IS SKIPPED:
1397 // the stationary vector is then a genuinely SIGNED measure, only its
1398 // aggregates over each phase block are probabilities, and deleting the
1399 // negative entries deletes real mass -- an M/CME/1 at rho = 0.3 came out
1400 // with QLen 0.4469 against the Pollaczek-Khinchine 0.3772. Every metric
1401 // below is linear in the vector and stays exact without the clamp.
1402 const bool signed_measure = !ctmc_all_phasetype(sn);
1403 std::vector<T> p = pivec;
1404 T tot = zero;
1405 for (std::size_t s = 0; s < n; ++s) {
1406 if (!signed_measure && num_traits<T>::to_double(p[s]) < GlobalConstants::Zero) p[s] = zero;
1407 tot += p[s];
1408 }
1409 if (num_traits<T>::to_double(tot) > 0)
1410 for (std::size_t s = 0; s < n; ++s) p[s] = T(p[s] / tot);
1411
1412 // System throughput is the arrival rate seen at each class's REFERENCE
1413 // station, which is what makes X a per-class quantity rather than a sum.
1414 for (std::size_t k = 1; k <= R; ++k) {
1415 const std::size_t refsf = sn.stateful_of_station(sn.classes[k - 1].refstat);
1416 for (std::size_t s = 0; s < n; ++s) a.XN[k - 1] += T(p[s] * r.arv_rates[s][refsf - 1][k - 1]);
1417 }
1418
1419 // The derived rates: pi * F * e over each filtration, the same reduction
1420 // the departure rates use, so the three are directly comparable.
1421 for (std::size_t ist = 1; ist <= M && ist <= r.start_filt.size(); ++ist)
1422 for (std::size_t k = 1; k <= R && k <= r.start_filt[ist - 1].size(); ++k) {
1423 T accs = zero, accp = zero;
1424 for (std::size_t s = 0; s < n; ++s) {
1425 T rows = zero, rowp = zero;
1426 for (std::size_t ns = 0; ns < n; ++ns) {
1427 rows += r.start_filt[ist - 1][k - 1](s, ns);
1428 rowp += r.preempt_filt[ist - 1][k - 1](s, ns);
1429 }
1430 accs += T(p[s] * rows);
1431 accp += T(p[s] * rowp);
1432 }
1433 a.StartN(ist - 1, k - 1) = accs;
1434 a.PreemptN(ist - 1, k - 1) = accp;
1435 }
1436
1437 for (std::size_t ist = 1; ist <= M; ++ist) {
1438 const std::size_t isf = sn.stateful_of_station(ist);
1439 const std::size_t ind = sn.node_of_station(ist);
1440 const bool is_source = sn.stations[ist - 1].nodetype == NodeType::Source;
1441 const double S = sn.stations[ist - 1].nservers;
1442 std::vector<std::size_t> ph(R, 1), shift(R, 0);
1443 std::size_t w = 0;
1444 for (std::size_t r2 = 0; r2 < R; ++r2) {
1445 ph[r2] = sn.phasessz_of(ist, r2 + 1);
1446 shift[r2] = w;
1447 w += ph[r2];
1448 }
1449 const std::size_t nvar = sn.nvars_of(ind);
1450
1451 for (std::size_t k = 1; k <= R; ++k)
1452 for (std::size_t s = 0; s < n; ++s)
1453 a.TN(ist - 1, k - 1) += T(p[s] * r.dep_rates[s][isf - 1][k - 1]);
1454
1455 if (is_source) {
1456 // `to_marginal` encodes a Source as nir = Inf, an infinite
1457 // reservoir. That is a statement about the ENCODING, not a queue
1458 // length; reading it as one gives Q = Inf and then R = Q/T = Inf.
1459 continue;
1460 }
1461
1462 for (std::size_t s = 0; s < n; ++s) {
1463 if (num_traits<T>::to_double(p[s]) == 0) continue;
1464 const qn::Marginal<T> m =
1465 qn::to_marginal(sn, ist, r.space[s].local[isf - 1], ph, shift, nvar);
1466 for (std::size_t k = 1; k <= R; ++k) a.QN(ist - 1, k - 1) += T(p[s] * m.nir[k - 1]);
1467 }
1468
1469 const SchedStrategy sched = sn.stations[ist - 1].sched;
1470 // PAS / order-independent: utilization is the IN-SERVICE occupancy, not
1471 // the offered load. The two coincide only when a job engages a single
1472 // server, which is exactly what a pass-and-swap station does not do.
1473 if (sched == SchedStrategy::PAS) {
1474 for (std::size_t s = 0; s < n; ++s) {
1475 if (num_traits<T>::to_double(p[s]) == 0) continue;
1476 const qn::Marginal<T> m =
1477 qn::to_marginal(sn, ist, r.space[s].local[isf - 1], ph, shift, nvar);
1478 for (std::size_t k = 0; k < R; ++k)
1479 a.UN(ist - 1, k) += T(p[s] * m.sir[k] / num_traits<T>::from_double(S));
1480 }
1481 continue;
1482 }
1483 // A load-dependent station has no single service rate, so `T*E[S]/c` is
1484 // not its utilization: the reference accumulates the per-state capacity
1485 // share weighted by the lld factor and divides by the EFFECTIVE server
1486 // count max(c, max lld), which is what the scaling can deliver.
1487 // A class- or joint-dependent station takes the SAME branch: none of the
1488 // three has a single service rate, so `T*E[S]/c` is not its utilization.
1489 // The reference gates all three on one `isempty(lld) && isempty(cd) &&
1490 // isempty(jd)` test, and the cd/jd cases are then OVERWRITTEN by the
1491 // declared-peak normalization at the end of this function -- this branch
1492 // is what a jd-only station keeps, and what a cd station holds until the
1493 // peak pass replaces it.
1494 const std::vector<T>& lld = sn.stations[ist - 1].lldscaling;
1495 if (!lld.empty() || sn.stations[ist - 1].cdscaling || sn.stations[ist - 1].jdscaling) {
1496 double ceff = S;
1497 for (std::size_t j = 0; j < lld.size(); ++j)
1498 ceff = std::max(ceff, num_traits<T>::to_double(lld[j]));
1499 const bool share = sched == SchedStrategy::PS || sched == SchedStrategy::DPS ||
1500 sched == SchedStrategy::GPS || sched == SchedStrategy::LPS;
1501 for (std::size_t s = 0; s < n; ++s) {
1502 if (num_traits<T>::to_double(p[s]) == 0) continue;
1503 const qn::Marginal<T> m =
1504 qn::to_marginal(sn, ist, r.space[s].local[isf - 1], ph, shift, nvar);
1505 double ni = 0;
1506 for (std::size_t k = 0; k < R; ++k) ni += num_traits<T>::to_double(m.nir[k]);
1507 if (ni <= 0) continue;
1508 double lldnow = 1.0;
1509 if (!lld.empty()) {
1510 const std::size_t li = std::min<std::size_t>(
1511 lld.size(), std::max<std::size_t>(1, static_cast<std::size_t>(ni)));
1512 lldnow = num_traits<T>::to_double(lld[li - 1]);
1513 }
1514 if (share) {
1515 T wtot = zero;
1516 for (std::size_t k = 0; k < R; ++k)
1517 wtot += T(m.nir[k] * sn.stations[ist - 1].schedparam[k]);
1518 if (num_traits<T>::to_double(wtot) <= 0) continue;
1519 for (std::size_t k = 0; k < R; ++k)
1520 a.UN(ist - 1, k) +=
1521 T(p[s] * (m.nir[k] * sn.stations[ist - 1].schedparam[k] / wtot) *
1522 num_traits<T>::from_double(lldnow / ceff));
1523 } else {
1524 double sirtot = 0;
1525 for (std::size_t k = 0; k < R; ++k)
1526 sirtot += num_traits<T>::to_double(m.sir[k]);
1527 if (sirtot <= 0) continue;
1528 for (std::size_t k = 0; k < R; ++k)
1529 a.UN(ist - 1, k) += T(p[s] * num_traits<T>::from_double(
1531 sirtot * lldnow / ceff));
1532 }
1533 }
1534 continue;
1535 }
1536 if (sched == SchedStrategy::INF) {
1537 // An infinite server is "utilized" by every job it holds: there is
1538 // no queueing, so utilization and queue length coincide.
1539 for (std::size_t k = 1; k <= R; ++k) a.UN(ist - 1, k - 1) = a.QN(ist - 1, k - 1);
1540 } else {
1541 // A class that can be DROPPED here -- an open class at a station
1542 // with a finite capacity -- must be measured on the CARRIED rate
1543 // alone. The offered rate counts arrivals that never entered
1544 // service, so `max` would report the offered load as utilization:
1545 // for an M/M/1/4 with lambda = 0.6 that is 0.6 against the true
1546 // 1 - p0 = 0.566. Where nothing can be dropped the two estimates
1547 // agree in steady state and the max only guards numerical noise.
1548 // A class a G-network SIGNAL can annihilate is droppable for the
1549 // same reason a capacity-limited one is: the job leaves without a
1550 // completion, so the offered rate is not what the server did.
1551 const std::vector<bool> lossy = ctmc_signal_lossy(sn, r, p, isf);
1552 // A station inside a DROP region loses arrivals it cannot admit,
1553 // exactly as a finite per-station capacity does, so the same
1554 // carried-rate rule applies there.
1555 bool in_drop = false;
1556 for (std::size_t f = 0; f < sn.regions.size() && !in_drop; ++f) {
1557 bool has_drop = false;
1558 for (std::size_t rr = 0; rr < sn.regions[f].rule.size(); ++rr)
1559 if (sn.regions[f].rule[rr] == lang::DropStrategy::DROP) has_drop = true;
1560 if (has_drop && ist - 1 < sn.regions[f].members.size() &&
1561 sn.regions[f].members[ist - 1])
1562 in_drop = true;
1563 }
1564 for (std::size_t k = 1; k <= R; ++k) {
1565 const bool can_drop =
1566 (!std::isfinite(sn.njobs()[k - 1]) &&
1567 (std::isfinite(sn.cap[ist - 1]) ||
1568 std::isfinite(sn.classcap[ist - 1][k - 1]) || in_drop)) ||
1569 lossy[k - 1];
1570 const lang::Distrib<T>& d = sn.service[ist - 1][k - 1];
1571 if (d.disabled || d.D0.rows() == 0) continue;
1572 mam::Map<T> mp;
1573 mp.D0 = d.D0;
1574 mp.D1 = d.D1;
1575 const T mean = mam::map_mean(mp);
1576 const T u_dep = T(a.TN(ist - 1, k - 1) * mean / num_traits<T>::from_double(S));
1577 if (can_drop) {
1578 a.UN(ist - 1, k - 1) = u_dep;
1579 continue;
1580 }
1581 T arv = zero;
1582 for (std::size_t s = 0; s < n; ++s)
1583 arv += T(p[s] * r.arv_rates[s][isf - 1][k - 1]);
1584 const T u_arv = T(arv * mean / num_traits<T>::from_double(S));
1585 a.UN(ist - 1, k - 1) =
1587 : u_dep;
1588 }
1589 // For a lossy class the carried rate is still not exact unless the
1590 // service is exponential, so the in-service occupancy read off the
1591 // state space REPLACES it -- see `ctmc_signal_busy`.
1592 bool anylossy = false;
1593 for (std::size_t k = 0; k < R; ++k) anylossy = anylossy || lossy[k];
1594 if (anylossy) {
1595 const std::vector<T> unb = ctmc_signal_busy(sn, ist, r, p, isf);
1596 for (std::size_t k = 0; k < R; ++k)
1597 if (lossy[k]) a.UN(ist - 1, k) = unb[k];
1598 }
1599 }
1600 }
1601
1602 // TRUE BAS: the held job is counted at its DESTINATION, not where it sits.
1603 //
1604 // The state has it at the blocking station -- that is what the marker means --
1605 // but it has FINISHED service there and is on its way out, so reporting it in
1606 // the upstream queue length would double-count the time it spends waiting for
1607 // room. The reference moves it, and only when the destination is unambiguous:
1608 // with several downstream stations there is no single place to move it to, so
1609 // it is left where it sits rather than assigned arbitrarily.
1610 for (std::size_t ist = 1; ist <= M && !sn.isbasblocking.empty(); ++ist) {
1611 const std::size_t ind = sn.node_of_station(ist);
1612 const std::size_t isf = sn.stateful_of_station(ist);
1613 if (ind == 0 || isf == 0 || ind > sn.isbasblocking.size()) continue;
1614 if (!sn.isbasblocking[ind - 1]) continue;
1615 const std::vector<std::size_t> dests = sn.downstream_stations(ind);
1616 if (dests.size() != 1) continue;
1617 const std::size_t jst = sn.nodes[dests[0] - 1].station;
1618 if (jst == 0) continue;
1619 std::vector<std::size_t> ph2(R, 1), sh2(R, 0);
1620 std::size_t w2 = 0;
1621 for (std::size_t k = 0; k < R; ++k) {
1622 ph2[k] = sn.phasessz_of(ist, k + 1);
1623 sh2[k] = w2;
1624 w2 += ph2[k];
1625 }
1626 const std::size_t nv2 = sn.nvars_of(ind);
1627 for (std::size_t s = 0; s < n; ++s) {
1628 if (num_traits<T>::to_double(p[s]) == 0) continue;
1629 const std::vector<T>& row = r.space[s].local[isf - 1];
1630 if (row.empty() || num_traits<T>::to_double(row.back()) != 1) continue;
1631 const qn::Marginal<T> m = qn::to_marginal(sn, ist, row, ph2, sh2, nv2);
1632 for (std::size_t k = 0; k < R; ++k) {
1633 // A blocked state holds EXACTLY ONE completed job, so the count
1634 // is capped at 1: the rest of the queue has not finished and
1635 // stays where it is.
1636 const double nk = num_traits<T>::to_double(m.nir[k]);
1637 if (!(nk > 0)) continue;
1638 const T shift = T(p[s] * num_traits<T>::from_double(std::min(nk, 1.0)));
1639 a.QN(ist - 1, k) -= shift;
1640 a.QN(jst - 1, k) += shift;
1641 }
1642 }
1643 }
1644
1645 // DECLARED-PEAK UTILIZATION at a class- or joint-dependent station. The
1646 // per-state capacity share accumulated above is a busy-server probability,
1647 // and at a dependent station that is not what utilization means: a beta_r(n)
1648 // emulating extra servers would report more than one. The reference REPLACES
1649 // it by T/mu/peak, restoring the T*S/c convention against the DECLARED peak.
1650 //
1651 // The `all njobs finite` guard is the reference's and is not a convenience:
1652 // with an open class the state space is a TRUNCATION, so the throughput this
1653 // divides is the truncated chain's and the ratio is not a utilization of the
1654 // model. The busy-server estimate above at least stays a probability, so it
1655 // is what an open dependent station keeps.
1656 //
1657 // THE TWO PEAKS MULTIPLY. beta_r(n) and eta_i(n) scale the SAME nominal rate
1658 // and the event layer folds them multiplicatively (`cd_factor`), so the peak
1659 // attainable rate is `rates * cdpeak * jdpeak`. The reference used to run two
1660 // independent blocks each ASSIGNING UN, which let the jd peak overwrite the cd
1661 // one and dropped a factor `cdpeak` at a station carrying both; that was fixed
1662 // in `solver_ctmc_analyzer.m` / `solver_ctmc_avg_from_pi.m` together with this
1663 // port, and it is what SolverSSA already did
1664 // (`solver_ssa_analyzer_serial.m:87-90`).
1665 bool all_closed = true;
1666 for (std::size_t k = 0; k < R; ++k)
1667 if (!std::isfinite(sn.njobs()[k])) all_closed = false;
1668 if (all_closed) {
1669 for (std::size_t ist = 1; ist <= M; ++ist) {
1670 const qn::Station<T>& st = sn.stations[ist - 1];
1671 const bool has_cd = static_cast<bool>(st.cdscaling);
1672 const bool has_jd = static_cast<bool>(st.jdscaling);
1673 if (!has_cd && !has_jd) continue;
1674 for (std::size_t k = 0; k < R; ++k) {
1675 double bmax = 1.0;
1676 if (has_cd)
1677 bmax *= k < st.cdscalingpeak.size()
1679 : 0.0;
1680 if (has_jd)
1681 bmax *= k < st.jdscalingpeak.size()
1683 : 0.0;
1684 const double mu = num_traits<T>::to_double(sn.rates(ist - 1, k));
1685 a.UN(ist - 1, k) = (std::isfinite(mu) && mu > 0 && bmax > 0)
1686 ? T(a.TN(ist - 1, k) /
1687 num_traits<T>::from_double(mu * bmax))
1688 : zero;
1689 }
1690 }
1691 }
1692
1693 // SYNCHRONOUS CALLS. A caller blocked waiting for its reply still HOLDS its
1694 // server, so those jobs belong in QLen and Util even though they are not in
1695 // service here. They are NOT in the response time: response time is time
1696 // spent AT the station, and the blocked job is at its callee.
1697 Matrix<T> qn_blocked(M, R, zero);
1698 if (!sn.replyblock.empty()) {
1699 for (std::size_t ist = 1; ist <= M; ++ist) {
1700 const std::size_t ind = sn.node_of_station(ist);
1701 const std::size_t isf = sn.stateful_of_station(ist);
1702 if (isf == 0 || sn.replyblock.size() < ind) continue;
1703 bool any = false;
1704 for (std::size_t k = 0; k < sn.replyblock[ind - 1].size(); ++k)
1705 any = any || sn.replyblock[ind - 1][k];
1706 if (!any) continue;
1707 const qn::ReplyBlockInfo info = qn::reply_block_info(sn, ind);
1708 if (info.width == 0) continue;
1709 const double S2 = sn.stations[ist - 1].nservers;
1710 // The counters are the LAST columns of the local row, one per
1711 // calling class, in the order `reply_block_info` lists them.
1712 for (std::size_t s = 0; s < n; ++s) {
1713 if (num_traits<T>::to_double(p[s]) == 0) continue;
1714 const std::vector<T>& row = r.space[s].local[isf - 1];
1715 for (std::size_t pos = 0; pos < info.classes.size(); ++pos) {
1716 const std::size_t col = row.size() - info.width + pos;
1717 const std::size_t cls = info.classes[pos];
1718 qn_blocked(ist - 1, cls - 1) += T(p[s] * row[col]);
1719 }
1720 }
1721 for (std::size_t k = 0; k < R; ++k) {
1722 a.QN(ist - 1, k) += qn_blocked(ist - 1, k);
1723 a.UN(ist - 1, k) += T(qn_blocked(ist - 1, k) / num_traits<T>::from_double(S2));
1724 }
1725 }
1726 }
1727
1728 // Little's law per station, then per class at the reference station.
1729 for (std::size_t k = 1; k <= R; ++k) {
1730 for (std::size_t ist = 1; ist <= M; ++ist)
1731 a.RN(ist - 1, k - 1) =
1732 num_traits<T>::to_double(a.TN(ist - 1, k - 1)) > 0
1733 ? T((a.QN(ist - 1, k - 1) - qn_blocked(ist - 1, k - 1)) /
1734 a.TN(ist - 1, k - 1))
1735 : zero;
1736 const double nk = sn.njobs()[k - 1];
1737 a.CN[k - 1] = std::isfinite(nk) && num_traits<T>::to_double(a.XN[k - 1]) > 0
1738 ? T(num_traits<T>::from_double(nk) / a.XN[k - 1])
1739 : zero;
1740 }
1741 return a;
1742}
1743
1744} // namespace ctmc
1745} // namespace line
1746
1747#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::vector< std::size_t > stateful_nodes
1-based node indices, ascending
std::vector< NodeDef > nodes
every node, in creation order
A network plus its refreshed NetworkStruct.
static void step(const char *fmt,...)
Write one progress line.
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
The exception types the port throws.
Running progress log of a LINE solver run (the "solver console").
LU factorization with partial pivoting, templated on the number type.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
void ctmc_eliminate_vanishing(CtmcResult< T > &res)
Port of the "now remove immediate transitions" block of solver_ctmc.m (:812-870): eliminate the vanis...
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.
CtmcResult< T > solver_ctmc(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space, const std::vector< Sync< T > > &sync, const std::vector< qn::GlobalSync< T > > &gsync=std::vector< qn::GlobalSync< T > >(), bool want_filtration=false, const std::vector< qn::FjSync< T > > &fjsync=std::vector< qn::FjSync< T > >())
Port of the generator assembly of solver_ctmc.m.
bool ctmc_all_phasetype(const NetworkStruct< T > &sn)
MATLAB's all(sn.isph(:)), read off the matrices instead of off a flag.
std::vector< T > ctmc_signal_busy(const NetworkStruct< T > &sn, std::size_t ist, const CtmcResult< T > &r, const std::vector< T > &p, std::size_t isf)
Port of ctmc_signal_busy: the exact per-class busy-server fraction, read off the enumerated state spa...
Matrix< T > ctmc_gd_factor(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space)
Tabulates the globally state-dependent rate scaling phi(n) declared through set_global_dependence,...
Matrix< T > ctmc_state_space_aggr(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space)
Port of StateSpaceAggr: the per-(station, class) job counts of every state, as an (nstates x nstation...
std::vector< std::size_t > ctmc_find_vanishing_states(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space, const std::vector< qn::GlobalSync< T > > &gsync, bool isfjaug)
Port of ctmc_find_vanishing_states (solver_ctmc.m:928): the indices of the VANISHING (zero-sojourn) g...
std::vector< NetState< T > > reachable_space_generator(const NetworkStruct< T > &sn, const NetState< T > &init, const std::vector< Sync< T > > &sync, const std::vector< qn::GlobalSync< T > > &gsync=std::vector< qn::GlobalSync< T > >(), std::size_t maxst=3000000, const std::vector< qn::FjSync< T > > &fjsync=std::vector< qn::FjSync< T > >(), const std::vector< std::size_t > &cutoff=std::vector< std::size_t >(), const std::vector< std::vector< std::size_t > > &cutoff_mat=std::vector< std::vector< std::size_t > >())
Port of State.reachableSpaceGenerator: the states reachable from init.
std::vector< bool > ctmc_signal_lossy(const NetworkStruct< T > &sn, const CtmcResult< T > &r, const std::vector< T > &p, std::size_t isf)
Port of ctmc_signal_lossy: classes a G-network signal can annihilate here.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:363
EventType
The events a state can undergo, with the values of MATLAB EventType.
Definition lang_types.h:111
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
StochCompResult< T > ctmc_stochcomp(const Matrix< T > &Q, const std::vector< std::size_t > &I)
Definition dtmc_solve.h:150
EventOutcome< T > after_event_join(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls)
Port of State.afterEventJoin: an event at a Join node of an FJ-augmented struct.
ReplyBlockInfo reply_block_info(const NetworkStruct< T > &sn, std::size_t ind)
Defined below; the departure branch records a server held for a reply.
Marginal< T > to_marginal(const NetworkStruct< T > &sn, std::size_t ist, const std::vector< T > &state_i, const std::vector< std::size_t > &phasesz, const std::vector< std::size_t > &phaseshift, std::size_t nvar=0)
Port of State.toMarginal for a STATION, one state row at a time.
Definition state.h:130
GlobalOutcome< T > after_global_event(const NetworkStruct< T > &sn, const NetState< T > &glspace, const GlobalSync< T > &gl)
Port of State.afterGlobalEvent: an SPN mode ENABLEs or FIREs.
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.
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...
GlobalOutcome< T > after_fj_event(const NetworkStruct< T > &sn, const FjSync< T > &e, const NetState< T > &gl)
Port of State.afterFJEvent: fire ONE entry of the fork firing list.
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
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
A queueing network and its refreshed NetworkStruct.
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...
The mean performance metrics a stationary vector maps to.
std::vector< T > CN
(nclasses) system throughput and response time
std::vector< T > XN
Matrix< T > StartN
The DERIVED rates, (nstations x nclasses): how often per unit time a class-r service STARTS at statio...
Matrix< T > TN
(nstations x nclasses)
The generator, the state space it is indexed by, and the event rates.
Definition solver_ctmc.h:57
std::vector< std::vector< std::vector< T > > > dep_rates_imm
std::vector< std::vector< Matrix< T > > > start_filt
The DERIVED START and PREEMPT filtrations, indexed [station-1][class-1]: the rate at which a transiti...
Definition solver_ctmc.h:93
std::vector< NetState< T > > space
row i of Q is space[i]
Definition solver_ctmc.h:59
std::vector< std::vector< std::vector< T > > > arv_rates
arvRates / depRates, indexed [state][stateful-1][class-1]: the total rate of arrivals into,...
Definition solver_ctmc.h:67
std::vector< std::vector< Matrix< T > > > preempt_filt
Definition solver_ctmc.h:93
std::vector< std::vector< std::vector< T > > > dep_rates
Definition solver_ctmc.h:67
std::vector< Matrix< T > > filt
Dfilt, MATLAB's EVENT FILTRATION: filt[a] holds only the rates that synchronization a contributed,...
Definition solver_ctmc.h:82
std::vector< std::vector< std::vector< T > > > arv_rates_imm
The parts of arv_rates / dep_rates contributed by those same immediate sources.
std::vector< std::size_t > vanishing
The rows the purge restated, i.e.
Matrix< T > Q
(n x n) infinitesimal generator
Definition solver_ctmc.h:58
Matrix< T > Qimm
Qimm, the IMMEDIATE-ONLY part of Q: the arcs contributed by a Router or Fork pass-through,...
static constexpr double Zero
Definition lang_types.h:670
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2140
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2141
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:759
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
Matrix< T > S
stochastic complement on the selected states
Definition dtmc_solve.h:137
What one event produces at one node: the successor rows, their rates and their probabilities,...
std::vector< T > prob
per-row probability of the choice
std::vector< std::vector< T > > space
successor local state rows
std::vector< T > rate
per-row rate, -1 on a passive half
One fork firing synchronization: sn.fjsync{k}.
std::size_t fork
1-based Fork node
std::vector< std::size_t > branchheads
1-based node per branch
std::size_t cls
1-based ORIGINAL class being forked
std::vector< std::size_t > auxclasses
the tag's auxiliary class per branch
What one global event produces: a whole network state per outcome.
std::vector< T > rate
std::vector< NetState< T > > space
std::vector< T > prob
std::vector< bool > completion
True where the outcome is a firing COMPLETION, i.e.
A GLOBAL synchronization: an SPN mode event and the place arcs it drives.
What State.toMarginal returns for one station and one state row.
Definition state.h:51
std::vector< T > nir
jobs per class
Definition state.h:53
std::vector< T > sir
jobs in service per class
Definition state.h:54
One half of a GLOBAL synchronization: a mode event at a node.
std::size_t mode
1-based mode index
std::size_t node
1-based node index (a Transition, or a place)
T weight
arc multiplicity
std::size_t cls
1-based class the arc moves
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2140
std::vector< std::vector< T > > local
local[isf] is that node's state row
Definition state.h:2141
Where node ind keeps its reply-block counters inside the local vars.
std::vector< std::size_t > classes
1-based calling classes holding a block
One station of the network.
std::vector< T > jdscalingpeak
sn.jdscalingpeak for this station: the declared peak joint-dependent scaling per class.
CdScaling< T > jdscaling
sn.jdscaling for this station: MATLAB's Station.ljdScaling, the JOINT dependence map eta_i(n),...
std::vector< T > cdscalingpeak
sn.cdscalingpeak for this station: the DECLARED peak rate scaling per class, empty when the station i...
CdScaling< T > cdscaling
sn.cdscaling for this station: the class-dependence map, empty when unset.
One synchronization: an ACTIVE event and the PASSIVE event it drives.
SyncEvent< T > passive
SyncEvent< T > active