LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_stationary.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_CTMC_STATIONARY_H
6#define LINE_SOLVERS_CTMC_CTMC_STATIONARY_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `matlab/src/solvers/CTMC/ctmc_stationary.m`: the single entry point
12 * for the stationary distribution of a CTMC generated from a NetworkStruct.
13 *
14 * All the stationary mass of a reducible chain lives in its bottom strongly
15 * connected components, each weighted by the probability of being absorbed in
16 * it from the declared initial state; every other state is transient and
17 * carries zero. The block decomposition handles the irreducible case as the
18 * degenerate one BSCC / no transient states, so every solve goes through it and
19 * no dispatch can disagree with the algorithm about whether a chain is
20 * reducible.
21 *
22 * The reference's local `ctmc_initial_distribution` turns (StateSpace, sn) into
23 * the row of the initial state; here that job already belongs to
24 * `analyzer_detail::init_state_index`, so this file takes the INDEX and stays
25 * free of any `sn` dependency. `npos` means the initial state is absent from
26 * the enumerated space -- stochastic complementation may have removed it, an
27 * SPN whose immediate ENABLE states were eliminated being the usual case -- and
28 * makes the block decomposition start in the SCCs with no incoming transition.
29 */
30
31#include <cstddef>
32#include <string>
33#include <vector>
34
37#include "line/util/error.h"
38#include "line/util/matrix.h"
39#include "line/num/number.h"
40
41namespace line {
42namespace ctmc {
43
44/** Magnitude above which an off-diagonal generator entry counts as an arc. */
45static const double kArcTol = 1e-12;
46
47template <class T>
49 std::vector<T> pi; ///< stationary distribution, length N
50 bool seeded = false; ///< true when a declared initial state selected the answer
51 std::size_t nbscc = 0; ///< number of closed communicating classes
52 /**
53 * Empty unless the chain is an UNSEEDED reducible mixture. The library
54 * never writes to stderr (see `util/error.h`), so the caller decides
55 * whether to surface it; the CLI prints it.
56 */
57 std::string warning;
58};
59
60namespace stationary_detail {
61
62/**
63 * Port of the reference's `warn_if_unseeded_mixture`.
64 *
65 * Without a seed the block decomposition invents a start distribution -- here a
66 * uniform one over the SCCs with no incoming transition -- and no property of
67 * the model implies it: on a reducible chain the stationary distribution is
68 * fixed only by the initial state. Nor is it the product-form weighting, which
69 * weights the recurrent classes by their unnormalized Kelly mass. The invented
70 * start is order-independent and therefore looks more reproducible than the
71 * answer the declared initial state selects, but that reproducibility is bought
72 * by discarding the one input that makes the problem well posed.
73 *
74 * The fallback itself differs across codebases (python weights ALL the SCCs
75 * equally, MATLAB and this port the source SCCs), which is a second reason not
76 * to read the number as the model's answer.
77 */
78template <class T>
79std::size_t count_bscc(const Matrix<T>& Q) {
80 const std::size_t n = Q.rows();
81 if (n < 2) return 0;
82 const T zero = num_traits<T>::from_int(0);
83 // SIGN IS NOT A CRITERION: an ME generator embeds genuinely negative
84 // off-diagonal entries, so the adjacency is taken on the MAGNITUDE.
85 Matrix<T> A(n, n, zero);
86 for (std::size_t i = 0; i < n; ++i)
87 for (std::size_t j = 0; j < n; ++j) {
88 if (i == j) continue;
89 const double a = num_traits<T>::to_double(Q(i, j));
90 if ((a < 0 ? -a : a) > kArcTol) A(i, j) = num_traits<T>::from_int(1);
91 }
93 std::size_t nbscc = 0;
94 for (std::size_t c = 0; c < s.recurrent.size(); ++c)
95 if (s.recurrent[c]) ++nbscc;
96 return nbscc;
97}
98
99} // namespace stationary_detail
100
101/**
102 * @param Q generator; the diagonal is recomputed by the block decomposition
103 * @param init_index row of the declared initial state, or npos when absent
104 */
105template <class T>
107 std::size_t init_index = static_cast<std::size_t>(-1)) {
108 const std::size_t npos = static_cast<std::size_t>(-1);
109 const std::size_t n = Q.rows();
110 if (Q.cols() != n) throw InputError("ctmc_stationary: generator is not square");
111
113 std::vector<T> pi0;
114 if (init_index != npos) {
115 if (init_index >= n) throw InputError("ctmc_stationary: initial state index out of range");
116 pi0.assign(n, num_traits<T>::from_int(0));
117 pi0[init_index] = num_traits<T>::from_int(1);
118 out.seeded = true;
119 }
120
121 out.nbscc = stationary_detail::count_bscc(Q);
122 if (!out.seeded && out.nbscc > 1) {
123 out.warning =
124 "SolverCTMC: the generator has " + std::to_string(out.nbscc) +
125 " closed communicating classes and the declared initial state could not be located in "
126 "the enumerated state space, so the solve starts from a distribution the model never "
127 "stated (uniform over the SCCs with no incoming transition). On a reducible chain the "
128 "stationary distribution is determined only by the initial state, so this answer is "
129 "not the model's. Call setState on the stations so the class the model actually starts "
130 "in is the one solved.";
131 }
132
133 out.pi = mc::ctmc_solve_reducible_blkdecomp(Q, pi0).pi;
134 return out;
135}
136
137} // namespace ctmc
138} // namespace line
139
140#endif // LINE_SOLVERS_CTMC_CTMC_STATIONARY_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
Limiting distribution of a reducible CTMC by direct block decomposition of the generator.
The exception types the port throws.
Dense matrix and non-owning view.
CtmcStationaryResult< T > ctmc_stationary(const Matrix< T > &Q, std::size_t init_index=static_cast< std::size_t >(-1))
static const double kArcTol
Magnitude above which an off-diagonal generator entry counts as an arc.
BlkDecompResult< T > ctmc_solve_reducible_blkdecomp(const Matrix< T > &Qin, const std::vector< T > &pin, double reachTol=1e-15, double zeroColTol=1e-12)
Limiting distribution of a reducible CTMC by direct block decomposition of the generator.
SccResult stronglyconncomp(const Matrix< T > &A)
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
Number-type abstraction for the templated API port.
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
std::vector< T > pi
stationary distribution, length N
bool seeded
true when a declared initial state selected the answer
std::string warning
Empty unless the chain is an UNSEEDED reducible mixture.
std::size_t nbscc
number of closed communicating classes
std::vector< bool > recurrent
recurrent[c-1] is true when component c has no edge leaving it.