LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_solve_reducible_blkdecomp.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_API_MC_CTMC_SOLVE_REDUCIBLE_BLKDECOMP_H
6#define LINE_API_MC_CTMC_SOLVE_REDUCIBLE_BLKDECOMP_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Limiting distribution of a reducible CTMC by direct block decomposition of
12 * the generator.
13 *
14 * Templated port of matlab/src/api/mc/ctmc_solve_reducible_blkdecomp.m and
15 * jar/src/main/java/jline/api/mc/Ctmc_solve_reducible_blkdecomp.java. Unlike
16 * ctmc_solve_reducible it never uniformizes: the states are split into
17 * transient and recurrent classes by strong connectivity, the expected sojourn
18 * of the transient part is obtained from sojourn Q_tt = -p0_t (Q_tt is Hurwitz,
19 * so this is a plain non-singular solve), the absorption probabilities follow
20 * as hit = sojourn Q_ta + p0_r, and each recurrent class contributes its own
21 * stationary vector scaled by the probability of reaching it.
22 *
23 * EXACT, AND THE EXACTNESS IS THE POINT. Every step is a finite linear solve
24 * over the field of the rates: strong connectivity is combinatorial, Q_tt is
25 * inverted once per starting class, and each recurrent class goes through
26 * ctmc_solve. At Rational the absorption probabilities are exact rationals,
27 * where the reference computes them in double precision on a matrix that is
28 * ill-conditioned precisely when the transient class is nearly closed -- the
29 * regime the routine exists to handle.
30 *
31 * The two thresholds MATLAB uses are structural tests on the input, not
32 * convergence criteria, and are exposed as parameters: a reachability
33 * probability below 1e-15 is treated as unreachable, and a state whose column
34 * of |Q| sums below 1e-12 is treated as having no incoming rate.
35 */
36
37#include <algorithm>
38#include <cstddef>
39#include <vector>
40
43#include "line/num/number.h"
44#include "line/util/error.h"
45#include "line/util/lu.h"
46#include "line/util/matrix.h"
47
48namespace line {
49namespace mc {
50
51template <class T>
53 std::vector<T> pi; ///< limiting distribution, length N
54 Matrix<T> pis; ///< numSCC x N, one row per starting component
55 Matrix<T> pi0; ///< numSCC x N, the uniform-in-component starting vectors
56 std::vector<std::size_t> scc; ///< component index of each state, 1-based
57 std::vector<bool> isrec; ///< recurrence flag per component
58};
59
60/**
61 * @brief Limiting distribution of a reducible CTMC by direct block
62 * decomposition of the generator.
63 *
64 * @param Qin generator; the diagonal is recomputed
65 * @param pin initial distribution; empty if not available
66 * @param reachTol probability below which a recurrent class counts as unreached
67 * @param zeroColTol column-sum threshold for a state with no incoming rate
68 */
69template <class T>
70BlkDecompResult<T> ctmc_solve_reducible_blkdecomp(const Matrix<T>& Qin, const std::vector<T>& pin,
71 double reachTol = 1e-15,
72 double zeroColTol = 1e-12) {
73 const std::size_t N = Qin.rows();
74 if (Qin.cols() != N) throw InputError("ctmc_solve_reducible_blkdecomp: generator is not square");
75 if (!pin.empty() && pin.size() != N)
76 throw InputError("ctmc_solve_reducible_blkdecomp: initial vector has the wrong length");
77 const T zero = num_traits<T>::from_int(0);
78 const T one = num_traits<T>::from_int(1);
79
80 const Matrix<T> Q = ctmc_makeinfgen(Qin);
81
82 // Adjacency from the off-diagonal entries, which are non-negative in a
83 // generator, so "non-zero" and MATLAB's "> 0" coincide.
84 Matrix<T> Adj = Q;
85 for (std::size_t i = 0; i < N; ++i) Adj(i, i) = zero;
86 const SccResult s = stronglyconncomp(Adj);
87 const std::size_t numSCC = s.numSCC();
88
90 r.scc = s.scc;
91 r.isrec = s.recurrent;
92
93 if (numSCC == 1) {
94 r.pi = ctmc_solve(Q);
95 r.pis = Matrix<T>(1, N);
96 for (std::size_t j = 0; j < N; ++j) r.pis(0, j) = r.pi[j];
97 r.pi0 = Matrix<T>();
98 return r;
99 }
100
101 std::vector<std::size_t> transStates, recStates, transSccIds, recSccIds;
102 for (std::size_t c = 0; c < numSCC; ++c) {
103 if (s.recurrent[c]) {
104 recSccIds.push_back(c);
105 recStates.insert(recStates.end(), s.members[c].begin(), s.members[c].end());
106 } else {
107 transSccIds.push_back(c);
108 transStates.insert(transStates.end(), s.members[c].begin(), s.members[c].end());
109 }
110 }
111 std::sort(transStates.begin(), transStates.end());
112 std::sort(recStates.begin(), recStates.end());
113 const std::size_t nt = transStates.size(), nr = recStates.size();
114 if (nr == 0)
115 throw NumericError(
116 "ctmc_solve_reducible_blkdecomp: no recurrent class, the chain admits no limiting "
117 "distribution");
118
119 // Q_tt (transposed, ready for the solve) and Q_ta.
120 Matrix<T> Q_ttT(nt, nt), Q_ta(nt, nr);
121 std::vector<std::size_t> lupiv;
122 Matrix<T> LU;
123 if (nt > 0) {
124 for (std::size_t a = 0; a < nt; ++a) {
125 for (std::size_t b = 0; b < nt; ++b) Q_ttT(b, a) = Q(transStates[a], transStates[b]);
126 for (std::size_t b = 0; b < nr; ++b) Q_ta(a, b) = Q(transStates[a], recStates[b]);
127 }
128 LU = Q_ttT;
129 lupiv = lu_factor(LU);
130 }
131
132 const T rtol = num_traits<T>::from_double(reachTol);
133 r.pis = Matrix<T>(numSCC, N, zero);
134 r.pi0 = Matrix<T>(numSCC, N, zero);
135 // Position of each state within recStates, for the per-class gather below.
136 std::vector<std::size_t> recPos(N, static_cast<std::size_t>(-1));
137 for (std::size_t k = 0; k < nr; ++k) recPos[recStates[k]] = k;
138
139 for (std::size_t c = 0; c < numSCC; ++c) {
140 std::vector<T> p0(N, zero);
141 const T w = one / num_traits<T>::from_int(static_cast<long>(s.members[c].size()));
142 for (std::size_t a : s.members[c]) p0[a] = w;
143 for (std::size_t j = 0; j < N; ++j) r.pi0(c, j) = p0[j];
144
145 std::vector<T> hit(nr, zero);
146 if (nt > 0) {
147 bool anyT = false;
148 std::vector<T> rhs(nt);
149 for (std::size_t a = 0; a < nt; ++a) {
150 rhs[a] = -p0[transStates[a]];
151 if (rhs[a] != zero) anyT = true;
152 }
153 if (anyT) {
154 // Solve sojourn * Q_tt = -p0_t, i.e. Q_tt' * sojourn' = -p0_t'.
155 std::vector<T> sojourn = rhs;
156 lu_solve(LU, lupiv, sojourn);
157 for (std::size_t b = 0; b < nr; ++b) {
158 T acc = zero;
159 for (std::size_t a = 0; a < nt; ++a) acc += sojourn[a] * Q_ta(a, b);
160 hit[b] = acc;
161 }
162 }
163 }
164 for (std::size_t k = 0; k < nr; ++k) hit[k] += p0[recStates[k]];
165
166 for (std::size_t cr : recSccIds) {
167 const std::vector<std::size_t>& idx = s.members[cr];
168 T reach = zero;
169 for (std::size_t a : idx) reach += hit[recPos[a]];
170 if (reach < rtol) continue;
171 if (idx.size() == 1) {
172 r.pis(c, idx[0]) = reach;
173 } else {
174 const std::vector<T> pi_c = ctmc_solve(detail::submatrix(Q, idx));
175 for (std::size_t k = 0; k < idx.size(); ++k) r.pis(c, idx[k]) = pi_c[k] * reach;
176 }
177 }
178 }
179
180 // Probability of starting in each component.
181 std::vector<T> pinl(numSCC, zero);
182 if (pin.empty()) {
183 const T ztol = num_traits<T>::from_double(zeroColTol);
184 for (std::size_t c = 0; c < numSCC; ++c) pinl[c] = one;
185 for (std::size_t j = 0; j < N; ++j) {
186 T cs = zero;
187 for (std::size_t i = 0; i < N; ++i) cs += num_abs(T(Q(i, j)));
188 if (cs < ztol) pinl[s.scc[j] - 1] = zero;
189 }
190 T tot = zero;
191 for (const T& v : pinl) tot += v;
192 if (tot > zero) {
193 for (T& v : pinl) v /= tot;
194 } else {
195 for (std::size_t c = 0; c < numSCC; ++c)
196 pinl[c] = one / num_traits<T>::from_int(static_cast<long>(numSCC));
197 }
198 } else {
199 for (std::size_t c = 0; c < numSCC; ++c) {
200 T acc = zero;
201 for (std::size_t a : s.members[c]) acc += pin[a];
202 pinl[c] = acc;
203 }
204 }
205
206 r.pi.assign(N, zero);
207 for (std::size_t c = 0; c < numSCC; ++c) {
208 if (!(pinl[c] > zero)) continue;
209 for (std::size_t j = 0; j < N; ++j) r.pi[j] += r.pis(c, j) * pinl[c];
210 }
211 if (transSccIds.size() == 1 && pin.empty())
212 for (std::size_t j = 0; j < N; ++j) r.pi[j] = r.pis(transSccIds[0], j);
213
214 T tot = zero;
215 for (const T& v : r.pi) tot += v;
216 if (tot > zero)
217 for (T& v : r.pi) v /= tot;
218 return r;
219}
220
221/** Overload without an initial vector. */
222template <class T>
224 double zeroColTol = 1e-12) {
225 return ctmc_solve_reducible_blkdecomp(Q, std::vector<T>(), reachTol, zeroColTol);
226}
227
228} // namespace mc
229} // namespace line
230
231#endif // LINE_API_MC_CTMC_SOLVE_REDUCIBLE_BLKDECOMP_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
Steady-state distribution of a continuous-time Markov chain.
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
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.
Matrix< T > ctmc_makeinfgen(const Matrix< T > &Q)
Set the diagonal so that every row sums to zero (ctmc_makeinfgen).
Definition ctmc_solve.h:58
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
SccResult stronglyconncomp(const Matrix< T > &A)
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
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
T num_abs(const T &v)
Definition number.h:172
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
Number-type abstraction for the templated API port.
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
Matrix< T > pi0
numSCC x N, the uniform-in-component starting vectors
std::vector< bool > isrec
recurrence flag per component
std::vector< T > pi
limiting distribution, length N
Matrix< T > pis
numSCC x N, one row per starting component
std::vector< std::size_t > scc
component index of each state, 1-based
std::size_t numSCC() const
std::vector< bool > recurrent
recurrent[c-1] is true when component c has no edge leaving it.
std::vector< std::size_t > scc
Component index of each state, 1-based as in MATLAB (0 is never used).
std::vector< std::vector< std::size_t > > members
Member states of each component, ascending.