LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mfq_ld_solve.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_SOLVE_H
6#define LINE_API_MAM_MFQ_LD_SOLVE_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * First- and second-order level-dependent (multi-regime) Markovian fluid
12 * queues: the matrix-exponential building blocks of the stationary law.
13 *
14 * Port of matlab/src/api/mam/mfq_ld_solve.m and the BUTools
15 * SecondOrderLevelDependentFluidSolve it wraps. The generator, the drift and
16 * optionally the VARIANCE change at threshold levels, giving a piecewise
17 * homogeneous first- or second-order (Brownian) fluid queue. Setting every
18 * variance cell to zero reduces it to first order. The blocks it returns are
19 * exactly what mfq_ld_mean and mfq_ld_distr consume, so this closes that
20 * family rather than extending it.
21 *
22 * METHOD. Per regime, the states with neither drift nor variance are censored
23 * out, and the remainder is split into three classes: positive drift with no
24 * variance, negative drift with no variance, and any state with variance. The
25 * density is anchored at both ends of the regime, forward from its lower
26 * threshold and backward from its upper one, and each direction's exponent
27 * comes from a QBD. The second-order terms are what force the QBD form: with a
28 * variance the balance equation is second order in the level, and multiplying
29 * through by a constant c chosen from the spectrum turns it into the matrix
30 * QUADRATIC of a discrete QBD, from whose R the exponent is recovered as
31 * K = (R - I) c. c is the smallest value that keeps the transformed triple
32 * substochastic, taken as the maximum over the drift states of -Q_ii/R_ii and
33 * over the variance states of the larger root of the discriminant, floored at 1.
34 *
35 * WHICH QBD SOLVER, AND WHY NO NEW ONE WAS NEEDED. The reference calls
36 * QBD_CR(Bm, Lm, Fm), and the R it returns satisfies
37 *
38 * Fm + R Lm + R^2 Bm = 0,
39 *
40 * which is EXACTLY the contract of the port's existing qbd_R,
41 * F + R L + R^2 B = 0, under the direct mapping B = Bm, L = Lm, F = Fm. So the
42 * cyclic-reduction routine did not have to be transcribed.
43 *
44 * That equation was established by MEASUREMENT, not by reading the argument
45 * names, and the first reading was wrong. QBD_CR's own error message rejects a
46 * triple whose sum is not "(sub)stochastic", which invites the conclusion that
47 * it wants the discrete-time form R = A0 + R A1 + R^2 A2 with A0 = Bm; the
48 * triples this caller builds sum to a GENERATOR, and the shift that reading
49 * implies (L = Lm - I) produces a completely different and wrong K. Feeding the
50 * actual regime-1 triple of a two-state first-order instance through MATLAB and
51 * evaluating all four candidate residuals settles it in one run: 2.2e-16 for the
52 * form above against 1.25, 1.5 and 2.75 for the others. The test asserts that
53 * residual directly on the port's own R, so the identity is pinned rather than
54 * inferred.
55 *
56 * The boundary system then couples the K+1 point masses to the 2K density
57 * initial vectors through flux conservation at every threshold, the boundary
58 * conditions that reflective and absorbing states impose, and continuity of the
59 * second-order density across a threshold. One normalization row replaces the
60 * first flux equation.
61 *
62 * REFERENCE DEFECT, reported and NOT worked around. mfq_ld_solve CRASHES ON ITS
63 * OWN DEFAULT ARGUMENTS whenever there is more than one regime:
64 *
65 * Q = {[-2 2;1 -1],[-3 3;2 -2]}; R = {diag([1 -1]), diag([0.5 -2])};
66 * S = {zeros(2), zeros(2)}; mfq_ld_solve(Q,R,S,[1 3])
67 * Error: The logical indices contain a true value outside of the array
68 * bounds. SecondOrderLevelDependentFluidSolve line 202.
69 *
70 * Line 24 defaults boundaryL to zeros(K,N), a K x N MATRIX, while the
71 * documented contract (line 8) and every use site want ONE ITEM PER BACKGROUND
72 * STATE, a length-N vector; line 202 then indexes ix = 1:N with that K x N
73 * logical mask. boundaryU inherits it. K = 1 survives only because zeros(1,N)
74 * happens to be the right shape. Passing explicit length-N vectors works and
75 * gives a correct answer. This port takes the boundary flags as length-N
76 * vectors, which is the documented contract, and defaults them to reflective,
77 * so it does not reproduce the crash.
78 *
79 * ARITHMETIC. Templated on T and gated on num_traits<T>::has_transcendental:
80 * the QBD iteration is tolerance-terminated and expm is a Pade approximation.
81 * As in mfq_ld_distr, the ONLY eigenvalue computation is the branch test inside
82 * the normalization's integral of a matrix exponential, which selects between
83 * two algebraically equivalent formulas and never supplies a value that reaches
84 * the result, so Real instantiation is honest here. Contrast mfq_multiregime,
85 * where the Schur factors ARE the basis of the answer and double is the ceiling.
86 */
87
88#include <cmath>
89#include <cstddef>
90#include <vector>
91
95#include "line/api/mam/qbd_r.h"
96#include "line/num/number.h"
97#include "line/util/error.h"
98#include "line/util/expm.h"
99#include "line/util/linalg.h"
100#include "line/util/lu.h"
101#include "line/util/matrix.h"
102
103namespace line {
104namespace mam {
105
106/** Boundary behaviour of one background state at a reflecting level. */
107enum class FluidBoundary { Reflective = 0, Absorbing = 1 };
108
109namespace ld_solve_detail {
110
111/** Rows ri, columns ci of A. */
112template <class T>
113Matrix<T> pick(const Matrix<T>& A, const std::vector<std::size_t>& ri,
114 const std::vector<std::size_t>& ci) {
115 Matrix<T> B(ri.size(), ci.size(), num_traits<T>::from_int(0));
116 for (std::size_t i = 0; i < ri.size(); ++i)
117 for (std::size_t j = 0; j < ci.size(); ++j) B(i, j) = A(ri[i], ci[j]);
118 return B;
119}
120
121/** Whether v contains x. */
122inline bool has(const std::vector<std::size_t>& v, std::size_t x) {
123 for (std::size_t y : v)
124 if (y == x) return true;
125 return false;
126}
127
128/**
129 * The R of the reference's QBD_CR(Bm, Lm, Fm), which satisfies
130 * Fm + R Lm + R^2 Bm = 0 and is therefore the port's qbd_R under the direct
131 * mapping B = Bm, L = Lm, F = Fm. See the file header for how that was
132 * established and for the reading it displaced.
133 */
134template <class T>
135Matrix<T> qbd_cr_R(const Matrix<T>& Bm, const Matrix<T>& Lm, const Matrix<T>& Fm, const T& tol) {
136 return qbd_R(Bm, Lm, Fm, 200000u, tol);
137}
138
139} // namespace ld_solve_detail
140
141/**
142 * Solve a first- or second-order level-dependent fluid queue.
143 *
144 * @param Q per-regime generators, K of them
145 * @param R per-regime DIAGONAL drift matrices, K of them
146 * @param S per-regime DIAGONAL variance matrices; all zero = first order
147 * @param Thr the K thresholds
148 * @param boundaryL per background state, the behaviour at the lower boundary;
149 * empty means every state reflective
150 * @param boundaryU likewise at the upper boundary; empty means the same as
151 * boundaryL, as in the reference
152 * @param Qt boundary generators, K+1 of them; empty means
153 * {Q[0], ..., Q[K-1], Q[K-1]}, a single entry is replicated
154 * @param prec tolerance for the state classification and the QBD solves
155 */
156template <class T>
158 const std::vector<Matrix<T>>& R,
159 const std::vector<Matrix<T>>& S,
160 const std::vector<T>& Thr,
161 const std::vector<FluidBoundary>& boundaryL,
162 const std::vector<FluidBoundary>& boundaryU,
163 const std::vector<Matrix<T>>& Qt, const T& prec) {
165 "mfq_ld_solve runs a tolerance-terminated QBD iteration and evaluates expm");
166 using namespace ld_solve_detail;
167 using std::sqrt;
168 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
169 const T two = num_traits<T>::from_int(2);
170 const std::size_t K = Thr.size();
171 if (K == 0) throw InputError("mfq_ld_solve: at least one regime is required");
172 if (Q.size() != K || R.size() != K || S.size() != K)
173 throw InputError("mfq_ld_solve: Q, R and S must have one entry per regime");
174 const std::size_t N = Q[0].rows();
175 if (N == 0) throw InputError("mfq_ld_solve: the background chain is empty");
176
177 std::vector<FluidBoundary> bL = boundaryL, bU = boundaryU;
178 if (bL.empty()) bL.assign(N, FluidBoundary::Reflective);
179 if (bU.empty()) bU = bL;
180 if (bL.size() != N || bU.size() != N)
181 throw InputError(
182 "mfq_ld_solve: the boundary flags are one per BACKGROUND STATE, a vector of length N");
183 std::vector<Matrix<T>> Qtk = Qt;
184 if (Qtk.empty()) {
185 for (std::size_t k = 0; k < K; ++k) Qtk.push_back(Q[k]);
186 Qtk.push_back(Q[K - 1]);
187 } else if (Qtk.size() == 1) {
188 while (Qtk.size() < K + 1) Qtk.push_back(Qtk[0]);
189 }
190 if (Qtk.size() != K + 1) throw InputError("mfq_ld_solve: expected K+1 boundary generators");
191
192 std::vector<T> Tv(K + 1, zero);
193 for (std::size_t k = 0; k < K; ++k) Tv[k + 1] = Thr[k];
194
195 // ---- per-regime forward and backward exponents ----
196 std::vector<Matrix<T>> Sh(K); // S/2, the form every later formula uses
197 std::vector<Matrix<T>> KF(K), KB(K), cloF(K), cloB(K);
198 std::vector<std::size_t> Np(K, 0), Nn(K, 0), Ns(K, 0), NbF(K, 0), NbB(K, 0);
199 std::vector<std::vector<std::size_t>> vixp(K), vixn(K), vix0(K), vixs(K);
200 for (std::size_t k = 0; k < K; ++k) {
201 if (Q[k].rows() != N || R[k].rows() != N || S[k].rows() != N)
202 throw InputError("mfq_ld_solve: a regime has the wrong order");
203 Sh[k] = Matrix<T>(N, N, zero);
204 for (std::size_t i = 0; i < N; ++i)
205 for (std::size_t j = 0; j < N; ++j) Sh[k](i, j) = S[k](i, j) / two;
206
207 std::vector<std::size_t> ix0, ixn0;
208 for (std::size_t i = 0; i < N; ++i) {
209 if (num_abs(R[k](i, i)) <= prec && Sh[k](i, i) <= prec) ix0.push_back(i);
210 else ixn0.push_back(i);
211 }
212 const std::size_t Nzero = ix0.size(), Nnz = ixn0.size();
213 if (Nnz == 0)
214 throw InputError("mfq_ld_solve: a regime has neither drift nor variance in any state");
215
216 Matrix<T> Qv = pick(Q[k], ixn0, ixn0);
217 Matrix<T> Wz(Nnz, Nzero, zero); // Qn0 inv(-Q00), reused for the closing matrices
218 if (Nzero > 0) {
219 Matrix<T> negQ00 = pick(Q[k], ix0, ix0);
220 for (std::size_t i = 0; i < Nzero; ++i)
221 for (std::size_t j = 0; j < Nzero; ++j) negQ00(i, j) = -negQ00(i, j);
222 Wz = matmul(pick(Q[k], ixn0, ix0), inverse(negQ00));
223 Qv = mfq_detail::add(Qv, matmul(Wz, pick(Q[k], ix0, ixn0)));
224 }
225 const Matrix<T> Rv = pick(R[k], ixn0, ixn0);
226 const Matrix<T> Sv = pick(Sh[k], ixn0, ixn0);
227
228 std::vector<std::size_t> ixp, ixn, ixs;
229 for (std::size_t i = 0; i < Nnz; ++i) {
230 if (Sv(i, i) > prec) ixs.push_back(i);
231 else if (Rv(i, i) > prec) ixp.push_back(i);
232 else if (Rv(i, i) < -prec) ixn.push_back(i);
233 }
234 Np[k] = ixp.size();
235 Nn[k] = ixn.size();
236 Ns[k] = ixs.size();
237
238 // The discriminant of the second-order balance equation, shared by both
239 // directions: Rv^2 - 2 (2 Sv) Qv on the variance states.
240 std::vector<T> discr(ixs.size(), zero);
241 for (std::size_t i = 0; i < ixs.size(); ++i) {
242 const std::size_t q = ixs[i];
243 discr[i] = Rv(q, q) * Rv(q, q) -
244 two * (two * Sv(q, q)) * Qv(q, q);
245 }
246
247 // ---- FORWARD ----
248 {
249 T c = one;
250 for (std::size_t q : ixp) {
251 const T v = -Qv(q, q) / Rv(q, q);
252 if (v > c) c = v;
253 }
254 for (std::size_t i = 0; i < ixs.size(); ++i)
255 if (discr[i] > zero) {
256 const std::size_t q = ixs[i];
257 const T v = (-Rv(q, q) + sqrt(discr[i])) / (two * Sv(q, q));
258 if (v > c) c = v;
259 }
260 std::vector<std::size_t> ixbF = ixs;
261 ixbF.insert(ixbF.end(), ixp.begin(), ixp.end());
262 NbF[k] = ixbF.size();
263 const std::size_t nb = NbF[k], nn = Nn[k];
264 Matrix<T> Bm(nb + nn, nb + nn, zero), Lm(nb + nn, nb + nn, zero),
265 Fm(nb + nn, nb + nn, zero);
266 const Matrix<T> Sbb = pick(Sv, ixbF, ixbF), Rbb = pick(Rv, ixbF, ixbF);
267 const Matrix<T> Qbb = pick(Qv, ixbF, ixbF), Qbn = pick(Qv, ixbF, ixn);
268 const Matrix<T> Qnb = pick(Qv, ixn, ixbF), Qnn = pick(Qv, ixn, ixn);
269 const Matrix<T> Rnn = pick(Rv, ixn, ixn);
270 for (std::size_t i = 0; i < nb; ++i)
271 for (std::size_t j = 0; j < nb; ++j) {
272 Bm(i, j) = c * Sbb(i, j);
273 Lm(i, j) = -Rbb(i, j) - two * c * Sbb(i, j);
274 Fm(i, j) = Qbb(i, j) / c + c * Sbb(i, j) + Rbb(i, j);
275 }
276 for (std::size_t i = 0; i < nb; ++i)
277 for (std::size_t j = 0; j < nn; ++j) Fm(i, nb + j) = Qbn(i, j) / c;
278 for (std::size_t i = 0; i < nn; ++i) {
279 for (std::size_t j = 0; j < nb; ++j) Lm(nb + i, j) = Qnb(i, j) / c;
280 for (std::size_t j = 0; j < nn; ++j) {
281 Bm(nb + i, nb + j) = -Rnn(i, j);
282 Lm(nb + i, nb + j) = Qnn(i, j) / c + Rnn(i, j);
283 }
284 }
285 const Matrix<T> QR = qbd_cr_R(Bm, Lm, Fm, prec);
286 KF[k] = Matrix<T>(nb, nb, zero);
287 for (std::size_t i = 0; i < nb; ++i)
288 for (std::size_t j = 0; j < nb; ++j)
289 KF[k](i, j) = (QR(i, j) - (i == j ? one : zero)) * c;
290 // clovF has a column per non-zero-drift state: identity on ixbF,
291 // Psi on ixn.
292 Matrix<T> clov(nb, Nnz, zero);
293 for (std::size_t i = 0; i < nb; ++i) clov(i, ixbF[i]) = one;
294 for (std::size_t i = 0; i < nb; ++i)
295 for (std::size_t j = 0; j < nn; ++j) clov(i, ixn[j]) = QR(i, nb + j);
296 cloF[k] = Matrix<T>(nb, N, zero);
297 for (std::size_t i = 0; i < nb; ++i)
298 for (std::size_t j = 0; j < Nnz; ++j) cloF[k](i, ixn0[j]) = clov(i, j);
299 if (Nzero > 0) {
300 const Matrix<T> z = matmul(clov, Wz);
301 for (std::size_t i = 0; i < nb; ++i)
302 for (std::size_t j = 0; j < Nzero; ++j) cloF[k](i, ix0[j]) = z(i, j);
303 }
304 }
305
306 // ---- BACKWARD ----
307 {
308 T c = one;
309 for (std::size_t q : ixn) {
310 const T v = -Qv(q, q) / (-Rv(q, q));
311 if (v > c) c = v;
312 }
313 for (std::size_t i = 0; i < ixs.size(); ++i)
314 if (discr[i] > zero) {
315 const std::size_t q = ixs[i];
316 const T v = (Rv(q, q) + sqrt(discr[i])) / (two * Sv(q, q));
317 if (v > c) c = v;
318 }
319 std::vector<std::size_t> ixbB = ixs;
320 ixbB.insert(ixbB.end(), ixn.begin(), ixn.end());
321 NbB[k] = ixbB.size();
322 const std::size_t nb = NbB[k], np = Np[k];
323 Matrix<T> Bm(nb + np, nb + np, zero), Lm(nb + np, nb + np, zero),
324 Fm(nb + np, nb + np, zero);
325 const Matrix<T> Sbb = pick(Sv, ixbB, ixbB), Rbb = pick(Rv, ixbB, ixbB);
326 const Matrix<T> Qbb = pick(Qv, ixbB, ixbB), Qbp = pick(Qv, ixbB, ixp);
327 const Matrix<T> Qpb = pick(Qv, ixp, ixbB), Qpp = pick(Qv, ixp, ixp);
328 const Matrix<T> Rpp = pick(Rv, ixp, ixp);
329 for (std::size_t i = 0; i < nb; ++i)
330 for (std::size_t j = 0; j < nb; ++j) {
331 Bm(i, j) = c * Sbb(i, j);
332 Lm(i, j) = Rbb(i, j) - two * c * Sbb(i, j);
333 Fm(i, j) = Qbb(i, j) / c + c * Sbb(i, j) - Rbb(i, j);
334 }
335 for (std::size_t i = 0; i < nb; ++i)
336 for (std::size_t j = 0; j < np; ++j) Fm(i, nb + j) = Qbp(i, j) / c;
337 for (std::size_t i = 0; i < np; ++i) {
338 for (std::size_t j = 0; j < nb; ++j) Lm(nb + i, j) = Qpb(i, j) / c;
339 for (std::size_t j = 0; j < np; ++j) {
340 Bm(nb + i, nb + j) = Rpp(i, j);
341 Lm(nb + i, nb + j) = Qpp(i, j) / c - Rpp(i, j);
342 }
343 }
344 const Matrix<T> QR = qbd_cr_R(Bm, Lm, Fm, prec);
345 KB[k] = Matrix<T>(nb, nb, zero);
346 for (std::size_t i = 0; i < nb; ++i)
347 for (std::size_t j = 0; j < nb; ++j)
348 KB[k](i, j) = (QR(i, j) - (i == j ? one : zero)) * c;
349 Matrix<T> clov(nb, Nnz, zero);
350 for (std::size_t i = 0; i < nb; ++i) clov(i, ixbB[i]) = one;
351 for (std::size_t i = 0; i < nb; ++i)
352 for (std::size_t j = 0; j < np; ++j) clov(i, ixp[j]) = QR(i, nb + j);
353 cloB[k] = Matrix<T>(nb, N, zero);
354 for (std::size_t i = 0; i < nb; ++i)
355 for (std::size_t j = 0; j < Nnz; ++j) cloB[k](i, ixn0[j]) = clov(i, j);
356 if (Nzero > 0) {
357 const Matrix<T> z = matmul(clov, Wz);
358 for (std::size_t i = 0; i < nb; ++i)
359 for (std::size_t j = 0; j < Nzero; ++j) cloB[k](i, ix0[j]) = z(i, j);
360 }
361 }
362
363 // Index sets lifted back to the original state numbering.
364 for (std::size_t q : ixp) vixp[k].push_back(ixn0[q]);
365 for (std::size_t q : ixn) vixn[k].push_back(ixn0[q]);
366 for (std::size_t q : ixs) vixs[k].push_back(ixn0[q]);
367 vix0[k] = ix0;
368 }
369
370 // ---- boundary system ----
371 // Unknown layout: masses[0], then per regime iniF, iniB, masses[k+1].
372 std::vector<std::size_t> pp;
373 pp.push_back(0);
374 pp.push_back(N);
375 for (std::size_t k = 0; k < K; ++k) {
376 pp.push_back(pp.back() + NbF[k]);
377 pp.push_back(pp.back() + NbB[k]);
378 pp.push_back(pp.back() + N);
379 }
380 // pp[0] = masses[0], pp[1] = iniF[0], pp[2] = iniB[0], pp[3] = masses[1], ...
381 const std::size_t Neq = pp.back();
382 Matrix<T> M(Neq, Neq, zero);
383
384 auto rowF = [&](std::size_t k) { return pp[1 + 3 * k]; };
385 auto rowB = [&](std::size_t k) { return pp[2 + 3 * k]; };
386 auto rowM = [&](std::size_t k) { return k == 0 ? std::size_t(0) : pp[3 * k]; };
387
388 // Flux conservation at every threshold.
389 for (std::size_t k = 0; k <= K; ++k) {
390 const std::size_t col = k * N;
391 for (std::size_t i = 0; i < N; ++i)
392 for (std::size_t j = 0; j < N; ++j) M(rowM(k) + i, col + j) = -Qtk[k](i, j);
393 if (k > 0) {
394 const std::size_t g = k - 1;
395 const Matrix<T> E = expm(KF[g], T(Tv[k] - Tv[k - 1]));
396 // expm(KF Tk) (-cloF R + KF cloF S)
397 const Matrix<T> aa = mfq_detail::sub(
398 matmul(matmul(E, matmul(KF[g], cloF[g])), Sh[g]),
399 matmul(matmul(E, cloF[g]), R[g]));
400 for (std::size_t i = 0; i < NbF[g]; ++i)
401 for (std::size_t j = 0; j < N; ++j) M(rowF(g) + i, col + j) = aa(i, j);
402 const Matrix<T> bb = mfq_detail::sub(
403 mfq_detail::scale(matmul(cloB[g], R[g]), T(-one)),
404 matmul(matmul(KB[g], cloB[g]), Sh[g]));
405 for (std::size_t i = 0; i < NbB[g]; ++i)
406 for (std::size_t j = 0; j < N; ++j) M(rowB(g) + i, col + j) = bb(i, j);
407 }
408 if (k < K) {
409 const Matrix<T> cc = mfq_detail::sub(matmul(cloF[k], R[k]),
410 matmul(matmul(KF[k], cloF[k]), Sh[k]));
411 for (std::size_t i = 0; i < NbF[k]; ++i)
412 for (std::size_t j = 0; j < N; ++j) M(rowF(k) + i, col + j) = cc(i, j);
413 const Matrix<T> E = expm(KB[k], T(Tv[k + 1] - Tv[k]));
414 const Matrix<T> dd = matmul(
415 E, mfq_detail::add(matmul(cloB[k], R[k]), matmul(matmul(KB[k], cloB[k]), Sh[k])));
416 for (std::size_t i = 0; i < NbB[k]; ++i)
417 for (std::size_t j = 0; j < N; ++j) M(rowB(k) + i, col + j) = dd(i, j);
418 }
419 }
420
421 // Boundary and continuity conditions.
422 std::size_t col = (K + 1) * N;
423 for (std::size_t k = 0; k <= K; ++k) {
424 if (k == 0) {
425 // No mass in an up-drift state, nor in a reflective variance state.
426 std::vector<std::size_t> ixr0;
427 for (std::size_t q : vixs[0])
428 if (bL[q] == FluidBoundary::Reflective) ixr0.push_back(q);
429 std::vector<std::size_t> sel = vixp[0];
430 sel.insert(sel.end(), ixr0.begin(), ixr0.end());
431 for (std::size_t i = 0; i < sel.size(); ++i) M(rowM(0) + sel[i], col + i) = one;
432 col += sel.size();
433 // Zero density in an absorbing variance state.
434 std::vector<std::size_t> ixa0;
435 for (std::size_t q : vixs[0])
436 if (bL[q] == FluidBoundary::Absorbing) ixa0.push_back(q);
437 const Matrix<T> pdfB = matmul(expm(KB[0], T(Tv[1] - Tv[0])), cloB[0]);
438 for (std::size_t i = 0; i < ixa0.size(); ++i) {
439 for (std::size_t r = 0; r < NbF[0]; ++r)
440 M(rowF(0) + r, col + i) = cloF[0](r, ixa0[i]);
441 for (std::size_t r = 0; r < NbB[0]; ++r)
442 M(rowB(0) + r, col + i) = pdfB(r, ixa0[i]);
443 }
444 col += ixa0.size();
445 } else if (k == K) {
446 std::vector<std::size_t> ixrB;
447 for (std::size_t q : vixs[K - 1])
448 if (bU[q] == FluidBoundary::Reflective) ixrB.push_back(q);
449 std::vector<std::size_t> sel = vixn[K - 1];
450 sel.insert(sel.end(), ixrB.begin(), ixrB.end());
451 for (std::size_t i = 0; i < sel.size(); ++i) M(rowM(K) + sel[i], col + i) = one;
452 col += sel.size();
453 std::vector<std::size_t> ixaB;
454 for (std::size_t q : vixs[K - 1])
455 if (bU[q] == FluidBoundary::Absorbing) ixaB.push_back(q);
456 const Matrix<T> pdfF =
457 matmul(expm(KF[K - 1], T(Tv[K] - Tv[K - 1])), cloF[K - 1]);
458 for (std::size_t i = 0; i < ixaB.size(); ++i) {
459 for (std::size_t r = 0; r < NbF[K - 1]; ++r)
460 M(rowF(K - 1) + r, col + i) = pdfF(r, ixaB[i]);
461 for (std::size_t r = 0; r < NbB[K - 1]; ++r)
462 M(rowB(K - 1) + r, col + i) = cloB[K - 1](r, ixaB[i]);
463 }
464 col += ixaB.size();
465 } else {
466 const std::size_t g = k - 1; // regime below, 0-based
467 // No mass except where the drift reverses across the threshold.
468 std::vector<std::size_t> st0;
469 for (std::size_t q = 0; q < N; ++q) {
470 const bool keep = (has(vixp[g], q) && has(vixn[k], q)) || has(vix0[g], q) ||
471 has(vix0[k], q);
472 if (!keep) st0.push_back(q);
473 }
474 for (std::size_t i = 0; i < st0.size(); ++i) M(rowM(k) + st0[i], col + i) = one;
475 col += st0.size();
476 // Continuity of the second-order density across the threshold.
477 std::vector<std::size_t> sts;
478 for (std::size_t q = 0; q < N; ++q) {
479 const bool isS = has(vixs[g], q) || has(vixs[k], q);
480 const bool excl = has(vixn[k], q) || has(vixp[g], q);
481 if (isS && !excl) sts.push_back(q);
482 }
483 Matrix<T> sqSg(N, N, zero), sqSk(N, N, zero);
484 for (std::size_t q = 0; q < N; ++q) {
485 sqSg(q, q) = sqrt(Sh[g](q, q));
486 sqSk(q, q) = sqrt(Sh[k](q, q));
487 }
488 const Matrix<T> BelowF = matmul(
489 matmul(expm(KF[g], T(Tv[k] - Tv[k - 1])),
490 mfq_detail::scale(cloF[g], T(-one))),
491 sqSg);
492 const Matrix<T> BelowB =
493 matmul(mfq_detail::scale(cloB[g], T(-one)), sqSg);
494 const Matrix<T> AboveF = matmul(cloF[k], sqSk);
495 const Matrix<T> AboveB =
496 matmul(matmul(expm(KB[k], T(Tv[k + 1] - Tv[k])), cloB[k]), sqSk);
497 for (std::size_t i = 0; i < sts.size(); ++i) {
498 for (std::size_t r = 0; r < NbF[g]; ++r)
499 M(rowF(g) + r, col + i) = BelowF(r, sts[i]);
500 for (std::size_t r = 0; r < NbB[g]; ++r)
501 M(rowB(g) + r, col + i) = BelowB(r, sts[i]);
502 for (std::size_t r = 0; r < NbF[k]; ++r)
503 M(rowF(k) + r, col + i) = AboveF(r, sts[i]);
504 for (std::size_t r = 0; r < NbB[k]; ++r)
505 M(rowB(k) + r, col + i) = AboveB(r, sts[i]);
506 }
507 col += sts.size();
508 }
509 }
510 if (col != Neq)
511 throw NumericError(
512 "mfq_ld_solve: the boundary conditions do not close the system; check the drift and "
513 "variance pattern across the thresholds");
514
515 // Normalization replaces the first flux equation: total mass plus the
516 // integral of every regime's density is one.
517 {
518 std::vector<T> h(Neq, zero);
519 for (std::size_t i = 0; i < N; ++i) h[i] = one;
520 for (std::size_t k = 0; k < K; ++k) {
521 Matrix<T> sF, sB;
522 mfq_ld_detail::integ_exp_pair(KF[k], KB[k], T(Tv[k + 1] - Tv[k]), sF, sB);
523 const Matrix<T> gF = matmul(sF, cloF[k]);
524 const Matrix<T> gB = matmul(sB, cloB[k]);
525 for (std::size_t i = 0; i < NbF[k]; ++i) {
526 T s = zero;
527 for (std::size_t j = 0; j < N; ++j) s += gF(i, j);
528 h[rowF(k) + i] = s;
529 }
530 for (std::size_t i = 0; i < NbB[k]; ++i) {
531 T s = zero;
532 for (std::size_t j = 0; j < N; ++j) s += gB(i, j);
533 h[rowB(k) + i] = s;
534 }
535 for (std::size_t i = 0; i < N; ++i) h[rowM(k + 1) + i] = one;
536 }
537 for (std::size_t i = 0; i < Neq; ++i) M(i, 0) = h[i];
538 }
539
540 // b M = rhs, i.e. M^T b^T = rhs^T.
541 Matrix<T> Mt(Neq, Neq, zero);
542 for (std::size_t i = 0; i < Neq; ++i)
543 for (std::size_t j = 0; j < Neq; ++j) Mt(i, j) = M(j, i);
544 std::vector<T> rhs(Neq, zero);
545 rhs[0] = one;
546 const std::vector<T> b = solve(Mt, rhs);
547
549 out.Thr = Thr;
550 out.masses.assign(K + 1, std::vector<T>(N, zero));
551 for (std::size_t k = 0; k <= K; ++k)
552 for (std::size_t j = 0; j < N; ++j) out.masses[k][j] = b[rowM(k) + j];
553 for (std::size_t k = 0; k < K; ++k) {
554 out.iniF.push_back(std::vector<T>(b.begin() + static_cast<long>(rowF(k)),
555 b.begin() + static_cast<long>(rowF(k) + NbF[k])));
556 out.iniB.push_back(std::vector<T>(b.begin() + static_cast<long>(rowB(k)),
557 b.begin() + static_cast<long>(rowB(k) + NbB[k])));
558 out.KF.push_back(KF[k]);
559 out.KB.push_back(KB[k]);
560 out.cloF.push_back(cloF[k]);
561 out.cloB.push_back(cloB[k]);
562 }
563 return out;
564}
565
566/** mfq_ld_solve with reflective boundaries, Qt = Q and the default prec = 1e-14. */
567template <class T>
569 const std::vector<Matrix<T>>& R,
570 const std::vector<Matrix<T>>& S,
571 const std::vector<T>& Thr) {
572 return mfq_ld_solve(Q, R, S, Thr, std::vector<FluidBoundary>(), std::vector<FluidBoundary>(),
573 std::vector<Matrix<T>>(), T(num_traits<T>::from_double(1e-14)));
574}
575
576} // namespace mam
577} // namespace line
578
579#endif // LINE_API_MAM_MFQ_LD_SOLVE_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.
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 density and distribution of a first- or second-order level-dependent (multi-regime) Markov...
Stationary mean fluid level E[X] of a first- or second-order level-dependent (multi-regime) Markovian...
Core of the Markovian fluid queue: the fundamental matrices Psi, K, U and the matrix-exponential stat...
FluidBoundary
Boundary behaviour of one background state at a reflecting level.
Matrix< T > qbd_R(const Matrix< T > &B, const Matrix< T > &L, const Matrix< T > &F, unsigned iter_max, const T &tol)
R by successive substitutions (qbd_R.m): iterate R <- -(F + R^2 B) L^-1.
Definition qbd_r.h:184
LevelDependentFluidBlocks< T > mfq_ld_solve(const std::vector< Matrix< T > > &Q, const std::vector< Matrix< T > > &R, const std::vector< Matrix< T > > &S, const std::vector< T > &Thr, const std::vector< FluidBoundary > &boundaryL, const std::vector< FluidBoundary > &boundaryU, const std::vector< Matrix< T > > &Qt, const T &prec)
Solve a first- or second-order level-dependent fluid queue.
T num_abs(const T &v)
Definition number.h:172
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
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Number-type abstraction for the templated API port.
Quasi-birth-death processes: the rate matrix R, the fundamental matrix G, the caudal characteristic,...
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