LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mfq_ld_distr.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_MFQ_LD_DISTR_H
6#define LINE_API_MAM_MFQ_LD_DISTR_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Stationary density and distribution of a first- or second-order
12 * level-dependent (multi-regime) Markovian fluid queue, evaluated at requested
13 * fluid levels.
14 *
15 * Port of matlab/src/api/mam/mfq_ld_distr.m and the BUTools
16 * LevelDependentFluidStationaryDistr it wraps. Its inputs are the
17 * matrix-exponential building blocks that mfq_ld_solve returns, the same
18 * LevelDependentFluidBlocks that mfq_ld_mean consumes, so the two are
19 * interchangeable consumers of one solve.
20 *
21 * THE DENSITY. Over regime k, that is over the level interval
22 * [T(k), T(k+1)], the stationary density is anchored at both ends,
23 *
24 * pi_k(x) = iniF_k exp(KF_k (x - T(k))) cloF_k
25 * + iniB_k exp(KB_k (T(k+1) - x)) cloB_k,
26 *
27 * with point masses at the K+1 thresholds. Four quantities are offered:
28 * Pdf the per-state density
29 * Pdfd its derivative
30 * Cdf P(X < p), so a point mass sitting exactly at p is EXCLUDED
31 * Cdfm P(X <= p), so that mass is INCLUDED
32 * The Cdf/Cdfm distinction is not cosmetic here: a level-dependent fluid queue
33 * puts genuine atoms at its thresholds, so the two differ by a finite amount at
34 * every threshold and by nothing anywhere else.
35 *
36 * INTEGRATING A SINGULAR EXPONENT. The cumulative forms need
37 * int_0^L exp(M u) du. When M is non-singular that is
38 * (-M)^-1 (I - exp(M L)); when M has a zero eigenvalue -- which is the normal
39 * case for one of the two directions, not an edge case -- the inverse does not
40 * exist and the reference deflates it instead. With l and r the left and right
41 * null vectors of M normalized so that l r = 1,
42 *
43 * int_0^L exp(M u) du = (-(M - r l))^-1 (I - exp((M - r l) L))
44 * + r l (L + exp(-L) - 1),
45 *
46 * the rank-one shift moving the zero eigenvalue to -1 and its contribution
47 * being added back in closed form. The null vectors come from BUTools CRPSolve,
48 * which is a LINEAR SOLVE and not an eigenvector computation: it replaces the
49 * first column of M by ones and solves, which is legitimate because M has zero
50 * row sums by construction. That is worth stating because it is the reason this
51 * function does NOT need an eigendecomposition for its arithmetic.
52 *
53 * WHERE THE EIGENVALUES DO ENTER, AND WHY Real IS STILL HONEST. The reference
54 * chooses which of KF, KB to deflate by comparing min |eig(KF)| against
55 * min |eig(KB)|, i.e. by asking which is closer to singular. That comparison is
56 * the ONLY use of an eigendecomposition in the whole function, and its result
57 * is a BRANCH SELECTION, a discrete choice between two formulas for the same
58 * integral. No eigenvalue flows into any returned number. The port therefore
59 * converts the two matrices to double for the comparison alone, exactly as
60 * util/eig.h instructs its callers to do, and carries out the integral itself
61 * in T. At Real50 the returned values are genuinely Real50-accurate; what is
62 * computed in double is which of two algebraically equivalent routes to take.
63 * If the build has no LAPACK, eig_values refuses by name, and this function
64 * refuses with it rather than guessing the branch.
65 *
66 * ARITHMETIC. Gated on num_traits<T>::has_transcendental: expm is a
67 * tolerance-controlled Pade approximation in any arithmetic, and the deflation
68 * formula evaluates exp(-L). See the paragraph above for why the LAPACK
69 * dependency does not reduce this to double.
70 */
71
72#include <cmath>
73#include <complex>
74#include <cstddef>
75#include <vector>
76
78#include "line/num/number.h"
79#include "line/util/eig.h"
80#include "line/util/error.h"
81#include "line/util/expm.h"
82#include "line/util/linalg.h"
83#include "line/util/lu.h"
84#include "line/util/matrix.h"
85
86namespace line {
87namespace mam {
88
89/** Which functional of the stationary level law to evaluate. */
90enum class FluidDistrKind {
91 Pdf, ///< per-state density
92 Pdfd, ///< derivative of the density
93 Cdf, ///< P(X < p), excluding an atom sitting exactly at p
94 Cdfm ///< P(X <= p), including it
95};
96
97namespace mfq_ld_detail {
98
99/**
100 * BUTools CRPSolve: the stationary vector of a continuous-time RATIONAL
101 * process, pi M = 0 with sum(pi) = 1. M needs zero row sums but, unlike a
102 * generator, is not required to have non-negative off-diagonals. Replacing the
103 * first column by ones turns the singular system into a non-singular one whose
104 * unique solution is the normalized null vector, so this is a linear solve and
105 * NOT an eigenvector computation.
106 */
107template <class T>
108std::vector<T> crp_solve(const Matrix<T>& M) {
109 const std::size_t n = M.rows();
110 if (M.cols() != n) throw InputError("crp_solve: matrix is not square");
111 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
112 // pi M' = m with M' = M, first column replaced by ones, and m = e_1.
113 // Transposed for the row-vector solve.
114 Matrix<T> A(n, n, zero);
115 for (std::size_t i = 0; i < n; ++i)
116 for (std::size_t j = 0; j < n; ++j) A(j, i) = (j == 0) ? one : M(i, j);
117 std::vector<T> rhs(n, zero);
118 rhs[0] = one;
119 return solve(A, rhs);
120}
121
122/** Smallest eigenvalue modulus of M, computed in double: a branch test only. */
123template <class T>
124double min_abs_eig(const Matrix<T>& M) {
125 const std::size_t n = M.rows();
126 Matrix<double> Md(n, n, 0.0);
127 for (std::size_t i = 0; i < n; ++i)
128 for (std::size_t j = 0; j < n; ++j) Md(i, j) = num_traits<T>::to_double(M(i, j));
129 const std::vector<std::complex<double>> ev = eig_values(Md);
130 double best = std::abs(ev[0]);
131 for (const std::complex<double>& v : ev) {
132 const double a = std::abs(v);
133 if (a < best) best = a;
134 }
135 return best;
136}
137
138/**
139 * int_0^L exp(M u) du for a SINGULAR M, by the rank-one deflation of the
140 * reference: shift the zero eigenvalue to -1 with M - r l, integrate the
141 * non-singular shift, and add the deflated direction's contribution
142 * r l (L + exp(-L) - 1) back in closed form.
143 */
144template <class T>
145Matrix<T> integ_exp_singular(const Matrix<T>& M, const T& L) {
146 using std::exp;
147 const std::size_t n = M.rows();
148 const T one = num_traits<T>::from_int(1);
149 Matrix<T> Mt(n, n);
150 for (std::size_t i = 0; i < n; ++i)
151 for (std::size_t j = 0; j < n; ++j) Mt(i, j) = M(j, i);
152 std::vector<T> l = crp_solve(M); // left null vector
153 std::vector<T> r = crp_solve(Mt); // right null vector, as a row of M'
154 T lr = num_traits<T>::from_int(0);
155 for (std::size_t i = 0; i < n; ++i) lr += l[i] * r[i];
156 if (lr == num_traits<T>::from_int(0))
157 throw NumericError("mfq_ld_distr: the null vectors of a regime exponent are orthogonal");
158 for (T& v : l) v /= lr;
159
160 Matrix<T> rl(n, n); // the rank-one r l
161 for (std::size_t i = 0; i < n; ++i)
162 for (std::size_t j = 0; j < n; ++j) rl(i, j) = r[i] * l[j];
163 Matrix<T> S = M;
164 for (std::size_t i = 0; i < n; ++i)
165 for (std::size_t j = 0; j < n; ++j) S(i, j) -= rl(i, j);
166
167 Matrix<T> negS(n, n);
168 for (std::size_t i = 0; i < n; ++i)
169 for (std::size_t j = 0; j < n; ++j) negS(i, j) = -S(i, j);
170 const Matrix<T> E = expm(S, L);
171 Matrix<T> ImE = eye<T>(n);
172 for (std::size_t i = 0; i < n; ++i)
173 for (std::size_t j = 0; j < n; ++j) ImE(i, j) -= E(i, j);
174 Matrix<T> out = matmul(inverse(negS), ImE);
175 const T w = L + exp(-L) - one;
176 for (std::size_t i = 0; i < n; ++i)
177 for (std::size_t j = 0; j < n; ++j) out(i, j) += rl(i, j) * w;
178 return out;
179}
180
181/** int_0^L exp(M u) du for a NON-singular M. */
182template <class T>
183Matrix<T> integ_exp_regular(const Matrix<T>& M, const T& L) {
184 const std::size_t n = M.rows();
185 Matrix<T> negM(n, n);
186 for (std::size_t i = 0; i < n; ++i)
187 for (std::size_t j = 0; j < n; ++j) negM(i, j) = -M(i, j);
188 const Matrix<T> E = expm(M, L);
189 Matrix<T> ImE = eye<T>(n);
190 for (std::size_t i = 0; i < n; ++i)
191 for (std::size_t j = 0; j < n; ++j) ImE(i, j) -= E(i, j);
192 return matmul(inverse(negM), ImE);
193}
194
195/**
196 * The pair of integrals for one regime. The direction whose exponent is closer
197 * to singular is deflated, the other inverted directly; see the header note on
198 * why deciding that in double costs the result nothing.
199 */
200template <class T>
201void integ_exp_pair(const Matrix<T>& KA, const Matrix<T>& KB, const T& L, Matrix<T>& JA,
202 Matrix<T>& JB) {
203 if (min_abs_eig(KA) > min_abs_eig(KB)) {
204 JA = integ_exp_regular(KA, L);
205 JB = integ_exp_singular(KB, L);
206 } else {
207 JA = integ_exp_singular(KA, L);
208 JB = integ_exp_regular(KB, L);
209 }
210}
211
212} // namespace mfq_ld_detail
213
214/**
215 * Stationary density or distribution of a level-dependent fluid queue.
216 *
217 * @param b the building blocks returned by mfq_ld_solve
218 * @param what which functional to evaluate
219 * @param points the fluid levels at which to evaluate it
220 * @return one row of N per-state values per requested point
221 */
222template <class T>
223std::vector<std::vector<T>> mfq_ld_distr(const LevelDependentFluidBlocks<T>& b,
224 FluidDistrKind what, const std::vector<T>& points) {
226 "mfq_ld_distr evaluates matrix exponentials");
227 using namespace mfq_ld_detail;
228 const T zero = num_traits<T>::from_int(0);
229 const std::size_t K = b.Thr.size();
230 if (K == 0) throw InputError("mfq_ld_distr: at least one regime is required");
231 if (b.masses.size() != K + 1)
232 throw InputError("mfq_ld_distr: expected K+1 point-mass vectors");
233 if (b.iniF.size() != K || b.KF.size() != K || b.cloF.size() != K || b.iniB.size() != K ||
234 b.KB.size() != K || b.cloB.size() != K)
235 throw InputError("mfq_ld_distr: expected K forward and K backward blocks");
236 const std::size_t N = b.masses[0].size();
237
238 std::vector<T> Tv(K + 1, zero);
239 for (std::size_t k = 0; k < K; ++k) Tv[k + 1] = b.Thr[k];
240 const bool cumulate = (what == FluidDistrKind::Cdf || what == FluidDistrKind::Cdfm);
241
242 std::vector<std::vector<T>> res;
243 res.reserve(points.size());
244 for (const T& p : points) {
245 if (p < zero) throw InputError("mfq_ld_distr: the evaluation points must be non-negative");
246 std::vector<T> pres(N, zero);
247 // cumulative-form threshold walk rationale: see _kb/03-api-layer.md (cpp port notes: mam)
248 std::size_t k = 0;
249 while (k < K && p >= Tv[k]) {
250 if (cumulate) {
251 if (k > 0) {
252 Matrix<T> sF, sB;
253 integ_exp_pair(b.KF[k - 1], b.KB[k - 1], T(Tv[k] - Tv[k - 1]), sF, sB);
254 const std::vector<T> vF =
255 vecmul(vecmul(b.iniF[k - 1], sF), b.cloF[k - 1]);
256 const std::vector<T> vB =
257 vecmul(vecmul(b.iniB[k - 1], sB), b.cloB[k - 1]);
258 for (std::size_t j = 0; j < N; ++j) pres[j] += vF[j] + vB[j];
259 }
260 // Cdf excludes an atom sitting exactly at p, Cdfm includes it.
261 if (p > Tv[k] || what == FluidDistrKind::Cdfm)
262 for (std::size_t j = 0; j < N; ++j) pres[j] += b.masses[k][j];
263 }
264 ++k;
265 }
266 if (k == K && p == Tv[K] && what == FluidDistrKind::Cdfm)
267 for (std::size_t j = 0; j < N; ++j) pres[j] += b.masses[K][j];
268 // The regime holding p is the one below threshold k.
269 const std::size_t kk = k - 1;
270 const T prem = p - Tv[kk];
271 const T Tk = Tv[kk + 1] - Tv[kk];
272
273 if (what == FluidDistrKind::Pdf) {
274 const std::vector<T> vF =
275 vecmul(vecmul(b.iniF[kk], expm(b.KF[kk], prem)), b.cloF[kk]);
276 const std::vector<T> vB =
277 vecmul(vecmul(b.iniB[kk], expm(b.KB[kk], T(Tk - prem))), b.cloB[kk]);
278 for (std::size_t j = 0; j < N; ++j) pres[j] = vF[j] + vB[j];
279 } else if (what == FluidDistrKind::Pdfd) {
280 const std::vector<T> vF = vecmul(
281 vecmul(vecmul(b.iniF[kk], b.KF[kk]), expm(b.KF[kk], prem)), b.cloF[kk]);
282 const std::vector<T> vB =
283 vecmul(vecmul(vecmul(b.iniB[kk], b.KB[kk]), expm(b.KB[kk], T(Tk - prem))),
284 b.cloB[kk]);
285 for (std::size_t j = 0; j < N; ++j) pres[j] = vF[j] - vB[j];
286 } else {
287 Matrix<T> sF, sB;
288 integ_exp_pair(b.KF[kk], b.KB[kk], prem, sF, sB);
289 const std::vector<T> vF = vecmul(vecmul(b.iniF[kk], sF), b.cloF[kk]);
290 const std::vector<T> vB = vecmul(
291 vecmul(vecmul(b.iniB[kk], expm(b.KB[kk], T(Tk - prem))), sB), b.cloB[kk]);
292 for (std::size_t j = 0; j < N; ++j) pres[j] += vF[j] + vB[j];
293 }
294 res.push_back(pres);
295 }
296 return res;
297}
298
299} // namespace mam
300} // namespace line
301
302#endif // LINE_API_MAM_MFQ_LD_DISTR_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
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
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.
Stationary mean fluid level E[X] of a first- or second-order level-dependent (multi-regime) Markovian...
std::vector< std::vector< T > > mfq_ld_distr(const LevelDependentFluidBlocks< T > &b, FluidDistrKind what, const std::vector< T > &points)
Stationary density or distribution of a level-dependent fluid queue.
FluidDistrKind
Which functional of the stationary level law to evaluate.
@ Pdfd
derivative of the density
@ Cdf
P(X < p), excluding an atom sitting exactly at p.
@ Pdf
per-state density
@ Cdfm
P(X <= p), including it.
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
std::vector< std::complex< double > > eig_values(const Matrix< double > &A)
Eigenvalues of a general real square matrix, in LAPACK's order.
Definition eig.h:59
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Number-type abstraction for the templated API port.
The matrix-exponential building blocks of a multi-regime fluid queue, the output of mfq_ld_solve.
Definition mfq_ld_mean.h:68
std::vector< std::vector< T > > masses
K+1 point-mass vectors of length N.
Definition mfq_ld_mean.h:69
std::vector< T > Thr
K regime thresholds T(1)..T(K).
Definition mfq_ld_mean.h:76
std::vector< Matrix< T > > KF
K forward matrix exponents.
Definition mfq_ld_mean.h:71
std::vector< std::vector< T > > iniB
K backward initial vectors.
Definition mfq_ld_mean.h:73
std::vector< Matrix< T > > KB
K backward matrix exponents.
Definition mfq_ld_mean.h:74
std::vector< std::vector< T > > iniF
K forward initial vectors.
Definition mfq_ld_mean.h:70
std::vector< Matrix< T > > cloF
K forward closing matrices.
Definition mfq_ld_mean.h:72
std::vector< Matrix< T > > cloB
K backward closing matrices.
Definition mfq_ld_mean.h:75