LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_classical.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_CLASSICAL_H
6#define LINE_API_MC_CTMC_CLASSICAL_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Classical stationary iterative solvers for pi Q = 0, sum(pi) = 1: the power
12 * method on the uniformised DTMC, Jacobi, Gauss-Seidel and SOR (Stewart 1994,
13 * ch. 3; Haverkort 1998, section 15.1.3).
14 *
15 * Templated port of matlab/src/api/mc/ctmc_power.m, ctmc_jacobi.m,
16 * ctmc_gaussseidel.m and ctmc_sor.m, which share the loop of
17 * matlab/src/api/mc/private/stationary_sweep_iterate.m. The JAR twin is
18 * jline.api.mc.StationarySweep and the Python twin
19 * line_solver.api.mc.classical. All four codebases run the same loop:
20 *
21 * x = p0 / sum(p0) (uniform by default); for k = 0..maxiter:
22 * relres = ||x Q||_1 / max|q_ii|; stop with flag 0 if relres <= tol,
23 * flag 1 if k == maxiter; otherwise sweep, renormalise to sum one,
24 * and stop with flag 4 if the sweep sum is zero or not finite.
25 *
26 * Sweeps, with A = Q' split as D + L + U:
27 * power x <- x + (x Q)'/q, q = 1.1 max|q_ii| (aperiodic by the
28 * self-loop of P = I + Q/q, so periodic chains converge)
29 * jacobi x <- -D^{-1} (L + U) x
30 * gaussseidel x <- (D + L)^{-1} (-U x)
31 * sor x <- (D + w L)^{-1} ((1 - w) D - w U) x
32 *
33 * Flag 2 reports a zero diagonal (an absorbing state) for every method but the
34 * power method, which needs no diagonal inverse. Jacobi does not converge when
35 * the embedded jump chain is periodic, for instance on any birth-death chain;
36 * it then reports flag 1. On a reducible generator the limit depends on p0.
37 *
38 * SOR with omega <= 0 selects the adaptive rule: start at omega = 1 and every
39 * 10 sweeps estimate the Gauss-Seidel spectral radius from the ratio of
40 * successive update norms, lambda, then set
41 * mu^2 = (lambda + omega - 1)^2 / (lambda omega^2),
42 * omega = min(2 / (1 + sqrt(1 - mu^2)), 1.95)
43 * when that increases omega (Young's formula for consistently ordered
44 * matrices, Hageman and Young 1981). If the residual later grows above 100
45 * times the reference residual recorded at the last increase, omega reverts to
46 * 1 and adaptation stops.
47 *
48 * Like GMRES these stop on a residual tolerance, so the exact instantiation is
49 * refused at compile time: there is nothing exact for the iteration to reach.
50 */
51
52#include <algorithm>
53#include <cmath>
54#include <cstddef>
55#include <string>
56#include <vector>
57
58#include "line/num/number.h"
59#include "line/util/error.h"
60#include "line/util/matrix.h"
61
62namespace line {
63namespace mc {
64
65constexpr double CLASSICAL_DEFAULT_TOL = 1e-12;
66constexpr long CLASSICAL_DEFAULT_MAXITER = 10000;
67
68template <class T>
70 std::vector<T> p; ///< stationary distribution estimate, sums to one
71 int flag; ///< 0 converged, 1 iteration cap, 2 zero diagonal, 4 breakdown
72 double relres; ///< ||p Q||_1 / max|q_ii| at return
73 long iter; ///< sweeps performed
74 double omega; ///< relaxation factor at return (1 for Gauss-Seidel, 0 otherwise)
75};
76
77namespace detail {
78
79enum class SweepKind { Power, Jacobi, GaussSeidel, Sor };
80
81template <class T>
82inline bool classical_isfinite(const T& v) {
83 const T a = num_abs(T(v));
84 return a < T(a + num_traits<T>::from_int(1));
85}
86
87template <class T>
88T classical_residual_l1(const std::vector<T>& x, const Matrix<T>& Q) {
89 const std::size_t n = Q.rows();
90 T s = num_traits<T>::from_int(0);
91 for (std::size_t j = 0; j < n; ++j) {
92 T r = num_traits<T>::from_int(0);
93 for (std::size_t i = 0; i < n; ++i) r += x[i] * Q(i, j);
94 s += num_abs(r);
95 }
96 return s;
97}
98
99template <class T>
100ClassicalResult<T> stationary_sweep_iterate(SweepKind kind, const Matrix<T>& Q, double tol, long maxiter,
101 double omega, const std::vector<T>& p0) {
102 static_assert(num_traits<T>::has_transcendental,
103 "the classical iterative solvers stop on a residual tolerance and have no exact "
104 "instantiation; use ctmc_solve for exact arithmetic");
105 const std::size_t n = Q.rows();
106 if (Q.cols() != n) throw InputError("stationary_sweep_iterate: generator is not square");
107 if (n == 0) throw InputError("stationary_sweep_iterate: empty generator");
108 if (tol <= 0) tol = CLASSICAL_DEFAULT_TOL;
109 if (maxiter <= 0) maxiter = CLASSICAL_DEFAULT_MAXITER;
110 const T zero = num_traits<T>::from_int(0);
111 const T one = num_traits<T>::from_int(1);
112
113 std::vector<T> x(n);
114 if (p0.empty()) {
115 for (std::size_t i = 0; i < n; ++i) x[i] = one / num_traits<T>::from_int(static_cast<long>(n));
116 } else {
117 if (p0.size() != n) throw InputError("stationary_sweep_iterate: p0 has the wrong length");
118 T s = zero;
119 for (const T& v : p0) s += v;
120 for (std::size_t i = 0; i < n; ++i) x[i] = p0[i] / s;
121 }
122
123 const bool adaptive = (kind == SweepKind::Sor) && !(omega > 0);
124 if (kind == SweepKind::Sor) {
125 if (adaptive) {
126 omega = 1.0;
127 } else if (!(omega > 0 && omega < 2)) {
128 throw InputError("ctmc_sor: relaxation factor omega must lie in (0,2)");
129 }
130 } else if (kind == SweepKind::GaussSeidel) {
131 omega = 1.0;
132 } else {
133 omega = 0.0;
134 }
135
136 const double ADAPT_EVERY = 10, OMEGA_CAP = 1.95, DIVERGE_FACTOR = 100;
137 T qmax = zero;
138 bool zeroDiag = false;
139 for (std::size_t i = 0; i < n; ++i) {
140 const T a = num_abs(Q(i, i));
141 if (a > qmax) qmax = a;
142 if (Q(i, i) == zero) zeroDiag = true;
143 }
144 ClassicalResult<T> res{x, 0, 0.0, 0, omega};
145 if (qmax == zero) return res;
146 if (kind != SweepKind::Power && zeroDiag) {
147 res.flag = 2;
148 res.relres = num_traits<T>::to_double(classical_residual_l1(x, Q) / qmax);
149 return res;
150 }
151 const T q = qmax * num_traits<T>::from_rational(11, 10);
152
153 bool frozen = false, haveRef = false;
154 double dPrev = std::nan(""), dCur = std::nan(""), relresRef = 0.0;
155 long iter = 0;
156 std::vector<T> r(n), xn(n);
157 for (long k = 0; k <= maxiter; ++k) {
158 for (std::size_t j = 0; j < n; ++j) {
159 T s = zero;
160 for (std::size_t i = 0; i < n; ++i) s += x[i] * Q(i, j);
161 r[j] = s;
162 }
163 T rn = zero;
164 for (const T& v : r) rn += num_abs(v);
165 const double relres = num_traits<T>::to_double(rn / qmax);
166 res.relres = relres;
167 if (!haveRef) {
168 relresRef = relres;
169 haveRef = true;
170 }
171 if (relres <= tol) {
172 res.flag = 0;
173 res.p = x;
174 res.iter = iter;
175 res.omega = omega;
176 return res;
177 }
178 if (k == maxiter) break;
179 if (adaptive && !frozen && relres > DIVERGE_FACTOR * relresRef) {
180 omega = 1.0;
181 frozen = true;
182 }
183 switch (kind) {
184 case SweepKind::Power:
185 for (std::size_t i = 0; i < n; ++i) xn[i] = x[i] + r[i] / q;
186 break;
187 case SweepKind::Jacobi:
188 for (std::size_t i = 0; i < n; ++i) {
189 T s = zero;
190 for (std::size_t j = 0; j < n; ++j)
191 if (j != i) s += Q(j, i) * x[j]; // (A - D) x with A = Q'
192 xn[i] = -s / Q(i, i);
193 }
194 break;
195 default: {
196 // (D + w L) xn = ((1 - w) D - w U) x, by forward substitution.
197 const T w = num_traits<T>::from_double(omega);
198 for (std::size_t i = 0; i < n; ++i) {
199 T s = zero;
200 for (std::size_t j = 0; j < i; ++j) s += Q(j, i) * xn[j];
201 for (std::size_t j = i + 1; j < n; ++j) s += Q(j, i) * x[j];
202 xn[i] = ((one - w) * Q(i, i) * x[i] - w * s) / Q(i, i);
203 }
204 break;
205 }
206 }
207 T s = zero;
208 bool finite = true;
209 for (const T& v : xn) {
210 s += v;
211 if (!classical_isfinite(v)) finite = false;
212 }
213 if (!finite || !classical_isfinite(s) || s == zero) {
214 res.flag = 4;
215 res.p = x;
216 res.iter = iter;
217 res.omega = omega;
218 return res;
219 }
220 T d = zero;
221 for (std::size_t i = 0; i < n; ++i) {
222 xn[i] /= s;
223 d += num_abs(T(xn[i] - x[i]));
224 }
225 dPrev = dCur;
226 dCur = num_traits<T>::to_double(d);
227 x.swap(xn);
228 iter = k + 1;
229 if (adaptive && !frozen && (iter % static_cast<long>(ADAPT_EVERY)) == 0 && dPrev > 0) {
230 const double lambda = dCur / dPrev;
231 if (lambda > 0 && lambda < 1) {
232 const double mu2 = (lambda + omega - 1) * (lambda + omega - 1) / (lambda * omega * omega);
233 if (mu2 >= 0 && mu2 < 1) {
234 const double omegaNew = std::min(2.0 / (1.0 + std::sqrt(1.0 - mu2)), OMEGA_CAP);
235 if (omegaNew > omega) {
236 omega = omegaNew;
237 relresRef = num_traits<T>::to_double(classical_residual_l1(x, Q) / qmax);
238 }
239 }
240 }
241 }
242 }
243 res.flag = 1;
244 res.p = x;
245 res.iter = iter;
246 res.omega = omega;
247 return res;
248}
249
250} // namespace detail
251
252/**
253 * @brief Power method on the uniformised DTMC P = I + Q/q, q = 1.1 max|q_ii|.
254 * @param Q generator (row sums zero)
255 * @param tol residual tolerance on ||p Q||_1 / max|q_ii| (<= 0 for 1e-12)
256 * @param maxiter sweep cap (<= 0 for 10000)
257 * @param p0 initial vector, empty for uniform
258 */
259template <class T>
261 long maxiter = CLASSICAL_DEFAULT_MAXITER, const std::vector<T>& p0 = {}) {
262 return detail::stationary_sweep_iterate(detail::SweepKind::Power, Q, tol, maxiter, 0.0, p0);
263}
264
265/** @brief Jacobi iteration on Q' x = 0 with renormalisation (see file comment). */
266template <class T>
268 long maxiter = CLASSICAL_DEFAULT_MAXITER, const std::vector<T>& p0 = {}) {
269 return detail::stationary_sweep_iterate(detail::SweepKind::Jacobi, Q, tol, maxiter, 0.0, p0);
270}
271
272/** @brief Forward Gauss-Seidel iteration on Q' x = 0 with renormalisation. */
273template <class T>
275 long maxiter = CLASSICAL_DEFAULT_MAXITER, const std::vector<T>& p0 = {}) {
276 return detail::stationary_sweep_iterate(detail::SweepKind::GaussSeidel, Q, tol, maxiter, 1.0, p0);
277}
278
279/**
280 * @brief Successive over-relaxation on Q' x = 0 with renormalisation.
281 * @param omega relaxation factor in (0,2); <= 0 selects the adaptive rule
282 */
283template <class T>
285 long maxiter = CLASSICAL_DEFAULT_MAXITER, double omega = 0.0,
286 const std::vector<T>& p0 = {}) {
287 return detail::stationary_sweep_iterate(detail::SweepKind::Sor, Q, tol, maxiter, omega, p0);
288}
289
290/**
291 * Whether a linsolver name selects one of the classical solvers, as accepted
292 * by ctmc_solve in all four codebases: "power", "jacobi", "gaussseidel", "sor".
293 */
294inline bool is_classical_linsolver(const std::string& name) {
295 return name == "power" || name == "jacobi" || name == "gaussseidel" || name == "sor";
296}
297
298/** Run the classical solver named by is_classical_linsolver with defaults. */
299template <class T>
300ClassicalResult<T> ctmc_classical_by_name(const std::string& name, const Matrix<T>& Q) {
301 if (name == "power") return ctmc_power(Q);
302 if (name == "jacobi") return ctmc_jacobi(Q);
303 if (name == "gaussseidel") return ctmc_gaussseidel(Q);
304 if (name == "sor") return ctmc_sor(Q);
305 throw InputError("ctmc_classical_by_name: unknown classical solver '" + name + "'");
306}
307
308} // namespace mc
309} // namespace line
310
311#endif // LINE_API_MC_CTMC_CLASSICAL_H
InputError(const std::string &what)
Definition error.h:105
The exception types the port throws.
Dense matrix and non-owning view.
bool is_classical_linsolver(const std::string &name)
Whether a linsolver name selects one of the classical solvers, as accepted by ctmc_solve in all four ...
ClassicalResult< T > ctmc_power(const Matrix< T > &Q, double tol=CLASSICAL_DEFAULT_TOL, long maxiter=CLASSICAL_DEFAULT_MAXITER, const std::vector< T > &p0={})
Power method on the uniformised DTMC P = I + Q/q, q = 1.1 max|q_ii|.
ClassicalResult< T > ctmc_jacobi(const Matrix< T > &Q, double tol=CLASSICAL_DEFAULT_TOL, long maxiter=CLASSICAL_DEFAULT_MAXITER, const std::vector< T > &p0={})
Jacobi iteration on Q' x = 0 with renormalisation (see file comment).
ClassicalResult< T > ctmc_sor(const Matrix< T > &Q, double tol=CLASSICAL_DEFAULT_TOL, long maxiter=CLASSICAL_DEFAULT_MAXITER, double omega=0.0, const std::vector< T > &p0={})
Successive over-relaxation on Q' x = 0 with renormalisation.
constexpr long CLASSICAL_DEFAULT_MAXITER
ClassicalResult< T > ctmc_classical_by_name(const std::string &name, const Matrix< T > &Q)
Run the classical solver named by is_classical_linsolver with defaults.
ClassicalResult< T > ctmc_gaussseidel(const Matrix< T > &Q, double tol=CLASSICAL_DEFAULT_TOL, long maxiter=CLASSICAL_DEFAULT_MAXITER, const std::vector< T > &p0={})
Forward Gauss-Seidel iteration on Q' x = 0 with renormalisation.
constexpr double CLASSICAL_DEFAULT_TOL
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_abs(const T &v)
Definition number.h:184
Number-type abstraction for the templated API port.
long iter
sweeps performed
double omega
relaxation factor at return (1 for Gauss-Seidel, 0 otherwise)
double relres
||p Q||_1 / max|q_ii| at return
int flag
0 converged, 1 iteration cap, 2 zero diagonal, 4 breakdown
std::vector< T > p
stationary distribution estimate, sums to one