LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mfq_prio_queue.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_PRIO_QUEUE_H
6#define LINE_API_MAM_MFQ_PRIO_QUEUE_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Fluid priority queue: per-class fluid level and sojourn time of an
12 * MMAP[K]/PH[K]/1-type continuous fluid queue served in priority order.
13 *
14 * Port of matlab/src/api/mam/mfq_prio_queue.m and the BUTools FluidPrioQueue it
15 * wraps, which implements G. Horvath, "Efficient analysis of the
16 * MMAP[K]/PH[K]/1 priority queue", EJOR 246(1):128-139, 2015. A background
17 * chain with generator Q modulates the per-class fluid input rates R (one row
18 * per class) and the server drains fluid at the constant rate d, higher
19 * priority fluid first.
20 *
21 * PRIORITY ORDER: ROW K IS THE HIGHEST PRIORITY, ROW 1 THE LOWEST. This is the
22 * opposite of the obvious reading and is worth stating first, because a port
23 * that assumes row 1 is highest produces plausible and wrong numbers. Two
24 * things in the reference fix it: the workload seen by class k is built from
25 * sum(R(k:end,:)), i.e. class k together with every class ABOVE it, and the
26 * k == K branch analyses class K as if no other class existed, which only the
27 * highest priority class can be.
28 *
29 * METHOD. For each class k, the workload of classes k..K is a fluid queue with
30 * net drift diag(sum(R(k:end,:)))/d - I, solved by mfq_general_solve. That
31 * gives (mass0, ini, K, clo), which is put in the canonical similarity where
32 * the closing vector sums to one. For the highest priority class the answer
33 * follows directly from those matrices: its sojourn time is the matrix
34 * exponential law they define, and its fluid level is the ordinary fluid queue
35 * with constant service rate d (the needQL half of BUTools FluFluQueue, which
36 * is a handful of lines given mfq_general_solve, so it is inlined here rather
37 * than ported as a separate entry point).
38 *
39 * For a lower priority class the server is interrupted by everything above it,
40 * so the measure is a BUSY PERIOD REWARD of the fluid queue formed by stacking
41 * the class-k workload on top of the background chain:
42 * F = [K clo; 0 Q], C = [I 0; 0 diag(sum(R(k+1:end,:)))/d - I],
43 * D = [0 0; 0 I] for sojourn time, or
44 * D = [0 0; 0 diag(R(k,:))] for fluid level.
45 * Its moments come from a recursion in the derivatives of F(v) closed by a
46 * Sylvester solve at each order; its distribution comes from ERLANGIZATION,
47 * replacing the deterministic horizon t by an Erlang of order L and rate L/t,
48 * which converges as O(1/L) and is why erlMaxOrder is an explicit option rather
49 * than a hidden constant.
50 *
51 * AN EMPTY UP-DRIFT SET IS AN ANSWER HERE, NOT AN ERROR. The workload drift
52 * diag(sum(R(k:end,:)))/d - I is entirely negative whenever classes k..K never
53 * together exceed the service rate, and then that class simply never queues:
54 * every moment is zero and every distribution is one at every point. MATLAB
55 * returns exactly that, and it looks like a broken reference until the drift
56 * signs are checked. mfq_general_solve returns the degenerate law (no density,
57 * a point mass at zero equal to the stationary distribution) rather than
58 * refusing, which is what makes this path work.
59 *
60 * DOUBLE ONLY. mfq_general_solve is gated on transcendental arithmetic and the
61 * erlangization is a tolerance-controlled truncation, but neither of those is
62 * the binding constraint: this function is written on Matrix<double> for the
63 * same reason mfq_multiregime is, namely that it is only ever consumed
64 * alongside them, and templating it would advertise a precision tier nobody
65 * has asked for and no test covers. If a Real50 instantiation is ever wanted,
66 * nothing in the algorithm forbids it -- unlike mfq_multiregime, there is no
67 * eigendecomposition anywhere in this path.
68 *
69 * References: G. Horvath, EJOR 246(1):128-139, 2015.
70 */
71
72#include <cmath>
73#include <cstddef>
74#include <vector>
75
79#include "line/num/number.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 measures to compute, and the numerical options. */
91 std::size_t flMoms = 0; ///< number of fluid level moments, 0 = not wanted
92 std::vector<double> flDistr; ///< levels at which the fluid CDF is wanted
93 std::size_t stMoms = 0; ///< number of sojourn time moments
94 std::vector<double> stDistr; ///< times at which the sojourn CDF is wanted
95 double prec = 1e-14; ///< Riccati and matrix-quadratic tolerance
96 std::size_t erlMaxOrder = 200; ///< Erlang order of the erlangization
97 std::vector<std::size_t> classes; ///< 1-based classes to analyze, empty = all
98};
99
100/** One entry per analyzed class, in the order given by FluidPrioOptions::classes. */
102 std::vector<std::size_t> classes; ///< the classes analyzed, 1-based
103 std::vector<std::vector<double>> flMoms; ///< fluid level moments per class
104 std::vector<std::vector<double>> flDistr; ///< fluid level CDF per class
105 std::vector<std::vector<double>> stMoms; ///< sojourn time moments per class
106 std::vector<std::vector<double>> stDistr; ///< sojourn time CDF per class
107};
108
109namespace prio_detail {
110
111using multiregime_detail::blk;
112using multiregime_detail::expm0;
113using multiregime_detail::sylvester;
114
115/** MATLAB lyap(A, B, C) solves A X + X B + C = 0, i.e. sylvester(A, B, -C). */
116inline Matrix<double> lyap(const Matrix<double>& A, const Matrix<double>& B,
117 const Matrix<double>& C) {
118 Matrix<double> negC = C;
119 for (std::size_t i = 0; i < negC.rows(); ++i)
120 for (std::size_t j = 0; j < negC.cols(); ++j) negC(i, j) = -negC(i, j);
121 return sylvester(A, B, negC);
122}
123
124/** Diagonal matrix from a vector. */
125inline Matrix<double> dg(const std::vector<double>& v) {
126 Matrix<double> m(v.size(), v.size(), 0.0);
127 for (std::size_t i = 0; i < v.size(); ++i) m(i, i) = v[i];
128 return m;
129}
130
131/** A with every entry negated. */
132inline Matrix<double> neg(const Matrix<double>& A) {
133 Matrix<double> B = A;
134 for (std::size_t i = 0; i < B.rows(); ++i)
135 for (std::size_t j = 0; j < B.cols(); ++j) B(i, j) = -B(i, j);
136 return B;
137}
138
139/** Binomial coefficient, exact for the small orders reached here. */
140inline double nchoosek(std::size_t n, std::size_t k) {
141 double r = 1.0;
142 for (std::size_t i = 0; i < k; ++i)
143 r = r * static_cast<double>(n - i) / static_cast<double>(i + 1);
144 return std::floor(r + 0.5);
145}
146
147/**
148 * The n-th derivative of inv(v R - Q), valid even when R has zero diagonal
149 * entries. BUTools DReward: the zero-rate states are censored out, the
150 * derivative is taken on the remaining block in closed form, and the result is
151 * lifted back through the censoring operator.
152 */
153inline Matrix<double> dreward(const Matrix<double>& Q, const Matrix<double>& R, std::size_t n,
154 double prec) {
155 const std::size_t NQ = Q.rows();
156 std::vector<std::size_t> ixz, ixp;
157 for (std::size_t i = 0; i < NQ; ++i)
158 if (std::fabs(R(i, i)) <= prec) ixz.push_back(i);
159 // Both signs go into the non-zero group, in the reference's order: the
160 // positive rates first, then the negative ones.
161 for (std::size_t i = 0; i < NQ; ++i)
162 if (R(i, i) > prec) ixp.push_back(i);
163 for (std::size_t i = 0; i < NQ; ++i)
164 if (R(i, i) < -prec) ixp.push_back(i);
165 const std::size_t Nz = ixz.size(), Np = ixp.size();
166
167 const Matrix<double> Rp = multiregime_detail::pick(R, ixp, ixp);
168 const Matrix<double> Qpp = multiregime_detail::pick(Q, ixp, ixp);
169 const Matrix<double> Qpz = multiregime_detail::pick(Q, ixp, ixz);
170 const Matrix<double> Qzp = multiregime_detail::pick(Q, ixz, ixp);
171 const Matrix<double> Qzz = multiregime_detail::pick(Q, ixz, ixz);
172 const Matrix<double> iRp = inverse(Rp);
173 Matrix<double> inner = neg(Qpp);
174 if (Nz > 0)
175 inner = mfq_detail::sub(inner, matmul(matmul(Qpz, inverse(neg(Qzz))), Qzp));
176 // dXvn = (-1)^n n! (iRp inner)^-(n+1) iRp.
177 const Matrix<double> base = inverse(matmul(iRp, inner));
178 Matrix<double> pw = eye<double>(Np);
179 for (std::size_t i = 0; i <= n; ++i) pw = matmul(pw, base);
180 double fact = 1.0;
181 for (std::size_t i = 2; i <= n; ++i) fact *= static_cast<double>(i);
182 const double sgn = (n % 2 == 0) ? 1.0 : -1.0;
183 Matrix<double> dXvn = matmul(pw, iRp);
184 for (std::size_t i = 0; i < Np; ++i)
185 for (std::size_t j = 0; j < Np; ++j) dXvn(i, j) *= sgn * fact;
186
187 // Lift back: drpar is block [[Z Qzp dX Qpz Z, Z Qzp dX], [dX Qpz Z, dX]]
188 // in the (z, p) ordering, then permuted to the original state order.
189 Matrix<double> out(NQ, NQ, 0.0);
190 if (Nz > 0) {
191 const Matrix<double> Z = inverse(neg(Qzz));
192 const Matrix<double> ZQzp = matmul(Z, Qzp);
193 const Matrix<double> QpzZ = matmul(Qpz, Z);
194 const Matrix<double> tl = matmul(matmul(ZQzp, dXvn), QpzZ);
195 const Matrix<double> tr = matmul(ZQzp, dXvn);
196 const Matrix<double> bl = matmul(dXvn, QpzZ);
197 for (std::size_t i = 0; i < Nz; ++i) {
198 for (std::size_t j = 0; j < Nz; ++j) out(ixz[i], ixz[j]) = tl(i, j);
199 for (std::size_t j = 0; j < Np; ++j) out(ixz[i], ixp[j]) = tr(i, j);
200 }
201 for (std::size_t i = 0; i < Np; ++i)
202 for (std::size_t j = 0; j < Nz; ++j) out(ixp[i], ixz[j]) = bl(i, j);
203 }
204 for (std::size_t i = 0; i < Np; ++i)
205 for (std::size_t j = 0; j < Np; ++j) out(ixp[i], ixp[j]) = dXvn(i, j);
206 return out;
207}
208
209/** The (z, p, n) partition of a fluid model by the sign of diag(C). */
210struct SignSplit {
211 std::vector<std::size_t> ixz, ixp, ixn;
212};
213
214inline SignSplit sign_split(const Matrix<double>& C, double prec) {
215 SignSplit s;
216 for (std::size_t i = 0; i < C.rows(); ++i) {
217 if (std::fabs(C(i, i)) <= prec) s.ixz.push_back(i);
218 else if (C(i, i) > prec) s.ixp.push_back(i);
219 else s.ixn.push_back(i);
220 }
221 return s;
222}
223
224/**
225 * Embed an Np x Nn block back into the full NF x NF state space at the (p, n)
226 * position, undoing the sign permutation. This is the reference's
227 * iPer [0; 0 X; 0] Per.
228 */
229inline Matrix<double> embed_pn(const Matrix<double>& X, const SignSplit& s, std::size_t NF) {
230 Matrix<double> out(NF, NF, 0.0);
231 for (std::size_t i = 0; i < s.ixp.size(); ++i)
232 for (std::size_t j = 0; j < s.ixn.size(); ++j) out(s.ixp[i], s.ixn[j]) = X(i, j);
233 return out;
234}
235
236/**
237 * Moments of the busy period reward of the fluid model (F, C, D), BUTools
238 * BusyPeriodRewardMoms. Returns numOfMoms + 1 matrices, the first being Psi.
239 */
240inline std::vector<Matrix<double>> busy_period_reward_moms(const Matrix<double>& F,
241 const Matrix<double>& C,
242 const Matrix<double>& D,
243 std::size_t numOfMoms, double prec) {
244 const std::size_t NF = F.rows();
245 const SignSplit s = sign_split(C, prec);
246 const std::size_t Nz = s.ixz.size(), Np = s.ixp.size(), Nn = s.ixn.size();
247
248 const Matrix<double> Fzz = multiregime_detail::pick(F, s.ixz, s.ixz);
249 const Matrix<double> Fpz = multiregime_detail::pick(F, s.ixp, s.ixz);
250 const Matrix<double> Fmz = multiregime_detail::pick(F, s.ixn, s.ixz);
251 const Matrix<double> Fzp = multiregime_detail::pick(F, s.ixz, s.ixp);
252 const Matrix<double> Fpp = multiregime_detail::pick(F, s.ixp, s.ixp);
253 const Matrix<double> Fmp = multiregime_detail::pick(F, s.ixn, s.ixp);
254 const Matrix<double> Fzm = multiregime_detail::pick(F, s.ixz, s.ixn);
255 const Matrix<double> Fpm = multiregime_detail::pick(F, s.ixp, s.ixn);
256 const Matrix<double> Fmm = multiregime_detail::pick(F, s.ixn, s.ixn);
257 const Matrix<double> Cm = multiregime_detail::pick(C, s.ixn, s.ixn);
258 const Matrix<double> Cp = multiregime_detail::pick(C, s.ixp, s.ixp);
259 const Matrix<double> Dm = multiregime_detail::pick(D, s.ixn, s.ixn);
260 const Matrix<double> Dp = multiregime_detail::pick(D, s.ixp, s.ixp);
261 const Matrix<double> Dz = multiregime_detail::pick(D, s.ixz, s.ixz);
262 const Matrix<double> iCp = inverse(Cp);
263 const Matrix<double> iCm = inverse(neg(Cm));
264
265 // Zeroth derivatives, censoring the zero-drift block.
266 Matrix<double> Zf(Nz, Nz, 0.0);
267 if (Nz > 0) Zf = inverse(neg(Fzz));
268 auto cens = [&](const Matrix<double>& A, const Matrix<double>& Az,
269 const Matrix<double>& Zb) -> Matrix<double> {
270 if (Nz == 0) return A;
271 return mfq_detail::add(A, matmul(matmul(Az, Zf), Zb));
272 };
273 std::vector<Matrix<double>> Fppd(numOfMoms + 1), Fpmd(numOfMoms + 1), Fmpd(numOfMoms + 1),
274 Fmmd(numOfMoms + 1);
275 Fppd[0] = matmul(iCp, cens(Fpp, Fpz, Fzp));
276 Fpmd[0] = matmul(iCp, cens(Fpm, Fpz, Fzm));
277 Fmpd[0] = matmul(iCm, cens(Fmp, Fmz, Fzp));
278 Fmmd[0] = matmul(iCm, cens(Fmm, Fmz, Fzm));
279 for (std::size_t i = 1; i <= numOfMoms; ++i) {
280 const Matrix<double> dr = (Nz > 0) ? dreward(Fzz, Dz, i, prec) : Matrix<double>(0, 0, 0.0);
281 Fppd[i] = matmul(matmul(matmul(iCp, Fpz), dr), Fzp);
282 Fpmd[i] = matmul(matmul(matmul(iCp, Fpz), dr), Fzm);
283 Fmpd[i] = matmul(matmul(matmul(iCm, Fmz), dr), Fzp);
284 Fmmd[i] = matmul(matmul(matmul(iCm, Fmz), dr), Fzm);
285 if (i == 1) {
286 Fppd[i] = mfq_detail::sub(Fppd[i], matmul(iCp, Dp));
287 Fmmd[i] = mfq_detail::sub(Fmmd[i], matmul(iCm, Dm));
288 }
289 }
290
291 const FluidFundamental<double> ff =
292 mfq_fundamental(Fppd[0], Fpmd[0], Fmpd[0], Fmmd[0], prec, 150u, RiccatiMethod::ADDA);
293 const Matrix<double>& Psi = ff.Psi;
294
295 std::vector<Matrix<double>> BPM(numOfMoms + 1);
296 BPM[0] = Psi;
297 for (std::size_t i = 1; i <= numOfMoms; ++i) {
298 Matrix<double> X = mfq_detail::add(neg(matmul(matmul(Psi, Fmpd[i]), Psi)), Fpmd[i]);
299 for (std::size_t m = 0; m + 1 <= i; ++m) {
300 const double c = nchoosek(i, m);
301 Matrix<double> t = mfq_detail::add(
302 matmul(mfq_detail::add(Fppd[i - m], matmul(Psi, Fmpd[i - m])), BPM[m]),
303 matmul(BPM[m], mfq_detail::add(Fmmd[i - m], matmul(Fmpd[i - m], Psi))));
304 X = mfq_detail::add(X, mfq_detail::scale(t, c));
305 }
306 for (std::size_t l = 1; l + 1 <= i; ++l)
307 for (std::size_t m = 1; m + l <= i; ++m) {
308 const double c = nchoosek(i, l) * nchoosek(i - l, m);
309 const Matrix<double> t = matmul(matmul(BPM[l], Fmpd[i - l - m]), BPM[m]);
310 X = mfq_detail::add(X, mfq_detail::scale(t, c));
311 }
312 BPM[i] = lyap(mfq_detail::add(Fppd[0], matmul(Psi, Fmpd[0])),
313 mfq_detail::add(Fmmd[0], matmul(Fmpd[0], Psi)), X);
314 }
315 for (std::size_t i = 0; i <= numOfMoms; ++i) BPM[i] = embed_pn(BPM[i], s, NF);
316 (void)Np;
317 (void)Nn;
318 return BPM;
319}
320
321/** Result of the erlangized busy period reward distribution. */
322struct BusyPeriodDistr {
323 Matrix<double> pr; ///< accumulated, embedded in the full state space
324 std::vector<Matrix<double>> Pn; ///< the per-order terms, likewise embedded
325};
326
327/**
328 * Distribution of the busy period reward at horizon t, BUTools
329 * BusyPeriodRewardDistr, by erlangization: the deterministic horizon t is
330 * replaced by an Erlang of order L = erlMaxOrder and rate nu = L/t, which
331 * converges as O(1/L). L is an option and not a constant precisely because it
332 * is the accuracy knob.
333 */
334inline BusyPeriodDistr busy_period_reward_distr(const Matrix<double>& F, const Matrix<double>& C,
335 const Matrix<double>& D, double t,
336 std::size_t L, double prec) {
337 const std::size_t NF = F.rows();
338 const SignSplit s = sign_split(C, prec);
339 const std::size_t Nz = s.ixz.size();
340
341 const Matrix<double> Fzz = multiregime_detail::pick(F, s.ixz, s.ixz);
342 const Matrix<double> Fpz = multiregime_detail::pick(F, s.ixp, s.ixz);
343 const Matrix<double> Fmz = multiregime_detail::pick(F, s.ixn, s.ixz);
344 const Matrix<double> Fzp = multiregime_detail::pick(F, s.ixz, s.ixp);
345 const Matrix<double> Fpp = multiregime_detail::pick(F, s.ixp, s.ixp);
346 const Matrix<double> Fmp = multiregime_detail::pick(F, s.ixn, s.ixp);
347 const Matrix<double> Fzm = multiregime_detail::pick(F, s.ixz, s.ixn);
348 const Matrix<double> Fpm = multiregime_detail::pick(F, s.ixp, s.ixn);
349 const Matrix<double> Fmm = multiregime_detail::pick(F, s.ixn, s.ixn);
350 const Matrix<double> Cm = multiregime_detail::pick(C, s.ixn, s.ixn);
351 const Matrix<double> Cp = multiregime_detail::pick(C, s.ixp, s.ixp);
352 const Matrix<double> Dm = multiregime_detail::pick(D, s.ixn, s.ixn);
353 const Matrix<double> Dp = multiregime_detail::pick(D, s.ixp, s.ixp);
354 const Matrix<double> Dz = multiregime_detail::pick(D, s.ixz, s.ixz);
355 const Matrix<double> iCp = inverse(Cp);
356 const Matrix<double> iCm = inverse(neg(Cm));
357
358 const double nu = static_cast<double>(L) / t;
359 Matrix<double> Z(Nz, Nz, 0.0);
360 if (Nz > 0) Z = inverse(mfq_detail::sub(mfq_detail::scale(Dz, nu), Fzz));
361 auto zc = [&](const Matrix<double>& A, const Matrix<double>& Az,
362 const Matrix<double>& Zb) -> Matrix<double> {
363 if (Nz == 0) return A;
364 return mfq_detail::add(A, matmul(matmul(Az, Z), Zb));
365 };
366 const Matrix<double> Fpp_e =
367 matmul(iCp, zc(mfq_detail::sub(Fpp, mfq_detail::scale(Dp, nu)), Fpz, Fzp));
368 const Matrix<double> Fpm_e = matmul(iCp, zc(Fpm, Fpz, Fzm));
369 const Matrix<double> Fmp_e = matmul(iCm, zc(Fmp, Fmz, Fzp));
370 const Matrix<double> Fmm_e =
371 matmul(iCm, zc(mfq_detail::sub(Fmm, mfq_detail::scale(Dm, nu)), Fmz, Fzm));
372 const Matrix<double> Psie =
373 mfq_fundamental(Fpp_e, Fpm_e, Fmp_e, Fmm_e, prec, 150u, RiccatiMethod::ADDA).Psi;
374
375 std::vector<Matrix<double>> Pn;
376 Pn.push_back(Psie);
377 Matrix<double> pr = Psie;
378 const Matrix<double> AM = mfq_detail::add(Fpp_e, matmul(Psie, Fmp_e));
379 const Matrix<double> BM = mfq_detail::add(Fmm_e, matmul(Fmp_e, Psie));
380 const Matrix<double> iCpDp = mfq_detail::scale(matmul(iCp, Dp), nu);
381 const Matrix<double> iCmDm = mfq_detail::scale(matmul(iCm, Dm), nu);
382 const Matrix<double> iCmFmp = matmul(iCm, Fmp);
383 const Matrix<double> iCpFpz = (Nz > 0) ? matmul(iCp, Fpz) : Matrix<double>(Fpp.rows(), 0, 0.0);
384 const Matrix<double> iCmFmz = (Nz > 0) ? matmul(iCm, Fmz) : Matrix<double>(Fmm.rows(), 0, 0.0);
385 const Matrix<double> nuDzZ = (Nz > 0) ? matmul(mfq_detail::scale(Dz, nu), Z)
386 : Matrix<double>(0, 0, 0.0);
387
388 for (std::size_t n = 1; n + 1 <= L; ++n) {
389 Matrix<double> CM = mfq_detail::add(matmul(iCpDp, Pn[n - 1]), matmul(Pn[n - 1], iCmDm));
390 for (std::size_t i = 1; i + 1 <= n; ++i)
391 CM = mfq_detail::add(CM, matmul(matmul(Pn[i], iCmFmp), Pn[n - i]));
392 if (Nz > 0) {
393 // Z (nu Dz Z)^n, built once per n by one extra multiplication.
394 Matrix<double> ZnuN = Z;
395 for (std::size_t i = 0; i < n; ++i) ZnuN = matmul(ZnuN, nuDzZ);
396 CM = mfq_detail::add(CM, matmul(matmul(iCpFpz, ZnuN), Fzm));
397 CM = mfq_detail::sub(
398 CM, matmul(matmul(matmul(matmul(Psie, iCmFmz), ZnuN), Fzp), Psie));
399 for (std::size_t i = 0; i + 1 <= n; ++i) {
400 Matrix<double> Zk = Z;
401 for (std::size_t q = 0; q < n - i; ++q) Zk = matmul(Zk, nuDzZ);
402 const Matrix<double> tail =
403 mfq_detail::add(Fzm, matmul(Fzp, Psie));
404 CM = mfq_detail::add(CM, matmul(matmul(matmul(Pn[i], iCmFmz), Zk), tail));
405 CM = mfq_detail::add(
406 CM, matmul(matmul(matmul(mfq_detail::add(iCpFpz, matmul(Psie, iCmFmz)), Zk),
407 Fzp),
408 Pn[i]));
409 }
410 for (std::size_t i = 1; i + 1 <= n; ++i)
411 for (std::size_t j = 1; j + i <= n; ++j) {
412 Matrix<double> Zk = Z;
413 for (std::size_t q = 0; q < n - i - j; ++q) Zk = matmul(Zk, nuDzZ);
414 CM = mfq_detail::add(
415 CM, matmul(matmul(matmul(matmul(Pn[i], iCmFmz), Zk), Fzp), Pn[j]));
416 }
417 }
418 const Matrix<double> PM = lyap(AM, BM, CM);
419 Pn.push_back(PM);
420 pr = mfq_detail::add(pr, PM);
421 }
422
423 BusyPeriodDistr out;
424 out.pr = embed_pn(pr, s, NF);
425 out.Pn.reserve(Pn.size());
426 for (const Matrix<double>& P : Pn) out.Pn.push_back(embed_pn(P, s, NF));
427 return out;
428}
429
430} // namespace prio_detail
431
432/**
433 * Fluid priority queue.
434 *
435 * @param Q generator of the modulating chain, N x N
436 * @param R per-class fluid input rates, K x N; ROW K IS THE HIGHEST PRIORITY
437 * @param d constant fluid service rate, positive
438 * @param opt which measures to compute and the numerical options
439 */
441 const FluidPrioOptions& opt) {
442 using namespace prio_detail;
443 const std::size_t N = Q.rows();
444 const std::size_t K = R.rows();
445 if (Q.cols() != N) throw InputError("mfq_prio_queue: Q must be square");
446 if (R.cols() != N)
447 throw InputError("mfq_prio_queue: R must have one column per background state");
448 if (K == 0) throw InputError("mfq_prio_queue: at least one fluid class is required");
449 if (d <= opt.prec) throw InputError("mfq_prio_queue: the fluid service rate must be positive");
450 for (std::size_t k = 0; k < K; ++k)
451 for (std::size_t j = 0; j < N; ++j)
452 if (R(k, j) < -opt.prec)
453 throw InputError("mfq_prio_queue: the fluid arrival rate cannot be negative");
454 if (opt.erlMaxOrder < 2)
455 throw InputError("mfq_prio_queue: erlMaxOrder must be at least 2");
456
457 std::vector<std::size_t> classes = opt.classes;
458 if (classes.empty())
459 for (std::size_t k = 1; k <= K; ++k) classes.push_back(k);
460 for (std::size_t c : classes)
461 if (c < 1 || c > K) throw InputError("mfq_prio_queue: class index out of range");
462
463 const std::vector<double> pi = mc::ctmc_solve(Q);
464 std::vector<double> lambda(K, 0.0);
465 for (std::size_t k = 0; k < K; ++k)
466 for (std::size_t j = 0; j < N; ++j) lambda[k] += pi[j] * R(k, j);
467
468 FluidPrioResult out;
469 out.classes = classes;
470 for (std::size_t ci = 0; ci < classes.size(); ++ci) {
471 const std::size_t k = classes[ci] - 1; // 0-based
472 if (lambda[k] <= 0.0)
473 throw InputError("mfq_prio_queue: a requested class has zero mean input rate");
474
475 // Workload of classes k..K-1 (0-based), i.e. class k and every class
476 // above it in priority.
477 std::vector<double> agg(N, 0.0);
478 for (std::size_t kk = k; kk < K; ++kk)
479 for (std::size_t j = 0; j < N; ++j) agg[j] += R(kk, j);
480 std::vector<double> drift(N, 0.0);
481 for (std::size_t j = 0; j < N; ++j) drift[j] = agg[j] / d - 1.0;
483 mfq_general_solve(Q, dg(drift), Matrix<double>(0, 0), opt.prec);
484 const std::size_t KN = gs.K.rows();
485
486 // clok, and the canonical similarity in which the closing vector sums
487 // to one; kappa is the initial vector in that basis.
488 Matrix<double> clok = matmul(gs.clo, dg(std::vector<double>(N, 1.0)));
489 for (std::size_t i = 0; i < KN; ++i)
490 for (std::size_t j = 0; j < N; ++j) clok(i, j) = gs.clo(i, j) * R(k, j) / lambda[k];
491 std::vector<double> ini = gs.ini;
492 Matrix<double> Km = gs.K;
493 if (KN > 0) {
494 const Matrix<double> iKm = inverse(neg(Km));
495 std::vector<double> rowsum(KN, 0.0);
496 for (std::size_t i = 0; i < KN; ++i)
497 for (std::size_t j = 0; j < N; ++j) rowsum[i] += clok(i, j);
498 const std::vector<double> delta = mulvec(iKm, rowsum);
499 for (std::size_t i = 0; i < KN; ++i)
500 if (delta[i] == 0.0)
501 throw NumericError(
502 "mfq_prio_queue: the canonical similarity is singular for this class");
503 Matrix<double> K0(KN, KN, 0.0);
504 for (std::size_t i = 0; i < KN; ++i)
505 for (std::size_t j = 0; j < KN; ++j) K0(i, j) = Km(i, j) * delta[j] / delta[i];
506 Matrix<double> K1(KN, N, 0.0);
507 for (std::size_t i = 0; i < KN; ++i)
508 for (std::size_t j = 0; j < N; ++j) K1(i, j) = clok(i, j) / delta[i];
509 std::vector<double> kappa(KN, 0.0);
510 for (std::size_t i = 0; i < KN; ++i) kappa[i] = ini[i] * delta[i];
511 Km = K0;
512 clok = K1;
513 ini = kappa;
514 }
515
516 // mass0 weighted by this class's input, the probability that a drop
517 // arrives to an empty higher-priority workload.
518 double mass0R = 0.0;
519 for (std::size_t j = 0; j < N; ++j) mass0R += gs.mass0[j] * R(k, j) / lambda[k];
520
521 const bool highest = (k + 1 == K);
522 if (highest) {
523 // ---- highest priority class: no interruption ----
524 if (opt.stMoms > 0) {
525 std::vector<double> m(opt.stMoms, 0.0);
526 if (KN > 0) {
527 const Matrix<double> iK = inverse(neg(Km));
528 Matrix<double> pw = iK;
529 double fact = 1.0;
530 for (std::size_t i = 1; i <= opt.stMoms; ++i) {
531 fact *= static_cast<double>(i);
532 pw = matmul(pw, iK);
533 const std::vector<double> v = vecmul(vecmul(ini, pw), clok);
534 double sum = 0.0;
535 for (double x : v) sum += x;
536 m[i - 1] = fact * sum;
537 }
538 }
539 out.stMoms.push_back(m);
540 }
541 if (!opt.stDistr.empty()) {
542 std::vector<double> v(opt.stDistr.size(), 0.0);
543 for (std::size_t x = 0; x < opt.stDistr.size(); ++x) {
544 double acc = mass0R;
545 if (KN > 0) {
546 const Matrix<double> iK = inverse(neg(Km));
547 Matrix<double> ImE = eye<double>(KN);
548 const Matrix<double> E = expm0(Km, opt.stDistr[x]);
549 for (std::size_t i = 0; i < KN; ++i)
550 for (std::size_t j = 0; j < KN; ++j) ImE(i, j) -= E(i, j);
551 const std::vector<double> w =
552 vecmul(vecmul(vecmul(ini, iK), ImE), clok);
553 for (double y : w) acc += y;
554 }
555 v[x] = acc;
556 }
557 out.stDistr.push_back(v);
558 }
559 if (opt.flMoms > 0 || !opt.flDistr.empty()) {
560 // needQL/FluFluQueue rationale: see _kb/03-api-layer.md (cpp port notes: mam)
561 std::vector<double> dr(N, 0.0);
562 for (std::size_t j = 0; j < N; ++j) dr[j] = R(k, j) - d;
564 mfq_general_solve(Q, dg(dr), Q, opt.prec);
565 const std::size_t FN = fs.K.rows();
566 if (opt.flMoms > 0) {
567 std::vector<double> m(opt.flMoms, 0.0);
568 if (FN > 0) {
569 const Matrix<double> iK = inverse(neg(fs.K));
570 Matrix<double> pw = iK;
571 double fact = 1.0;
572 for (std::size_t i = 1; i <= opt.flMoms; ++i) {
573 fact *= static_cast<double>(i);
574 pw = matmul(pw, iK);
575 const std::vector<double> v = vecmul(vecmul(fs.ini, pw), fs.clo);
576 double sum = 0.0;
577 for (double x : v) sum += x;
578 m[i - 1] = fact * sum;
579 }
580 }
581 out.flMoms.push_back(m);
582 }
583 if (!opt.flDistr.empty()) {
584 std::vector<double> v(opt.flDistr.size(), 0.0);
585 double m0 = 0.0;
586 for (double x : fs.mass0) m0 += x;
587 for (std::size_t x = 0; x < opt.flDistr.size(); ++x) {
588 double acc = m0;
589 if (FN > 0) {
590 const Matrix<double> iK = inverse(neg(fs.K));
591 Matrix<double> ImE = eye<double>(FN);
592 const Matrix<double> E = expm0(fs.K, opt.flDistr[x]);
593 for (std::size_t i = 0; i < FN; ++i)
594 for (std::size_t j = 0; j < FN; ++j) ImE(i, j) -= E(i, j);
595 const std::vector<double> w =
596 vecmul(vecmul(vecmul(fs.ini, ImE), iK), fs.clo);
597 for (double y : w) acc += y;
598 }
599 v[x] = acc;
600 }
601 out.flDistr.push_back(v);
602 }
603 }
604 continue;
605 }
606
607 // busy period reward rationale: see _kb/03-api-layer.md (cpp port notes: mam)
608 std::vector<double> lower(N, 0.0);
609 for (std::size_t kk = k + 1; kk < K; ++kk)
610 for (std::size_t j = 0; j < N; ++j) lower[j] += R(kk, j);
611 const std::size_t NF = KN + N;
612 Matrix<double> F(NF, NF, 0.0), Cmat(NF, NF, 0.0);
613 for (std::size_t i = 0; i < KN; ++i) {
614 for (std::size_t j = 0; j < KN; ++j) F(i, j) = Km(i, j);
615 for (std::size_t j = 0; j < N; ++j) F(i, KN + j) = clok(i, j);
616 Cmat(i, i) = 1.0;
617 }
618 for (std::size_t i = 0; i < N; ++i) {
619 for (std::size_t j = 0; j < N; ++j) F(KN + i, KN + j) = Q(i, j);
620 Cmat(KN + i, KN + i) = lower[i] / d - 1.0;
621 }
622 std::vector<double> inis(NF, 0.0);
623 for (std::size_t i = 0; i < KN; ++i) inis[i] = ini[i];
624
625 auto make_D = [&](bool fluid) {
626 Matrix<double> D(NF, NF, 0.0);
627 for (std::size_t i = 0; i < N; ++i)
628 D(KN + i, KN + i) = fluid ? R(k, i) : 1.0;
629 return D;
630 };
631
632 if (opt.stMoms > 0) {
633 const std::vector<Matrix<double>> Tmp =
634 busy_period_reward_moms(F, Cmat, make_D(false), opt.stMoms, opt.prec);
635 std::vector<double> m(opt.stMoms, 0.0);
636 for (std::size_t i = 1; i < Tmp.size(); ++i) {
637 const std::vector<double> v = vecmul(inis, Tmp[i]);
638 double sum = 0.0;
639 for (double x : v) sum += x;
640 m[i - 1] = ((i % 2 == 0) ? 1.0 : -1.0) * sum;
641 }
642 out.stMoms.push_back(m);
643 }
644 if (!opt.stDistr.empty()) {
645 std::vector<double> v(opt.stDistr.size(), 0.0);
646 for (std::size_t x = 0; x < opt.stDistr.size(); ++x) {
647 const BusyPeriodDistr bp = busy_period_reward_distr(
648 F, Cmat, make_D(false), opt.stDistr[x], opt.erlMaxOrder, opt.prec);
649 const std::vector<double> w = vecmul(inis, bp.pr);
650 double acc = mass0R;
651 for (double y : w) acc += y;
652 v[x] = acc;
653 }
654 out.stDistr.push_back(v);
655 }
656 if (opt.flMoms > 0) {
657 const std::vector<Matrix<double>> Tmp =
658 busy_period_reward_moms(F, Cmat, make_D(true), opt.flMoms, opt.prec);
659 // Keep only the background columns, then move from the law right
660 // after a departure to the law at a random point in time.
661 std::vector<std::vector<double>> FLDPn(Tmp.size(), std::vector<double>(N, 0.0));
662 for (std::size_t i = 0; i < Tmp.size(); ++i) {
663 const std::vector<double> row = vecmul(inis, Tmp[i]);
664 for (std::size_t j = 0; j < N; ++j) FLDPn[i][j] = row[KN + j];
665 if (i == 0)
666 for (std::size_t j = 0; j < N; ++j)
667 FLDPn[i][j] += gs.mass0[j] * R(k, j) / lambda[k];
668 }
669 // iTerm = inv(ones(N,1) pi - Q), the fundamental matrix of Q.
670 Matrix<double> T2(N, N, 0.0);
671 for (std::size_t i = 0; i < N; ++i)
672 for (std::size_t j = 0; j < N; ++j) T2(i, j) = pi[j] - Q(i, j);
673 const Matrix<double> iTerm = inverse(T2);
674 std::vector<std::vector<double>> FLPn;
675 FLPn.push_back(pi);
676 std::vector<double> m(opt.flMoms, 0.0);
677 for (std::size_t n = 1; n <= opt.flMoms; ++n) {
678 const double nd = static_cast<double>(n);
679 std::vector<double> a(N, 0.0);
680 for (std::size_t j = 0; j < N; ++j)
681 a[j] = -FLDPn[n - 1][j] + FLPn[n - 1][j] * R(k, j) / lambda[k];
682 const std::vector<double> aT = vecmul(a, iTerm);
683 double sumP = 0.0;
684 for (std::size_t j = 0; j < N; ++j) sumP += FLDPn[n][j];
685 for (std::size_t j = 0; j < N; ++j) sumP += nd * aT[j] * R(k, j);
686 std::vector<double> b(N, 0.0);
687 for (std::size_t j = 0; j < N; ++j)
688 b[j] = -FLPn[n - 1][j] * R(k, j) + FLDPn[n - 1][j] * lambda[k];
689 const std::vector<double> bT = vecmul(b, iTerm);
690 std::vector<double> P(N, 0.0);
691 for (std::size_t j = 0; j < N; ++j) P[j] = sumP * pi[j] + nd * bT[j];
692 FLPn.push_back(P);
693 double sum = 0.0;
694 for (double x : P) sum += x;
695 m[n - 1] = ((n % 2 == 0) ? 1.0 : -1.0) * sum;
696 }
697 out.flMoms.push_back(m);
698 }
699 if (!opt.flDistr.empty()) {
700 std::vector<double> v(opt.flDistr.size(), 0.0);
701 for (std::size_t x = 0; x < opt.flDistr.size(); ++x) {
702 const double nu = static_cast<double>(opt.erlMaxOrder) / opt.flDistr[x];
703 const BusyPeriodDistr bp = busy_period_reward_distr(
704 F, Cmat, make_D(true), opt.flDistr[x], opt.erlMaxOrder, opt.prec);
705 Matrix<double> Wm(N, N, 0.0);
706 for (std::size_t i = 0; i < N; ++i)
707 for (std::size_t j = 0; j < N; ++j)
708 Wm(i, j) = (i == j ? nu * R(k, i) : 0.0) - Q(i, j);
709 const Matrix<double> iW = inverse(Wm);
710 std::vector<double> Psiy(N, 0.0);
711 {
712 const std::vector<double> row = vecmul(inis, bp.Pn[0]);
713 std::vector<double> a(N, 0.0);
714 for (std::size_t j = 0; j < N; ++j)
715 a[j] = gs.mass0[j] * R(k, j) / lambda[k] + row[KN + j];
716 const std::vector<double> t = vecmul(a, iW);
717 for (std::size_t j = 0; j < N; ++j) Psiy[j] = lambda[k] * nu * t[j];
718 }
719 for (std::size_t i = 1; i < bp.Pn.size(); ++i) {
720 const std::vector<double> row = vecmul(inis, bp.Pn[i]);
721 std::vector<double> a(N, 0.0);
722 for (std::size_t j = 0; j < N; ++j)
723 a[j] = lambda[k] * row[KN + j] + Psiy[j] * R(k, j);
724 const std::vector<double> t = vecmul(a, iW);
725 for (std::size_t j = 0; j < N; ++j) Psiy[j] = nu * t[j];
726 }
727 double acc = 0.0;
728 for (double y : Psiy) acc += y;
729 v[x] = acc;
730 }
731 out.flDistr.push_back(v);
732 }
733 }
734 return out;
735}
736
737} // namespace mam
738} // namespace line
739
740#endif // LINE_API_MAM_MFQ_PRIO_QUEUE_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
Steady-state distribution of a continuous-time Markov chain.
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.
Multi-regime FEEDBACK Markovian fluid queue: density, density derivative and distribution of the flui...
Core of the Markovian fluid queue: the fundamental matrices Psi, K, U and the matrix-exponential stat...
FluidFundamental< T > mfq_fundamental(const Matrix< T > &Fpp, const Matrix< T > &Fpm, const Matrix< T > &Fmp, const Matrix< T > &Fmm, const T &precision, unsigned maxNumIt, RiccatiMethod method)
Psi, K and U of a fluid queue whose drifts have been normalized to +-1.
Definition mfq_solve.h:196
FluidPrioResult mfq_prio_queue(const Matrix< double > &Q, const Matrix< double > &R, double d, const FluidPrioOptions &opt)
Fluid priority queue.
GeneralFluidSolution< T > mfq_general_solve(const Matrix< T > &Q, const Matrix< T > &R, const Matrix< T > &Q0, const T &prec)
Stationary law of a general Markovian fluid model, pi(x) = ini exp(K x) clo above level zero plus the...
Definition mfq_solve.h:299
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
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 > mulvec(const Matrix< T > &A, const std::vector< T > &v)
Matrix times column vector, A v.
Definition linalg.h:62
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
Number-type abstraction for the templated API port.
Which measures to compute, and the numerical options.
std::size_t flMoms
number of fluid level moments, 0 = not wanted
std::size_t stMoms
number of sojourn time moments
std::vector< double > flDistr
levels at which the fluid CDF is wanted
std::size_t erlMaxOrder
Erlang order of the erlangization.
double prec
Riccati and matrix-quadratic tolerance.
std::vector< double > stDistr
times at which the sojourn CDF is wanted
std::vector< std::size_t > classes
1-based classes to analyze, empty = all
One entry per analyzed class, in the order given by FluidPrioOptions::classes.
std::vector< std::size_t > classes
the classes analyzed, 1-based
std::vector< std::vector< double > > stDistr
sojourn time CDF per class
std::vector< std::vector< double > > flDistr
fluid level CDF per class
std::vector< std::vector< double > > flMoms
fluid level moments per class
std::vector< std::vector< double > > stMoms
sojourn time moments per class
Stationary matrix-exponential solution of a general Markovian fluid model.
Definition mfq_solve.h:280
Matrix< T > K
matrix exponent of the density, Np x Np
Definition mfq_solve.h:283
std::vector< T > mass0
P(level 0, state j), length N.
Definition mfq_solve.h:281
std::vector< T > ini
initial vector of the density, length Np
Definition mfq_solve.h:282
Matrix< T > clo
closing matrix of the density, Np x N
Definition mfq_solve.h:284