LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_foxglynn.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_FOXGLYNN_H
6#define LINE_API_MC_CTMC_FOXGLYNN_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Transient distribution of a CTMC by uniformization with Fox-Glynn Poisson
12 * weights.
13 *
14 * Templated port of matlab/src/api/mc/ctmc_foxglynn.m and
15 * jar/src/main/java/jline/api/mc/Ctmc_foxglynn.java. The mixing distribution
16 * Poisson(q t) is truncated to a window [L, R] carrying all but tol of its
17 * mass, and the weights on that window are built by the two-sided recursion
18 * w(k-1) = w(k) k / lambda, w(k+1) = w(k) lambda / (k+1)
19 * anchored at the mode with w(mode) = 1 and normalized at the end, so neither
20 * exp(-lambda) nor lambda^k / k! is ever evaluated and the method is free of
21 * the overflow and underflow that force ctmc_uniformization to split long
22 * horizons into segments.
23 *
24 * The window estimate is the Fox-Glynn (1988) one with Jansen's (2011)
25 * correction factor 1/(1 - exp(-(2/9) s)) on the right tail, and it is then
26 * certified in every regime by tightening or growing R (and L) against the
27 * Chernoff exponent lambda h(k/lambda), h(u) = u log u - u + 1. The estimate
28 * itself is asymptotic and valid only for lambda >= 25; the certification is
29 * what makes the result correct below that.
30 *
31 * GATED ON TRANSCENDENTAL ARITHMETIC: the truncation window is defined by a
32 * logarithmic tail bound and the whole construction is an approximation of
33 * exp(Qt) controlled by tol, so there is nothing exact to preserve. The window
34 * is located in double precision, exactly as the reference does -- it is an
35 * integer pair, and computing it in extended precision would move it by
36 * nothing -- while the weights and the vector-matrix recursion run in T, which
37 * is what a high-precision instantiation buys.
38 */
39
40#include <algorithm>
41#include <cmath>
42#include <cstddef>
43#include <vector>
44
46#include "line/num/number.h"
47#include "line/util/error.h"
48#include "line/util/matrix.h"
49
50namespace line {
51namespace mc {
52
53template <class T>
55 std::vector<T> pi; ///< distribution at time t
56 long left; ///< left truncation point
57 long right; ///< right truncation point
58 std::vector<T> w; ///< normalized Poisson weights on [left, right]
59};
60
61namespace detail {
62
63/** pi, spelled out so the header does not depend on the POSIX M_PI extension. */
64constexpr double FOXGLYNN_PI = 3.14159265358979323846;
65
66/** Chernoff exponent lambda h(k/lambda); exp(-e) dominates the Poisson tail. */
67inline double foxglynn_chernoff(double lambda, double k) {
68 if (k <= 0.0) return lambda;
69 return lambda - k + k * std::log(k / lambda);
70}
71
72/** Right truncation point R with P{X > R} <= tol/2. */
73inline long foxglynn_right(double lambda, double tol) {
74 const double target = std::log(2.0 / tol);
75 const double m = std::floor(lambda);
76 double r = m;
77 if (lambda >= 25.0) {
78 const double a = (1.0 + 1.0 / lambda) * std::exp(1.0 / 16.0) * std::sqrt(2.0);
79 const double spread = std::sqrt(2.0 * lambda);
80 for (int k = 1; k <= 64; ++k) {
81 const double shift = k * spread + 1.5;
82 const double d = 1.0 / (1.0 - std::exp(-(2.0 / 9.0) * shift));
83 const double bound = a * d * std::exp(-0.5 * k * k) / (k * std::sqrt(2.0 * FOXGLYNN_PI));
84 if (bound <= 0.5 * tol) {
85 r = m + std::ceil(shift);
86 break;
87 }
88 }
89 }
90 while (r > m && foxglynn_chernoff(lambda, r) >= target) r -= 1.0;
91 while (foxglynn_chernoff(lambda, r + 1.0) < target) r += 1.0;
92 return static_cast<long>(r);
93}
94
95/** Left truncation point L with P{X < L} <= tol/2, zero when none is admissible. */
96inline long foxglynn_left(double lambda, double tol) {
97 const double target = std::log(2.0 / tol);
98 const double m = std::floor(lambda);
99 if (foxglynn_chernoff(lambda, 0.0) < target) return 0;
100 double l = 0.0;
101 if (lambda >= 25.0) {
102 const double b = (1.0 + 1.0 / lambda) * std::exp(1.0 / (8.0 * lambda));
103 const double spread = std::sqrt(lambda);
104 for (int k = 1; k <= 64; ++k) {
105 const double bound = b * std::exp(-0.5 * k * k) / (k * std::sqrt(2.0 * FOXGLYNN_PI));
106 if (bound <= 0.5 * tol) {
107 l = m - std::floor(k * spread + 1.5);
108 break;
109 }
110 }
111 if (l < 0.0) l = 0.0;
112 }
113 while (l > 0.0 && foxglynn_chernoff(lambda, l - 1.0) < target) l -= 1.0;
114 while (l < m && foxglynn_chernoff(lambda, l) >= target) l += 1.0;
115 return static_cast<long>(l);
116}
117
118/**
119 * Poisson weights on [left, right], by the two-sided recursion. With normalize
120 * set the sum is accumulated in increasing order of magnitude, as the reference
121 * does with sort(w), and the window is rescaled to one, so the truncated tails
122 * are redistributed over it. Cleared, the anchor is instead scaled by the true
123 * mode probability, evaluated once in double through a log-gamma, so the
124 * returned values are the Poisson probabilities themselves and 1 - sum(w) is
125 * the discarded tail rather than being absorbed; ctmc_fau needs that, its error
126 * being reported as missing mass rather than as a bound.
127 */
128template <class T>
129std::vector<T> foxglynn_poisson(const T& lambda, long left, long right, double lambdaDouble,
130 bool normalize = true) {
131 const std::size_t len = static_cast<std::size_t>(right - left + 1);
132 std::vector<T> w(len, num_traits<T>::from_int(0));
133 long m = static_cast<long>(std::floor(lambdaDouble));
134 if (m < left) m = left;
135 if (m > right) m = right;
136 w[static_cast<std::size_t>(m - left)] = num_traits<T>::from_int(1);
137 for (long k = m; k >= left + 1; --k)
138 w[static_cast<std::size_t>(k - 1 - left)] =
139 w[static_cast<std::size_t>(k - left)] * num_traits<T>::from_int(k) / lambda;
140 for (long k = m; k <= right - 1; ++k)
141 w[static_cast<std::size_t>(k + 1 - left)] =
142 w[static_cast<std::size_t>(k - left)] * lambda / num_traits<T>::from_int(k + 1);
143 if (!normalize) {
144 const double logMode = -lambdaDouble + static_cast<double>(m) * std::log(lambdaDouble) -
145 std::lgamma(static_cast<double>(m) + 1.0);
146 const T scale = num_traits<T>::from_double(std::exp(logMode));
147 for (T& v : w) v *= scale;
148 return w;
149 }
150 std::vector<T> sorted = w;
151 std::sort(sorted.begin(), sorted.end());
152 T s = num_traits<T>::from_int(0);
153 for (const T& v : sorted) s += v;
154 if (s == num_traits<T>::from_int(0)) throw NumericError("ctmc_foxglynn: Poisson weights vanish");
155 for (T& v : w) v /= s;
156 return w;
157}
158
159} // namespace detail
160
161/**
162 * @brief Transient distribution of a CTMC by uniformization with Fox-Glynn
163 * Poisson weights.
164 *
165 * @param pi0 initial distribution (row vector)
166 * @param Q generator
167 * @param t time horizon
168 * @param tol total Poisson tail mass discarded (MATLAB default 1e-12)
169 * @param maxiter cap on the right truncation point; <= 0 leaves it uncapped
170 */
171template <class T>
172FoxGlynnResult<T> ctmc_foxglynn(const std::vector<T>& pi0, const Matrix<T>& Q, const T& t,
173 double tol = 1e-12, long maxiter = -1) {
175 "ctmc_foxglynn requires transcendental arithmetic: the truncation window is "
176 "defined by a logarithmic Poisson tail bound, and the result is an "
177 "approximation of exp(Qt) controlled by tol rather than an exact quantity");
178 const std::size_t n = Q.rows();
179 if (Q.cols() != n) throw InputError("ctmc_foxglynn: generator is not square");
180 if (pi0.size() != n) throw InputError("ctmc_foxglynn: pi0 has the wrong length");
181 if (tol <= 0.0) tol = 1e-12;
182
183 // q = 1.1 max |q_ii|, the rate the reference uses.
184 T qmax = num_traits<T>::from_int(0);
185 for (std::size_t i = 0; i < n; ++i) {
186 const T a = num_abs(T(Q(i, i)));
187 if (a > qmax) qmax = a;
188 }
189 const T q = qmax * num_traits<T>::from_rational(11, 10);
190 const T lambda = q * t;
191 const double lambdaDouble = num_traits<T>::to_double(lambda);
192 if (!(num_traits<T>::to_double(q) > 0.0) || !(lambdaDouble > 0.0)) {
194 r.pi = pi0;
195 r.left = 0;
196 r.right = 0;
197 r.w.assign(1, num_traits<T>::from_int(1));
198 return r;
199 }
200
201 long left = detail::foxglynn_left(lambdaDouble, tol);
202 long right = detail::foxglynn_right(lambdaDouble, tol);
203 if (maxiter > 0 && right > maxiter) {
204 right = maxiter;
205 left = std::min(left, right);
206 }
207
209 r.left = left;
210 r.right = right;
211 r.w = detail::foxglynn_poisson(lambda, left, right, lambdaDouble);
212
213 const Matrix<T> Qs = detail::uniformized_matrix(Q, q);
214 r.pi.assign(n, num_traits<T>::from_int(0));
215 std::vector<T> P = pi0;
216 for (long k = 0; k <= right; ++k) {
217 if (k >= left) {
218 const T& wk = r.w[static_cast<std::size_t>(k - left)];
219 for (std::size_t i = 0; i < n; ++i) r.pi[i] += wk * P[i];
220 }
221 if (k < right) P = detail::vecmat(P, Qs);
222 }
223 return r;
224}
225
226} // namespace mc
227} // namespace line
228
229#endif // LINE_API_MC_CTMC_FOXGLYNN_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
Transient distribution of a CTMC by uniformization (Jensen's method), and the time-averaged distribut...
The exception types the port throws.
Dense matrix and non-owning view.
FoxGlynnResult< T > ctmc_foxglynn(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 with Fox-Glynn Poisson weights.
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
long right
right truncation point
long left
left truncation point
std::vector< T > pi
distribution at time t
std::vector< T > w
normalized Poisson weights on [left, right]