LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
dtmc_solve.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_H
6#define LINE_API_MC_DTMC_SOLVE_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Equilibrium distribution of a discrete-time Markov chain, and stochastic
12 * complementation.
13 *
14 * dtmc_solve is the port of matlab/lib/kpctoolbox/mc/dtmc_solve.m: the
15 * stationary vector of P is the stationary vector of the generator P - I, so
16 * the whole implementation delegates to ctmc_solve. Keeping that delegation
17 * literal matters for parity, since every reducibility and trimming rule then
18 * lives in exactly one place.
19 *
20 * ctmc_stochcomp is the port of matlab/src/api/mc/ctmc_stochcomp.m: the
21 * stochastic complement of the state subset I,
22 * S = Q11 + Q12 (-Q22)^-1 Q21,
23 * which is itself a generator on I with the same stationary distribution up to
24 * renormalization. The MATLAB version switches to GMRES above 6000 states;
25 * there is no iterative path here yet, and none is needed for the exact
26 * arithmetic, where an iterative method has no meaning.
27 */
28
29#include <cstddef>
30#include <cstring>
31#include <mutex>
32#include <type_traits>
33#include <utility>
34#include <vector>
35
37#include "line/num/number.h"
38#include "line/util/error.h"
39#include "line/util/lu.h"
40#include "line/util/matrix.h"
41
42namespace line {
43namespace mc {
44
45namespace detail {
46
47/**
48 * Exact sameness of two matrix entries, for the dtmc_solve memo below.
49 *
50 * For a hardware float this is a BIT comparison, not `==`: `==` says -0.0 equals
51 * +0.0 and says NaN equals nothing, and the memo must not conflate two inputs
52 * that could produce different output bits, nor be clever about NaN. Bits make
53 * both cases a miss, which is always safe. Other arithmetics (the multiprecision
54 * reals, the exact rationals) have no signed zero and no NaN, so value equality
55 * is exact sameness there.
56 */
57template <class T>
58inline typename std::enable_if<std::is_floating_point<T>::value, bool>::type dtmc_same(
59 const T& a, const T& b) {
60 return std::memcmp(&a, &b, sizeof(T)) == 0;
61}
62
63template <class T>
64inline typename std::enable_if<!std::is_floating_point<T>::value, bool>::type dtmc_same(
65 const T& a, const T& b) {
66 return a == b;
67}
68
69template <class T>
70bool dtmc_same_matrix(const Matrix<T>& a, const Matrix<T>& b) {
71 if (a.rows() != b.rows() || a.cols() != b.cols()) return false;
72 for (std::size_t i = 0; i < a.rows(); ++i)
73 for (std::size_t j = 0; j < a.cols(); ++j)
74 if (!dtmc_same(a(i, j), b(i, j))) return false;
75 return true;
76}
77
78/**
79 * Bounded memo, one instantiation per arithmetic type.
80 *
81 * The layered fixed point asks dtmc_solve the same question over and over: a
82 * layer's routing does not change between SolverLN iterations, only its rates
83 * do, and visits do not depend on rates, so the per-chain body of
84 * sn_refresh_visits (NetworkStruct::refresh_chains) re-solves one identical
85 * chain per layer per iteration. Native Python measured 2410 calls carrying ONE
86 * distinct matrix on lqn_ofbiz. The twins are Dtmc_solve.java,
87 * python/line_solver/api/mc/dtmc.py and matlab/lib/kpctoolbox/mc/dtmc_solve.m;
88 * keep the four in step.
89 */
90template <class T>
91struct DtmcSolveMemo {
92 static constexpr std::size_t kMax = 32;
93 std::vector<std::pair<Matrix<T>, std::vector<T>>> entries;
94 std::mutex mu;
95
96 static DtmcSolveMemo& instance() {
97 static DtmcSolveMemo m;
98 return m;
99 }
100};
101
102} // namespace detail
103
104/** Stationary distribution of a stochastic matrix P. */
105template <class T>
106std::vector<T> dtmc_solve(const Matrix<T>& P) {
107 const std::size_t n = P.rows();
108 if (P.cols() != n) throw InputError("dtmc_solve: transition matrix is not square");
109
110 detail::DtmcSolveMemo<T>& memo = detail::DtmcSolveMemo<T>::instance();
111 {
112 std::lock_guard<std::mutex> lk(memo.mu);
113 for (std::size_t k = memo.entries.size(); k-- > 0;) {
114 if (detail::dtmc_same_matrix(memo.entries[k].first, P)) {
115 // Returned BY VALUE, so a caller that writes into the result cannot
116 // reach the stored copy.
117 return memo.entries[k].second;
118 }
119 }
120 }
121
122 Matrix<T> Q = P;
123 const T one = num_traits<T>::from_int(1);
124 for (std::size_t i = 0; i < n; ++i) Q(i, i) -= one;
125 std::vector<T> pi = ctmc_solve(Q);
126
127 {
128 std::lock_guard<std::mutex> lk(memo.mu);
129 memo.entries.emplace_back(P, pi);
130 if (memo.entries.size() > detail::DtmcSolveMemo<T>::kMax) memo.entries.erase(memo.entries.begin());
131 }
132 return pi;
133}
134
135template <class T>
137 Matrix<T> S; ///< stochastic complement on the selected states
138 Matrix<T> Q11; ///< the four blocks, as MATLAB returns them
142 Matrix<T> T12; ///< Q12 (-Q22)^-1 Q21, the correction term
143};
144
145/**
146 * @param Q generator
147 * @param I state subset to keep; defaults to the first ceil(n/2) states
148 */
149template <class T>
150StochCompResult<T> ctmc_stochcomp(const Matrix<T>& Q, const std::vector<std::size_t>& I) {
151 const std::size_t n = Q.rows();
152 if (Q.cols() != n) throw InputError("ctmc_stochcomp: generator is not square");
153
154 std::vector<bool> selected(n, false);
155 for (std::size_t k : I) {
156 if (k >= n) throw InputError("ctmc_stochcomp: state index out of range");
157 selected[k] = true;
158 }
159 std::vector<std::size_t> Ic;
160 for (std::size_t i = 0; i < n; ++i)
161 if (!selected[i]) Ic.push_back(i);
162 if (I.empty()) throw InputError("ctmc_stochcomp: empty state subset");
163
165 r.Q11 = detail::submatrix(Q, I);
166 r.Q22 = detail::submatrix(Q, Ic);
167 r.Q12 = Matrix<T>(I.size(), Ic.size());
168 r.Q21 = Matrix<T>(Ic.size(), I.size());
169 for (std::size_t a = 0; a < I.size(); ++a)
170 for (std::size_t b = 0; b < Ic.size(); ++b) r.Q12(a, b) = Q(I[a], Ic[b]);
171 for (std::size_t a = 0; a < Ic.size(); ++a)
172 for (std::size_t b = 0; b < I.size(); ++b) r.Q21(a, b) = Q(Ic[a], I[b]);
173
174 if (Ic.empty()) {
175 r.T12 = Matrix<T>(I.size(), I.size(), num_traits<T>::from_int(0));
176 r.S = r.Q11;
177 return r;
178 }
179
180 // T = (-Q22) \ Q21, solved column by column with one shared factorization.
181 Matrix<T> A = r.Q22;
182 for (std::size_t i = 0; i < A.rows(); ++i)
183 for (std::size_t j = 0; j < A.cols(); ++j) A(i, j) = -A(i, j);
184 Matrix<T> LU = A;
185 const std::vector<std::size_t> piv = lu_factor(LU);
186
187 Matrix<T> Tm(Ic.size(), I.size());
188 for (std::size_t c = 0; c < I.size(); ++c) {
189 std::vector<T> rhs(Ic.size());
190 for (std::size_t i = 0; i < Ic.size(); ++i) rhs[i] = r.Q21(i, c);
191 lu_solve(LU, piv, rhs);
192 for (std::size_t i = 0; i < Ic.size(); ++i) Tm(i, c) = rhs[i];
193 }
194
195 // T = Q12 * T; S = Q11 + T.
196 r.T12 = Matrix<T>(I.size(), I.size(), num_traits<T>::from_int(0));
197 for (std::size_t a = 0; a < I.size(); ++a)
198 for (std::size_t c = 0; c < I.size(); ++c) {
200 for (std::size_t k = 0; k < Ic.size(); ++k) s += r.Q12(a, k) * Tm(k, c);
201 r.T12(a, c) = s;
202 }
203 r.S = Matrix<T>(I.size(), I.size());
204 for (std::size_t a = 0; a < I.size(); ++a)
205 for (std::size_t c = 0; c < I.size(); ++c) r.S(a, c) = r.Q11(a, c) + r.T12(a, c);
206 return r;
207}
208
209/** Default subset: the first ceil(n/2) states, as in MATLAB. */
210template <class T>
212 const std::size_t half = (Q.rows() + 1) / 2;
213 std::vector<std::size_t> I(half);
214 for (std::size_t i = 0; i < half; ++i) I[i] = i;
215 return ctmc_stochcomp(Q, I);
216}
217
218} // namespace mc
219} // namespace line
220
221#endif // LINE_API_MC_DTMC_SOLVE_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
Steady-state distribution of a continuous-time Markov chain.
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
StochCompResult< T > ctmc_stochcomp(const Matrix< T > &Q, const std::vector< std::size_t > &I)
Definition dtmc_solve.h:150
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.
Matrix< T > S
stochastic complement on the selected states
Definition dtmc_solve.h:137
Matrix< T > Q11
the four blocks, as MATLAB returns them
Definition dtmc_solve.h:138
Matrix< T > T12
Q12 (-Q22)^-1 Q21, the correction term.
Definition dtmc_solve.h:142