LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldqbd.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_MAM_LDQBD_H
6#define LINE_API_MAM_LDQBD_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Level-dependent QBD processes with finitely many levels: the rate matrices
12 * R^(n) by the backward matrix continued fraction, and the stationary
13 * distribution.
14 *
15 * Templated port of matlab/src/api/mam/ldqbd_R.m, ldqbd_pi.m and ldqbd.m,
16 * which implement Algorithms 1 and 3 of T. Phung-Duc, H. Masuyama,
17 * S. Kasahara, Y. Takahashi, "A Simple Algorithm for the Rate Matrices of
18 * Level-Dependent QBD Processes", QTNA 2010.
19 *
20 * The generator is block tridiagonal over levels 0..N,
21 *
22 * Q = | Q1(0) Q0(0) 0 ... |
23 * | Q2(1) Q1(1) Q0(1) ... |
24 * | 0 Q2(2) Q1(2) ... |
25 * | ... Q2(N) Q1(N) |
26 *
27 * with Q0[n] the up-block from level n, Q1[n] the local block at level n and
28 * Q2[n] the down-block from level n. The dimensions may vary with the level,
29 * so R^(n) is (order of level n-1) x (order of level n). The recursion runs
30 * downwards from the top level,
31 *
32 * R^(N) = Q0[N-1] (-Q1[N])^-1
33 * R^(n) = Q0[n-1] (-Q1[n] - R^(n+1) Q2[n+1])^-1, n = N-1, ..., 1
34 *
35 * and the stationary vectors follow from the level-0 balance
36 * pi_0 (Q1[0] + R^(1) Q2[1]) = 0 and pi_n = pi_{n-1} R^(n), normalized over
37 * all levels.
38 *
39 * INDEXING. The reference passes Q0, Q1, Q2 as MATLAB cell arrays with three
40 * different origins: Q0{k} is Q0^(k-1), Q1{k} is Q1^(k-1) and Q2{k} is
41 * Q2^(k). The port takes three std::vector, all indexed by LEVEL:
42 * q0[n] for n = 0..N-1, q1[n] for n = 0..N, q2[n] for n = 1..N, with q2[0]
43 * required to be present but unused (a level-0 down-block does not exist), so
44 * q2.size() == q1.size(). This removes the origin mismatch that makes the
45 * MATLAB call sites hard to read; the tests cross-check against MATLAB with
46 * the shift applied explicitly.
47 *
48 * ARITHMETIC. The recursion is FINITE -- N matrix inverses and N products --
49 * with no tolerance and no iteration, so ldqbd_R and ldqbd_pi instantiate at
50 * Rational and return the rate matrices and the stationary law as exact
51 * fractions. That is unusual for a QBD: the level-independent case needs
52 * cyclic reduction and can only ever be approximate (see qbd_r.h), whereas a
53 * level-dependent chain with a finite top level is a finite linear algebra
54 * problem. The one exception is the singular fallback, see below.
55 *
56 * SINGULAR LEVELS. The reference tests abs(det(U)) > 1e-14 and falls back to
57 * Q0 * pinv(U) when it fails. An absolute determinant threshold is not
58 * meaningful at exact arithmetic (a rational matrix is singular or it is not,
59 * and det scales like the n-th power of the entries), so the port splits:
60 * - inexact T keeps the reference behaviour, |det(U)| <= 1e-14 routes to the
61 * LAPACK pseudo-inverse of util/svd.h, in double, with the result lifted
62 * back into T;
63 * - exact T tests for an exactly singular U and raises NumericError naming
64 * the level, because a pseudo-inverse of a rational matrix is not rational
65 * and silently returning a rounded one would make the "exact" label false.
66 */
67
68#include <cstddef>
69#include <string>
70#include <vector>
71
72#include "line/api/mam/qbd_r.h"
73#include "line/num/number.h"
74#include "line/util/error.h"
75#include "line/util/linalg.h"
76#include "line/util/lu.h"
77#include "line/util/matrix.h"
78#include "line/util/svd.h"
79
80namespace line {
81namespace mam {
82
83namespace ldqbd_detail {
84
85/** Determinant through the same LU the port uses everywhere else. */
86template <class T>
87T det_lu(const Matrix<T>& A) {
88 const std::size_t n = A.rows();
89 if (A.cols() != n) throw InputError("ldqbd: determinant of a non-square matrix");
90 if (n == 0) return num_traits<T>::from_int(1);
91 Matrix<T> LU = A;
92 std::vector<std::size_t> piv;
93 try {
94 piv = lu_factor(LU);
95 } catch (const NumericError&) {
96 return num_traits<T>::from_int(0); // an exactly zero pivot: singular
97 }
98 T d = num_traits<T>::from_int(1);
99 for (std::size_t i = 0; i < n; ++i) d *= LU(i, i);
100 for (std::size_t k = 0; k < n; ++k)
101 if (piv[k] != k) d = -d;
102 return d;
103}
104
105/** X U^-1, with the reference's pseudo-inverse fallback when U is singular. */
106template <class T>
107Matrix<T> right_divide(const Matrix<T>& X, const Matrix<T>& U, std::size_t level) {
108 const T d = det_lu(U);
109 bool singular;
110 if (num_traits<T>::is_exact) {
111 singular = (d == num_traits<T>::from_int(0));
112 } else {
113 singular = !(num_abs(T(d)) > num_traits<T>::from_double(1e-14));
114 }
115 if (!singular) return matmul(X, inverse(U));
116
117 if (num_traits<T>::is_exact)
118 throw NumericError("ldqbd_R: the local block at level " + std::to_string(level) +
119 " is exactly singular; its pseudo-inverse is not rational, so the "
120 "exact instantiation refuses rather than returning a rounded value");
121 Matrix<double> Ud(U.rows(), U.cols());
122 for (std::size_t i = 0; i < U.rows(); ++i)
123 for (std::size_t j = 0; j < U.cols(); ++j) Ud(i, j) = num_traits<T>::to_double(U(i, j));
124 const Matrix<double> P = pinv(Ud);
125 Matrix<T> Pt(P.rows(), P.cols());
126 for (std::size_t i = 0; i < P.rows(); ++i)
127 for (std::size_t j = 0; j < P.cols(); ++j) Pt(i, j) = num_traits<T>::from_double(P(i, j));
128 return matmul(X, Pt);
129}
130
131} // namespace ldqbd_detail
132
133/**
134 * Rate matrices R^(1), ..., R^(N) of a level-dependent QBD (ldqbd_R.m).
135 *
136 * @param q0 up-blocks, q0[n] from level n to n+1, n = 0..N-1
137 * @param q1 local blocks, q1[n] at level n, n = 0..N
138 * @param q2 down-blocks, q2[n] from level n to n-1, n = 1..N; q2[0] is
139 * required to be present so that the vectors line up by level, and
140 * is never read
141 * @return R indexed by level, R[n] for n = 1..N; R[0] is empty
142 */
143template <class T>
144std::vector<Matrix<T>> ldqbd_R(const std::vector<Matrix<T>>& q0, const std::vector<Matrix<T>>& q1,
145 const std::vector<Matrix<T>>& q2) {
146 if (q1.size() < 2) throw InputError("ldqbd_R: at least two levels are required");
147 const std::size_t N = q1.size() - 1;
148 if (q0.size() < N) throw InputError("ldqbd_R: q0 must hold the up-blocks of levels 0..N-1");
149 if (q2.size() < N + 1) throw InputError("ldqbd_R: q2 must be indexed by level, 0..N");
150
151 std::vector<Matrix<T>> R(N + 1);
152 // Top level: R^(N) = Q0[N-1] (-Q1[N])^-1.
153 R[N] = ldqbd_detail::right_divide(
154 q0[N - 1], qbd_detail::mscale(q1[N], T(num_traits<T>::from_int(-1))), N);
155
156 for (std::size_t n = N - 1; n >= 1; --n) {
157 const Matrix<T> RQ2 = matmul(R[n + 1], q2[n + 1]);
158 const Matrix<T> U =
159 qbd_detail::msub(qbd_detail::mscale(q1[n], T(num_traits<T>::from_int(-1))), RQ2);
160 R[n] = ldqbd_detail::right_divide(q0[n - 1], U, n);
161 }
162 return R;
163}
164
165/** Stationary distribution of a level-dependent QBD, per level and per phase. */
166template <class T>
167struct LdqbdPi {
168 std::vector<std::vector<T>> pi_level; ///< pi_level[n], one entry per phase of level n
169 std::vector<T> pi; ///< pi[n] = sum of pi_level[n], the level marginal
170};
171
172/**
173 * Stationary distribution of a level-dependent QBD given its rate matrices
174 * (ldqbd_pi.m).
175 *
176 * The level-0 vector solves pi_0 (Q1[0] + R^(1) Q2[1]) = 0, the higher levels
177 * follow from pi_n = pi_{n-1} R^(n), and the whole family is normalized to
178 * unit total mass.
179 *
180 * DIVERGENCE FROM THE REFERENCE, deliberate and tested. MATLAB extracts the
181 * level-0 vector from an eigendecomposition: it takes the eigenvector of A'
182 * whose eigenvalue has smallest modulus, then applies real(), abs() and a
183 * normalization. Three problems with that, all avoided here:
184 * - abs() silently turns a genuinely signed null vector into a nonnegative
185 * one, hiding a mis-specified generator instead of reporting it;
186 * - the eigenvector is only accurate to the square root of the eigenvalue
187 * separation, whereas the null vector of an exactly known matrix is a
188 * linear solve;
189 * - it forces the whole routine through a double-precision eigensolver, so
190 * no exact instantiation would be possible.
191 * The port solves x A = 0 with sum(x) = 1 directly (qbd_detail::statvec), one
192 * linear system, exact at Rational. On a well-posed instance the two agree to
193 * roundoff; the tests check that against MATLAB and separately check the
194 * balance residual ||pi_0 A||_inf, which the eigen route cannot drive to zero.
195 *
196 * The scalar-level-0 special case of the reference (a level 0 of order one is
197 * seeded with pi_0 = 1 instead of solving anything) is reproduced, because for
198 * an order-one level the balance equation is 1 x 1 and any nonzero scalar is
199 * its solution up to the final normalization.
200 */
201template <class T>
202LdqbdPi<T> ldqbd_pi(const std::vector<Matrix<T>>& R, const std::vector<Matrix<T>>& q0,
203 const std::vector<Matrix<T>>& q1, const std::vector<Matrix<T>>& q2) {
204 (void)q0;
205 if (q1.size() < 2) throw InputError("ldqbd_pi: at least two levels are required");
206 const std::size_t N = q1.size() - 1;
207 if (R.size() != N + 1) throw InputError("ldqbd_pi: R must be indexed by level, 0..N");
208 const T zero = num_traits<T>::from_int(0);
209
210 LdqbdPi<T> out;
211 out.pi_level.resize(N + 1);
212
213 if (q1[0].rows() == 1) {
214 out.pi_level[0] = std::vector<T>(1, num_traits<T>::from_int(1));
215 } else {
216 const Matrix<T> A = qbd_detail::madd(q1[0], matmul(R[1], q2[1]));
217 out.pi_level[0] = qbd_detail::statvec(A);
218 }
219
220 for (std::size_t n = 1; n <= N; ++n) out.pi_level[n] = vecmul(out.pi_level[n - 1], R[n]);
221
222 T total = zero;
223 for (std::size_t n = 0; n <= N; ++n)
224 for (const T& v : out.pi_level[n]) total += v;
225 if (total == zero) throw NumericError("ldqbd_pi: the stationary vector has zero total mass");
226
227 out.pi.assign(N + 1, zero);
228 for (std::size_t n = 0; n <= N; ++n) {
229 for (T& v : out.pi_level[n]) v /= total;
230 T s = zero;
231 for (const T& v : out.pi_level[n]) s += v;
232 out.pi[n] = s;
233 }
234 return out;
235}
236
237/** R and the stationary distribution together (ldqbd.m). */
238template <class T>
240 std::vector<Matrix<T>> R;
242};
243
244/** Solve a level-dependent QBD: rate matrices and stationary law (ldqbd.m). */
245template <class T>
246LdqbdResult<T> ldqbd(const std::vector<Matrix<T>>& q0, const std::vector<Matrix<T>>& q1,
247 const std::vector<Matrix<T>>& q2) {
248 LdqbdResult<T> out;
249 out.R = ldqbd_R(q0, q1, q2);
250 out.pi = ldqbd_pi(out.R, q0, q1, q2);
251 return out;
252}
253
254} // namespace mam
255} // namespace line
256
257#endif // LINE_API_MAM_LDQBD_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
LdqbdPi< T > ldqbd_pi(const std::vector< Matrix< T > > &R, const std::vector< Matrix< T > > &q0, const std::vector< Matrix< T > > &q1, const std::vector< Matrix< T > > &q2)
Stationary distribution of a level-dependent QBD given its rate matrices (ldqbd_pi....
Definition ldqbd.h:202
std::vector< Matrix< T > > ldqbd_R(const std::vector< Matrix< T > > &q0, const std::vector< Matrix< T > > &q1, const std::vector< Matrix< T > > &q2)
Rate matrices R^(1), ..., R^(N) of a level-dependent QBD (ldqbd_R.m).
Definition ldqbd.h:144
LdqbdResult< T > ldqbd(const std::vector< Matrix< T > > &q0, const std::vector< Matrix< T > > &q1, const std::vector< Matrix< T > > &q2)
Solve a level-dependent QBD: rate matrices and stationary law (ldqbd.m).
Definition ldqbd.h:246
Matrix< double > pinv(const Matrix< double > &A)
Moore-Penrose pseudo-inverse, A^+ = V diag(1/s_i) U^T over the singular values above max(m,...
Definition svd.h:93
T num_abs(const T &v)
Definition number.h:172
std::vector< T > vecmul(const std::vector< T > &v, const Matrix< T > &A)
Row vector times matrix, v A.
Definition linalg.h:50
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
Number-type abstraction for the templated API port.
Quasi-birth-death processes: the rate matrix R, the fundamental matrix G, the caudal characteristic,...
Stationary distribution of a level-dependent QBD, per level and per phase.
Definition ldqbd.h:167
std::vector< T > pi
pi[n] = sum of pi_level[n], the level marginal
Definition ldqbd.h:169
std::vector< std::vector< T > > pi_level
pi_level[n], one entry per phase of level n
Definition ldqbd.h:168
R and the stationary distribution together (ldqbd.m).
Definition ldqbd.h:239
std::vector< Matrix< T > > R
Definition ldqbd.h:240
LdqbdPi< T > pi
Definition ldqbd.h:241
Singular value decomposition WITH the singular vectors, and the Moore-Penrose pseudo-inverse built fr...