LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
libqbd_taylor.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 * The algorithm ported here is libQBD by S. Astaf'ev (IAMR Karelian Research
6 * Centre RAS), BSD-3-Clause; the license ships in
7 * matlab/lib/thirdparty/libQBD/LICENSE.
8 */
9#ifndef LINE_API_MAM_LIBQBD_TAYLOR_H
10#define LINE_API_MAM_LIBQBD_TAYLOR_H
11
12/**
13 * @file
14 * @ingroup api_mam
15 * Transient distribution of a level-independent-in-the-tail QBD by an adaptive
16 * Taylor series (libQBD `QBD` + `TaylorSeriesAdaptive`).
17 *
18 * `solver_mam_ldqbd_transient.m` uses this for the INFINITE-buffer case, where
19 * there is no finite generator to exponentiate. The method advances
20 * pi(t) in steps of 1/|min diagonal|, and at each step sums the Taylor series
21 *
22 * pi(t + h) = sum_k (h Q)^k / k! pi(t)
23 *
24 * in the UNIFORMIZED time h = 1/min_elem, truncating when the tail bound
25 *
26 * ||d_k||_1 P(k+2, 2) e^2 2^-(k+1)
27 *
28 * falls below the requested error. `P(a,x)` is the regularized lower incomplete
29 * gamma function, which is what makes this a genuine a-posteriori bound rather
30 * than a heuristic cutoff, and it is the one special function the port had to
31 * add (`gammainc_lower` below).
32 *
33 * THE STATE SPACE GROWS AS IT MUST. The distribution is a list of per-level row
34 * vectors; multiplying by the generator can push mass one level higher, so
35 * `mull_by_row_vector` appends a level whenever the new top carries any mass.
36 * That is how an infinite buffer is handled without a truncation parameter:
37 * the represented depth is whatever the elapsed time has actually reached.
38 *
39 * Levels above the last supplied one REPEAT it. `get_A_*` clamps the index, so
40 * `add_final_level` defines the repeating block and every deeper level reuses
41 * it, which is the QBD structure itself rather than an approximation.
42 *
43 * ARITHMETIC. Gated on transcendental: the truncation test evaluates an
44 * incomplete gamma and an exponential.
45 */
46
47#include <algorithm>
48#include <cmath>
49#include <cstddef>
50#include <limits>
51#include <vector>
52
53#include "line/num/number.h"
54#include "line/util/error.h"
55#include "line/util/linalg.h"
56#include "line/util/matrix.h"
57
58namespace line {
59namespace mam {
60
61/**
62 * Regularized lower incomplete gamma P(a, x), MATLAB's `gammainc(x, a, 'lower')`.
63 *
64 * Series below the transition point, continued fraction above it (Numerical
65 * Recipes 6.2); evaluated in double because it is a TRUNCATION TEST, not a
66 * returned quantity -- the distribution itself is accumulated in T.
67 */
68inline double gammainc_lower(double a, double x) {
69 if (x < 0.0 || a <= 0.0) throw InputError("gammainc_lower: a must be positive and x >= 0");
70 if (x == 0.0) return 0.0;
71 const double gln = std::lgamma(a);
72 if (x < a + 1.0) {
73 double ap = a, del = 1.0 / a, sum = del;
74 for (int n = 0; n < 1000; ++n) {
75 ap += 1.0;
76 del *= x / ap;
77 sum += del;
78 if (std::fabs(del) < std::fabs(sum) * 1e-16) break;
79 }
80 return sum * std::exp(-x + a * std::log(x) - gln);
81 }
82 const double tiny = 1e-300;
83 double b = x + 1.0 - a, c = 1.0 / tiny, d = 1.0 / b, h = d;
84 for (int i = 1; i <= 1000; ++i) {
85 const double an = -static_cast<double>(i) * (static_cast<double>(i) - a);
86 b += 2.0;
87 d = an * d + b;
88 if (std::fabs(d) < tiny) d = tiny;
89 c = b + an / c;
90 if (std::fabs(c) < tiny) c = tiny;
91 d = 1.0 / d;
92 const double del = d * c;
93 h *= del;
94 if (std::fabs(del - 1.0) < 1e-16) break;
95 }
96 return 1.0 - std::exp(-x + a * std::log(x) - gln) * h;
97}
98
99/** A QBD's level blocks, as libQBD's `QBD` class holds them. */
100template <class T>
102 public:
103 /** Level zero from its upward block alone; the local block is the row-sum negative. */
105 if (!A0_.empty() || !Ap_.empty())
106 throw InputError("LibQbdProcess: level zero already exists");
107 Ap_.push_back(Aplus);
108 A0_.push_back(diag_negrowsum(Aplus));
109 }
111 if (!A0_.empty() || !Ap_.empty())
112 throw InputError("LibQbdProcess: level zero already exists");
113 A0_.push_back(A0);
114 Ap_.push_back(Aplus);
115 }
116 /** A level from its down and up blocks; the local block closes the rows. */
117 void add_level(const Matrix<T>& Aminus, const Matrix<T>& Aplus) {
118 check_filled();
119 Am_.push_back(Aminus);
120 A0_.push_back(diag_negrowsum2(Aminus, Aplus));
121 Ap_.push_back(Aplus);
122 }
123 void add_level(const Matrix<T>& Aminus, const Matrix<T>& A0, const Matrix<T>& Aplus) {
124 check_filled();
125 Am_.push_back(Aminus);
126 A0_.push_back(A0);
127 Ap_.push_back(Aplus);
128 }
129 /** The repeating level: its up block is the previous one, reused for ever. */
131 check_filled();
132 const Matrix<T> prevAp = Ap_.back();
133 Am_.push_back(Aminus);
134 A0_.push_back(diag_negrowsum2(Aminus, prevAp));
135 Ap_.push_back(prevAp);
136 }
138 check_filled();
139 const Matrix<T> prevAp = Ap_.back();
140 Am_.push_back(Aminus);
141 A0_.push_back(A0);
142 Ap_.push_back(prevAp);
143 }
144
145 bool empty() const { return A0_.empty(); }
146
147 /** Blocks at a level, with every level above the last one REPEATING it. */
148 const Matrix<T>& A0(std::size_t level) const {
149 return A0_[std::min(level, A0_.size() - 1)];
150 }
151 const Matrix<T>& Aplus(std::size_t level) const {
152 return Ap_[std::min(level, Ap_.size() - 1)];
153 }
154 const Matrix<T>& Aminus(std::size_t level) const {
155 if (level == 0) throw InputError("LibQbdProcess: A_minus at level zero is undefined");
156 return Am_[std::min(level, Am_.size()) - 1];
157 }
158 /** The most negative diagonal entry over every local block. */
159 T min_element() const {
161 for (const Matrix<T>& M : A0_)
162 for (std::size_t i = 0; i < M.rows(); ++i)
163 if (M(i, i) < v) v = M(i, i);
164 return v;
165 }
166
167 /**
168 * vec Q scaled by `cons`, where `vec` is one row vector per level.
169 *
170 * The result may be ONE LEVEL LONGER than the input: mass pushed above the
171 * current top is kept whenever it is nonzero, which is what lets the
172 * representation grow with the elapsed time instead of being truncated.
173 */
174 std::vector<std::vector<T>> mul_row(const std::vector<std::vector<T>>& vec,
175 const T& cons) const {
176 const T zero = num_traits<T>::from_int(0);
177 const std::size_t n = vec.size();
178 if (n == 0) throw InputError("LibQbdProcess: an empty vector was passed");
179 std::vector<std::vector<T>> res(n + 1);
180 for (std::size_t j = 0; j <= n; ++j) {
181 const std::size_t dim =
182 (j < n) ? A0(j).cols() : Aplus(n - 1).cols();
183 res[j].assign(dim, zero);
184 if (j > 0) accumulate(res[j], vec[j - 1], Aplus(j - 1));
185 if (j < n) accumulate(res[j], vec[j], A0(j));
186 if (j + 1 < n) accumulate(res[j], vec[j + 1], Aminus(j + 1));
187 for (T& v : res[j]) v *= cons;
188 }
189 // The reference keeps the new top level only when it carries mass, and
190 // only in the n >= 3 branch; below that it always appends.
191 if (n >= 3) {
192 double nrm = 0.0;
193 for (const T& v : res[n]) nrm += std::fabs(num_traits<T>::to_double(v));
194 if (!(nrm > 0.0)) res.pop_back();
195 }
196 return res;
197 }
198
199 private:
200 static Matrix<T> diag_negrowsum(const Matrix<T>& A) {
201 const T zero = num_traits<T>::from_int(0);
202 Matrix<T> D(A.rows(), A.rows(), zero);
203 for (std::size_t i = 0; i < A.rows(); ++i) {
204 T s = zero;
205 for (std::size_t j = 0; j < A.cols(); ++j) s += A(i, j);
206 D(i, i) = -s;
207 }
208 return D;
209 }
210 static Matrix<T> diag_negrowsum2(const Matrix<T>& A, const Matrix<T>& B) {
211 const T zero = num_traits<T>::from_int(0);
212 Matrix<T> D(A.rows(), A.rows(), zero);
213 for (std::size_t i = 0; i < A.rows(); ++i) {
214 T s = zero;
215 for (std::size_t j = 0; j < A.cols(); ++j) s += A(i, j);
216 for (std::size_t j = 0; j < B.cols(); ++j) s += B(i, j);
217 D(i, i) = -s;
218 }
219 return D;
220 }
221 static void accumulate(std::vector<T>& out, const std::vector<T>& v, const Matrix<T>& M) {
222 if (v.size() != M.rows()) return; // a level whose width does not meet this block
223 for (std::size_t i = 0; i < M.rows(); ++i) {
224 if (v[i] == num_traits<T>::from_int(0)) continue;
225 for (std::size_t j = 0; j < M.cols() && j < out.size(); ++j) out[j] += v[i] * M(i, j);
226 }
227 }
228
229 void check_filled() const {
230 if (A0_.size() != Ap_.size() || Ap_.size() != Am_.size() + 1)
231 throw InputError("LibQbdProcess: unfilled levels found");
232 }
233
234 std::vector<Matrix<T>> Ap_, A0_, Am_;
235};
236
237/** What the adaptive Taylor series returns: the reference grid and its laws. */
238template <class T>
240 std::vector<double> times; ///< the reference points
241 std::vector<std::vector<std::vector<T>>> dists; ///< per point, per level, per phase
242};
243
244/**
245 * libQBD's `TaylorSeriesAdaptive`, restricted to the reference grid that
246 * `solver_mam_ldqbd_transient` reads (`get_reference_times` and
247 * `get_reference_dists`); the interpolation to arbitrary points is not ported
248 * because no caller in this tree asks for it.
249 *
250 * @param pi0 initial law, one row vector per level
251 * @param error per-step truncation target (the reference passes options.tol)
252 * @param max_time advance until the grid covers this horizon
253 * @param proc the level-dependent QBD being integrated
254 */
255template <class T>
257 const std::vector<std::vector<T>>& pi0,
258 double error, double max_time) {
260 "taylor_series_adaptive evaluates an incomplete gamma truncation bound");
261 if (proc.empty()) throw InputError("taylor_series_adaptive: the generator is empty");
262 const double min_elem = -num_traits<T>::to_double(proc.min_element());
263 if (!(min_elem > 0.0))
264 throw NumericError("taylor_series_adaptive: the generator has no negative diagonal");
265 const unsigned max_degree = 177u; // libQBD's get_max_factor() for double
266
268 out.times.push_back(0.0);
269 out.dists.push_back(pi0);
270
271 const T min_elem_inv = num_traits<T>::from_double(1.0 / min_elem);
272 while (out.times.back() < max_time) {
273 std::vector<std::vector<T>> deriv = out.dists.back();
274 std::vector<std::vector<T>> res = deriv;
275 unsigned k = 0;
276 double two_delta_in_n = 0.5;
277 double er = std::numeric_limits<double>::infinity();
278 while (er > error && k < max_degree) {
279 deriv = proc.mul_row(deriv, min_elem_inv);
280 // res += deriv / (k+1)!
281 const double c = std::exp(-std::lgamma(static_cast<double>(k) + 2.0));
282 const T ct = num_traits<T>::from_double(c);
283 if (res.size() < deriv.size()) res.resize(deriv.size());
284 for (std::size_t l = 0; l < deriv.size(); ++l) {
285 if (res[l].size() < deriv[l].size())
286 res[l].resize(deriv[l].size(), num_traits<T>::from_int(0));
287 for (std::size_t i = 0; i < deriv[l].size(); ++i) res[l][i] += ct * deriv[l][i];
288 }
289 double nrm = 0.0;
290 for (const std::vector<T>& lv : deriv)
291 for (const T& v : lv) nrm += std::fabs(num_traits<T>::to_double(v));
292 er = nrm * gammainc_lower(static_cast<double>(k) + 2.0, 2.0) * std::exp(2.0) *
293 two_delta_in_n;
294 two_delta_in_n *= 0.5;
295 ++k;
296 }
297 out.dists.push_back(res);
298 out.times.push_back(out.times.back() + 1.0 / min_elem);
299 }
300 return out;
301}
302
303} // namespace mam
304} // namespace line
305
306#endif // LINE_API_MAM_LIBQBD_TAYLOR_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
A QBD's level blocks, as libQBD's QBD class holds them.
const Matrix< T > & Aplus(std::size_t level) const
void add_zero_level(const Matrix< T > &Aplus)
Level zero from its upward block alone; the local block is the row-sum negative.
const Matrix< T > & A0(std::size_t level) const
Blocks at a level, with every level above the last one REPEATING it.
std::vector< std::vector< T > > mul_row(const std::vector< std::vector< T > > &vec, const T &cons) const
vec Q scaled by cons, where vec is one row vector per level.
void add_zero_level(const Matrix< T > &A0, const Matrix< T > &Aplus)
void add_final_level(const Matrix< T > &Aminus, const Matrix< T > &A0)
void add_final_level(const Matrix< T > &Aminus)
The repeating level: its up block is the previous one, reused for ever.
void add_level(const Matrix< T > &Aminus, const Matrix< T > &Aplus)
A level from its down and up blocks; the local block closes the rows.
T min_element() const
The most negative diagonal entry over every local block.
void add_level(const Matrix< T > &Aminus, const Matrix< T > &A0, const Matrix< T > &Aplus)
const Matrix< T > & Aminus(std::size_t level) const
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Dense matrix and non-owning view.
double gammainc_lower(double a, double x)
Regularized lower incomplete gamma P(a, x), MATLAB's gammainc(x, a, 'lower').
TaylorSeriesResult< T > taylor_series_adaptive(const LibQbdProcess< T > &proc, const std::vector< std::vector< T > > &pi0, double error, double max_time)
libQBD's TaylorSeriesAdaptive, restricted to the reference grid that solver_mam_ldqbd_transient reads...
Number-type abstraction for the templated API port.
What the adaptive Taylor series returns: the reference grid and its laws.
std::vector< double > times
the reference points
std::vector< std::vector< std::vector< T > > > dists
per point, per level, per phase