LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldqbd_censor.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_CENSOR_H
6#define LINE_API_MAM_LDQBD_CENSOR_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Stationary distribution of a chain of connected LDQBDs by successive
12 * censoring.
13 *
14 * Templated port of matlab/src/api/mam/ldqbd_censor.m, implementing the
15 * algorithm of N. Baer, A. Al Hanbali, R. J. Boucherie, J. C. W. van Ommeren,
16 * "A successive censoring algorithm for a system of connected
17 * LDQBD-processes", Annals of Operations Research 310:389-410, 2022.
18 *
19 * The state space is split into S sets, each a level-dependent QBD on its
20 * own; set s has levels 1..L_s and level l of set s has phases[s][l] phases.
21 * States are ordered by (set, level, phase). Transitions between sets are
22 * arbitrary. Sets 1..S-1 are censored out in turn,
23 *
24 * Q^{k+1}_{ij} = Q^k_{ij} + Q^k_{ik} (-Q^k_{kk})^{-1} Q^k_{kj}, i, j > k,
25 *
26 * the last LDQBD is solved by level reduction (Gaver), and the vector is
27 * expanded back with pi_k = (sum_{i>k} pi_i Q^k_{ik}) (-Q^k_{kk})^{-1}. Each
28 * transient LDQBD is factored once by block-tridiagonal LU (block Thomas,
29 * S_l = D_l - W_l S_{l-1}^{-1} U_{l-1}); its inverse is never formed.
30 *
31 * STRUCTURE. The method applies exactly when every censored diagonal block
32 * Q^k_{kk} is block tridiagonal by level. The update is a sum of nonnegative
33 * terms, so the numerical pattern equals the structural one and the check is
34 * made directly on Q^k_{kk}; a failure raises LINE:unsupported:Structure
35 * (another order of the sets may succeed, Remark 2 of the paper).
36 *
37 * ARITHMETIC. The algorithm is finite (no tolerance, no iteration), so it
38 * instantiates at Rational and returns the exact stationary law. Exact T
39 * raises NumericError on an exactly singular Schur complement; inexact T also
40 * raises when the 1-norm reciprocal condition of a block is below machine
41 * epsilon, matching the MATLAB rcond test. The row-sum check is exact at
42 * Rational and relative (tol) otherwise.
43 */
44
45#include <cstddef>
46#include <map>
47#include <string>
48#include <vector>
49
50#include "line/num/number.h"
51#include "line/util/error.h"
52#include "line/util/linalg.h"
53#include "line/util/lu.h"
54#include "line/util/matrix.h"
55
56namespace line {
57namespace mam {
58
59/** Stationary distribution of a chain of connected LDQBDs. */
60template <class T>
62 std::vector<T> pi; ///< in the ordering of Q
63 std::vector<std::vector<std::vector<T>>> pi_cells; ///< pi_cells[s][l], level l of set s
64};
65
67
68template <class T>
69T zero() { return num_traits<T>::from_int(0); }
70
71template <class T>
72double norm1(const Matrix<T>& A) {
73 double best = 0.0;
74 for (std::size_t j = 0; j < A.cols(); ++j) {
75 double s = 0.0;
76 for (std::size_t i = 0; i < A.rows(); ++i) s += num_traits<T>::to_double(num_abs(T(A(i, j))));
77 if (s > best) best = s;
78 }
79 return best;
80}
81
82/** Inverse of a Schur complement, refusing a singular one. */
83template <class T>
84Matrix<T> checked_inverse(const Matrix<T>& D, std::size_t level, std::size_t set) {
85 const std::string msg = "ldqbd_censor: the Schur complement of level " + std::to_string(level + 1) +
86 " of set " + std::to_string(set + 1) + " is singular; the chain may be reducible.";
87 Matrix<T> inv;
88 try {
89 inv = inverse(D);
90 } catch (const NumericError&) {
91 throw NumericError(ErrorKind{"Singular"}, msg);
92 }
94 const double rc = 1.0 / (norm1(D) * norm1(inv));
95 if (!(rc >= 2.220446049250313e-16)) throw NumericError(ErrorKind{"Singular"}, msg);
96 }
97 return inv;
98}
99
100template <class T>
101Matrix<T> block(const Matrix<T>& M, std::size_t r0, std::size_t nr, std::size_t c0, std::size_t nc) {
102 Matrix<T> B(nr, nc, zero<T>());
103 for (std::size_t i = 0; i < nr; ++i)
104 for (std::size_t j = 0; j < nc; ++j) B(i, j) = M(r0 + i, c0 + j);
105 return B;
106}
107
108/** Block-tridiagonal LU of M = -Q_kk: S on the diagonal, G below, U above. */
109template <class T>
110struct Factor {
111 std::vector<std::size_t> P, off;
112 std::vector<Matrix<T>> S, Sinv, G, U;
113
114 Factor(const Matrix<T>& M, const std::vector<std::size_t>& P_, std::size_t set, bool last_singular)
115 : P(P_), off(P_.size() + 1, 0) {
116 const std::size_t L = P.size();
117 for (std::size_t l = 0; l < L; ++l) off[l + 1] = off[l] + P[l];
118 S.resize(L);
119 Sinv.resize(L);
120 G.resize(L);
121 U.resize(L);
122 for (std::size_t l = 0; l < L; ++l) {
123 Matrix<T> D = block(M, off[l], P[l], off[l], P[l]);
124 if (l > 0) {
125 const Matrix<T> W = block(M, off[l], P[l], off[l - 1], P[l - 1]);
126 G[l] = matmul(W, Sinv[l - 1]);
127 const Matrix<T> GU = matmul(G[l], U[l - 1]);
128 for (std::size_t i = 0; i < D.rows(); ++i)
129 for (std::size_t j = 0; j < D.cols(); ++j) D(i, j) -= GU(i, j);
130 }
131 if (l + 1 < L) U[l] = block(M, off[l], P[l], off[l + 1], P[l + 1]);
132 S[l] = D;
133 if (l + 1 < L || !last_singular) Sinv[l] = checked_inverse(D, l, set);
134 }
135 }
136
137 std::size_t levels() const { return P.size(); }
138
139 /** X = M^{-1} Y. */
140 Matrix<T> colsolve(const Matrix<T>& Y) const {
141 const std::size_t L = levels(), m = Y.cols();
142 std::vector<Matrix<T>> Z(L), X(L);
143 for (std::size_t l = 0; l < L; ++l) {
144 Z[l] = block(Y, off[l], P[l], 0, m);
145 if (l > 0) {
146 const Matrix<T> GZ = matmul(G[l], Z[l - 1]);
147 for (std::size_t i = 0; i < P[l]; ++i)
148 for (std::size_t j = 0; j < m; ++j) Z[l](i, j) -= GZ(i, j);
149 }
150 }
151 X[L - 1] = matmul(Sinv[L - 1], Z[L - 1]);
152 for (std::size_t l = L - 1; l-- > 0;) {
153 const Matrix<T> UX = matmul(U[l], X[l + 1]);
154 for (std::size_t i = 0; i < P[l]; ++i)
155 for (std::size_t j = 0; j < m; ++j) Z[l](i, j) -= UX(i, j);
156 X[l] = matmul(Sinv[l], Z[l]);
157 }
158 Matrix<T> out(Y.rows(), m, zero<T>());
159 for (std::size_t l = 0; l < L; ++l)
160 for (std::size_t i = 0; i < P[l]; ++i)
161 for (std::size_t j = 0; j < m; ++j) out(off[l] + i, j) = X[l](i, j);
162 return out;
163 }
164
165 /** x = y M^{-1}. */
166 std::vector<T> rowsolve(const std::vector<T>& y) const {
167 const std::size_t L = levels();
168 std::vector<std::vector<T>> w(L);
169 for (std::size_t l = 0; l < L; ++l) {
170 std::vector<T> r(y.begin() + off[l], y.begin() + off[l + 1]);
171 if (l > 0) {
172 const std::vector<T> wu = vecmul(w[l - 1], U[l - 1]);
173 for (std::size_t i = 0; i < r.size(); ++i) r[i] -= wu[i];
174 }
175 w[l] = vecmul(r, Sinv[l]);
176 }
177 std::vector<T> x(y.size(), zero<T>());
178 for (std::size_t i = 0; i < P[L - 1]; ++i) x[off[L - 1] + i] = w[L - 1][i];
179 for (std::size_t l = L - 1; l-- > 0;) {
180 const std::vector<T> xn(x.begin() + off[l + 1], x.begin() + off[l + 2]);
181 const std::vector<T> xg = vecmul(xn, G[l + 1]);
182 for (std::size_t i = 0; i < P[l]; ++i) x[off[l] + i] = w[l][i] - xg[i];
183 }
184 return x;
185 }
186};
187
188/** Throws LINE:unsupported:Structure if M is not block tridiagonal by level. */
189template <class T>
190void check(const Matrix<T>& M, const std::vector<std::size_t>& P, std::size_t set) {
191 std::vector<std::size_t> lev;
192 for (std::size_t l = 0; l < P.size(); ++l)
193 for (std::size_t i = 0; i < P[l]; ++i) lev.push_back(l + 1);
194 for (std::size_t i = 0; i < M.rows(); ++i)
195 for (std::size_t j = 0; j < M.cols(); ++j) {
196 const std::size_t d = lev[i] > lev[j] ? lev[i] - lev[j] : lev[j] - lev[i];
197 if (d > 1 && !(M(i, j) == zero<T>()))
198 throw UnsupportedError(
199 ErrorKind{"Structure"},
200 "ldqbd_censor: set " + std::to_string(set + 1) +
201 " is not block tridiagonal by level after censoring out the sets before it "
202 "(transition from level " + std::to_string(lev[i]) + " to level " +
203 std::to_string(lev[j]) + "); successive censoring does not apply in this set order.");
204 }
205}
206
207} // namespace ldqbd_censor_detail
208
209/**
210 * Stationary distribution of a chain of connected LDQBDs (ldqbd_censor.m).
211 *
212 * @param Q generator ordered by (set, level, phase)
213 * @param phases phases[s][l] is the number of phases of level l of set s
214 * @param tol relative tolerance on the row sums (ignored at exact T,
215 * where the rows must sum to exactly zero)
216 */
217template <class T>
218LdqbdCensorResult<T> ldqbd_censor(const Matrix<T>& Q, const std::vector<std::vector<std::size_t>>& phases,
219 double tol = 1e-10) {
220 using namespace ldqbd_censor_detail;
221 if (phases.empty()) throw InputError(ErrorKind{"Argument"}, "ldqbd_censor: phases must give one vector of phase counts per set.");
222 const std::size_t S = phases.size();
223 std::vector<std::size_t> nset(S, 0), setOff(S + 1, 0);
224 for (std::size_t s = 0; s < S; ++s) {
225 if (phases[s].empty())
226 throw InputError(ErrorKind{"Argument"}, "ldqbd_censor: phases{" + std::to_string(s + 1) +
227 "} must be a non-empty vector of positive integers.");
228 for (std::size_t p : phases[s]) {
229 if (p < 1)
230 throw InputError(ErrorKind{"Argument"}, "ldqbd_censor: phases{" + std::to_string(s + 1) +
231 "} must be a non-empty vector of positive integers.");
232 nset[s] += p;
233 }
234 setOff[s + 1] = setOff[s] + nset[s];
235 }
236 const std::size_t n = setOff[S];
237 if (Q.rows() != Q.cols()) throw InputError(ErrorKind{"Argument"}, "ldqbd_censor: Q must be square.");
238 if (Q.rows() != n)
239 throw InputError(ErrorKind{"Argument"}, "ldqbd_censor: Q has order " + std::to_string(Q.rows()) +
240 " but the phase counts add up to " + std::to_string(n) + ".");
241
242 // sparse row store of the working generator, indexed by global state
243 std::vector<std::map<std::size_t, T>> rows(n);
244 double scale = 1.0;
245 for (std::size_t i = 0; i < n; ++i) {
246 const double d = num_traits<T>::to_double(num_abs(T(Q(i, i))));
247 if (d > scale) scale = d;
248 }
249 for (std::size_t i = 0; i < n; ++i) {
250 T rs = zero<T>();
251 for (std::size_t j = 0; j < n; ++j) {
252 const T v = Q(i, j);
253 if (v == zero<T>()) continue;
254 if (i != j && v < zero<T>())
255 throw InputError(ErrorKind{"Argument"}, "ldqbd_censor: the off-diagonal entries of Q must be nonnegative.");
256 rows[i][j] = v;
257 rs += v;
258 }
259 const bool bad = num_traits<T>::is_exact ? !(rs == zero<T>())
260 : num_traits<T>::to_double(num_abs(rs)) > tol * scale;
261 if (bad) throw InputError(ErrorKind{"Argument"}, "ldqbd_censor: the rows of Q must sum to zero.");
262 }
263
264 // successive censoring of sets 1..S-1
265 std::vector<Factor<T>> fac;
266 std::vector<std::vector<std::size_t>> bRows(S);
267 std::vector<std::vector<std::vector<T>>> bVals(S);
268 for (std::size_t k = 0; k + 1 < S; ++k) {
269 const std::size_t o0 = setOff[k], o1 = setOff[k + 1], nk = nset[k];
270 Matrix<T> M(nk, nk, zero<T>());
271 std::map<std::size_t, std::size_t> colPos;
272 for (std::size_t i = 0; i < nk; ++i)
273 for (const auto& e : rows[o0 + i]) {
274 if (e.first >= o0 && e.first < o1) M(i, e.first - o0) = -e.second;
275 else if (e.first >= o1) colPos[e.first] = 0;
276 }
277 check(M, phases[k], k);
278 fac.emplace_back(M, phases[k], k, false);
279 std::vector<std::size_t> cols;
280 for (auto& c : colPos) {
281 c.second = cols.size();
282 cols.push_back(c.first);
283 }
284 Matrix<T> QkB(nk, cols.size(), zero<T>());
285 for (std::size_t i = 0; i < nk; ++i)
286 for (const auto& e : rows[o0 + i])
287 if (e.first >= o1) QkB(i, colPos[e.first]) = e.second;
288 for (std::size_t r = o1; r < n; ++r) {
289 std::vector<T> v;
290 for (const auto& e : rows[r]) {
291 if (e.first >= o0 && e.first < o1 && !(e.second == zero<T>())) {
292 if (v.empty()) v.assign(nk, zero<T>());
293 v[e.first - o0] = e.second;
294 }
295 }
296 if (!v.empty()) {
297 bRows[k].push_back(r);
298 bVals[k].push_back(v);
299 }
300 }
301 if (!cols.empty() && !bRows[k].empty()) {
302 const Matrix<T> X = fac.back().colsolve(QkB);
303 for (std::size_t i = 0; i < bRows[k].size(); ++i) {
304 const std::vector<T> u = vecmul(bVals[k][i], X);
305 auto& row = rows[bRows[k][i]];
306 for (std::size_t j = 0; j < cols.size(); ++j) {
307 if (u[j] == zero<T>()) continue;
308 auto it = row.find(cols[j]);
309 if (it == row.end()) row[cols[j]] = u[j];
310 else it->second += u[j];
311 }
312 }
313 }
314 }
315
316 // intermediate step: level reduction on the last LDQBD
317 const std::size_t oS = setOff[S - 1], nS = nset[S - 1];
318 Matrix<T> MS(nS, nS, zero<T>());
319 for (std::size_t i = 0; i < nS; ++i)
320 for (const auto& e : rows[oS + i])
321 if (e.first >= oS) MS(i, e.first - oS) = -e.second;
322 check(MS, phases[S - 1], S - 1);
323 const Factor<T> f(MS, phases[S - 1], S - 1, true);
324 const std::size_t L = f.levels();
325 Matrix<T> A = f.S[L - 1];
326 for (std::size_t i = 0; i < A.rows(); ++i) A(i, 0) = num_traits<T>::from_int(1);
327 // x A = e1 <=> A' x' = e1'
328 Matrix<T> At(A.cols(), A.rows(), zero<T>());
329 for (std::size_t i = 0; i < A.rows(); ++i)
330 for (std::size_t j = 0; j < A.cols(); ++j) At(j, i) = A(i, j);
331 std::vector<T> e1(A.rows(), zero<T>());
332 e1[0] = num_traits<T>::from_int(1);
333 std::vector<T> xL;
334 try {
335 xL = solve(At, e1);
336 } catch (const NumericError&) {
337 throw NumericError(ErrorKind{"Singular"}, "ldqbd_censor: the last level of set " + std::to_string(S) +
338 " has no unique stationary vector; the chain may be reducible.");
339 }
340 std::vector<T> pB(n, zero<T>());
341 for (std::size_t i = 0; i < xL.size(); ++i) pB[oS + f.off[L - 1] + i] = xL[i];
342 for (std::size_t l = L - 1; l-- > 0;) {
343 const std::vector<T> xn(pB.begin() + oS + f.off[l + 1], pB.begin() + oS + f.off[l + 2]);
344 const std::vector<T> xg = vecmul(xn, f.G[l + 1]);
345 for (std::size_t i = 0; i < f.P[l]; ++i) pB[oS + f.off[l] + i] = -xg[i];
346 }
347
348 // expansion back through sets S-1..1
349 for (std::size_t k = S - 1; k-- > 0;) {
350 std::vector<T> y(nset[k], zero<T>());
351 for (std::size_t i = 0; i < bRows[k].size(); ++i) {
352 const T p = pB[bRows[k][i]];
353 if (p == zero<T>()) continue;
354 for (std::size_t t = 0; t < nset[k]; ++t) y[t] += p * bVals[k][i][t];
355 }
356 const std::vector<T> x = fac[k].rowsolve(y);
357 for (std::size_t t = 0; t < nset[k]; ++t) pB[setOff[k] + t] = x[t];
358 }
359 T tot = zero<T>();
360 for (const T& v : pB) tot += v;
362 res.pi.resize(n);
363 for (std::size_t i = 0; i < n; ++i) res.pi[i] = pB[i] / tot;
364 std::size_t pos = 0;
365 res.pi_cells.resize(S);
366 for (std::size_t s = 0; s < S; ++s)
367 for (std::size_t p : phases[s]) {
368 res.pi_cells[s].emplace_back(res.pi.begin() + pos, res.pi.begin() + pos + p);
369 pos += p;
370 }
371 return res;
372}
373
374} // namespace mam
375} // namespace line
376
377#endif // LINE_API_MAM_LDQBD_CENSOR_H
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The algorithm cannot produce a trustworthy answer on this instance: LINE:numerical:<kind>.
Definition error.h:110
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
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.
Matrix< T > block(const Matrix< T > &M, std::size_t r0, std::size_t nr, std::size_t c0, std::size_t nc)
double norm1(const Matrix< T > &A)
Matrix< T > checked_inverse(const Matrix< T > &D, std::size_t level, std::size_t set)
Inverse of a Schur complement, refusing a singular one.
void check(const Matrix< T > &M, const std::vector< std::size_t > &P, std::size_t set)
Throws LINE:unsupported:Structure if M is not block tridiagonal by level.
LdqbdCensorResult< T > ldqbd_censor(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &phases, double tol=1e-10)
Stationary distribution of a chain of connected LDQBDs (ldqbd_censor.m).
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
std::vector< T > vecmul(const std::vector< T > &v, const Matrix< T > &A)
Row vector times matrix, v A.
Definition linalg.h:50
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
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Stationary distribution of a chain of connected LDQBDs.
std::vector< T > pi
in the ordering of Q
std::vector< std::vector< std::vector< T > > > pi_cells
pi_cells[s][l], level l of set s
std::vector< T > rowsolve(const std::vector< T > &y) const
x = y M^{-1}.
Factor(const Matrix< T > &M, const std::vector< std::size_t > &P_, std::size_t set, bool last_singular)
Matrix< T > colsolve(const Matrix< T > &Y) const
X = M^{-1} Y.
std::vector< Matrix< T > > Sinv
std::vector< std::size_t > off