LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_kms.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_KMS_H
6#define LINE_API_MC_CTMC_KMS_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Koury-McAllister-Stewart aggregation-disaggregation for a nearly completely
12 * decomposable CTMC.
13 *
14 * Templated port of matlab/src/api/mc/ctmc_kms.m and
15 * jar/src/main/java/jline/api/mc/Ctmc_kms.java. Starting from the Courtois
16 * approximation, each sweep conditions the current iterate within every
17 * macro-state, aggregates those conditional vectors into a macro-state chain,
18 * solves it for the macro-state weights, and disaggregates by one block
19 * Gauss-Seidel sweep on the uniformized matrix:
20 *
21 * pn (D - U) = zn L, D = blkdiag(I - P), L and U the strictly block lower
22 * and upper parts of P, zn the reweighted conditional.
23 *
24 * Everything is done in the PERMUTED, macro-state-major index space, because
25 * that is the space P and the block offsets live in; ctmc_courtois returns its
26 * vector already mapped back to the original ordering, so the initial iterate
27 * is permuted on entry and the result is unpermuted on exit. Mixing the two
28 * spaces is the bug this port is written to avoid: with contiguous macro-states
29 * the two orderings coincide and the error is invisible.
30 *
31 * As in the reference there is no convergence test: the caller asks for a fixed
32 * number of sweeps. Above 6000 states the block solve is attempted by GMRES
33 * first, exactly as MATLAB does, since that is where the direct factorization
34 * stops fitting in memory; a non-zero GMRES flag falls back to the direct
35 * solve, so the result never depends on whether the iteration converged.
36 *
37 * GATED ON TRANSCENDENTAL ARITHMETIC: it is seeded by ctmc_courtois, whose
38 * epsMAX is an eigenvalue modulus, and its own large-system path is GMRES.
39 */
40
41#include <cstddef>
42#include <vector>
43
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>
57struct KmsResult {
58 std::vector<T> p; ///< estimate after numSteps sweeps, ORIGINAL ordering
59 std::vector<T> p_1; ///< the previous iterate, ORIGINAL ordering
60 std::vector<T> pcourt; ///< the Courtois starting point, ORIGINAL ordering
61 Matrix<T> Qperm; ///< Q reordered by macro-state
62 T eps; ///< NCD index, as ctmc_courtois defines it
63 T epsMAX; ///< maximum admissible NCD index
64};
65
66namespace detail {
67
68/**
69 * Solve x' M = rhs' for the row vector x, i.e. M' x = rhs. Uses GMRES first
70 * above the reference's 6000-state dispatch threshold, and the direct solve
71 * whenever GMRES does not converge.
72 */
73template <class T>
74std::vector<T> aggregation_block_solve(const Matrix<T>& M, const std::vector<T>& rhs) {
75 const std::size_t n = M.rows();
76 Matrix<T> Mt(n, n);
77 for (std::size_t i = 0; i < n; ++i)
78 for (std::size_t j = 0; j < n; ++j) Mt(i, j) = M(j, i);
79 if (n > GMRES_MIN_STATES) {
80 const GmresResult<T> g = ctmc_gmres(Mt, rhs);
81 if (g.flag == 0) return g.x;
82 // Short-recurrence retry before the cubic factorization, as in ctmc_solve.
83 const BicgstabResult<T> bs = ctmc_bicgstab(Mt, rhs);
84 if (bs.flag == 0) return bs.x;
85 }
86 return solve(Mt, rhs);
87}
88
89} // namespace detail
90
91/**
92 * @brief Koury-McAllister-Stewart aggregation-disaggregation for a nearly
93 * completely decomposable CTMC.
94 *
95 * @param Q generator
96 * @param MS macro-states partitioning 0..n-1
97 * @param numSteps number of aggregation-disaggregation sweeps
98 */
99template <class T>
100KmsResult<T> ctmc_kms(const Matrix<T>& Q, const std::vector<std::vector<std::size_t>>& MS,
101 std::size_t numSteps) {
103 "ctmc_kms requires transcendental arithmetic: it is seeded by ctmc_courtois, "
104 "whose epsMAX is an eigenvalue modulus, and its large-system block solve is "
105 "GMRES, which stops on a residual tolerance");
106 const std::size_t n = Q.rows();
107 if (Q.cols() != n) throw InputError("ctmc_kms: generator is not square");
108 const T zero = num_traits<T>::from_int(0);
109 const T one = num_traits<T>::from_int(1);
110 const std::size_t nMacro = MS.size();
111
112 const detail::CourtoisCore<T> c = detail::courtois_core(Q, MS, detail::courtois_default_rate(Q, MS));
113 const std::vector<T> pMacro = dtmc_solve(c.G);
114
115 // Block offsets in the permuted ordering.
116 std::vector<std::size_t> off(nMacro + 1, 0);
117 for (std::size_t i = 0; i < nMacro; ++i) off[i + 1] = off[i] + MS[i].size();
118
119 std::vector<T> pn(n, zero);
120 for (std::size_t i = 0; i < nMacro; ++i)
121 for (std::size_t a = off[i]; a < off[i + 1]; ++a) pn[a] = pMacro[i] * c.pmicro[a];
122
123 KmsResult<T> r;
124 r.pcourt = detail::unpermute_states(pn, c.v);
125 r.Qperm = c.Qperm;
126 r.eps = c.eps;
127 r.epsMAX = c.epsMAX;
128
129 std::vector<T> pn_1 = pn;
130 for (std::size_t step = 0; step < numSteps; ++step) {
131 pn_1 = pn;
132
133 // Aggregation: condition within each macro-state, then lump.
134 std::vector<T> pcond = pn_1;
135 for (std::size_t I = 0; I < nMacro; ++I) {
136 T s = zero;
137 for (std::size_t a = off[I]; a < off[I + 1]; ++a) s += pn_1[a];
138 if (s > zero)
139 for (std::size_t a = off[I]; a < off[I + 1]; ++a) pcond[a] /= s;
140 }
141
142 Matrix<T> G(nMacro, nMacro, zero);
143 for (std::size_t I = 0; I < nMacro; ++I)
144 for (std::size_t J = 0; J < nMacro; ++J) {
145 T acc = zero;
146 for (std::size_t b = off[J]; b < off[J + 1]; ++b) {
147 T s = zero;
148 for (std::size_t a = off[I]; a < off[I + 1]; ++a) s += c.P(b, a);
149 acc += pcond[b] * s;
150 }
151 G(I, J) = acc;
152 }
153 Matrix<T> Gt(nMacro, nMacro);
154 for (std::size_t i = 0; i < nMacro; ++i)
155 for (std::size_t j = 0; j < nMacro; ++j) Gt(i, j) = G(j, i);
156 const std::vector<T> w = dtmc_solve(Gt);
157
158 // Disaggregation: one block Gauss-Seidel sweep, pn (D - U) = zn L.
159 std::vector<T> zn(n, zero);
160 for (std::size_t I = 0; I < nMacro; ++I)
161 for (std::size_t a = off[I]; a < off[I + 1]; ++a) zn[a] = w[I] * pcond[a];
162
163 Matrix<T> M(n, n, zero);
164 std::vector<T> rhs(n, zero);
165 for (std::size_t I = 0; I < nMacro; ++I)
166 for (std::size_t J = 0; J < nMacro; ++J)
167 for (std::size_t a = off[I]; a < off[I + 1]; ++a)
168 for (std::size_t b = off[J]; b < off[J + 1]; ++b) {
169 if (I > J) {
170 rhs[b] += zn[a] * c.P(a, b); // (zn L)_b
171 } else if (I == J) {
172 M(a, b) = (a == b ? one : zero) - c.P(a, b);
173 } else {
174 M(a, b) = -c.P(a, b); // D - U
175 }
176 }
177
178 pn = detail::aggregation_block_solve(M, rhs);
179 T tot = zero;
180 for (const T& x : pn) tot += x;
181 if (tot == zero) throw NumericError("ctmc_kms: the disaggregation sweep returned a null vector");
182 for (T& x : pn) x /= tot;
183 }
184
185 r.p = detail::unpermute_states(pn, c.v);
186 r.p_1 = detail::unpermute_states(pn_1, c.v);
187 return r;
188}
189
190} // namespace mc
191} // namespace line
192
193#endif // LINE_API_MC_CTMC_KMS_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
Preconditioned stabilized biconjugate gradients, for the linear systems a generator produces.
Courtois decomposition of a nearly completely decomposable (NCD) CTMC.
Restarted GMRES with an ILUT preconditioner, for the linear systems a generator produces.
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.
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.
KmsResult< T > ctmc_kms(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &MS, std::size_t numSteps)
Koury-McAllister-Stewart aggregation-disaggregation for a nearly completely decomposable CTMC.
Definition ctmc_kms.h:100
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
std::vector< T > dtmc_solve(const Matrix< T > &P)
Stationary distribution of a stochastic matrix P.
Definition dtmc_solve.h:106
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
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
Number-type abstraction for the templated API port.
std::vector< T > x
solution
int flag
0 converged, 1 iteration limit, 3 stagnation, 4 breakdown
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::vector< T > pcourt
the Courtois starting point, ORIGINAL ordering
Definition ctmc_kms.h:60
T epsMAX
maximum admissible NCD index
Definition ctmc_kms.h:63
T eps
NCD index, as ctmc_courtois defines it.
Definition ctmc_kms.h:62
std::vector< T > p
estimate after numSteps sweeps, ORIGINAL ordering
Definition ctmc_kms.h:58
Matrix< T > Qperm
Q reordered by macro-state.
Definition ctmc_kms.h:61
std::vector< T > p_1
the previous iterate, ORIGINAL ordering
Definition ctmc_kms.h:59