LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mfq_multiregime.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_MULTIREGIME_H
6#define LINE_API_MAM_MFQ_MULTIREGIME_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Multi-regime FEEDBACK Markovian fluid queue: density, density derivative and
12 * distribution of the fluid level.
13 *
14 * Port of matlab/src/api/mam/mfq_multiregime.m and the BUTools multiregime it
15 * wraps, which implements H. E. Kankaya and N. Akar, "Solving Multi-Regime
16 * Feedback Fluid Queues". The generator and the drift rates are regime
17 * dependent, and separate FEEDBACK generators and rates govern the behaviour at
18 * each threshold, which is what distinguishes this from the level-dependent
19 * model behind mfq_ld_solve: there the thresholds only separate regimes, here
20 * the process can behave differently while sitting exactly on one.
21 *
22 * METHOD. Per regime, the zero-drift states are censored out and the remaining
23 * generator is rescaled by the drifts, giving A = Qbar diag(1/R). The spectrum
24 * of A splits into zero, negative and positive parts, and the three
25 * corresponding invariant subspaces carry the constant, the decaying-upward and
26 * the decaying-downward components of the density. A block-triangularizing
27 * similarity Y is built from the ordered real Schur form of A plus two
28 * Sylvester solves, after which the density in regime k is
29 *
30 * pi_k(x) = a0_k L0_k + an_k exp(An_k (x - T_k)) Ln_k
31 * + ap_k exp(-Ap_k (T_{k+1} - x)) Lp_k.
32 *
33 * The unknown coefficients, together with the point masses at the K+1
34 * thresholds, solve one linear system assembled from the reference's equations
35 * (8)-(16): flow balance at each boundary, the boundary conditions that the
36 * feedback rates impose on each state, and one normalization row that replaces
37 * the first balance equation.
38 *
39 * WHY THE SCHUR FORM, AND WHY NOT EIGENVECTORS. The eigenvector basis exists
40 * only when A is diagonalizable and is complex whenever the spectrum is;
41 * substituting it here would be a different algorithm wearing the same name.
42 * The real Schur basis is orthogonal and real for every real A, and the
43 * ordering by eigenvalue sign class is what isolates the three subspaces. That
44 * is why line/util/eig.h grew schur_decomposition and schur_reorder for this
45 * function.
46 *
47 * DOUBLE ONLY, AND HONESTLY SO. Unlike mfq_ld_distr -- where the only
48 * eigenvalues merely SELECT a branch and never reach a returned number -- here
49 * the Schur factors Z and T are the basis in which An, Ap, L0, Ln and Lp are
50 * expressed, so they enter every value the function returns. util/eig.h is
51 * double-only by design (the eigenvalues of a rational matrix are algebraic,
52 * not rational, and LAPACK cannot supply a multiprecision QR), so this function
53 * takes and returns Matrix<double> rather than being templated. Instantiating
54 * it at Real50 would advertise a precision it cannot deliver, since everything
55 * downstream of the Schur step would be carrying double-accurate inputs. It is
56 * registered as {Double} for that reason and no other. When the build has no
57 * LAPACK it refuses by name through schur_decomposition rather than guessing a
58 * basis.
59 *
60 * COMPARING THE OUTPUT AGAINST MATLAB. The Cdfm column at level zero is an
61 * EXACT ZERO in this port and comes back from MATLAB as -6.26e-17, its solve
62 * having rounded. Any comparison of that column must therefore be ABSOLUTE, not
63 * relative: a relative test against a value that is exactly zero fails on a
64 * difference of one ulp and reports a defect that is not there. The same holds
65 * for any state carrying no mass at a threshold. This is stated here rather
66 * than only in the test so that a later reader does not "tidy" the absolute
67 * comparison into a relative one and manufacture a failure.
68 *
69 * THE SYLVESTER EQUATIONS ARE NOT THE OBSTACLE. A X + X B = C vectorizes to
70 * (I kron A + B^T kron I) vec(X) = vec(C), one ordinary linear solve at these
71 * block sizes, needing no Bartels-Stewart and no Schur form of its own. The
72 * reference calls MATLAB's sylvester, which is Bartels-Stewart; the two compute
73 * the same X, and at the sizes reached here the direct solve is not the slower
74 * one.
75 */
76
77#include <cmath>
78#include <cstddef>
79#include <vector>
80
82#include "line/num/number.h"
83#include "line/util/eig.h"
84#include "line/util/error.h"
85#include "line/util/expm.h"
86#include "line/util/linalg.h"
87#include "line/util/lu.h"
88#include "line/util/matrix.h"
89
90namespace line {
91namespace mam {
92
93/** Return value of mfq_multiregime: one row of N per-state values per point. */
95 std::vector<std::vector<double>> pdf; ///< density at each pdf point
96 std::vector<std::vector<double>> pdfd; ///< derivative of the density
97 std::vector<std::vector<double>> cdf; ///< P(X < p)
98 std::vector<std::vector<double>> cdfm; ///< P(X <= p)
99};
100
101namespace multiregime_detail {
102
103/**
104 * Solve the Sylvester equation A X + X B = C for X, with A of order n, B of
105 * order m and C of size n x m, by the Kronecker form
106 * (I_m kron A + B^T kron I_n) vec(X) = vec(C), vec being column-major.
107 */
108inline Matrix<double> sylvester(const Matrix<double>& A, const Matrix<double>& B,
109 const Matrix<double>& C) {
110 const std::size_t n = A.rows(), m = B.rows();
111 if (A.cols() != n || B.cols() != m || C.rows() != n || C.cols() != m)
112 throw InputError("mfq_multiregime sylvester: blocks are not conformable");
113 Matrix<double> X(n, m, 0.0);
114 if (n == 0 || m == 0) return X;
115 Matrix<double> K(n * m, n * m, 0.0);
116 std::vector<double> rhs(n * m, 0.0);
117 for (std::size_t j = 0; j < m; ++j)
118 for (std::size_t i = 0; i < n; ++i) {
119 const std::size_t row = j * n + i;
120 for (std::size_t k = 0; k < n; ++k) K(row, j * n + k) += A(i, k);
121 for (std::size_t k = 0; k < m; ++k) K(row, k * n + i) += B(k, j);
122 rhs[row] = C(i, j);
123 }
124 const std::vector<double> x = solve(K, rhs);
125 for (std::size_t j = 0; j < m; ++j)
126 for (std::size_t i = 0; i < n; ++i) X(i, j) = x[j * n + i];
127 return X;
128}
129
130/**
131 * expm of a possibly EMPTY block. A regime whose spectrum has no negative (or
132 * no positive) part leaves An (or Ap) at order zero, which is a normal state of
133 * this model and not a degenerate one: it simply means the density has no
134 * component decaying in that direction. util/expm.h rejects an empty matrix, so
135 * the empty case is answered here with the empty matrix rather than by relaxing
136 * that check for every other caller.
137 */
138inline Matrix<double> expm0(const Matrix<double>& A, double t) {
139 if (A.rows() == 0) return Matrix<double>(0, 0, 0.0);
140 return expm(A, t);
141}
142
143/** Sub-block A(r0..r0+nr-1, c0..c0+nc-1); an empty request gives an empty matrix. */
144inline Matrix<double> blk(const Matrix<double>& A, std::size_t r0, std::size_t c0, std::size_t nr,
145 std::size_t nc) {
146 Matrix<double> B(nr, nc, 0.0);
147 for (std::size_t i = 0; i < nr; ++i)
148 for (std::size_t j = 0; j < nc; ++j) B(i, j) = A(r0 + i, c0 + j);
149 return B;
150}
151
152/** Rows of A selected by ri, columns by ci. */
153inline Matrix<double> pick(const Matrix<double>& A, const std::vector<std::size_t>& ri,
154 const std::vector<std::size_t>& ci) {
155 Matrix<double> B(ri.size(), ci.size(), 0.0);
156 for (std::size_t i = 0; i < ri.size(); ++i)
157 for (std::size_t j = 0; j < ci.size(); ++j) B(i, j) = A(ri[i], ci[j]);
158 return B;
159}
160
161/** The per-regime quantities the assembly and the evaluation both need. */
162struct Regime {
163 Matrix<double> An, Ap; ///< negative and positive spectral blocks
164 Matrix<double> L0, Ln, Lp; ///< the three closing matrices, each ? x N
165 Matrix<double> M0, MT, Mi; ///< regime transfer matrices at 0, at T and integrated
166 std::size_t nzero = 0, nneg = 0, npos = 0;
167};
168
169} // namespace multiregime_detail
170
171/**
172 * Multi-regime feedback fluid queue.
173 *
174 * @param Q per-regime generators, K of them; a single entry is
175 * replicated across all regimes, as in the reference
176 * @param R per-regime drift RATE VECTORS of length N (not matrices)
177 * @param Qt boundary generators, K+1 of them; empty means
178 * {Q[0], Q[0], ..., Q[K-1]}, a single entry is replicated
179 * @param Rt boundary rate vectors, K+1 of them; empty means
180 * {R[0], R[0], ..., R[K-1]}
181 * @param Thr the K thresholds
182 * @param pdfpoints levels at which the density and its derivative are wanted
183 * @param cdfpoints levels at which the distribution is wanted
184 */
186 const std::vector<std::vector<double>>& R,
187 const std::vector<Matrix<double>>& Qt,
188 const std::vector<std::vector<double>>& Rt,
189 const std::vector<double>& Thr,
190 const std::vector<double>& pdfpoints,
191 const std::vector<double>& cdfpoints) {
192 using namespace multiregime_detail;
193 const std::size_t K = R.size();
194 if (K == 0) throw InputError("mfq_multiregime: at least one regime is required");
195 if (Thr.size() != K) throw InputError("mfq_multiregime: one threshold per regime is required");
196 const std::size_t N = R[0].size();
197 if (N == 0) throw InputError("mfq_multiregime: the background chain is empty");
198
199 // Convenience expansions, exactly the reference's.
200 std::vector<Matrix<double>> Qk = Q;
201 if (Qk.size() == 1)
202 for (std::size_t k = 1; k < K; ++k) Qk.push_back(Qk[0]);
203 if (Qk.size() != K) throw InputError("mfq_multiregime: expected one generator per regime");
204 std::vector<Matrix<double>> Qtk = Qt;
205 if (Qtk.empty()) {
206 Qtk.push_back(Qk[0]);
207 for (std::size_t k = 0; k < K; ++k) Qtk.push_back(Qk[k]);
208 } else if (Qtk.size() == 1) {
209 for (std::size_t k = 1; k < K + 1; ++k) Qtk.push_back(Qtk[0]);
210 }
211 if (Qtk.size() != K + 1)
212 throw InputError("mfq_multiregime: expected K+1 boundary generators");
213 std::vector<std::vector<double>> Rtk = Rt;
214 if (Rtk.empty()) {
215 Rtk.push_back(R[0]);
216 for (std::size_t k = 0; k < K; ++k) Rtk.push_back(R[k]);
217 }
218 if (Rtk.size() != K + 1)
219 throw InputError("mfq_multiregime: expected K+1 boundary rate vectors");
220 for (std::size_t k = 0; k < K; ++k) {
221 if (R[k].size() != N || Qk[k].rows() != N || Qk[k].cols() != N)
222 throw InputError("mfq_multiregime: a regime's Q or R has the wrong order");
223 }
224
225 // Thresholds with the implicit zero in front.
226 std::vector<double> Tv(K + 1, 0.0);
227 for (std::size_t k = 0; k < K; ++k) Tv[k + 1] = Thr[k];
228
229 // ---- per-regime transfer matrices ----
230 std::vector<Regime> reg(K);
231 std::vector<std::size_t> Nnz(K, 0), Npos(K + 1, 0);
232 for (std::size_t k = 0; k < K; ++k) {
233 std::vector<std::size_t> zix, nzix;
234 for (std::size_t i = 0; i < N; ++i) (R[k][i] == 0.0 ? zix : nzix).push_back(i);
235 const std::size_t Nn = nzix.size(), Nz = zix.size();
236 if (Nn == 0)
237 throw InputError("mfq_multiregime: a regime has no state with a non-zero drift");
238
239 // Censor the zero-drift states, then rescale by the drifts.
240 Matrix<double> Qnk = pick(Qk[k], nzix, nzix);
241 Matrix<double> Wz(Nn, Nz, 0.0); // Q(nz,z) inv(-Q(z,z)), reused for the L blocks
242 if (Nz > 0) {
243 Matrix<double> negQzz = pick(Qk[k], zix, zix);
244 for (std::size_t i = 0; i < Nz; ++i)
245 for (std::size_t j = 0; j < Nz; ++j) negQzz(i, j) = -negQzz(i, j);
246 Matrix<double> iQzz(Nz, Nz, 0.0);
247 try {
248 iQzz = inverse(negQzz);
249 } catch (const NumericError&) {
250 throw NumericError(
251 "mfq_multiregime: the zero-drift states of a regime form a closed set, so the "
252 "fluid can be trapped at a constant level and the regime has no stationary "
253 "law");
254 }
255 Wz = matmul(pick(Qk[k], nzix, zix), iQzz);
256 Qnk = mfq_detail::add(Qnk, matmul(Wz, pick(Qk[k], zix, nzix)));
257 }
258 Matrix<double> A(Nn, Nn, 0.0);
259 for (std::size_t i = 0; i < Nn; ++i)
260 for (std::size_t j = 0; j < Nn; ++j) A(i, j) = Qnk(i, j) / R[k][nzix[j]];
261
262 // ordered real Schur form rationale: see _kb/03-api-layer.md (cpp port notes: mam)
263 const RealSchur s0 = schur_decomposition(A);
264 std::vector<double> key(Nn, 0.0);
265 std::size_t nzero = 0;
266 for (std::size_t i = 0; i < Nn; ++i) {
267 const double d = s0.T(i, i);
268 key[i] = (std::fabs(d) < 1e-10 ? 5.0 : 0.0) + (d < 0.0 ? 2.0 : 0.0) +
269 (d > 0.0 ? 1.0 : 0.0);
270 if (std::fabs(d) < 1e-10) ++nzero;
271 }
272 // A 2 x 2 block is one complex pair and must not be split; give both
273 // rows the key of the first, as MATLAB requires of a select vector.
274 for (std::size_t i = 0; i + 1 < Nn; ++i)
275 if (s0.T(i + 1, i) != 0.0) key[i + 1] = key[i];
276 const RealSchur s = schur_reorder(s0, key);
277
278 std::size_t nneg = 0, npos = 0;
279 for (std::size_t i = nzero; i < Nn; ++i) {
280 if (s.T(i, i) < 0.0) ++nneg;
281 else if (s.T(i, i) > 0.0) ++npos;
282 }
283 if (nzero + nneg + npos != Nn)
284 throw NumericError(
285 "mfq_multiregime: the spectrum of a regime could not be split into zero, negative "
286 "and positive parts");
287
288 // X1 solves 0 X1 + X1 (-D22) = D12, X2 solves Dnn X2 - X2 Dpp = Dnp.
289 const Matrix<double> D22 = blk(s.T, nzero, nzero, Nn - nzero, Nn - nzero);
290 Matrix<double> negD22 = D22;
291 for (std::size_t i = 0; i < negD22.rows(); ++i)
292 for (std::size_t j = 0; j < negD22.cols(); ++j) negD22(i, j) = -negD22(i, j);
293 const Matrix<double> X1 =
294 sylvester(Matrix<double>(nzero, nzero, 0.0), negD22,
295 blk(s.T, 0, nzero, nzero, Nn - nzero));
296 Matrix<double> negDpp = blk(s.T, nzero + nneg, nzero + nneg, npos, npos);
297 for (std::size_t i = 0; i < npos; ++i)
298 for (std::size_t j = 0; j < npos; ++j) negDpp(i, j) = -negDpp(i, j);
299 const Matrix<double> X2 = sylvester(blk(s.T, nzero, nzero, nneg, nneg), negDpp,
300 blk(s.T, nzero, nzero + nneg, nneg, npos));
301
302 // Y = Z [I -X1; 0 I] [I 0 0; 0 I -X2; 0 0 I].
304 for (std::size_t i = 0; i < nzero; ++i)
305 for (std::size_t j = 0; j < Nn - nzero; ++j) B1(i, nzero + j) = -X1(i, j);
307 for (std::size_t i = 0; i < nneg; ++i)
308 for (std::size_t j = 0; j < npos; ++j) B2(nzero + i, nzero + nneg + j) = -X2(i, j);
309 const Matrix<double> Y = matmul(matmul(s.Z, B1), B2);
310 const Matrix<double> iY = inverse(Y);
311 const Matrix<double> At = matmul(matmul(iY, A), Y);
312
313 Regime& rg = reg[k];
314 rg.nzero = nzero;
315 rg.nneg = nneg;
316 rg.npos = npos;
317 rg.An = blk(At, nzero, nzero, nneg, nneg);
318 rg.Ap = blk(At, nzero + nneg, nzero + nneg, npos, npos);
319
320 // L blocks: the non-zero-drift columns are the rows of iY, the
321 // zero-drift ones are routed through the censoring operator.
322 const Matrix<double> iY0 = blk(iY, 0, 0, nzero, Nn);
323 const Matrix<double> iYn = blk(iY, nzero, 0, nneg, Nn);
324 const Matrix<double> iYp = blk(iY, nzero + nneg, 0, npos, Nn);
325 const Matrix<double> Z0 = (Nz > 0) ? matmul(iY0, Wz) : Matrix<double>(nzero, 0, 0.0);
326 const Matrix<double> Zn = (Nz > 0) ? matmul(iYn, Wz) : Matrix<double>(nneg, 0, 0.0);
327 const Matrix<double> Zp = (Nz > 0) ? matmul(iYp, Wz) : Matrix<double>(npos, 0, 0.0);
328 rg.L0 = Matrix<double>(nzero, N, 0.0);
329 rg.Ln = Matrix<double>(nneg, N, 0.0);
330 rg.Lp = Matrix<double>(npos, N, 0.0);
331 for (std::size_t j = 0; j < Nn; ++j) {
332 for (std::size_t i = 0; i < nzero; ++i) rg.L0(i, nzix[j]) = iY0(i, j);
333 for (std::size_t i = 0; i < nneg; ++i) rg.Ln(i, nzix[j]) = iYn(i, j);
334 for (std::size_t i = 0; i < npos; ++i) rg.Lp(i, nzix[j]) = iYp(i, j);
335 }
336 for (std::size_t j = 0; j < Nz; ++j) {
337 for (std::size_t i = 0; i < nzero; ++i) rg.L0(i, zix[j]) = Z0(i, j);
338 for (std::size_t i = 0; i < nneg; ++i) rg.Ln(i, zix[j]) = Zn(i, j);
339 for (std::size_t i = 0; i < npos; ++i) rg.Lp(i, zix[j]) = Zp(i, j);
340 }
341
342 const double Tk = Tv[k + 1] - Tv[k];
343 Matrix<double> negApT = rg.Ap;
344 for (std::size_t i = 0; i < npos; ++i)
345 for (std::size_t j = 0; j < npos; ++j) negApT(i, j) = -negApT(i, j);
346 const Matrix<double> EAn = expm0(rg.An, Tk);
347 const Matrix<double> EAp = expm0(negApT, Tk); // exp(-Ap Tk)
348
349 // M0 = [L0; Ln; exp(-Ap Tk) Lp], MT = [L0; exp(An Tk) Ln; Lp].
350 rg.M0 = Matrix<double>(Nn, N, 0.0);
351 rg.MT = Matrix<double>(Nn, N, 0.0);
352 rg.Mi = Matrix<double>(Nn, N, 0.0);
353 const Matrix<double> EApLp = matmul(EAp, rg.Lp);
354 const Matrix<double> EAnLn = matmul(EAn, rg.Ln);
355 for (std::size_t i = 0; i < nzero; ++i)
356 for (std::size_t j = 0; j < N; ++j) {
357 rg.M0(i, j) = rg.L0(i, j);
358 rg.MT(i, j) = rg.L0(i, j);
359 rg.Mi(i, j) = Tk * rg.L0(i, j);
360 }
361 for (std::size_t i = 0; i < nneg; ++i)
362 for (std::size_t j = 0; j < N; ++j) {
363 rg.M0(nzero + i, j) = rg.Ln(i, j);
364 rg.MT(nzero + i, j) = EAnLn(i, j);
365 }
366 for (std::size_t i = 0; i < npos; ++i)
367 for (std::size_t j = 0; j < N; ++j) {
368 rg.M0(nzero + nneg + i, j) = EApLp(i, j);
369 rg.MT(nzero + nneg + i, j) = rg.Lp(i, j);
370 }
371 // Mi = [Tk L0; (-An)^-1 (I - exp(An Tk)) Ln; Ap^-1 (I - exp(-Ap Tk)) Lp].
372 if (nneg > 0) {
373 Matrix<double> negAn = rg.An;
374 for (std::size_t i = 0; i < nneg; ++i)
375 for (std::size_t j = 0; j < nneg; ++j) negAn(i, j) = -negAn(i, j);
376 Matrix<double> ImE = eye<double>(nneg);
377 for (std::size_t i = 0; i < nneg; ++i)
378 for (std::size_t j = 0; j < nneg; ++j) ImE(i, j) -= EAn(i, j);
379 const Matrix<double> blkn = matmul(matmul(inverse(negAn), ImE), rg.Ln);
380 for (std::size_t i = 0; i < nneg; ++i)
381 for (std::size_t j = 0; j < N; ++j) rg.Mi(nzero + i, j) = blkn(i, j);
382 }
383 if (npos > 0) {
384 Matrix<double> ImE = eye<double>(npos);
385 for (std::size_t i = 0; i < npos; ++i)
386 for (std::size_t j = 0; j < npos; ++j) ImE(i, j) -= EAp(i, j);
387 const Matrix<double> blkp = matmul(matmul(inverse(rg.Ap), ImE), rg.Lp);
388 for (std::size_t i = 0; i < npos; ++i)
389 for (std::size_t j = 0; j < N; ++j) rg.Mi(nzero + nneg + i, j) = blkp(i, j);
390 }
391
392 Nnz[k] = Nn;
393 Npos[k + 1] = Npos[k] + Nn;
394 }
395
396 // linear system layout: see _kb/03-api-layer.md (cpp port notes: mam)
397 const std::size_t d = (K + 1) * N;
398 const std::size_t Neq = d + Npos[K];
399 Matrix<double> M(Neq, Neq, 0.0);
400 std::size_t p = 0;
401
402 // eq. (12): balance at level zero.
403 for (std::size_t j = 0; j < N; ++j)
404 for (std::size_t i = 0; i < N; ++i) M(i, p + j) = -Qtk[0](i, j);
405 for (std::size_t j = 0; j < N; ++j)
406 for (std::size_t i = 0; i < Nnz[0]; ++i)
407 M(d + Npos[0] + i, p + j) = reg[0].M0(i, j) * R[0][j];
408 p += N;
409 // eq. (13): balance at each interior threshold.
410 for (std::size_t k = 0; k + 1 < K; ++k) {
411 for (std::size_t j = 0; j < N; ++j)
412 for (std::size_t i = 0; i < N; ++i) M((k + 1) * N + i, p + j) = -Qtk[k + 1](i, j);
413 for (std::size_t j = 0; j < N; ++j) {
414 for (std::size_t i = 0; i < Nnz[k + 1]; ++i)
415 M(d + Npos[k + 1] + i, p + j) = reg[k + 1].M0(i, j) * R[k + 1][j];
416 for (std::size_t i = 0; i < Nnz[k]; ++i)
417 M(d + Npos[k] + i, p + j) = -reg[k].MT(i, j) * R[k][j];
418 }
419 p += N;
420 }
421 // eq. (16): balance at the top threshold.
422 for (std::size_t j = 0; j < N; ++j)
423 for (std::size_t i = 0; i < N; ++i) M(K * N + i, p + j) = -Qtk[K](i, j);
424 for (std::size_t j = 0; j < N; ++j)
425 for (std::size_t i = 0; i < Nnz[K - 1]; ++i)
426 M(d + Npos[K - 1] + i, p + j) = -reg[K - 1].MT(i, j) * R[K - 1][j];
427 p += N;
428
429 // eq. (8): no mass at level zero in an up-drift state.
430 for (std::size_t m = 0; m < N; ++m)
431 if (R[0][m] > 0.0) M(m, p++) = 1.0;
432 // eq. (11): no mass at the top threshold in a down-drift state.
433 for (std::size_t m = 0; m < N; ++m)
434 if (R[K - 1][m] < 0.0) M(K * N + m, p++) = 1.0;
435 // eq. (9): no mass where the drift keeps its sign across a threshold.
436 for (std::size_t k = 0; k + 1 < K; ++k)
437 for (std::size_t m = 0; m < N; ++m)
438 if ((R[k][m] > 0.0 && R[k + 1][m] > 0.0) || (R[k][m] < 0.0 && R[k + 1][m] < 0.0))
439 M((k + 1) * N + m, p++) = 1.0;
440 // eq. (10): nor where the drift reverses upward through a live boundary.
441 for (std::size_t k = 0; k + 1 < K; ++k)
442 for (std::size_t m = 0; m < N; ++m)
443 if (R[k][m] < 0.0 && R[k + 1][m] > 0.0 && Rtk[k + 1][m] != 0.0)
444 M((k + 1) * N + m, p++) = 1.0;
445 // eq. (14): continuity of the density from below at a threshold.
446 for (std::size_t k = 0; k + 1 < K; ++k)
447 for (std::size_t m = 0; m < N; ++m)
448 if (R[k][m] < 0.0 && Rtk[k + 1][m] >= 0.0) {
449 for (std::size_t i = 0; i < Nnz[k]; ++i)
450 M(d + Npos[k] + i, p) = reg[k].MT(i, m);
451 ++p;
452 }
453 // eq. (15): and from above.
454 for (std::size_t k = 0; k + 1 < K; ++k)
455 for (std::size_t m = 0; m < N; ++m)
456 if (R[k + 1][m] > 0.0 && Rtk[k + 1][m] <= 0.0) {
457 for (std::size_t i = 0; i < Nnz[k + 1]; ++i)
458 M(d + Npos[k + 1] + i, p) = reg[k + 1].M0(i, m);
459 ++p;
460 }
461 if (p != Neq)
462 throw NumericError(
463 "mfq_multiregime: the boundary conditions do not close the system; check that the "
464 "feedback rates Rt satisfy the model's continuity requirements");
465
466 // Normalization replaces the FIRST equation: total mass plus the integral
467 // of every regime's density is one.
468 for (std::size_t i = 0; i < d; ++i) M(i, 0) = 1.0;
469 for (std::size_t k = 0; k < K; ++k)
470 for (std::size_t i = 0; i < Nnz[k]; ++i) {
471 double s = 0.0;
472 for (std::size_t j = 0; j < N; ++j) s += reg[k].Mi(i, j);
473 M(d + Npos[k] + i, 0) = s;
474 }
475
476 // sol M = rhs, i.e. M^T sol^T = rhs^T.
477 Matrix<double> Mt(Neq, Neq, 0.0);
478 for (std::size_t i = 0; i < Neq; ++i)
479 for (std::size_t j = 0; j < Neq; ++j) Mt(i, j) = M(j, i);
480 std::vector<double> rhs(Neq, 0.0);
481 rhs[0] = 1.0;
482 const std::vector<double> sol = solve(Mt, rhs);
483
484 // ---- extract the masses and the density coefficients ----
485 std::vector<std::vector<double>> masses(K + 1, std::vector<double>(N, 0.0));
486 for (std::size_t k = 0; k <= K; ++k)
487 for (std::size_t j = 0; j < N; ++j) masses[k][j] = sol[k * N + j];
488 std::vector<std::vector<double>> a0(K), an(K), ap(K);
489 for (std::size_t k = 0; k < K; ++k) {
490 const std::size_t base = d + Npos[k];
491 a0[k].assign(sol.begin() + static_cast<long>(base),
492 sol.begin() + static_cast<long>(base + reg[k].nzero));
493 an[k].assign(sol.begin() + static_cast<long>(base + reg[k].nzero),
494 sol.begin() + static_cast<long>(base + reg[k].nzero + reg[k].nneg));
495 ap[k].assign(sol.begin() + static_cast<long>(base + reg[k].nzero + reg[k].nneg),
496 sol.begin() + static_cast<long>(base + Nnz[k]));
497 }
498
499 // The regime holding a point, by the reference's walk.
500 auto regime_of = [&](double p_) -> std::size_t {
501 std::size_t k = 0;
502 while (k < K && p_ >= Tv[k]) ++k;
503 return k - 1;
504 };
505
507 for (double pt : pdfpoints) {
508 if (pt < 0.0) throw InputError("mfq_multiregime: the evaluation points must be non-negative");
509 const std::size_t k = regime_of(pt);
510 Matrix<double> negAp = reg[k].Ap;
511 for (std::size_t i = 0; i < reg[k].npos; ++i)
512 for (std::size_t j = 0; j < reg[k].npos; ++j) negAp(i, j) = -negAp(i, j);
513 const Matrix<double> En = expm0(reg[k].An, pt - Tv[k]);
514 const Matrix<double> Ep = expm0(negAp, Tv[k + 1] - pt);
515 std::vector<double> f(N, 0.0), fd(N, 0.0);
516 {
517 const std::vector<double> v0 = vecmul(a0[k], reg[k].L0);
518 const std::vector<double> vn = vecmul(vecmul(an[k], En), reg[k].Ln);
519 const std::vector<double> vp = vecmul(vecmul(ap[k], Ep), reg[k].Lp);
520 for (std::size_t j = 0; j < N; ++j) f[j] = v0[j] + vn[j] + vp[j];
521 const std::vector<double> dn =
522 vecmul(vecmul(vecmul(an[k], reg[k].An), En), reg[k].Ln);
523 const std::vector<double> dp =
524 vecmul(vecmul(vecmul(ap[k], reg[k].Ap), Ep), reg[k].Lp);
525 for (std::size_t j = 0; j < N; ++j) fd[j] = dn[j] + dp[j];
526 }
527 out.pdf.push_back(f);
528 out.pdfd.push_back(fd);
529 }
530
531 for (double c : cdfpoints) {
532 if (c < 0.0) throw InputError("mfq_multiregime: the evaluation points must be non-negative");
533 std::vector<double> cres(N, 0.0), cresm(N, 0.0);
534 std::size_t k = 0;
535 while (k < K && c >= Tv[k]) {
536 if (k > 0) {
537 std::vector<double> coef;
538 coef.insert(coef.end(), a0[k - 1].begin(), a0[k - 1].end());
539 coef.insert(coef.end(), an[k - 1].begin(), an[k - 1].end());
540 coef.insert(coef.end(), ap[k - 1].begin(), ap[k - 1].end());
541 const std::vector<double> v = vecmul(coef, reg[k - 1].Mi);
542 for (std::size_t j = 0; j < N; ++j) {
543 cres[j] += v[j];
544 cresm[j] += v[j];
545 }
546 }
547 for (std::size_t j = 0; j < N; ++j) cresm[j] += masses[k][j];
548 if (c > Tv[k])
549 for (std::size_t j = 0; j < N; ++j) cres[j] += masses[k][j];
550 ++k;
551 }
552 if (k == K && c == Tv[K])
553 for (std::size_t j = 0; j < N; ++j) cresm[j] += masses[K][j];
554 const std::size_t kk = k - 1;
555 const double crem = c - Tv[kk];
556 const double Tk = Tv[kk + 1] - Tv[kk];
557 Matrix<double> negAn = reg[kk].An, negAp = reg[kk].Ap;
558 for (std::size_t i = 0; i < reg[kk].nneg; ++i)
559 for (std::size_t j = 0; j < reg[kk].nneg; ++j) negAn(i, j) = -negAn(i, j);
560 for (std::size_t i = 0; i < reg[kk].npos; ++i)
561 for (std::size_t j = 0; j < reg[kk].npos; ++j) negAp(i, j) = -negAp(i, j);
562 std::vector<double> val(N, 0.0);
563 {
564 const std::vector<double> v0 = vecmul(a0[kk], reg[kk].L0);
565 for (std::size_t j = 0; j < N; ++j) val[j] += v0[j] * crem;
566 }
567 if (reg[kk].nneg > 0) {
568 Matrix<double> ImE = eye<double>(reg[kk].nneg);
569 const Matrix<double> E = expm0(reg[kk].An, crem);
570 for (std::size_t i = 0; i < reg[kk].nneg; ++i)
571 for (std::size_t j = 0; j < reg[kk].nneg; ++j) ImE(i, j) -= E(i, j);
572 const std::vector<double> v =
573 vecmul(vecmul(vecmul(an[kk], inverse(negAn)), ImE), reg[kk].Ln);
574 for (std::size_t j = 0; j < N; ++j) val[j] += v[j];
575 }
576 if (reg[kk].npos > 0) {
577 const Matrix<double> Ea = expm0(negAp, Tk);
578 const Matrix<double> Eb = expm0(negAp, Tk - crem);
579 Matrix<double> D = Ea;
580 for (std::size_t i = 0; i < reg[kk].npos; ++i)
581 for (std::size_t j = 0; j < reg[kk].npos; ++j) D(i, j) -= Eb(i, j);
582 const std::vector<double> v =
583 vecmul(vecmul(vecmul(ap[kk], inverse(negAp)), D), reg[kk].Lp);
584 for (std::size_t j = 0; j < N; ++j) val[j] += v[j];
585 }
586 for (std::size_t j = 0; j < N; ++j) {
587 cres[j] += val[j];
588 cresm[j] += val[j];
589 }
590 out.cdf.push_back(cres);
591 out.cdfm.push_back(cresm);
592 }
593 return out;
594}
595
596} // namespace mam
597} // namespace line
598
599#endif // LINE_API_MAM_MFQ_MULTIREGIME_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
The algorithm cannot proceed on this instance (singular matrix, ...).
Definition error.h:43
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.
Core of the Markovian fluid queue: the fundamental matrices Psi, K, U and the matrix-exponential stat...
MultiRegimeResult mfq_multiregime(const std::vector< Matrix< double > > &Q, const std::vector< std::vector< double > > &R, const std::vector< Matrix< double > > &Qt, const std::vector< std::vector< double > > &Rt, const std::vector< double > &Thr, const std::vector< double > &pdfpoints, const std::vector< double > &cdfpoints)
Multi-regime feedback fluid queue.
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
RealSchur schur_reorder(const RealSchur &s, const std::vector< double > &key)
Reorder the diagonal blocks of a real Schur form into DESCENDING key order, stably,...
Definition eig.h:248
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 > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
RealSchur schur_decomposition(const Matrix< double > &A)
Real Schur factorization of a general square matrix (LAPACK dgees, unsorted).
Definition eig.h:182
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Number-type abstraction for the templated API port.
Real Schur factorization A = Z T Z^T, with Z orthogonal and T upper quasi-triangular: 1 x 1 diagonal ...
Definition eig.h:169
Matrix< double > Z
orthogonal Schur vectors
Definition eig.h:170
Matrix< double > T
upper quasi-triangular factor
Definition eig.h:171
Return value of mfq_multiregime: one row of N per-state values per point.
std::vector< std::vector< double > > cdf
P(X < p).
std::vector< std::vector< double > > cdfm
P(X <= p).
std::vector< std::vector< double > > pdfd
derivative of the density
std::vector< std::vector< double > > pdf
density at each pdf point