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. The lumped transient part is acyclic, so that solve is a
34 * back-substitution in reverse topological order, no iteration and no
35 * tolerance, so the whole routine instantiates at Rational and returns the
36 * true limiting distribution of a rational chain.
37 *
38 * The only tolerance left is the one MATLAB uses to decide that a state has no
39 * incoming mass (column sum below 1e-12) when no initial vector is supplied;
40 * it is a structural test on the input, not a convergence criterion, and it is
41 * exposed as a parameter.
42 */
43
44#include <cstddef>
45#include <utility>
46#include <vector>
47
51#include "line/num/number.h"
52#include "line/util/error.h"
53#include "line/util/lu.h"
54#include "line/util/matrix.h"
55
56namespace line {
57namespace mc {
58
59template <class T>
61 std::vector<T> pi; ///< limiting distribution, length N
62 Matrix<T> pis; ///< numSCC x N, limiting vector per starting component
63 Matrix<T> pi0; ///< numSCC x numSCC, the lumped starting vectors (empty if irreducible)
64 std::vector<std::size_t> scc; ///< component index of each state, 1-based
65 std::vector<bool> isrec; ///< recurrence flag per component
66 Matrix<T> Pl; ///< lumped chain
67 Matrix<T> pil; ///< numSCC x numSCC, limiting vector of the lumped chain
68};
69
70namespace detail {
71
72/**
73 * Limiting matrix lim_k Pl^k of a lumped chain whose recurrent states are
74 * absorbing, in closed form. See the header note: this replaces MATLAB's
75 * spectd / power-iteration pair and is exact.
76 */
77template <class T>
78Matrix<T> lumped_limiting_matrix(const Matrix<T>& Pl, const std::vector<bool>& isrec) {
79 const std::size_t m = Pl.rows();
80 const T zero = num_traits<T>::from_int(0);
81 const T one = num_traits<T>::from_int(1);
82
83 std::vector<std::size_t> tr, rec;
84 for (std::size_t i = 0; i < m; ++i) (isrec[i] ? rec : tr).push_back(i);
85
86 Matrix<T> PI(m, m, zero);
87 for (std::size_t i : rec) PI(i, i) = one;
88 if (tr.empty()) return PI;
89 if (rec.empty())
90 throw NumericError(
91 "dtmc_solve_reducible: the chain has no recurrent component, so no limiting "
92 "distribution exists");
93
94 // (I - Pl_TT) X = Pl_TR. Pl lumps strongly connected components, so its transient part is ACYCLIC and
95 // (I - Pl_TT) is triangular up to a permutation: back-substitution in reverse topological order is the
96 // exact solve. A dense LU was O(|T|^3) and never finished on a flattened LQN (2879 transient components).
97 std::vector<std::vector<std::size_t>> succ(m);
98 for (std::size_t i : tr)
99 for (std::size_t j = 0; j < m; ++j)
100 if (j != i && Pl(i, j) != zero) succ[i].push_back(j);
101 // Post-order DFS over transient components: a component is emitted after all its successors.
102 std::vector<char> mark(m, 0);
103 std::vector<std::size_t> order;
104 order.reserve(tr.size());
105 for (std::size_t root : tr) {
106 if (mark[root]) continue;
107 std::vector<std::pair<std::size_t, std::size_t>> stack(1, std::make_pair(root, std::size_t(0)));
108 mark[root] = 1;
109 while (!stack.empty()) {
110 const std::size_t v = stack.back().first;
111 std::size_t& k = stack.back().second;
112 if (k < succ[v].size()) {
113 const std::size_t w = succ[v][k++];
114 if (!isrec[w] && mark[w] == 1)
115 throw NumericError("dtmc_solve_reducible: the lumped transient chain is not acyclic");
116 if (!isrec[w] && !mark[w]) {
117 mark[w] = 1;
118 stack.push_back(std::make_pair(w, std::size_t(0)));
119 }
120 continue;
121 }
122 mark[v] = 2;
123 order.push_back(v);
124 stack.pop_back();
125 }
126 }
127 for (std::size_t v : order) {
128 const T d = one - Pl(v, v);
129 if (!(d > zero))
130 throw NumericError("dtmc_solve_reducible: a transient component reaches no recurrent one");
131 for (std::size_t c : rec) {
132 T acc = zero;
133 for (std::size_t w : succ[v]) {
134 const T pw = isrec[w] ? (w == c ? one : zero) : PI(w, c);
135 if (pw != zero) acc += Pl(v, w) * pw;
136 }
137 PI(v, c) = T(acc / d);
138 }
139 }
140 return PI;
141}
142
143} // namespace detail
144
145/**
146 * @brief Limiting distribution of a discrete-time Markov chain whose
147 * transition matrix may be reducible.
148 *
149 * @param P transition matrix, possibly reducible
150 * @param pin initial distribution; empty to let the routine pick one
151 * @param zeroColTol column-sum threshold below which a state is treated as
152 * having no incoming mass (MATLAB 1e-12)
153 */
154template <class T>
155ReducibleResult<T> dtmc_solve_reducible(const Matrix<T>& P, const std::vector<T>& pin,
156 double zeroColTol = 1e-12) {
157 const std::size_t N = P.rows();
158 if (P.cols() != N) throw InputError("dtmc_solve_reducible: transition matrix is not square");
159 if (!pin.empty() && pin.size() != N)
160 throw InputError("dtmc_solve_reducible: initial vector has the wrong length");
161 const T zero = num_traits<T>::from_int(0);
162 const T one = num_traits<T>::from_int(1);
163
164 const SccResult s = stronglyconncomp(P);
165 const std::size_t numSCC = s.numSCC();
166
168 r.scc = s.scc;
169 r.isrec = s.recurrent;
170
171 if (numSCC == 1) {
172 r.pi = dtmc_solve(P);
173 r.pis = Matrix<T>(1, N);
174 for (std::size_t j = 0; j < N; ++j) r.pis(0, j) = r.pi[j];
175 r.pi0 = Matrix<T>();
176 r.Pl = P;
177 r.pil = r.pis;
178 return r;
179 }
180
181 // Lumped chain: mass flowing between distinct components, row-normalized,
182 // then recurrent components made absorbing.
183 Matrix<T> Pl(numSCC, numSCC, zero);
184 for (std::size_t i = 0; i < numSCC; ++i)
185 for (std::size_t j = 0; j < numSCC; ++j) {
186 if (i == j) continue;
187 T acc = zero;
188 for (std::size_t a : s.members[i])
189 for (std::size_t b : s.members[j]) acc += P(a, b);
190 Pl(i, j) = acc;
191 }
192 Pl = dtmc_makestochastic(Pl);
193 for (std::size_t i = 0; i < numSCC; ++i)
194 if (s.recurrent[i]) {
195 for (std::size_t j = 0; j < numSCC; ++j) Pl(i, j) = zero;
196 Pl(i, i) = one;
197 }
198 r.Pl = Pl;
199
200 // Probability of starting in each component.
201 std::vector<T> pinl(numSCC, zero);
202 if (pin.empty()) {
203 const T tol = num_traits<T>::from_double(zeroColTol);
204 for (std::size_t i = 0; i < numSCC; ++i) pinl[i] = one;
205 for (std::size_t j = 0; j < N; ++j) {
206 T cs = zero;
207 for (std::size_t i = 0; i < N; ++i) cs += P(i, j);
208 if (cs < tol) pinl[s.scc[j] - 1] = zero;
209 }
210 T tot = zero;
211 for (const T& v : pinl) tot += v;
212 if (tot == zero) {
213 // empty-component uniform-weighting rationale: see _kb/03-api-layer.md (cpp port notes: mc)
214 for (std::size_t i = 0; i < numSCC; ++i)
215 pinl[i] = one / num_traits<T>::from_int(static_cast<long>(numSCC));
216 } else {
217 for (T& v : pinl) v /= tot;
218 }
219 } else {
220 for (std::size_t i = 0; i < numSCC; ++i) {
221 T acc = zero;
222 for (std::size_t a : s.members[i]) acc += pin[a];
223 pinl[i] = acc;
224 }
225 }
226
227 const Matrix<T> PI = detail::lumped_limiting_matrix(Pl, s.recurrent);
228
229 // Conditional limiting vector inside each component, computed once. It is
230 // computed for EVERY component, not only the ones some starting component
231 // reaches with positive weight: the `pis` rows below are addressed BY SCC
232 // INDEX by the single-transient-component branch at the end, so a row left
233 // unfilled is not an absent row, it is a row of zeros masquerading as a
234 // distribution. `dtmc_solve_reducible.m:153-158` carries the same note.
235 std::vector<std::vector<T>> within(numSCC);
236 for (std::size_t j = 0; j < numSCC; ++j)
237 within[j] = dtmc_solve(detail::submatrix(P, s.members[j]));
238
239 r.pi0 = Matrix<T>(numSCC, numSCC, zero);
240 r.pil = Matrix<T>(numSCC, numSCC, zero);
241 r.pis = Matrix<T>(numSCC, N, zero);
242 r.pi.assign(N, zero);
243 for (std::size_t i = 0; i < numSCC; ++i) {
244 r.pi0(i, i) = one;
245 for (std::size_t j = 0; j < numSCC; ++j) r.pil(i, j) = PI(i, j);
246 for (std::size_t j = 0; j < numSCC; ++j) {
247 if (r.pil(i, j) == zero) continue;
248 for (std::size_t k = 0; k < s.members[j].size(); ++k)
249 r.pis(i, s.members[j][k]) = r.pil(i, j) * within[j][k];
250 }
251 // Only a component that CAN be started in enters the mixture; every
252 // row is filled above regardless, for the reason given there.
253 if (!(pinl[i] > zero)) continue;
254 for (std::size_t k = 0; k < N; ++k) r.pi[k] += r.pis(i, k) * pinl[i];
255 }
256
257 // A single transient component and no explicit start: that component IS the
258 // starting state, so its row is the answer rather than the weighted mean.
259 std::size_t nTrans = 0, transIdx = 0;
260 for (std::size_t i = 0; i < numSCC; ++i)
261 if (!s.recurrent[i]) {
262 ++nTrans;
263 transIdx = i;
264 }
265 if (nTrans == 1 && pin.empty())
266 for (std::size_t k = 0; k < N; ++k) r.pi[k] = r.pis(transIdx, k);
267
268 return r;
269}
270
271/** Overload without an initial vector. */
272template <class T>
273ReducibleResult<T> dtmc_solve_reducible(const Matrix<T>& P, double zeroColTol = 1e-12) {
274 return dtmc_solve_reducible(P, std::vector<T>(), zeroColTol);
275}
276
277} // namespace mc
278} // namespace line
279
280#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
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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.