LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_getters.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_GETTERS_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_GETTERS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The remaining `@@SolverCTMC` accessors: `getGenerator` / `getInfGen`,
12 * `getStateSpace` / `getStateSpaceAggr` and the `getTranProb*` family. The
13 * symbolic getters live in solver_ctmc_symbolic.h.
14 *
15 * WHAT AN ACCESSOR HERE IS INDEXED BY. Every quantity below is read off the
16 * chain `solver_ctmc_analyzer` SOLVED, which is the enumerated space after the
17 * DROP regions have censored it and, on a reducible encoding, restricted to one
18 * weakly connected component. The reference's `getGenerator` and
19 * `getStateSpace` instead call `solver_ctmc` and `State.spaceGenerator`
20 * directly and so report the pre-restriction space, which on such a model has
21 * more rows than its own `pi` vector does. The states that differ carry zero
22 * stationary mass and no other accessor in this port can index them, so they
23 * are dropped here rather than handed back as rows nothing else accepts.
24 *
25 * WHY THE FILTRATION IS AN OUTPUT AND NOT A DERIVATION. `eventFilt` is not
26 * recoverable from Q: assembling the generator adds every synchronization's
27 * contribution into one entry, and no inspection of the sum says which event
28 * put what there. It has to be recorded while Q is built, which is what
29 * `CtmcOptions::keep_filtration` turns on, so the accessors that return it
30 * force that flag on rather than reporting an empty filtration.
31 */
32
33#include <algorithm>
34#include <cmath>
35#include <cstddef>
36#include <limits>
37#include <set>
38#include <string>
39#include <vector>
40
43#include "line/lang/qn/state.h"
49#include "line/util/error.h"
50#include "line/util/matrix.h"
51
53
54namespace line {
55namespace ctmc {
56
57/** `[infGen, eventFilt, ev]` of `@@SolverCTMC/getGenerator.m`. */
58template <class T>
60 Matrix<T> Q; ///< the infinitesimal generator
61 std::vector<NetState<T>> space; ///< row i of Q is space[i]
62 /**
63 * `eventFilt`: `filt[a]` holds only what synchronization `sync[a]`
64 * contributed, so `sum_a filt[a]` is the off-diagonal part of Q. The two
65 * vectors are indexed alike; that pairing is the whole content of the
66 * filtration and is why `sync` travels with it.
67 */
68 std::vector<Matrix<T>> filt;
69 std::vector<Sync<T>> sync; ///< `ev`, the reference's `sn.sync`
70 /**
71 * The DERIVED START/PREEMPT filtrations, indexed [station-1][class-1]. They
72 * are NOT part of `filt` and are NOT paired with `sync`: a START rides on an
73 * arc `filt` already carries, so a caller summing `filt` must not see them.
74 */
75 std::vector<std::vector<Matrix<T>>> start_filt, preempt_filt;
76};
77
78/** `[stateSpace, localStateSpace]` of `@@SolverCTMC/getStateSpace.m`. */
79template <class T>
81 /**
82 * The enumerated states. `space[s].local[f]` is already the reference's
83 * `localStateSpace{f}` row: this port keeps a state as its per-node blocks
84 * and never flattens it, so the local decomposition costs nothing to
85 * report and cannot disagree with the flat form.
86 */
87 std::vector<NetState<T>> space;
88 Matrix<T> flat; ///< the blocks concatenated, as MATLAB returns them
89 std::vector<std::size_t> node_width; ///< column width of each stateful node's block
90 /**
91 * `localStateSpace{f}`: one matrix per stateful node, its DISTINCT local
92 * rows in first-appearance order.
93 *
94 * WITHOUT THIS THE SECOND RETURN VALUE CANNOT BE FORMED. `getStateSpace.m`
95 * under `lang='cpp'` returns `{stateSpace}` -- a single cell holding the
96 * whole flat space -- because the local decomposition was not on the wire,
97 * and a caller that indexes `localStateSpace{f}` per node then reads the
98 * global space for every node. `node_width` alone is not enough: it says
99 * where a block ENDS, not which rows a node admits.
100 *
101 * IT IS THE ENUMERATED CHAIN'S DECOMPOSITION, not `spaceGenerator`'s raw
102 * per-node enumeration. The reference's `qnc.space{f}` is built before the
103 * cartesian product and can hold a local row no global state uses; every
104 * row here appears in `space`. On a model whose lattice admits every
105 * combination the two coincide, and where they differ this one is the
106 * decomposition of the chain that was actually solved.
107 */
108 std::vector<Matrix<T>> local;
109};
110
111/** The time-dependent answer of one `getTranProb*` query. */
112template <class T>
114 /**
115 * The reference returns `Pi_t = [t, pi_t]`, one matrix with time glued on
116 * as column 1. The two are kept apart here because they are not the same
117 * quantity and concatenating them forces every consumer to know that
118 * column 1 is not a probability.
119 */
120 std::vector<T> t;
121 Matrix<T> pit; ///< (ntimes x nstates) occupancy over the solved chain
122 Matrix<T> labels; ///< (nstates x width) the state descriptor the query asked for
123};
124
125/**
126 * `SolverCTMC.getStartRate` and `getPreemptRate`: the DERIVED rates the
127 * START/PREEMPT filtration reduces to.
128 *
129 * StartN(i,r) is how often per unit time a class-r service STARTS at station i,
130 * and PreemptN(i,r) how often a class-r job in service is pushed back into the
131 * buffer there. Both are computed by `solver_ctmc_avg_from_pi` already; what was
132 * missing was any way to ask for them, because `solver_ctmc_run_analyzer` returns an
133 * `mva::AvgResult` and that shape has no slot for a derived rate. The reference
134 * exposes them as plain getters, and so do these.
135 *
136 * They need the filtration, so the solve is repeated with `keep_filtration` set
137 * rather than read off a result that may not carry it: a caller who already has
138 * one passes the `CtmcSolution` overload instead and pays nothing.
139 *
140 * At a lossless station with no in-service abandonment
141 * StartN == TN + PreemptN, which is the identity to check them against.
142 */
143template <class T>
145 CtmcOptions o = opt;
146 o.keep_filtration = true;
147 return solver_ctmc_analyzer(sn, o).avg.StartN;
148}
149
150/** As above, for a caller whose solution already carries the filtration. */
151template <class T>
153 return d.avg.StartN;
154}
155
156/** `SolverCTMC.getPreemptRate`; see {@link ctmc_get_start_rate}. */
157template <class T>
159 CtmcOptions o = opt;
160 o.keep_filtration = true;
161 return solver_ctmc_analyzer(sn, o).avg.PreemptN;
162}
163
164/** As above, for a caller whose solution already carries the filtration. */
165template <class T>
167 return d.avg.PreemptN;
168}
169
170namespace getters_detail {
171
172/** Per-node block widths, which every state shares by construction. */
173template <class T>
174std::vector<std::size_t> node_widths(const std::vector<NetState<T>>& space) {
175 std::vector<std::size_t> w;
176 if (space.empty()) return w;
177 for (std::size_t f = 0; f < space[0].local.size(); ++f) w.push_back(space[0].local[f].size());
178 return w;
179}
180
181/** Concatenate the per-node blocks of every state into one matrix. */
182template <class T>
183Matrix<T> flatten(const std::vector<NetState<T>>& space) {
184 const std::vector<std::size_t> w = node_widths(space);
185 std::size_t total = 0;
186 for (std::size_t f = 0; f < w.size(); ++f) total += w[f];
187 Matrix<T> out(space.size(), total, num_traits<T>::from_int(0));
188 for (std::size_t s = 0; s < space.size(); ++s) {
189 std::size_t c = 0;
190 for (std::size_t f = 0; f < space[s].local.size(); ++f)
191 for (std::size_t j = 0; j < space[s].local[f].size(); ++j) out(s, c++) = space[s].local[f][j];
192 }
193 return out;
194}
195
196/**
197 * The DISTINCT local rows of each stateful node, in first-appearance order.
198 *
199 * First-appearance and not sorted, because the reference's `qnc.space{f}` is
200 * built in enumeration order and a caller that pairs a local index with a row
201 * of `space` must see the same order on both sides.
202 */
203template <class T>
204std::vector<Matrix<T>> local_spaces(const std::vector<NetState<T>>& space) {
205 std::vector<Matrix<T>> out;
206 if (space.empty()) return out;
207 const std::size_t NF = space[0].local.size();
208 for (std::size_t f = 0; f < NF; ++f) {
209 std::vector<std::vector<T>> rows;
210 std::set<std::vector<double>> seen;
211 for (std::size_t s = 0; s < space.size(); ++s) {
212 if (space[s].local.size() <= f) continue;
213 const std::vector<T>& r = space[s].local[f];
214 std::vector<double> key(r.size());
215 for (std::size_t j = 0; j < r.size(); ++j) key[j] = num_traits<T>::to_double(r[j]);
216 if (!seen.insert(key).second) continue;
217 rows.push_back(r);
218 }
219 const std::size_t w = rows.empty() ? 0 : rows[0].size();
220 Matrix<T> m(rows.size(), w, num_traits<T>::from_int(0));
221 for (std::size_t i = 0; i < rows.size(); ++i)
222 for (std::size_t j = 0; j < w && j < rows[i].size(); ++j) m(i, j) = rows[i][j];
223 out.push_back(m);
224 }
225 return out;
226}
227
228/**
229 * The stateful index of a node, refusing a node that carries no state.
230 *
231 * A stateless node -- a Router, a ClassSwitch -- has no block in any state, so
232 * there is nothing to slice out for it and the query is a caller error rather
233 * than an empty answer.
234 */
235template <class T>
236std::size_t stateful_or_throw(const NetworkStruct<T>& sn, std::size_t ind, const char* what) {
237 if (ind < 1 || ind > sn.nodes.size())
238 throw InputError(std::string(what) + ": the node index is out of range");
239 const std::size_t isf = sn.stateful_index(ind);
240 if (isf == 0)
241 throw InputError(std::string(what) +
242 ": node " + std::to_string(ind) +
243 " is stateless and holds no block of the network state");
244 return isf;
245}
246
247/**
248 * The timespan gate of the `getTranProb*` family.
249 *
250 * The reference refuses an infinite horizon by name because there is nothing to
251 * integrate to: `pi(t)` on [0, Inf) is the stationary vector, which is what
252 * `getProb*` already answers.
253 */
254template <class T>
255void check_timespan(const T& t0, const T& t1, const char* what) {
256 const double a = num_traits<T>::to_double(t0), b = num_traits<T>::to_double(t1);
257 if (!std::isfinite(a) || !std::isfinite(b) || !(b > a))
258 throw InputError(std::string(what) +
259 " requires a finite timespan [t0, t1] with t1 > t0; for the limit as t "
260 "grows use the stationary family (getProb, getProbAggr, getProbSys, "
261 "getProbSysAggr)");
262}
263
264} // namespace getters_detail
265
266/**
267 * Port of `@@SolverCTMC/getGenerator.m`: the generator, its event filtration and
268 * the synchronization list the filtration is indexed by.
269 *
270 * The synchronization list is rebuilt rather than carried, `refresh_sync` being
271 * a pure function of the struct: it returns the same list, in the same order,
272 * that indexed `filt` when the generator was assembled.
273 */
274template <class T>
276 if (d.chain.filt.empty())
277 throw InputError(
278 "ctmc_get_generator: the solution carries no event filtration, which cannot be "
279 "recovered from Q because its entries have already summed every synchronization's "
280 "contribution; re-solve with CtmcOptions::keep_filtration = true");
282 g.Q = d.chain.Q;
283 g.space = d.chain.space;
284 g.filt = d.chain.filt;
285 g.start_filt = d.chain.start_filt;
286 g.preempt_filt = d.chain.preempt_filt;
287 g.sync = refresh_sync(d.solved_sn ? *d.solved_sn : sn); // server pools add PHASE actions
288 return g;
289}
290
291/**
292 * As above, solving the chain first.
293 *
294 * `keep_filtration` is forced on because the filtration is half the answer and
295 * cannot be reconstructed afterwards; a caller who only wants Q should read
296 * `CtmcSolution::chain` instead and not pay for one n x n matrix per event.
297 */
298template <class T>
304
305/** `@@SolverCTMC/getInfGen.m`, a pure alias of `getGenerator` in the reference. */
306template <class T>
310
311/** `@@SolverCTMC/getInfGen.m`, solving the chain first. */
312template <class T>
316
317/**
318 * Port of `@@SolverCTMC/getStateSpace.m`.
319 *
320 * The reference derives `localStateSpace` by cutting the flat matrix at the
321 * width of each node's own space, a split that can only be got right by
322 * carrying those widths alongside. Here the state IS the split, so `flat` is
323 * the derived form and the widths are reported for a caller comparing columns
324 * against MATLAB.
325 */
326template <class T>
329 s.space = d.chain.space;
330 s.flat = getters_detail::flatten(d.chain.space);
331 s.node_width = getters_detail::node_widths(d.chain.space);
332 s.local = getters_detail::local_spaces(d.chain.space);
333 return s;
334}
335
336/** As above, solving the chain first. */
337template <class T>
341
342/** The answer of `@@SolverCTMC/getCdfFirstPassT.m`: the [F(t), t] curve with
343 * its grid, density and resolved state sets. */
345 std::vector<double> t; ///< the grid: the caller's, else 1000 points to the horizon
346 std::vector<double> F; ///< CDF at t, clamped to [0, 1]; DEFECTIVE under a taboo set
347 std::vector<double> f; ///< density at t
348 std::vector<std::size_t> source; ///< resolved 0-based rows; empty = conditional stationary
349 std::vector<std::size_t> target; ///< resolved 0-based rows
350 std::vector<std::size_t> taboo; ///< resolved 0-based rows of C minus B; empty = none
351 double reachprob = 0.0; ///< Pr{B entered before C}, the limit of F
352 double tabooprob = 0.0; ///< Pr{C entered before B}
353};
354
355namespace getters_detail {
356
357/**
358 * A state set given as 1-based ROW INDICES into the state space or as matrices
359 * of state rows, resolved to 0-based row indices against `flat`. An
360 * unrecognised row is an error rather than a silent drop, since a passage into
361 * a state that is not in the space is not a slow passage but an undefined one.
362 */
363template <class T>
364std::vector<std::size_t> resolve_state_set(const Matrix<double>& S, const Matrix<T>& flat,
365 const char* name) {
366 std::set<std::size_t> idx;
367 if (S.rows() == 0 || S.cols() == 0) return std::vector<std::size_t>();
368 const std::size_t n = flat.rows();
369 bool is_index_vector = (S.rows() == 1 || S.cols() == 1);
370 if (is_index_vector) {
371 for (std::size_t i = 0; i < S.rows() && is_index_vector; ++i)
372 for (std::size_t j = 0; j < S.cols(); ++j) {
373 const double v = S(i, j);
374 if (v != std::floor(v) || v < 1 || v > static_cast<double>(n)) {
375 is_index_vector = false;
376 break;
377 }
378 }
379 }
380 if (is_index_vector) {
381 for (std::size_t i = 0; i < S.rows(); ++i)
382 for (std::size_t j = 0; j < S.cols(); ++j)
383 idx.insert(static_cast<std::size_t>(S(i, j)) - 1);
384 } else {
385 if (S.cols() != flat.cols())
386 throw InputError(std::string("getCdfFirstPassT: a state row in set ") + name +
387 " has " + std::to_string(S.cols()) + " columns where the state "
388 "space has " + std::to_string(flat.cols()));
389 for (std::size_t i = 0; i < S.rows(); ++i) {
390 bool found = false;
391 for (std::size_t r = 0; r < n && !found; ++r) {
392 bool eq = true;
393 for (std::size_t j = 0; j < S.cols() && eq; ++j)
394 if (num_traits<T>::to_double(flat(r, j)) != S(i, j)) eq = false;
395 if (eq) {
396 idx.insert(r);
397 found = true;
398 }
399 }
400 if (!found)
401 throw InputError(std::string("A state given in set ") + name +
402 " is not in the state space.");
403 }
404 }
405 return std::vector<std::size_t>(idx.begin(), idx.end());
406}
407
408} // namespace getters_detail
409
410/**
411 * Port of `@@SolverCTMC/getCdfFirstPassT.m`: the distribution of the FIRST
412 * PASSAGE TIME from state set A into state set B, on the CTMC underlying the
413 * model. An empty A starts from the conditional stationary law on the
414 * complement of B.
415 *
416 * THIS IS NOT getCdfRespT. That getter times a tagged job between an arrival
417 * at a station and its departure, through the event filtration; this one times
418 * the chain between two sets of states the caller names, and answers questions
419 * the filtration cannot express -- the writer cycle time of a readers-writers
420 * model, the time to fill a buffer, the time to leave a degraded region.
421 *
422 * C is an optional TABOO set: a passage that enters C before B is lost, so F
423 * becomes the DEFECTIVE law Pr{B entered by t, and C not entered before B},
424 * which tends to `reachprob` < 1. An empty C is the plain first passage and
425 * keeps its own route (`ctmc_passage_time`), so its curve is unchanged to the
426 * last bit; the taboo route then supplies only the infinite entries of `tset`
427 * and the reach probability.
428 *
429 * `tset` is an optional grid replacing the default horizon. It may be short and
430 * non-uniform; an infinite entry returns `reachprob`.
431 *
432 * References: P. G. Harrison and W. J. Knottenbelt, "Passage Time Distributions
433 * in Large Markov Chains", 2002. C. Baier, B. Haverkort, H. Hermanns, J.-P.
434 * Katoen, "Model-checking algorithms for continuous-time Markov chains", IEEE
435 * TSE 29(6), 2003, for the taboo set (time-bounded until).
436 */
437template <class T>
439 const Matrix<double>& A, const Matrix<double>& B,
440 const Matrix<double>& C, std::vector<double> tset,
441 const std::string& method = "expm") {
443 "getCdfFirstPassT takes a matrix exponential and therefore requires an "
444 "arithmetic with transcendental functions");
445 const Matrix<T>& Q = d.chain.Q;
446 const std::size_t n = Q.rows();
447 const Matrix<T> flat = getters_detail::flatten(d.chain.space);
448
450 out.target = getters_detail::resolve_state_set(B, flat, "B");
451 if (out.target.empty())
452 throw InputError(
453 "The target state set B is empty: a first passage time into no state is undefined.");
454 out.source = getters_detail::resolve_state_set(A, flat, "A");
455 const std::vector<std::size_t> Cidx = getters_detail::resolve_state_set(C, flat, "C");
456
457 std::vector<T> pi0;
458 if (!out.source.empty()) {
459 pi0.assign(n, num_traits<T>::from_int(0));
461 static_cast<long>(out.source.size()));
462 for (std::size_t idx : out.source) pi0[idx] = w;
463 }
464
465 if (tset.empty()) {
466 // The horizon is chosen the way the response-time getter chooses it: 100
467 // events at the slowest rate in the chain.
468 double min_rate = std::numeric_limits<double>::infinity();
469 for (std::size_t i = 0; i < n; ++i)
470 for (std::size_t j = 0; j < n; ++j) {
471 const double v = std::abs(num_traits<T>::to_double(Q(i, j)));
472 if (v > GlobalConstants::FineTol && v < min_rate) min_rate = v;
473 }
474 const double thor = std::abs(100.0 / min_rate);
475 tset.assign(1000, 0.0);
476 for (std::size_t i = 0; i < tset.size(); ++i)
477 tset[i] = thor * static_cast<double>(i) / static_cast<double>(tset.size() - 1);
478 }
479
480 out.t = tset;
481 if (Cidx.empty()) {
482 std::vector<double> tfin, tinf;
483 for (double v : tset) (std::isinf(v) ? tinf : tfin).push_back(v);
484 const mc::PassageTaboo<T> pt =
485 mc::ctmc_passage_taboo(Q, pi0, out.target, Cidx, tinf, method);
486 const mc::PassageCurve<T> curve = mc::ctmc_passage_time(Q, pi0, out.target, tfin, method);
487 out.F.assign(tset.size(), 0.0);
488 out.f.assign(tset.size(), 0.0);
489 std::size_t a = 0, b = 0;
490 for (std::size_t i = 0; i < tset.size(); ++i) {
491 if (std::isinf(tset[i])) {
492 out.F[i] = pt.F[b++];
493 } else {
494 out.F[i] = curve.F[a];
495 out.f[i] = curve.f[a++];
496 }
497 }
498 out.taboo = pt.taboo;
501 } else {
502 const mc::PassageTaboo<T> pt = mc::ctmc_passage_taboo(Q, pi0, out.target, Cidx, tset, method);
503 out.F = pt.F;
504 out.f = pt.f;
505 out.taboo = pt.taboo;
508 }
509 return out;
510}
511
512/** The plain first passage, with no taboo set and the default horizon. */
513template <class T>
515 const Matrix<double>& A, const Matrix<double>& B,
516 const std::string& method = "expm") {
517 return ctmc_cdf_firstpasst(sn, d, A, B, Matrix<double>(0, 0), std::vector<double>(), method);
518}
519
520/** As above, solving the chain first. */
521template <class T>
523 const Matrix<double>& A, const Matrix<double>& B,
524 const Matrix<double>& C, const std::vector<double>& tset,
525 const std::string& method = "expm") {
526 return ctmc_cdf_firstpasst(sn, solver_ctmc_analyzer(sn, opt), A, B, C, tset, method);
527}
528
529/** As above, solving the chain first. */
530template <class T>
532 const Matrix<double>& A, const Matrix<double>& B,
533 const std::string& method = "expm") {
534 return ctmc_cdf_firstpasst(sn, solver_ctmc_analyzer(sn, opt), A, B, method);
535}
536
537/** The answer of `@@SolverCTMC/getFirstPassTMoments.m`. */
538template <class T>
540 std::vector<T> m; ///< (nmax) moments for a passage started uniformly in A
541 Matrix<T> mall; ///< (nstates x nmax), one row per starting state
542 std::vector<T> p; ///< (nstates) probability of entering B (before C), one row per state
543 std::vector<std::size_t> source; ///< resolved 0-based rows; empty = conditional stationary
544 std::vector<std::size_t> target; ///< resolved 0-based rows
545 std::vector<std::size_t> taboo; ///< resolved 0-based rows of C minus B; empty = none
546};
547
548/**
549 * Port of `@@SolverCTMC/getFirstPassTMoments.m`: moments of order 1..nmax of
550 * the first passage time from state set A into state set B.
551 *
552 * NO TRANSFORM INVERSION AND NO TIME GRID ARE INVOLVED. The moments come from
553 * Eq. 3 of Harrison and Knottenbelt (2002) -- one linear solve per order -- so
554 * they are EXACT and are not limited by the horizon a CDF would have to be
555 * truncated at. That is the whole reason this getter exists beside
556 * `ctmc_cdf_firstpasst`: the variance or the skewness of a passage time costs
557 * nmax solves here and a numerical integration of a truncated curve there.
558 *
559 * `mall` is zero on B and infinite where B cannot be reached, as the reference
560 * reports it. A and B name states as in `ctmc_cdf_firstpasst`.
561 *
562 * C is an optional TABOO set, named the same way. With it the moments are
563 * CONDITIONAL on entering B before C, E[T^k | B before C], and `mall` is NaN for
564 * a starting state from which B cannot be entered first. An empty C keeps the
565 * plain route (`ctmc_passage_moments`) unchanged. `p` is the per-state
566 * probability of entering B before C, which is one with an empty C wherever B
567 * is reached surely.
568 */
569template <class T>
571 const CtmcSolution<T>& d,
572 const Matrix<double>& A,
573 const Matrix<double>& B,
574 std::size_t nmax,
575 const Matrix<double>& C) {
577 "getFirstPassTMoments marks an unreachable target with an infinity and "
578 "therefore requires an arithmetic that has one");
579 const Matrix<T>& Q = d.chain.Q;
580 const std::size_t n = Q.rows();
581 const Matrix<T> flat = getters_detail::flatten(d.chain.space);
582
584 out.target = getters_detail::resolve_state_set(B, flat, "B");
585 if (out.target.empty())
586 throw InputError(
587 "The target state set B is empty: a first passage time into no state is undefined.");
588 out.source = getters_detail::resolve_state_set(A, flat, "A");
589 const std::vector<std::size_t> Cidx = getters_detail::resolve_state_set(C, flat, "C");
590
591 std::vector<T> pi0;
592 if (!out.source.empty()) {
593 pi0.assign(n, num_traits<T>::from_int(0));
595 static_cast<long>(out.source.size()));
596 for (std::size_t idx : out.source) pi0[idx] = w;
597 }
598
599 if (Cidx.empty()) {
600 const mc::PassageMoments<T> pm = mc::ctmc_passage_moments(Q, pi0, out.target, nmax);
601 out.m = pm.m;
602 out.mall = pm.mall;
603 out.p = mc::ctmc_passage_taboo(Q, pi0, out.target, Cidx).pB;
604 } else {
606 mc::ctmc_passage_taboo_moments(Q, pi0, out.target, Cidx, nmax);
607 out.m = pm.m;
608 out.mall = pm.mall;
609 out.p = pm.pB;
610 // C minus B, as the passage sees it: a state in both counts as B.
611 for (std::size_t c : Cidx)
612 if (!std::binary_search(out.target.begin(), out.target.end(), c))
613 out.taboo.push_back(c);
614 }
615 return out;
616}
617
618/** The plain first passage, with no taboo set. */
619template <class T>
621 const CtmcSolution<T>& d,
622 const Matrix<double>& A,
623 const Matrix<double>& B,
624 std::size_t nmax = 3) {
625 return ctmc_firstpasst_moments(sn, d, A, B, nmax, Matrix<double>(0, 0));
626}
627
628/** As above, solving the chain first. */
629template <class T>
631 const CtmcOptions& opt,
632 const Matrix<double>& A,
633 const Matrix<double>& B,
634 std::size_t nmax,
635 const Matrix<double>& C) {
636 return ctmc_firstpasst_moments(sn, solver_ctmc_analyzer(sn, opt), A, B, nmax, C);
637}
638
639/** As above, solving the chain first. */
640template <class T>
642 const CtmcOptions& opt,
643 const Matrix<double>& A,
644 const Matrix<double>& B,
645 std::size_t nmax = 3) {
647}
648
649/**
650 * Port of `@@SolverCTMC/getStateSpaceAggr.m`: the per-(station, class) job
651 * counts of every state, in column block order `(ist-1)*K + k`.
652 *
653 * The reference returns `[]` with a warning when the model has not been solved,
654 * since its copy is a by-product cached by a previous run. There is no such
655 * cache here: the aggregate is a function of the state space alone and is
656 * recomputed, so the accessor either answers or throws.
657 */
658template <class T>
663
664/** As above, for a caller who has already solved the chain. */
665template <class T>
669
670namespace getters_detail {
671
672/**
673 * The per-class marginal of one node's block, for every state.
674 *
675 * `prob_detail::marginal_of` is the single decoder of a local row into class
676 * counts -- it knows the phase blocks of a station and the leading count
677 * columns of a Cache -- and is reused rather than re-derived, so an aggregated
678 * transient query and an aggregated stationary one cannot disagree about what
679 * a state holds.
680 *
681 * A SOURCE ROW IS ZERO, not Inf. `to_marginal` reports an infinite reservoir
682 * for an EXT station, which describes the encoding rather than a queue length;
683 * `ctmc_state_space_aggr` zeroes it for the same reason, and a label matrix
684 * that disagreed with it would make the system and per-node aggregates
685 * inconsistent.
686 */
687template <class T>
688Matrix<T> node_marginal_labels(const NetworkStruct<T>& sn, std::size_t ind, std::size_t isf,
689 const std::vector<NetState<T>>& space) {
690 const std::size_t K = sn.nclasses;
691 const T zero = num_traits<T>::from_int(0);
692 Matrix<T> out(space.size(), K, zero);
693 const std::size_t ist = sn.nodes[ind - 1].station;
694 if (ist != 0 && sn.stations[ist - 1].nodetype == NodeType::Source) return out;
695 for (std::size_t s = 0; s < space.size(); ++s) {
696 const std::vector<T> m = prob_detail::marginal_of(sn, ind, space[s].local[isf - 1]);
697 for (std::size_t k = 0; k < K && k < m.size(); ++k) out(s, k) = m[k];
698 }
699 return out;
700}
701
702/**
703 * Refuse a (struct, trajectory) pair that cannot belong to one model.
704 *
705 * The overloads taking a ready `CtmcTransient` cannot verify in general that it
706 * was integrated from the struct they are handed, and they should not try: the
707 * point of those overloads is that four queries share one integration. This
708 * catches the one mismatch that would read past the end of a state instead of
709 * merely answering about the wrong model.
710 */
711template <class T>
712void check_pair(std::size_t isf, const std::vector<NetState<T>>& space, const char* what) {
713 if (!space.empty() && isf > space[0].local.size())
714 throw InputError(std::string(what) +
715 ": the transient solution has fewer stateful nodes than the model it "
716 "was queried with, so it was not integrated from that model");
717}
718
719/** The block of one node, for every state: the reference's `SSnode`. */
720template <class T>
721Matrix<T> node_labels(std::size_t isf, const std::vector<NetState<T>>& space) {
722 const std::size_t w = space.empty() ? 0 : space[0].local[isf - 1].size();
723 Matrix<T> out(space.size(), w, num_traits<T>::from_int(0));
724 for (std::size_t s = 0; s < space.size(); ++s)
725 for (std::size_t j = 0; j < w; ++j) out(s, j) = space[s].local[isf - 1][j];
726 return out;
727}
728
729/** The time grid and occupancy every `getTranProb*` query shares. */
730template <class T>
731CtmcTranProb<T> tran_common(const CtmcTransient<T>& tr) {
732 CtmcTranProb<T> p;
733 p.t = tr.t;
734 p.pit = tr.pit;
735 return p;
736}
737
738} // namespace getters_detail
739
740/**
741 * Port of `@@SolverCTMC/getTranProb.m`: pi(t) over the whole chain, labelled by
742 * one node's local state.
743 *
744 * IT IS NOT A PER-STATE TRANSIENT PROBABILITY, despite the name's symmetry with
745 * `getProb`. The reference returns the FULL occupancy vector together with the
746 * node's slice of the state space, leaving the caller to sum the rows sharing
747 * the local state it cares about; that is a strictly richer answer than one
748 * marginal and is reproduced as such.
749 *
750 * @param ind 1-based node index
751 * @param sn the refreshed network struct
752 * @param tr transient solution whose pi(t) is being labelled
753 */
754template <class T>
756 std::size_t ind) {
757 assert_phase_type_states(sn, "getTranProb");
758 const std::size_t isf = getters_detail::stateful_or_throw(sn, ind, "getTranProb");
759 getters_detail::check_pair(isf, tr.chain.chain.space, "getTranProb");
760 CtmcTranProb<T> p = getters_detail::tran_common(tr);
761 p.labels = getters_detail::node_labels(isf, tr.chain.chain.space);
762 return p;
763}
764
765/** As above, integrating the forward equation first. */
766template <class T>
768 std::size_t ind, const T& t0, const T& t1) {
770 "getTranProb integrates the forward equation, whose adaptive step controller "
771 "is transcendental; use --arith double or real");
772 assert_phase_type_states(sn, "getTranProb");
773 getters_detail::check_timespan(t0, t1, "getTranProb");
775}
776
777/**
778 * Port of `@@SolverCTMC/getTranProbAggr.m`: pi(t), labelled by one node's
779 * per-class job counts.
780 *
781 * THE REFERENCE SLICES THE AGGREGATE BY NODE INDEX, `SSa(:, (jnd-1)*K+1 :
782 * jnd*K)`, while `ctmc_ssg` writes that matrix in STATION blocks
783 * `(ist-1)*K+1 : ist*K`. The two indices coincide only when every node is a
784 * station, so on a model carrying a Router or a ClassSwitch the reference reads
785 * the wrong block. The labels are decoded from the node's own state here
786 * instead of sliced out of a station-indexed matrix, which sidesteps the
787 * mismatch and extends to a stateful non-station -- a Cache -- that no station
788 * block describes at all.
789 *
790 * @param ind 1-based node index
791 * @param sn the refreshed network struct
792 * @param tr transient solution whose pi(t) is being labelled
793 */
794template <class T>
796 std::size_t ind) {
797 assert_phase_type_states(sn, "getTranProbAggr");
798 const std::size_t isf = getters_detail::stateful_or_throw(sn, ind, "getTranProbAggr");
799 getters_detail::check_pair(isf, tr.chain.chain.space, "getTranProbAggr");
800 CtmcTranProb<T> p = getters_detail::tran_common(tr);
801 p.labels = getters_detail::node_marginal_labels(sn, ind, isf, tr.chain.chain.space);
802 return p;
803}
804
805/** As above, integrating the forward equation first. */
806template <class T>
808 std::size_t ind, const T& t0, const T& t1) {
810 "getTranProbAggr integrates the forward equation, whose adaptive step "
811 "controller is transcendental; use --arith double or real");
812 assert_phase_type_states(sn, "getTranProbAggr");
813 getters_detail::check_timespan(t0, t1, "getTranProbAggr");
815}
816
817/**
818 * Port of `@@SolverCTMC/getTranProbSys.m`: pi(t), labelled by the whole network
819 * state with its phases.
820 */
821template <class T>
823 assert_phase_type_states(sn, "getTranProbSys");
824 CtmcTranProb<T> p = getters_detail::tran_common(tr);
825 p.labels = getters_detail::flatten(tr.chain.chain.space);
826 return p;
827}
828
829/** As above, integrating the forward equation first. */
830template <class T>
832 const T& t0, const T& t1) {
834 "getTranProbSys integrates the forward equation, whose adaptive step "
835 "controller is transcendental; use --arith double or real");
836 assert_phase_type_states(sn, "getTranProbSys");
837 getters_detail::check_timespan(t0, t1, "getTranProbSys");
839}
840
841/**
842 * Port of `@@SolverCTMC/getTranProbSysAggr.m`: pi(t), labelled by the network's
843 * per-(station, class) job counts.
844 *
845 * The labels are `ctmc_state_space_aggr`, the same matrix the transient
846 * analyzer already integrates Q(t) and U(t) against, so a caller summing these
847 * rows by hand reproduces its `QNt` exactly.
848 */
849template <class T>
851 const CtmcTransient<T>& tr) {
852 assert_phase_type_states(sn, "getTranProbSysAggr");
853 CtmcTranProb<T> p = getters_detail::tran_common(tr);
854 p.labels = ctmc_state_space_aggr(sn, tr.chain.chain.space);
855 return p;
856}
857
858/** As above, integrating the forward equation first. */
859template <class T>
861 const T& t0, const T& t1) {
863 "getTranProbSysAggr integrates the forward equation, whose adaptive step "
864 "controller is transcendental; use --arith double or real");
865 assert_phase_type_states(sn, "getTranProbSysAggr");
866 getters_detail::check_timespan(t0, t1, "getTranProbSysAggr");
868}
869
870/*
871 * `@@SolverCTMC/getSymbolicGenerator.m` and `getSymbolicSolution.m` are NOT
872 * here: they live in solver_ctmc_symbolic.h as `ctmc_symbolic_generator` and
873 * `ctmc_symbolic_solution`.
874 *
875 * They were refused by name until 2026-07-31, on the grounds that this port had
876 * no computer algebra. Half of that was never true -- the generator is LINEAR in
877 * the event symbols, so it is one numeric filtration per event and needs no
878 * algebra to assemble -- and the other half stopped being true when `api/sym`
879 * gained a client for the line-sage-rest service the reference itself uses.
880 * They are kept in their own header so that this one, which every CTMC accessor
881 * includes, does not drag in a socket-using HTTP client.
882 */
883
884/*
885 * `@@SolverCTMC/getSjrnT.m` and `sjrnT.m` are aliases of `getCdfRespT` in the
886 * reference and are NOT defined here: an alias belongs beside the entry point
887 * it forwards to, and the CTMC response-time CDF lives in its own header (see
888 * `solver_mam_get_sjrn_t` in solver_mam_runner.h and `solver_nc_sjrnt` in
889 * solver_nc_cdf.h for the two established precedents). Defining it here would
890 * make this header depend on that one for nothing but a forwarding call.
891 */
892
893/**
894 * Port of `@@SolverCTMC/getAsymptoticVariance.m`: the asymptotic variance of the
895 * time-average of a reward along a sample path of this model's CTMC.
896 *
897 * WHAT IT IS FOR. A simulation estimate of a steady-state mean has a standard
898 * error that shrinks like sqrt(sigma^2/t), where sigma^2 is NOT the stationary
899 * variance of the reward but its ASYMPTOTIC variance, which also carries the
900 * autocorrelation of the path. That number is what says how long a run has to
901 * be, and `sim_runlength` turns it into a run length for a target precision. It
902 * cannot be guessed from the stationary variance: on M/M/1 the two differ by a
903 * factor that blows up like (1-rho)^-2.
904 *
905 * The reward is a function of the state ROW, evaluated on the state space the
906 * generator was built from.
907 */
908template <class T>
910 const NetworkStruct<T>& sn, const CtmcOptions& opt,
911 const std::function<T(const NetState<T>&)>& reward) {
913 std::vector<T> f;
914 f.reserve(g.space.size());
915 for (std::size_t i = 0; i < g.space.size(); ++i) f.push_back(reward(g.space[i]));
916 return sim::sim_asymvar_ctmc<T>(g.Q, f);
917}
918
919} // namespace ctmc
920} // namespace line
921
922#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_GETTERS_H
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
A network plus its refreshed NetworkStruct.
First passage times into a target STATE SET, for Markov and semi-Markov chains.
The exception types the port throws.
Dense matrix and non-owning view.
void assert_phase_type_states(const NetworkStruct< T > &sn, const std::string &what)
Port of @@SolverCTMC/assertPhaseTypeStates: refuse a query whose answer would be a per-state probabil...
CtmcGenerator< T > ctmc_get_generator(const NetworkStruct< T > &sn, const CtmcSolution< T > &d)
Port of @@SolverCTMC/getGenerator.m: the generator, its event filtration and the synchronization list...
CtmcTranProb< T > ctmc_get_tran_prob_sys(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr)
Port of @@SolverCTMC/getTranProbSys.m: pi(t), labelled by the whole network state with its phases.
CtmcTranProb< T > ctmc_get_tran_prob_sys_aggr(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr)
Port of @@SolverCTMC/getTranProbSysAggr.m: pi(t), labelled by the network's per-(station,...
CtmcGenerator< T > ctmc_get_infgen(const NetworkStruct< T > &sn, const CtmcSolution< T > &d)
@@SolverCTMC/getInfGen.m, a pure alias of getGenerator in the reference.
sim::AsymVarResult< T > ctmc_get_asymptotic_variance(const NetworkStruct< T > &sn, const CtmcOptions &opt, const std::function< T(const NetState< T > &)> &reward)
Port of @@SolverCTMC/getAsymptoticVariance.m: the asymptotic variance of the time-average of a reward...
CtmcStateSpace< T > ctmc_get_state_space(const NetworkStruct< T > &, const CtmcSolution< T > &d)
Port of @@SolverCTMC/getStateSpace.m.
CtmcTransient< T > solver_ctmc_transient_analyzer(const NetworkStruct< T > &sn_arg, const CtmcOptions &opt, const T &t0, const T &t1, const std::vector< T > &grid=std::vector< T >())
Port of solver_ctmc_transient_analyzer.m.
CtmcFirstPassageMoments< T > ctmc_firstpasst_moments(const NetworkStruct< T > &, const CtmcSolution< T > &d, const Matrix< double > &A, const Matrix< double > &B, std::size_t nmax, const Matrix< double > &C)
Port of @@SolverCTMC/getFirstPassTMoments.m: moments of order 1..nmax of the first passage time from ...
Matrix< T > ctmc_get_state_space_aggr(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Port of @@SolverCTMC/getStateSpaceAggr.m: the per-(station, class) job counts of every state,...
CtmcFirstPassage ctmc_cdf_firstpasst(const NetworkStruct< T > &, const CtmcSolution< T > &d, const Matrix< double > &A, const Matrix< double > &B, const Matrix< double > &C, std::vector< double > tset, const std::string &method="expm")
Port of @@SolverCTMC/getCdfFirstPassT.m: the distribution of the FIRST PASSAGE TIME from state set A ...
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....
Matrix< T > ctmc_get_preempt_rate(const NetworkStruct< T > &sn, const CtmcOptions &opt)
SolverCTMC.getPreemptRate; see ctmc_get_start_rate.
Matrix< T > ctmc_get_start_rate(const NetworkStruct< T > &sn, const CtmcOptions &opt)
SolverCTMC.getStartRate and getPreemptRate: the DERIVED rates the START/PREEMPT filtration reduces to...
CtmcTranProb< T > ctmc_get_tran_prob(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr, std::size_t ind)
Port of @@SolverCTMC/getTranProb.m: pi(t) over the whole chain, labelled by one node's local state.
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...
CtmcTranProb< T > ctmc_get_tran_prob_aggr(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr, std::size_t ind)
Port of @@SolverCTMC/getTranProbAggr.m: pi(t), labelled by one node's per-class job counts.
PassageMoments< T > ctmc_passage_moments(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, std::size_t nmax=1)
Moments of order 1..nmax of the first passage time into target.
PassageCurve< T > ctmc_passage_time(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< double > &tset, const std::string &method="expm", const std::string &lti_method="euler")
CDF and density of the first passage time on the grid tset: F(t) = 1 - alpha exp(St) 1 and f(t) = alp...
PassageTabooMoments< T > ctmc_passage_taboo_moments(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< std::size_t > &taboo, std::size_t nmax=1)
Moments of order 1..nmax of the first passage time into target, CONDITIONAL on entering it before tab...
PassageTaboo< T > ctmc_passage_taboo(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< std::size_t > &taboo, const std::vector< double > &tset=std::vector< double >(), const std::string &method="expm", const std::string &lti_method="euler")
First passage into target under the taboo set taboo, the reference's ctmc_passage_taboo.
AsymVarResult< T > sim_asymvar_ctmc(const Matrix< T > &A, const std::vector< T > &f, const std::vector< T > &pi=std::vector< T >())
Asymptotic variance of a reward on a CTMC: 2 sum_x pi(x)g(x)d(x) with g = f - E_pi[f] and A d = -g,...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Run-length planning for steady-state simulation.
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...
The SolverCTMC probability family: solver_ctmc_joint, _jointaggr, _marg, _margaggr,...
Port of solver_ctmc_transient_analyzer.m: the time-dependent counterpart of solver_ctmc_analyzer,...
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 answer of @@SolverCTMC/getFirstPassTMoments.m.
std::vector< T > p
(nstates) probability of entering B (before C), one row per state
Matrix< T > mall
(nstates x nmax), one row per starting state
std::vector< std::size_t > source
resolved 0-based rows; empty = conditional stationary
std::vector< std::size_t > target
resolved 0-based rows
std::vector< std::size_t > taboo
resolved 0-based rows of C minus B; empty = none
std::vector< T > m
(nmax) moments for a passage started uniformly in A
The answer of @@SolverCTMC/getCdfFirstPassT.m: the [F(t), t] curve with its grid, density and resolve...
std::vector< double > F
CDF at t, clamped to [0, 1]; DEFECTIVE under a taboo set.
std::vector< double > t
the grid: the caller's, else 1000 points to the horizon
double tabooprob
Pr{C entered before B}.
double reachprob
Pr{B entered before C}, the limit of F.
std::vector< std::size_t > target
resolved 0-based rows
std::vector< std::size_t > source
resolved 0-based rows; empty = conditional stationary
std::vector< double > f
density at t
std::vector< std::size_t > taboo
resolved 0-based rows of C minus B; empty = none
[infGen, eventFilt, ev] of @@SolverCTMC/getGenerator.m.
std::vector< std::vector< Matrix< T > > > preempt_filt
std::vector< NetState< T > > space
row i of Q is space[i]
std::vector< Matrix< T > > filt
eventFilt: filt[a] holds only what synchronization sync[a] contributed, so sum_a filt[a] is the off-d...
std::vector< Sync< T > > sync
ev, the reference's sn.sync
std::vector< std::vector< Matrix< T > > > start_filt
The DERIVED START/PREEMPT filtrations, indexed [station-1][class-1].
Matrix< T > Q
the infinitesimal generator
The SolverCTMC knobs this port honours.
bool keep_filtration
Keep the per-synchronization EVENT FILTRATION alongside Q.
Everything one CTMC solve produces.
std::shared_ptr< const NetworkStruct< T > > solved_sn
The struct the chain was built on, when it differs from the caller's in its state LAYOUT: set when he...
[stateSpace, localStateSpace] of @@SolverCTMC/getStateSpace.m.
std::vector< NetState< T > > space
The enumerated states.
std::vector< std::size_t > node_width
column width of each stateful node's block
std::vector< Matrix< T > > local
localStateSpace{f}: one matrix per stateful node, its DISTINCT local rows in first-appearance order.
Matrix< T > flat
the blocks concatenated, as MATLAB returns them
The time-dependent answer of one getTranProb* query.
Matrix< T > pit
(ntimes x nstates) occupancy over the solved chain
std::vector< T > t
The reference returns Pi_t = [t, pi_t], one matrix with time glued on as column 1.
Matrix< T > labels
(nstates x width) the state descriptor the query asked for
What one transient CTMC solve produces.
static constexpr double FineTol
Definition lang_types.h:765
A passage-time law on a grid.
std::vector< double > F
std::vector< double > f
Moments of a first passage time, [Mall, m] of the reference.
Matrix< T > mall
(nstates x nmax), zero on the target, inf where unreachable
std::vector< T > m
the pi0-weighted moment vector
Conditional moments under a taboo set, [mall, m, pB] of ctmc_passage_taboo_moments....
std::vector< T > m
the pi0-weighted conditional moments, NaN if B is never entered first
Matrix< T > mall
(nstates x nmax): zero on B, NaN where B cannot be entered first
std::vector< T > pB
(nstates) probability of entering B before C
First passage into a target set B under a TABOO set C, [F, f, out] of ctmc_passage_taboo....
std::vector< std::size_t > taboo
C minus B, sorted and unique.
T tabooprob
Pr{C entered before B}.
std::vector< double > F
DEFECTIVE CDF Pr{B entered by t, C not before}, clamped to [0, 1].
T reachprob
Pr{B entered before C}, the limit of F.
std::vector< double > f
its density alpha exp(St) sB
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2734
Second-order description of a steady-state estimator.