LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_uniformization.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_UNIFORMIZATION_H
6#define LINE_API_MC_CTMC_UNIFORMIZATION_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Transient distribution of a CTMC by uniformization (Jensen's method), and
12 * the time-averaged distribution over [0, t].
13 *
14 * Templated port of matlab/src/api/mc/ctmc_uniformization.m and
15 * matlab/src/api/mc/ctmc_timeaverage.m.
16 *
17 * pi(t) = pi0 exp(Qt) = sum_j Poisson(j; q t) pi0 P^j, P = I + Q/q
18 *
19 * with q = 1.1 max |diag(Q)|. The series is truncated at the first k whose
20 * Poisson tail falls below tol, and long horizons are split into segments of
21 * q t <= 500 exactly as MATLAB does, because the Poisson weights underflow
22 * before that bound is reached.
23 *
24 * Unlike the steady-state routines in ctmc_solve.h, this one is NOT exact in
25 * the rational field: exp(-q t) is transcendental and the truncation itself is
26 * an approximation controlled by tol. The static_assert makes that explicit at
27 * compile time rather than leaving a caller to discover it at runtime. The
28 * high-precision instantiation is still useful: it pushes the underflow of the
29 * Poisson weights far out, which is the failure mode that forces the
30 * segmentation in the first place.
31 */
32
33#include <cmath>
34#include <cstddef>
35#include <vector>
36
37#include "line/num/number.h"
38#include "line/util/error.h"
39#include "line/util/matrix.h"
40
41namespace line {
42namespace mc {
43
44template <class T>
46 std::vector<T> pi; ///< distribution at time t
47 std::size_t kmax; ///< number of Poisson terms actually used
48};
49
50template <class T>
52 std::vector<T> piTimeAvg; ///< time-averaged distribution over [0, t]
53 std::vector<T> piExit; ///< distribution at time t
54 std::size_t kmax;
55};
56
57namespace detail {
58
59/** Uniformization rate q = 1.1 max_i |Q(i,i)|, and P = I + Q/q. */
60template <class T>
61T uniformization_rate(const Matrix<T>& Q) {
62 const std::size_t n = Q.rows();
63 T qmax = num_traits<T>::from_int(0);
64 for (std::size_t i = 0; i < n; ++i) {
65 const T a = num_abs(Q(i, i));
66 if (a > qmax) qmax = a;
67 }
68 return qmax * num_traits<T>::from_rational(11, 10);
69}
70
71template <class T>
72Matrix<T> uniformized_matrix(const Matrix<T>& Q, const T& q) {
73 const std::size_t n = Q.rows();
74 Matrix<T> P(n, n);
75 for (std::size_t i = 0; i < n; ++i)
76 for (std::size_t j = 0; j < n; ++j)
77 P(i, j) = Q(i, j) / q + (i == j ? num_traits<T>::from_int(1) : num_traits<T>::from_int(0));
78 return P;
79}
80
81/** Row-vector times matrix, v P. */
82template <class T>
83std::vector<T> vecmat(const std::vector<T>& v, const Matrix<T>& P) {
84 const std::size_t n = P.rows();
85 std::vector<T> r(P.cols(), num_traits<T>::from_int(0));
86 for (std::size_t i = 0; i < n; ++i) {
87 if (v[i] == num_traits<T>::from_int(0)) continue;
88 for (std::size_t j = 0; j < P.cols(); ++j) r[j] += v[i] * P(i, j);
89 }
90 return r;
91}
92
93/** Truncation point: first k whose Poisson tail at qt is below tol. */
94template <class T>
95std::size_t poisson_truncation(double qt, double tol, long maxiter) {
96 if (maxiter <= 0)
97 maxiter = static_cast<long>(std::max(100.0, std::ceil(qt + 10.0 * std::sqrt(qt) + 20.0)));
98 double s = 1.0, r = 1.0;
99 std::size_t kmax = 1;
100 const double e = std::exp(-qt);
101 for (long iter = 0, k = 0; iter < maxiter; ++iter) {
102 ++k;
103 r = r * qt / static_cast<double>(k);
104 s += r;
105 kmax = static_cast<std::size_t>(k);
106 if ((1.0 - e * s) <= tol) break;
107 }
108 return kmax;
109}
110
111} // namespace detail
112
113/**
114 * @brief Transient distribution of a CTMC by uniformization (Jensen's
115 * method), and the time-averaged distribution over [0, t].
116 *
117 * @param pi0 initial distribution (row vector)
118 * @param Q generator
119 * @param t time horizon
120 * @param tol Poisson-tail truncation tolerance (MATLAB default 1e-12)
121 * @param maxiter iteration cap, <= 0 for the MATLAB heuristic
122 */
123template <class T>
124UniformizationResult<T> ctmc_uniformization(const std::vector<T>& pi0, const Matrix<T>& Q, const T& t,
125 double tol = 1e-12, long maxiter = -1) {
127 "ctmc_uniformization requires transcendental arithmetic: the Poisson weights "
128 "involve exp(-q t), which is not a rational function of the rates");
129 const std::size_t n = Q.rows();
130 if (Q.cols() != n) throw InputError("ctmc_uniformization: generator is not square");
131 if (pi0.size() != n) throw InputError("ctmc_uniformization: pi0 has the wrong length");
132
133 const T q = detail::uniformization_rate(Q);
134 if (q == num_traits<T>::from_int(0)) return {pi0, 0};
135
136 // Long horizons are split so that q t stays inside the range where the
137 // Poisson weights are representable, as in MATLAB (MAXQT = 500).
138 const double qt_full = num_traits<T>::to_double(q) * num_traits<T>::to_double(t);
139 const double MAXQT = 500.0;
140 if (qt_full > MAXQT) {
141 const long nSeg = static_cast<long>(std::ceil(qt_full / MAXQT));
142 const T tSeg = t / num_traits<T>::from_int(nSeg);
143 UniformizationResult<T> r{pi0, 0};
144 for (long s = 0; s < nSeg; ++s) {
145 UniformizationResult<T> step = ctmc_uniformization(r.pi, Q, tSeg, tol, maxiter);
146 r.pi = step.pi;
147 r.kmax = step.kmax;
148 }
149 return r;
150 }
151
152 const Matrix<T> P = detail::uniformized_matrix(Q, q);
153 const T qt = q * t;
154 const std::size_t kmax = detail::poisson_truncation<T>(num_traits<T>::to_double(qt), tol, maxiter);
155
156 using std::exp;
157 T ri = exp(-qt);
158 std::vector<T> pi(n);
159 for (std::size_t i = 0; i < n; ++i) pi[i] = pi0[i] * ri;
160 std::vector<T> Pk = pi0;
161 for (std::size_t j = 1; j <= kmax; ++j) {
162 Pk = detail::vecmat(Pk, P);
163 ri = ri * qt / num_traits<T>::from_int(static_cast<long>(j));
164 for (std::size_t i = 0; i < n; ++i) pi[i] += ri * Pk[i];
165 }
166 return {pi, kmax};
167}
168
169/**
170 * Time-averaged distribution (1/t) int_0^t pi(u) du, plus pi(t) itself.
171 * Port of ctmc_timeaverage.m, which accumulates the Poisson survival weights
172 * max(1 - W_j, 0) rather than integrating pi(u) numerically.
173 */
174template <class T>
175TimeAverageResult<T> ctmc_timeaverage(const std::vector<T>& pi0, const Matrix<T>& Q, const T& t,
176 double tol = 1e-12, long maxiter = -1) {
178 "ctmc_timeaverage requires transcendental arithmetic (Poisson weights)");
179 const std::size_t n = Q.rows();
180 if (Q.cols() != n) throw InputError("ctmc_timeaverage: generator is not square");
181 if (pi0.size() != n) throw InputError("ctmc_timeaverage: pi0 has the wrong length");
182
183 const T zero = num_traits<T>::from_int(0);
184 const T q = detail::uniformization_rate(Q);
185 if (q == zero) return {pi0, pi0, 0};
186
187 const double qt_full = num_traits<T>::to_double(q) * num_traits<T>::to_double(t);
188 const double MAXQT = 500.0;
189 if (qt_full > MAXQT) {
190 const long nSeg = static_cast<long>(std::ceil(qt_full / MAXQT));
191 const T tSeg = t / num_traits<T>::from_int(nSeg);
192 std::vector<T> cur = pi0, integral(n, zero);
193 std::size_t kmax = 0;
194 for (long s = 0; s < nSeg; ++s) {
195 TimeAverageResult<T> seg = ctmc_timeaverage(cur, Q, tSeg, tol, maxiter);
196 for (std::size_t i = 0; i < n; ++i) integral[i] += tSeg * seg.piTimeAvg[i];
197 cur = seg.piExit;
198 kmax = seg.kmax;
199 }
200 for (std::size_t i = 0; i < n; ++i) integral[i] /= t;
201 return {integral, cur, kmax};
202 }
203
204 const Matrix<T> P = detail::uniformized_matrix(Q, q);
205 const T qt = q * t;
206 const std::size_t kmax = detail::poisson_truncation<T>(num_traits<T>::to_double(qt), tol, maxiter);
207
208 using std::exp;
209 T w = exp(-qt); // Poisson pmf w_0
210 T W = w; // Poisson cdf W_0
211 std::vector<T> Pk = pi0;
212 std::vector<T> piExit(n), piIntSum(n);
213 const T one = num_traits<T>::from_int(1);
214 T tail = one - W;
215 if (tail < zero) tail = zero;
216 for (std::size_t i = 0; i < n; ++i) {
217 piExit[i] = w * Pk[i];
218 piIntSum[i] = tail * Pk[i];
219 }
220 for (std::size_t j = 1; j <= kmax; ++j) {
221 Pk = detail::vecmat(Pk, P);
222 w = w * qt / num_traits<T>::from_int(static_cast<long>(j));
223 W += w;
224 tail = one - W;
225 if (tail < zero) tail = zero;
226 for (std::size_t i = 0; i < n; ++i) {
227 piExit[i] += w * Pk[i];
228 piIntSum[i] += tail * Pk[i];
229 }
230 }
231 std::vector<T> avg(n);
232 for (std::size_t i = 0; i < n; ++i) avg[i] = piIntSum[i] / qt;
233 return {avg, piExit, kmax};
234}
235
236} // namespace mc
237} // namespace line
238
239#endif // LINE_API_MC_CTMC_UNIFORMIZATION_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
The exception types the port throws.
Dense matrix and non-owning view.
UniformizationResult< T > ctmc_uniformization(const std::vector< T > &pi0, const Matrix< T > &Q, const T &t, double tol=1e-12, long maxiter=-1)
Transient distribution of a CTMC by uniformization (Jensen's method), and the time-averaged distribut...
TimeAverageResult< T > ctmc_timeaverage(const std::vector< T > &pi0, const Matrix< T > &Q, const T &t, double tol=1e-12, long maxiter=-1)
Time-averaged distribution (1/t) int_0^t pi(u) du, plus pi(t) itself.
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
std::vector< T > piTimeAvg
time-averaged distribution over [0, t]
std::vector< T > piExit
distribution at time t
std::size_t kmax
number of Poisson terms actually used
std::vector< T > pi
distribution at time t