LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_ssd.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_SSD_H
6#define LINE_API_MC_CTMC_SSD_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Stochastically stable distribution of a perturbed Markov chain (Wicks 2009).
12 *
13 * Port of matlab/src/api/mc/ctmc_ssd.m and dtmc_ssd.m; JAR twin
14 * jline.api.mc.Ctmc_ssd, Python twin line_solver.api.mc.gth.ctmc_ssd. The
15 * off-diagonal rates behave as Q_eps(i,j) ~ C(i,j) eps^R(i,j) as eps -> 0+
16 * (rows are sources; a zero rate is R = Inf or C = 0), and the result is the
17 * exact limit of the stationary vector, no eps ever substituted. Q_eps must be
18 * irreducible for eps > 0. The recursion (see ctmc_ssd.m for the full account):
19 * 1. shift R to minimum 0 and take G0, the zero-resistance graph;
20 * 2. one closed class in G0: GTH on the constant chain over G0;
21 * 3. else a class of G0 with two or more states: drop its positive-resistance
22 * internal edges (Lemma 7.22) and censor all its states but one, with the
23 * constant N0 = (-Q0_ss)^{-1} (Theorem 7.31), in (C,R) arithmetic;
24 * 4. else divide the rates out of the closed singletons by eps^smin (Lemma
25 * 7.16) and keep the scaled solution on those states.
26 * (C,R) arithmetic: a sum keeps the least resistance and adds the tied costs, a
27 * product adds resistances and multiplies costs. Resistances are compared with
28 * a relative tolerance 1e-10, so the computation is double-valued.
29 * (J. R. Wicks, PhD thesis, Brown University, 2009, Ch. 7.)
30 */
31
32#include <algorithm>
33#include <cmath>
34#include <cstddef>
35#include <limits>
36#include <string>
37#include <vector>
38
41#include "line/util/error.h"
42#include "line/util/lu.h"
43#include "line/util/matrix.h"
44
45namespace line {
46namespace mc {
47
48namespace detail {
49
50using SsdMat = Matrix<double>;
51
52inline void ssd_mult(const SsdMat& C1, const SsdMat& R1, const SsdMat& C2, const SsdMat& R2, double tol,
53 SsdMat& Cc, SsdMat& Rc) {
54 const double inf = std::numeric_limits<double>::infinity();
55 const std::size_t a = C1.rows(), b = C1.cols(), c = C2.cols();
56 Cc = SsdMat(a, c, 0.0);
57 Rc = SsdMat(a, c, inf);
58 for (std::size_t t = 0; t < b; ++t)
59 for (std::size_t i = 0; i < a; ++i)
60 for (std::size_t j = 0; j < c; ++j) {
61 const double r = R1(i, t) + R2(t, j);
62 const double cc = C1(i, t) * C2(t, j);
63 if (!std::isfinite(r) || !(cc > 0.0)) continue;
64 if (r < Rc(i, j) - tol) {
65 Cc(i, j) = cc;
66 Rc(i, j) = r;
67 } else if (std::fabs(r - Rc(i, j)) <= tol) {
68 Cc(i, j) += cc;
69 }
70 }
71}
72
73inline void ssd_add_inplace(SsdMat& C1, SsdMat& R1, const SsdMat& C2, const SsdMat& R2, double tol) {
74 for (std::size_t i = 0; i < C1.rows(); ++i)
75 for (std::size_t j = 0; j < C1.cols(); ++j) {
76 if (!std::isfinite(R2(i, j))) continue;
77 if (R2(i, j) < R1(i, j) - tol) {
78 R1(i, j) = R2(i, j);
79 C1(i, j) = C2(i, j);
80 } else if (std::fabs(R2(i, j) - R1(i, j)) <= tol) {
81 C1(i, j) += C2(i, j);
82 }
83 }
84}
85
86inline std::vector<double> ssd_recurse(SsdMat C, SsdMat R, double tol, int depth) {
87 const double inf = std::numeric_limits<double>::infinity();
88 const std::size_t n = C.rows();
89 if (n == 1) return std::vector<double>{1.0};
90 if (depth <= 0)
91 throw NumericError(ErrorKind{"NonConvergence"},
92 "ctmc_ssd: the SSD recursion did not terminate; please report the input matrices");
93 double rmin = inf;
94 for (std::size_t i = 0; i < n; ++i)
95 for (std::size_t j = 0; j < n; ++j)
96 if (std::isfinite(R(i, j)) && R(i, j) < rmin) rmin = R(i, j);
97 SsdMat Z(n, n, 0.0);
98 for (std::size_t i = 0; i < n; ++i)
99 for (std::size_t j = 0; j < n; ++j)
100 if (std::isfinite(R(i, j))) {
101 R(i, j) -= rmin;
102 if (R(i, j) <= tol) {
103 R(i, j) = 0.0;
104 Z(i, j) = 1.0;
105 }
106 }
107 const SccResult s = stronglyconncomp(Z);
108 std::vector<std::size_t> closed;
109 for (std::size_t c = 0; c < s.recurrent.size(); ++c)
110 if (s.recurrent[c]) closed.push_back(c);
111 if (closed.size() == 1) {
112 std::vector<std::size_t> S = s.members[closed[0]];
113 std::sort(S.begin(), S.end());
114 SsdMat A(S.size(), S.size(), 0.0);
115 for (std::size_t a = 0; a < S.size(); ++a)
116 for (std::size_t b = 0; b < S.size(); ++b) A(a, b) = C(S[a], S[b]) * Z(S[a], S[b]);
117 bool ok = true;
118 const std::vector<double> x = gth_irreducible(A, ok);
119 std::vector<double> p(n, 0.0);
120 for (std::size_t k = 0; k < S.size(); ++k) p[S[k]] = x[k];
121 return p;
122 }
123 // Components are numbered by decreasing size, so component 0 is the largest.
124 if (s.members[0].size() > 1) {
125 std::vector<std::size_t> S = s.members[0];
126 std::sort(S.begin(), S.end());
127 std::vector<bool> inS(n, false);
128 for (std::size_t v : S) inS[v] = true;
129 for (std::size_t i : S)
130 for (std::size_t j : S)
131 if (i != j && Z(i, j) == 0.0) { // Lemma 7.22
132 C(i, j) = 0.0;
133 R(i, j) = inf;
134 }
135 const std::vector<std::size_t> sv(S.begin() + 1, S.end());
136 std::vector<std::size_t> keep;
137 for (std::size_t v = 0; v < n; ++v)
138 if (!inS[v] || v == S[0]) keep.push_back(v);
139 const std::size_t ns = sv.size(), m = keep.size();
140 SsdMat C0(n, n, 0.0);
141 for (std::size_t i = 0; i < n; ++i)
142 for (std::size_t j = 0; j < n; ++j) C0(i, j) = C(i, j) * Z(i, j);
143 SsdMat M(ns, ns, 0.0); // -Q0_ss
144 for (std::size_t a = 0; a < ns; ++a) {
145 double out = 0.0;
146 for (std::size_t j = 0; j < n; ++j) out += C0(sv[a], j);
147 for (std::size_t b = 0; b < ns; ++b) M(a, b) = -C0(sv[a], sv[b]);
148 M(a, a) += out;
149 }
150 SsdMat N0(ns, ns, 0.0), RN(ns, ns, inf);
151 for (std::size_t b = 0; b < ns; ++b) {
152 std::vector<double> e(ns, 0.0);
153 e[b] = 1.0;
154 const std::vector<double> col = solve(M, e);
155 for (std::size_t a = 0; a < ns; ++a) {
156 N0(a, b) = col[a] > 0.0 ? col[a] : 0.0;
157 if (N0(a, b) > 0.0) RN(a, b) = 0.0;
158 }
159 }
160 auto sub = [](const SsdMat& X, const std::vector<std::size_t>& r, const std::vector<std::size_t>& c) {
161 return gth_sub(X, r, c);
162 };
163 SsdMat Cks, Rks, Cv, Rv;
164 ssd_mult(sub(C, keep, sv), sub(R, keep, sv), N0, RN, tol, Cks, Rks);
165 ssd_mult(Cks, Rks, sub(C, sv, keep), sub(R, sv, keep), tol, Cv, Rv);
166 SsdMat Cr = sub(C, keep, keep), Rr = sub(R, keep, keep);
167 ssd_add_inplace(Cr, Rr, Cv, Rv, tol);
168 for (std::size_t i = 0; i < m; ++i) {
169 Cr(i, i) = 0.0;
170 Rr(i, i) = inf;
171 }
172 const std::vector<double> x = ssd_recurse(Cr, Rr, tol, depth - 1);
173 std::vector<double> p(n, 0.0);
174 for (std::size_t k = 0; k < m; ++k) p[keep[k]] = x[k];
175 std::vector<double> y(ns, 0.0); // x * C0(keep, s)
176 for (std::size_t a = 0; a < ns; ++a)
177 for (std::size_t k = 0; k < m; ++k) y[a] += x[k] * C0(keep[k], sv[a]);
178 for (std::size_t b = 0; b < ns; ++b) {
179 double v = 0.0;
180 for (std::size_t a = 0; a < ns; ++a) v += y[a] * N0(a, b);
181 p[sv[b]] = v;
182 }
183 double tot = 0.0;
184 for (double v : p) tot += v;
185 for (double& v : p) v /= tot;
186 return p;
187 }
188 // Every class of G0 is a single state and several are absorbing in G0.
189 std::vector<bool> isClosed(n, false);
190 for (std::size_t v = 0; v < n; ++v) isClosed[v] = s.recurrent[s.scc[v] - 1];
191 double smin = inf;
192 for (std::size_t i = 0; i < n; ++i)
193 if (isClosed[i])
194 for (std::size_t j = 0; j < n; ++j)
195 if (std::isfinite(R(i, j)) && R(i, j) < smin) smin = R(i, j);
196 for (std::size_t i = 0; i < n; ++i)
197 if (isClosed[i])
198 for (std::size_t j = 0; j < n; ++j)
199 if (std::isfinite(R(i, j))) R(i, j) -= smin;
200 std::vector<double> p = ssd_recurse(C, R, tol, depth - 1);
201 double tot = 0.0;
202 for (std::size_t v = 0; v < n; ++v) {
203 if (!isClosed[v]) p[v] = 0.0;
204 tot += p[v];
205 }
206 if (!(tot > 0.0))
207 throw NumericError(ErrorKind{"Degenerate"},
208 "ctmc_ssd: internal error: the scaled chain put no mass on the closed states");
209 for (double& v : p) v /= tot;
210 return p;
211}
212
213} // namespace detail
214
215/** @brief Stochastically stable distribution, rates C(i,j) eps^R(i,j) (Wicks 2009). */
216inline std::vector<double> ctmc_ssd(const Matrix<double>& Cin, const Matrix<double>& Rin) {
217 const double inf = std::numeric_limits<double>::infinity();
218 const std::size_t n = Cin.rows();
219 if (Cin.cols() != n || Rin.rows() != n || Rin.cols() != n)
220 throw InputError("ctmc_ssd: C and R must be square matrices of the same size");
221 if (n == 0) throw InputError("ctmc_ssd: empty input");
222 Matrix<double> C = Cin, R = Rin, adj(n, n, 0.0);
223 double rmax = 1.0;
224 for (std::size_t i = 0; i < n; ++i)
225 for (std::size_t j = 0; j < n; ++j) {
226 if (i == j) {
227 C(i, j) = 0.0;
228 R(i, j) = inf;
229 continue;
230 }
231 const double c = C(i, j), r = R(i, j);
232 if (std::isnan(c) || std::isnan(r) || c < 0.0 || std::isinf(c))
233 throw InputError("ctmc_ssd: costs must be finite and nonnegative, and neither C nor R may contain NaN");
234 if (r == -inf) throw InputError("ctmc_ssd: a resistance of -Inf is not a valid perturbation order");
235 if (c == 0.0 || std::isinf(r)) {
236 C(i, j) = 0.0;
237 R(i, j) = inf;
238 } else {
239 adj(i, j) = 1.0;
240 rmax = std::max(rmax, std::fabs(r));
241 }
242 }
243 if (n == 1) return std::vector<double>{1.0};
244 const SccResult s = stronglyconncomp(adj);
245 std::size_t nclosed = 0;
246 for (bool b : s.recurrent) nclosed += b ? 1 : 0;
247 if (nclosed != 1)
248 throw InputError("ctmc_ssd: the perturbed chain has " + std::to_string(nclosed) +
249 " closed classes for eps > 0, so it has no unique stationary distribution and no "
250 "stochastically stable distribution");
251 return detail::ssd_recurse(C, R, 1e-10 * rmax, static_cast<int>(4 * n + 8));
252}
253
254/** @brief SSD of a perturbed transition matrix P_eps(i,j) ~ C(i,j) eps^R(i,j); equals ctmc_ssd. */
255inline std::vector<double> dtmc_ssd(const Matrix<double>& C, const Matrix<double>& R) {
256 return ctmc_ssd(C, R);
257}
258
259} // namespace mc
260} // namespace line
261
262#endif // LINE_API_MC_CTMC_SSD_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
Grassmann-Taksar-Heyman (GTH) elimination for the stationary vector, and the limit matrix J = lim exp...
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
double tot(const std::vector< double > &pops)
std::vector< double > dtmc_ssd(const Matrix< double > &C, const Matrix< double > &R)
SSD of a perturbed transition matrix P_eps(i,j) ~ C(i,j) eps^R(i,j); equals ctmc_ssd.
Definition ctmc_ssd.h:255
std::vector< double > ctmc_ssd(const Matrix< double > &Cin, const Matrix< double > &Rin)
Stochastically stable distribution, rates C(i,j) eps^R(i,j) (Wicks 2009).
Definition ctmc_ssd.h:216
SccResult stronglyconncomp(const Matrix< T > &A)
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
std::vector< bool > recurrent
recurrent[c-1] is true when component c has no edge leaving it.