LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldqbd_mphc.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_MAM_LDQBD_MPHC_H
6#define LINE_API_MAM_LDQBD_MPHC_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Port of `ldqbd_mphc.m` and `ph_multisets.m`: the exact level-dependent QBD
12 * blocks of an M/PH/c queue.
13 *
14 * THE POINT OF THE MULTISET. The level is the number of jobs at the station,
15 * and the coordinate INSIDE a level is the multiset of the phases the min(n,c)
16 * busy servers sit in. The collapsed alternative -- one PH process run at
17 * min(n,c) times its speed -- gets the aggregate service rate right but forgets
18 * which phase each busy server is in, which turns c servers into one fast
19 * server whose remaining work is a single phase-type variable. Counting rather
20 * than ordering the phases costs nchoosek(min(n,c)+p-1, p-1) states per level
21 * instead of p^min(n,c), because identical servers are exchangeable.
22 *
23 * LEVEL SIZES GROW over the boundary levels 0..c and repeat above them, so the
24 * blocks joining differently sized neighbours are rectangular. `ldqbd`, its
25 * rate matrices and its stationary vector all accept that heterogeneity; level
26 * 0 is the single empty configuration.
27 *
28 * References: S. Asmussen and J.R. Moller, "Calculation of the steady state
29 * waiting time distribution in GI/PH/c and MAP/PH/c queues", Queueing Systems
30 * 37(1):9-29, 2001; M. F. Neuts, "Matrix-geometric solutions in stochastic
31 * models", Johns Hopkins University Press, 1981.
32 */
33
34#include <algorithm>
35#include <cmath>
36#include <cstddef>
37#include <map>
38#include <string>
39#include <vector>
40
41#include "line/util/error.h"
42#include "line/util/matrix.h"
43
44namespace line {
45namespace mam {
46
47/**
48 * The widest level is the repeating one, and the LD-QBD recursion inverts one
49 * matrix of that order per level, so that is the size worth guarding.
50 */
51inline constexpr std::size_t LDQBD_MPHC_MAX_CONFIGS = 2000;
52
53/**
54 * Configurations of k identical servers over p service phases.
55 *
56 * Rows are the compositions of k into p nonnegative parts: entry (r,i) is the
57 * number of the k busy servers sitting in phase i. The order is fixed and
58 * shared by every caller, so a configuration index means the same thing in each
59 * of them: the first part descends, hence k = 1 yields the identity rows
60 * e_1 ... e_p in phase order, which is what makes the c = 1 case coincide with
61 * plain phase indexing.
62 */
63inline std::vector<std::vector<int> > ph_multisets(std::size_t p, std::size_t k) {
64 std::vector<std::vector<int> > out;
65 if (k == 0) {
66 out.push_back(std::vector<int>(p, 0));
67 return out;
68 }
69 if (p == 1) {
70 out.push_back(std::vector<int>(1, static_cast<int>(k)));
71 return out;
72 }
73 for (int first = static_cast<int>(k); first >= 0; --first) {
74 const std::vector<std::vector<int> > rest =
75 ph_multisets(p - 1, k - static_cast<std::size_t>(first));
76 for (std::size_t i = 0; i < rest.size(); ++i) {
77 std::vector<int> row;
78 row.push_back(first);
79 row.insert(row.end(), rest[i].begin(), rest[i].end());
80 out.push_back(row);
81 }
82 }
83 return out;
84}
85
86/** The three block lists of a level-dependent QBD, as `ldqbd` takes them. */
87template <class T>
89 std::vector<Matrix<T> > Q0; // size Nlev : upward, level n -> n+1
90 std::vector<Matrix<T> > Q1; // size Nlev+1 : local
91 std::vector<Matrix<T> > Q2; // size Nlev+1 : downward, Q2[n] leaves level n (Q2[0] unused)
92};
93
94/**
95 * Block-tridiagonal generator of an M/PH/c queue with level-dependent arrivals.
96 *
97 * @param D0 service sub-generator (p x p), phase changes without completion
98 * @param D1 service completion block (p x p); D1 = (-D0*1)*alpha for a PH
99 * @param alpha length-p vector a server starts each new job in
100 * @param c number of identical servers (>= 1; capped at the top level)
101 * @param arrRate length Nlev+1; arrRate[n] is the arrival rate out of level n
102 * @param sf empty, or length >= Nlev: a multiplier on the station's TOTAL
103 * service rate at level n (load dependence), so each busy server
104 * runs at sf[n-1]/min(n,c) of nominal and sf[n-1] = min(n,c)
105 * reproduces the unscaled queue exactly
106 *
107 * Q2 carries an unused entry at index 0 so the three lists line up by level,
108 * matching what the C++ `ldqbd` expects.
109 */
110template <class T>
112 const std::vector<T>& alpha, double c,
113 const std::vector<T>& arrRate, const std::vector<T>& sf) {
114 const T zero = num_traits<T>::from_int(0);
115 const std::size_t p = D0.rows();
116 if (arrRate.empty())
117 throw InputError("ldqbd_mphc: needs at least one level above the empty one");
118 const std::size_t Nlev = arrRate.size() - 1;
119 if (Nlev < 1)
120 throw InputError("ldqbd_mphc: needs at least one level above the empty one");
121 if (D0.cols() != p || D1.rows() != p || D1.cols() != p || alpha.size() != p)
122 throw InputError("ldqbd_mphc: D0, D1 and alpha must all have the same order");
123 if (!sf.empty() && sf.size() < Nlev)
124 throw InputError("ldqbd_mphc: sf must give one total-service-rate factor per level");
125
126 // Servers that can never be busy do not need a coordinate: above the top
127 // level there is nothing left to serve.
128 const std::size_t cmax =
129 !std::isfinite(c) ? Nlev
130 : std::min(std::max<std::size_t>(1, static_cast<std::size_t>(
131 std::llround(c))),
132 Nlev);
133
134 std::vector<std::vector<std::vector<int> > > cfg(cmax + 1);
135 std::vector<std::map<std::vector<int>, std::size_t> > pos(cmax + 1);
136 std::vector<std::size_t> nCfg(cmax + 1, 0);
137 for (std::size_t k = 0; k <= cmax; ++k) {
138 cfg[k] = ph_multisets(p, k);
139 for (std::size_t r = 0; r < cfg[k].size(); ++r) pos[k][cfg[k][r]] = r;
140 nCfg[k] = cfg[k].size();
141 }
142
143 if (nCfg[cmax] > LDQBD_MPHC_MAX_CONFIGS)
144 throw UnsupportedError(
145 "ldqbd_mphc: the exact M/PH/c chain needs " + std::to_string(nCfg[cmax]) +
146 " configurations per level for " + std::to_string(cmax) + " servers and " +
147 std::to_string(p) +
148 " service phases, above the " + std::to_string(LDQBD_MPHC_MAX_CONFIGS) +
149 " the level-by-level inverses can carry. Use fewer phases (a lower-order fit), "
150 "fewer servers, or SolverCTMC/SolverLDES on this model");
151
152 // Completion rate out of each phase, summed over targets.
153 std::vector<T> t(p, zero);
154 for (std::size_t i = 0; i < p; ++i)
155 for (std::size_t j = 0; j < p; ++j) t[i] += D1(i, j);
156
157 // Structural blocks per busy-server count: LOC the within-level phase
158 // changes (with the full outflow on its diagonal), UP the entry of a newly
159 // busy server, DN a completion that leaves a server idle.
160 std::vector<Matrix<T> > LOC(cmax + 1), UP(cmax + 1), DN(cmax + 1);
161 for (std::size_t k = 0; k <= cmax; ++k) {
162 const std::vector<std::vector<int> >& Ck = cfg[k];
163 LOC[k] = Matrix<T>(nCfg[k], nCfg[k], zero);
164 for (std::size_t row = 0; row < nCfg[k]; ++row) {
165 const std::vector<int>& m = Ck[row];
166 for (std::size_t i = 0; i < p; ++i) {
167 if (m[i] == 0) continue;
168 const T mult = num_traits<T>::from_int(m[i]);
169 for (std::size_t j = 0; j < p; ++j) {
170 if (j == i) continue;
171 std::vector<int> mm = m;
172 --mm[i];
173 ++mm[j];
174 LOC[k](row, pos[k][mm]) += T(mult * D0(i, j));
175 }
176 // D0(i,i) is the total outflow of phase i, completions included
177 LOC[k](row, row) += T(mult * D0(i, i));
178 }
179 }
180
181 if (k < cmax) {
182 UP[k] = Matrix<T>(nCfg[k], nCfg[k + 1], zero);
183 for (std::size_t row = 0; row < nCfg[k]; ++row) {
184 const std::vector<int>& m = Ck[row];
185 for (std::size_t j = 0; j < p; ++j) {
186 std::vector<int> mm = m;
187 ++mm[j];
188 UP[k](row, pos[k + 1][mm]) += alpha[j];
189 }
190 }
191 }
192
193 if (k > 0) {
194 DN[k] = Matrix<T>(nCfg[k], nCfg[k - 1], zero);
195 for (std::size_t row = 0; row < nCfg[k]; ++row) {
196 const std::vector<int>& m = Ck[row];
197 for (std::size_t i = 0; i < p; ++i) {
198 if (m[i] == 0) continue;
199 std::vector<int> mm = m;
200 --mm[i];
201 DN[k](row, pos[k - 1][mm]) += T(num_traits<T>::from_int(m[i]) * t[i]);
202 }
203 }
204 }
205 }
206
207 // A completion at a full server bank takes the next waiting job at once, so
208 // the server stays busy and only its phase moves: the repeating down block.
209 Matrix<T> CDEP(nCfg[cmax], nCfg[cmax], zero);
210 for (std::size_t row = 0; row < nCfg[cmax]; ++row) {
211 const std::vector<int>& m = cfg[cmax][row];
212 for (std::size_t i = 0; i < p; ++i) {
213 if (m[i] == 0) continue;
214 const T mult = num_traits<T>::from_int(m[i]);
215 for (std::size_t j = 0; j < p; ++j) {
216 std::vector<int> mm = m;
217 --mm[i];
218 ++mm[j];
219 CDEP(row, pos[cmax][mm]) += T(mult * D1(i, j));
220 }
221 }
222 }
223
224 // Per-server speed. Without load dependence every busy server runs at its
225 // nominal rate; with it, the aggregate sf(n) is shared over the busy
226 // servers. sf(n) == min(n,c) is passed through as exactly one so the
227 // unscaled chain is reproduced bit for bit.
228 const T one = num_traits<T>::from_int(1);
229 std::vector<T> speed(Nlev + 1, one);
230 if (!sf.empty()) {
231 for (std::size_t n = 1; n <= Nlev; ++n) {
232 const std::size_t b = std::min(n, cmax);
233 const T bt = num_traits<T>::from_int(static_cast<int>(b));
234 if (!(sf[n - 1] == bt)) speed[n] = T(sf[n - 1] / bt);
235 }
236 }
237
239 out.Q0.assign(Nlev, Matrix<T>(1, 1, zero));
240 out.Q1.assign(Nlev + 1, Matrix<T>(1, 1, zero));
241 out.Q2.assign(Nlev + 1, Matrix<T>(1, 1, zero));
242
243 out.Q1[0] = Matrix<T>(1, 1, T(-arrRate[0])); // level 0: arrivals only
244 for (std::size_t n = 1; n <= Nlev; ++n) {
245 const std::size_t b = std::min(n, cmax);
246 Matrix<T> B(nCfg[b], nCfg[b], zero);
247 for (std::size_t i = 0; i < nCfg[b]; ++i) {
248 for (std::size_t j = 0; j < nCfg[b]; ++j) B(i, j) = T(speed[n] * LOC[b](i, j));
249 B(i, i) -= arrRate[n];
250 }
251 out.Q1[n] = B;
252 }
253
254 for (std::size_t n = 0; n + 1 <= Nlev; ++n) {
255 if (n < cmax) {
256 // a free server takes the job, starting it in a phase drawn from alpha
257 Matrix<T> B(nCfg[n], nCfg[n + 1], zero);
258 for (std::size_t i = 0; i < nCfg[n]; ++i)
259 for (std::size_t j = 0; j < nCfg[n + 1]; ++j) B(i, j) = T(arrRate[n] * UP[n](i, j));
260 out.Q0[n] = B;
261 } else {
262 // the job waits, so every busy phase is unchanged
263 Matrix<T> B(nCfg[cmax], nCfg[cmax], zero);
264 for (std::size_t i = 0; i < nCfg[cmax]; ++i) B(i, i) = arrRate[n];
265 out.Q0[n] = B;
266 }
267 }
268
269 for (std::size_t n = 1; n <= Nlev; ++n) {
270 if (n <= cmax) {
271 Matrix<T> B(nCfg[n], nCfg[n - 1], zero); // the server falls idle
272 for (std::size_t i = 0; i < nCfg[n]; ++i)
273 for (std::size_t j = 0; j < nCfg[n - 1]; ++j) B(i, j) = T(speed[n] * DN[n](i, j));
274 out.Q2[n] = B;
275 } else {
276 Matrix<T> B(nCfg[cmax], nCfg[cmax], zero); // it takes the next job
277 for (std::size_t i = 0; i < nCfg[cmax]; ++i)
278 for (std::size_t j = 0; j < nCfg[cmax]; ++j) B(i, j) = T(speed[n] * CDEP(i, j));
279 out.Q2[n] = B;
280 }
281 }
282
283 return out;
284}
285
286} // namespace mam
287} // namespace line
288
289#endif // LINE_API_MAM_LDQBD_MPHC_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
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
Dense matrix and non-owning view.
LdqbdMphcBlocks< T > ldqbd_mphc(const Matrix< T > &D0, const Matrix< T > &D1, const std::vector< T > &alpha, double c, const std::vector< T > &arrRate, const std::vector< T > &sf)
Block-tridiagonal generator of an M/PH/c queue with level-dependent arrivals.
Definition ldqbd_mphc.h:111
std::vector< std::vector< int > > ph_multisets(std::size_t p, std::size_t k)
Configurations of k identical servers over p service phases.
Definition ldqbd_mphc.h:63
constexpr std::size_t LDQBD_MPHC_MAX_CONFIGS
The widest level is the repeating one, and the LD-QBD recursion inverts one matrix of that order per ...
Definition ldqbd_mphc.h:51
The three block lists of a level-dependent QBD, as ldqbd takes them.
Definition ldqbd_mphc.h:88
std::vector< Matrix< T > > Q0
Definition ldqbd_mphc.h:89
std::vector< Matrix< T > > Q1
Definition ldqbd_mphc.h:90
std::vector< Matrix< T > > Q2
Definition ldqbd_mphc.h:91