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 * With an initial vector `pin` the answer is the limiting law reached FROM
37 * `pin`, `absorb_limiting(pin)` as the reference computes it; `pis` and `pi0`
38 * still report the uniform-start law of every component.
39 */
40
41#include <algorithm>
42#include <cstddef>
43#include <vector>
44
48#include "line/num/number.h"
49#include "line/util/error.h"
50#include "line/util/lu.h"
51#include "line/util/matrix.h"
52
53namespace line {
54namespace mc {
55
56template <class T>
58 std::vector<T> pi; ///< limiting distribution, length N
59 Matrix<T> pis; ///< numSCC x N, one row per starting component
60 Matrix<T> pi0; ///< numSCC x N, the uniform-in-component starting vectors
61 std::vector<std::size_t> scc; ///< component index of each state, 1-based
62 std::vector<bool> isrec; ///< recurrence flag per component
63};
64
65/**
66 * @brief Limiting distribution of a reducible CTMC by direct block
67 * decomposition of the generator.
68 *
69 * @param Qin generator; the diagonal is recomputed
70 * @param pin initial distribution; empty if not available
71 * @param reachTol probability below which a recurrent class counts as unreached
72 * @param zeroColTol column-sum threshold for a state with no incoming rate
73 */
74template <class T>
75BlkDecompResult<T> ctmc_solve_reducible_blkdecomp(const Matrix<T>& Qin, const std::vector<T>& pin,
76 double reachTol = 1e-15,
77 double zeroColTol = 1e-12) {
78 const std::size_t N = Qin.rows();
79 if (Qin.cols() != N) throw InputError("ctmc_solve_reducible_blkdecomp: generator is not square");
80 if (!pin.empty() && pin.size() != N)
81 throw InputError("ctmc_solve_reducible_blkdecomp: initial vector has the wrong length");
82 const T zero = num_traits<T>::from_int(0);
83 const T one = num_traits<T>::from_int(1);
84
85 const Matrix<T> Q = ctmc_makeinfgen(Qin);
86
87 // Adjacency from the off-diagonal entries, `abs(Adj) > GlobalConstants.ArcTol`
88 // as the reference tests it: an entry at or below the arc tolerance is not
89 // an edge.
91 Matrix<T> Adj = Q;
92 for (std::size_t i = 0; i < N; ++i) {
93 Adj(i, i) = zero;
94 for (std::size_t j = 0; j < N; ++j)
95 if (i != j && !(num_abs(T(Adj(i, j))) > arctol)) Adj(i, j) = zero;
96 }
97 const SccResult s = stronglyconncomp(Adj);
98 const std::size_t numSCC = s.numSCC();
99
101 r.scc = s.scc;
102 r.isrec = s.recurrent;
103
104 if (numSCC == 1) {
105 r.pi = ctmc_solve(Q);
106 r.pis = Matrix<T>(1, N);
107 for (std::size_t j = 0; j < N; ++j) r.pis(0, j) = r.pi[j];
108 r.pi0 = Matrix<T>();
109 return r;
110 }
111
112 std::vector<std::size_t> transStates, recStates, transSccIds, recSccIds;
113 for (std::size_t c = 0; c < numSCC; ++c) {
114 if (s.recurrent[c]) {
115 recSccIds.push_back(c);
116 recStates.insert(recStates.end(), s.members[c].begin(), s.members[c].end());
117 } else {
118 transSccIds.push_back(c);
119 transStates.insert(transStates.end(), s.members[c].begin(), s.members[c].end());
120 }
121 }
122 std::sort(transStates.begin(), transStates.end());
123 std::sort(recStates.begin(), recStates.end());
124 const std::size_t nt = transStates.size(), nr = recStates.size();
125 if (nr == 0)
126 throw NumericError(ErrorKind{"Infeasible"},
127 "ctmc_solve_reducible_blkdecomp: no recurrent class, the chain admits no limiting "
128 "distribution");
129
130 // Q_tt (transposed, ready for the solve) and Q_ta.
131 Matrix<T> Q_ttT(nt, nt), Q_ta(nt, nr);
132 std::vector<std::size_t> lupiv;
133 Matrix<T> LU;
134 bool lu_ready = false;
135 if (nt > 0) {
136 for (std::size_t a = 0; a < nt; ++a) {
137 for (std::size_t b = 0; b < nt; ++b) Q_ttT(b, a) = Q(transStates[a], transStates[b]);
138 for (std::size_t b = 0; b < nr; ++b) Q_ta(a, b) = Q(transStates[a], recStates[b]);
139 }
140 }
141
142 const T rtol = num_traits<T>::from_double(reachTol);
143 // Position of each state within recStates, for the per-class gather below.
144 std::vector<std::size_t> recPos(N, static_cast<std::size_t>(-1));
145 for (std::size_t k = 0; k < nr; ++k) recPos[recStates[k]] = k;
146 // The stationary law of a recurrent class does not depend on the starting component, so it
147 // is solved once and reused: re-solving it per component cost one dense LU per SCC.
148 std::vector<std::vector<T>> pirec(numSCC);
149
150 // The expected sojourn in the transient block from p0_t, the reference's
151 // `sojourn = (-p0_t) / Q_tt`: above GMRES_MIN_STATES the Krylov route is
152 // tried first (ctmc_gmres, then ctmc_bicgstab) and the direct solve is the
153 // fallback, as in the reference; below it, and under exact arithmetic, the
154 // LU of Q_tt' is factored once and reused for every starting vector.
155 auto transient_sojourn = [&](const std::vector<T>& rhs) {
156 std::vector<T> sojourn;
157 if constexpr (num_traits<T>::has_transcendental) {
158 if (nt > GMRES_MIN_STATES) {
159 const GmresResult<T> g = ctmc_gmres(Q_ttT, rhs);
160 if (g.flag == 0) {
161 sojourn = g.x;
162 } else {
163 const BicgstabResult<T> bb = ctmc_bicgstab(Q_ttT, rhs);
164 if (bb.flag == 0) sojourn = bb.x;
165 }
166 }
167 }
168 if (sojourn.empty()) {
169 if (!lu_ready) {
170 LU = Q_ttT;
171 lupiv = lu_factor(LU);
172 lu_ready = true;
173 }
174 sojourn = rhs;
175 lu_solve(LU, lupiv, sojourn);
176 }
177 return sojourn;
178 };
179
180 // The reference's absorb_limiting(p0): the limiting law reached from p0.
181 auto absorb_limiting = [&](const std::vector<T>& p0) {
182 std::vector<T> piv(N, zero);
183 std::vector<T> hit(nr, zero);
184 if (nt > 0) {
185 bool anyT = false;
186 std::vector<T> rhs(nt);
187 for (std::size_t a = 0; a < nt; ++a) {
188 rhs[a] = -p0[transStates[a]];
189 if (rhs[a] != zero) anyT = true;
190 }
191 if (anyT) {
192 // Solve sojourn * Q_tt = -p0_t, i.e. Q_tt' * sojourn' = -p0_t'.
193 const std::vector<T> sojourn = transient_sojourn(rhs);
194 for (std::size_t b = 0; b < nr; ++b) {
195 T acc = zero;
196 for (std::size_t a = 0; a < nt; ++a) acc += sojourn[a] * Q_ta(a, b);
197 hit[b] = acc;
198 }
199 }
200 }
201 for (std::size_t k = 0; k < nr; ++k) hit[k] += p0[recStates[k]];
202
203 for (std::size_t cr : recSccIds) {
204 const std::vector<std::size_t>& idx = s.members[cr];
205 T reach = zero;
206 for (std::size_t a : idx) reach += hit[recPos[a]];
207 if (reach < rtol) continue;
208 if (idx.size() == 1) {
209 piv[idx[0]] = reach;
210 } else {
211 if (pirec[cr].empty()) pirec[cr] = ctmc_solve(detail::submatrix(Q, idx));
212 const std::vector<T>& pi_c = pirec[cr];
213 for (std::size_t k = 0; k < idx.size(); ++k) piv[idx[k]] = pi_c[k] * reach;
214 }
215 }
216 return piv;
217 };
218
219 r.pis = Matrix<T>(numSCC, N, zero);
220 r.pi0 = Matrix<T>(numSCC, N, zero);
221 for (std::size_t c = 0; c < numSCC; ++c) {
222 std::vector<T> p0(N, zero);
223 const T w = one / num_traits<T>::from_int(static_cast<long>(s.members[c].size()));
224 for (std::size_t a : s.members[c]) p0[a] = w;
225 for (std::size_t j = 0; j < N; ++j) r.pi0(c, j) = p0[j];
226 const std::vector<T> piv = absorb_limiting(p0);
227 for (std::size_t j = 0; j < N; ++j) r.pis(c, j) = piv[j];
228 }
229
230 r.pi.assign(N, zero);
231 if (pin.empty()) {
232 // Probability of starting in each component: uniform over the components
233 // that have some incoming rate.
234 std::vector<T> pinl(numSCC, one);
235 const T ztol = num_traits<T>::from_double(zeroColTol);
236 for (std::size_t j = 0; j < N; ++j) {
237 T cs = zero;
238 for (std::size_t i = 0; i < N; ++i) cs += num_abs(T(Q(i, j)));
239 if (cs < ztol) pinl[s.scc[j] - 1] = zero;
240 }
241 T tot = zero;
242 for (const T& v : pinl) tot += v;
243 if (tot > zero) {
244 for (T& v : pinl) v /= tot;
245 } else {
246 for (std::size_t c = 0; c < numSCC; ++c)
247 pinl[c] = one / num_traits<T>::from_int(static_cast<long>(numSCC));
248 }
249 for (std::size_t c = 0; c < numSCC; ++c) {
250 if (!(pinl[c] > zero)) continue;
251 for (std::size_t j = 0; j < N; ++j) r.pi[j] += r.pis(c, j) * pinl[c];
252 }
253 if (transSccIds.size() == 1)
254 for (std::size_t j = 0; j < N; ++j) r.pi[j] = r.pis(transSccIds[0], j);
255 } else {
256 // The reference: the limiting law of the GIVEN initial vector, computed
257 // directly as absorb_limiting(pin). Lumping pin onto the components and
258 // mixing the uniform-start rows is NOT the same thing when pin puts its
259 // mass unevenly inside a multi-state transient component.
260 std::vector<T> p0 = pin;
261 T tot0 = zero;
262 for (const T& v : p0) tot0 += v;
263 if (tot0 > zero)
264 for (T& v : p0) v /= tot0;
265 r.pi = absorb_limiting(p0);
266 }
267
268 T tot = zero;
269 for (const T& v : r.pi) tot += v;
270 if (tot > zero)
271 for (T& v : r.pi) v /= tot;
272 return r;
273}
274
275/** Overload without an initial vector. */
276template <class T>
278 double zeroColTol = 1e-12) {
279 return ctmc_solve_reducible_blkdecomp(Q, std::vector<T>(), reachTol, zeroColTol);
280}
281
282} // namespace mc
283} // namespace line
284
285#endif // LINE_API_MC_CTMC_SOLVE_REDUCIBLE_BLKDECOMP_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
NumericError(const std::string &what)
Definition error.h:112
Steady-state distribution of a continuous-time Markov chain.
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
BicgstabResult< T > ctmc_bicgstab(const Matrix< T > &A, const std::vector< T > &b, double tol=1e-12, long maxit=0, const std::vector< T > &x0=std::vector< T >())
Preconditioned stabilized biconjugate gradients, for the linear systems a generator produces.
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
constexpr std::size_t GMRES_MIN_STATES
Order above which the direct sparse factorization is abandoned in favour of the Krylov path.
Definition ctmc_gmres.h:75
SccResult stronglyconncomp(const Matrix< T > &A)
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
GmresResult< T > ctmc_gmres(const Matrix< T > &A, const std::vector< T > &b, double tol=1e-12, long restart=0, long maxit=0, const std::vector< T > &x0=std::vector< T >())
Restarted GMRES with an ILUT preconditioner, for the linear systems a generator produces.
Definition ctmc_gmres.h:653
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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:184
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 ...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static constexpr double ArcTol
Below this an off-diagonal entry is NO ARC of the phase / state graph.
Definition lang_types.h:769
std::vector< T > x
solution
int flag
0 converged, 1 iteration limit, 3 stagnation, 4 breakdown
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
int flag
0 converged, 1 iteration limit, 3 stagnation/divergence
Definition ctmc_gmres.h:80
std::vector< T > x
solution
Definition ctmc_gmres.h:79
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.