LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
dtmc_solve_reducible.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_DTMC_SOLVE_REDUCIBLE_H
6#define LINE_API_MC_DTMC_SOLVE_REDUCIBLE_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Limiting distribution of a discrete-time Markov chain whose transition
12 * matrix may be reducible.
13 *
14 * Templated port of matlab/src/api/mc/dtmc_solve_reducible.m and
15 * jar/src/main/java/jline/api/mc/Dtmc_solve_reducible.java. The states are
16 * partitioned into strongly connected components; the components are lumped
17 * into a chain Pl on which the recurrent ones are made absorbing; the limiting
18 * matrix of Pl distributes the initial mass over the recurrent components; and
19 * within each component the conditional limiting vector is the stationary
20 * vector of the restricted chain.
21 *
22 * EXACT, DELIBERATELY, AND THIS IS WHERE THE PORT BEATS THE REFERENCE. MATLAB
23 * computes the limiting matrix of the lumped chain by spectral decomposition
24 * (spectd), with a power iteration capped at 1000 steps and tolerance 1e-10 as
25 * the fallback whenever the eigenvector matrix has condition number above
26 * 1e10 -- which is exactly the situation a lumped chain with repeated unit
27 * eigenvalues produces, so the fallback is the common case rather than the
28 * rare one, and it converges only linearly in the subdominant eigenvalue.
29 * Here the same matrix is obtained in closed form: the recurrent lumped states
30 * are absorbing, so the limit is the absorption probability
31 * PI(t, r) = [(I - Pl_TT)^-1 Pl_TR](t, r), PI(r, r') = delta,
32 * with I - Pl_TT non-singular because every transient component reaches a
33 * recurrent one. That is one finite linear solve, no iteration and no
34 * tolerance, so the whole routine instantiates at Rational and returns the
35 * true limiting distribution of a rational chain.
36 *
37 * The only tolerance left is the one MATLAB uses to decide that a state has no
38 * incoming mass (column sum below 1e-12) when no initial vector is supplied;
39 * it is a structural test on the input, not a convergence criterion, and it is
40 * exposed as a parameter.
41 */
42
43#include <cstddef>
44#include <vector>
45
49#include "line/num/number.h"
50#include "line/util/error.h"
51#include "line/util/lu.h"
52#include "line/util/matrix.h"
53
54namespace line {
55namespace mc {
56
57template <class T>
59 std::vector<T> pi; ///< limiting distribution, length N
60 Matrix<T> pis; ///< numSCC x N, limiting vector per starting component
61 Matrix<T> pi0; ///< numSCC x numSCC, the lumped starting vectors (empty if irreducible)
62 std::vector<std::size_t> scc; ///< component index of each state, 1-based
63 std::vector<bool> isrec; ///< recurrence flag per component
64 Matrix<T> Pl; ///< lumped chain
65 Matrix<T> pil; ///< numSCC x numSCC, limiting vector of the lumped chain
66};
67
68namespace detail {
69
70/**
71 * Limiting matrix lim_k Pl^k of a lumped chain whose recurrent states are
72 * absorbing, in closed form. See the header note: this replaces MATLAB's
73 * spectd / power-iteration pair and is exact.
74 */
75template <class T>
76Matrix<T> lumped_limiting_matrix(const Matrix<T>& Pl, const std::vector<bool>& isrec) {
77 const std::size_t m = Pl.rows();
78 const T zero = num_traits<T>::from_int(0);
79 const T one = num_traits<T>::from_int(1);
80
81 std::vector<std::size_t> tr, rec;
82 for (std::size_t i = 0; i < m; ++i) (isrec[i] ? rec : tr).push_back(i);
83
84 Matrix<T> PI(m, m, zero);
85 for (std::size_t i : rec) PI(i, i) = one;
86 if (tr.empty()) return PI;
87 if (rec.empty())
88 throw NumericError(
89 "dtmc_solve_reducible: the chain has no recurrent component, so no limiting "
90 "distribution exists");
91
92 // (I - Pl_TT) X = Pl_TR, solved with one factorization for all columns.
93 Matrix<T> A(tr.size(), tr.size(), zero);
94 for (std::size_t a = 0; a < tr.size(); ++a)
95 for (std::size_t b = 0; b < tr.size(); ++b)
96 A(a, b) = (a == b ? one : zero) - Pl(tr[a], tr[b]);
97 Matrix<T> LU = A;
98 const std::vector<std::size_t> piv = lu_factor(LU);
99 for (std::size_t c = 0; c < rec.size(); ++c) {
100 std::vector<T> rhs(tr.size());
101 for (std::size_t a = 0; a < tr.size(); ++a) rhs[a] = Pl(tr[a], rec[c]);
102 lu_solve(LU, piv, rhs);
103 for (std::size_t a = 0; a < tr.size(); ++a) PI(tr[a], rec[c]) = rhs[a];
104 }
105 return PI;
106}
107
108} // namespace detail
109
110/**
111 * @brief Limiting distribution of a discrete-time Markov chain whose
112 * transition matrix may be reducible.
113 *
114 * @param P transition matrix, possibly reducible
115 * @param pin initial distribution; empty to let the routine pick one
116 * @param zeroColTol column-sum threshold below which a state is treated as
117 * having no incoming mass (MATLAB 1e-12)
118 */
119template <class T>
120ReducibleResult<T> dtmc_solve_reducible(const Matrix<T>& P, const std::vector<T>& pin,
121 double zeroColTol = 1e-12) {
122 const std::size_t N = P.rows();
123 if (P.cols() != N) throw InputError("dtmc_solve_reducible: transition matrix is not square");
124 if (!pin.empty() && pin.size() != N)
125 throw InputError("dtmc_solve_reducible: initial vector has the wrong length");
126 const T zero = num_traits<T>::from_int(0);
127 const T one = num_traits<T>::from_int(1);
128
129 const SccResult s = stronglyconncomp(P);
130 const std::size_t numSCC = s.numSCC();
131
133 r.scc = s.scc;
134 r.isrec = s.recurrent;
135
136 if (numSCC == 1) {
137 r.pi = dtmc_solve(P);
138 r.pis = Matrix<T>(1, N);
139 for (std::size_t j = 0; j < N; ++j) r.pis(0, j) = r.pi[j];
140 r.pi0 = Matrix<T>();
141 r.Pl = P;
142 r.pil = r.pis;
143 return r;
144 }
145
146 // Lumped chain: mass flowing between distinct components, row-normalized,
147 // then recurrent components made absorbing.
148 Matrix<T> Pl(numSCC, numSCC, zero);
149 for (std::size_t i = 0; i < numSCC; ++i)
150 for (std::size_t j = 0; j < numSCC; ++j) {
151 if (i == j) continue;
152 T acc = zero;
153 for (std::size_t a : s.members[i])
154 for (std::size_t b : s.members[j]) acc += P(a, b);
155 Pl(i, j) = acc;
156 }
157 Pl = dtmc_makestochastic(Pl);
158 for (std::size_t i = 0; i < numSCC; ++i)
159 if (s.recurrent[i]) {
160 for (std::size_t j = 0; j < numSCC; ++j) Pl(i, j) = zero;
161 Pl(i, i) = one;
162 }
163 r.Pl = Pl;
164
165 // Probability of starting in each component.
166 std::vector<T> pinl(numSCC, zero);
167 if (pin.empty()) {
168 const T tol = num_traits<T>::from_double(zeroColTol);
169 for (std::size_t i = 0; i < numSCC; ++i) pinl[i] = one;
170 for (std::size_t j = 0; j < N; ++j) {
171 T cs = zero;
172 for (std::size_t i = 0; i < N; ++i) cs += P(i, j);
173 if (cs < tol) pinl[s.scc[j] - 1] = zero;
174 }
175 T tot = zero;
176 for (const T& v : pinl) tot += v;
177 if (tot == zero) {
178 // empty-component uniform-weighting rationale: see _kb/03-api-layer.md (cpp port notes: mc)
179 for (std::size_t i = 0; i < numSCC; ++i)
180 pinl[i] = one / num_traits<T>::from_int(static_cast<long>(numSCC));
181 } else {
182 for (T& v : pinl) v /= tot;
183 }
184 } else {
185 for (std::size_t i = 0; i < numSCC; ++i) {
186 T acc = zero;
187 for (std::size_t a : s.members[i]) acc += pin[a];
188 pinl[i] = acc;
189 }
190 }
191
192 const Matrix<T> PI = detail::lumped_limiting_matrix(Pl, s.recurrent);
193
194 // Conditional limiting vector inside each component, computed once. It is
195 // computed for EVERY component, not only the ones some starting component
196 // reaches with positive weight: the `pis` rows below are addressed BY SCC
197 // INDEX by the single-transient-component branch at the end, so a row left
198 // unfilled is not an absent row, it is a row of zeros masquerading as a
199 // distribution. `dtmc_solve_reducible.m:153-158` carries the same note.
200 std::vector<std::vector<T>> within(numSCC);
201 for (std::size_t j = 0; j < numSCC; ++j)
202 within[j] = dtmc_solve(detail::submatrix(P, s.members[j]));
203
204 r.pi0 = Matrix<T>(numSCC, numSCC, zero);
205 r.pil = Matrix<T>(numSCC, numSCC, zero);
206 r.pis = Matrix<T>(numSCC, N, zero);
207 r.pi.assign(N, zero);
208 for (std::size_t i = 0; i < numSCC; ++i) {
209 r.pi0(i, i) = one;
210 for (std::size_t j = 0; j < numSCC; ++j) r.pil(i, j) = PI(i, j);
211 for (std::size_t j = 0; j < numSCC; ++j) {
212 if (r.pil(i, j) == zero) continue;
213 for (std::size_t k = 0; k < s.members[j].size(); ++k)
214 r.pis(i, s.members[j][k]) = r.pil(i, j) * within[j][k];
215 }
216 // Only a component that CAN be started in enters the mixture; every
217 // row is filled above regardless, for the reason given there.
218 if (!(pinl[i] > zero)) continue;
219 for (std::size_t k = 0; k < N; ++k) r.pi[k] += r.pis(i, k) * pinl[i];
220 }
221
222 // A single transient component and no explicit start: that component IS the
223 // starting state, so its row is the answer rather than the weighted mean.
224 std::size_t nTrans = 0, transIdx = 0;
225 for (std::size_t i = 0; i < numSCC; ++i)
226 if (!s.recurrent[i]) {
227 ++nTrans;
228 transIdx = i;
229 }
230 if (nTrans == 1 && pin.empty())
231 for (std::size_t k = 0; k < N; ++k) r.pi[k] = r.pis(transIdx, k);
232
233 return r;
234}
235
236/** Overload without an initial vector. */
237template <class T>
238ReducibleResult<T> dtmc_solve_reducible(const Matrix<T>& P, double zeroColTol = 1e-12) {
239 return dtmc_solve_reducible(P, std::vector<T>(), zeroColTol);
240}
241
242} // namespace mc
243} // namespace line
244
245#endif // LINE_API_MC_DTMC_SOLVE_REDUCIBLE_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
Normalize a non-negative matrix into a stochastic transition matrix.
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
ReducibleResult< T > dtmc_solve_reducible(const Matrix< T > &P, const std::vector< T > &pin, double zeroColTol=1e-12)
Limiting distribution of a discrete-time Markov chain whose transition matrix may be reducible.
Matrix< T > dtmc_makestochastic(const Matrix< T > &Pin)
Normalize a non-negative matrix into a stochastic transition matrix.
SccResult stronglyconncomp(const Matrix< T > &A)
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
std::vector< T > dtmc_solve(const Matrix< T > &P)
Stationary distribution of a stochastic matrix P.
Definition dtmc_solve.h:106
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
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 > pil
numSCC x numSCC, limiting vector of the lumped chain
Matrix< T > pis
numSCC x N, limiting vector per starting component
std::vector< T > pi
limiting distribution, length N
Matrix< T > pi0
numSCC x numSCC, the lumped starting vectors (empty if irreducible)
std::vector< bool > isrec
recurrence flag per component
Matrix< T > Pl
lumped chain
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.