LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
npfqn_dps_morrison.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_NPFQN_DPS_MORRISON_H
6#define LINE_API_NPFQN_DPS_MORRISON_H
7
8/**
9 * @file
10 * @ingroup api_npfqn
11 * Two-term heavy-usage asymptotic approximation for a closed queueing network with one
12 * infinite-server (think) station and one discriminatory processor-sharing (DPS) station.
13 *
14 * Templated port of matlab/src/api/npfqn/npfqn_dps_morrison.m, cross-checked against
15 * jar/src/main/java/jline/api/npfqn/Npfqn_dps_morrison.java (identical term for term, including
16 * the sigma solve and the W_m recursion).
17 *
18 * Reference: J.A. Morrison, "Asymptotic analysis of a large closed queueing network with
19 * discriminatory processor sharing", Queueing Systems 9 (1991) 191-214.
20 *
21 * The network is NOT product-form, so nothing here computes a normalizing constant: the method
22 * expands the GENERATING FUNCTION of the balance equations. The substitution P(n) = <w,n> f(n)
23 * clears the DPS denominator and turns the balance recursion into a linear PDE with affine
24 * coefficients (eq. 2.5); rescaling z = 1 - xi/sqrt(N) and expanding in powers of N^(-1/2) leaves
25 * a degenerate leading operator whose kernel is the functions of the similarity variable eta, and
26 * the solvability condition along its characteristic gives an ODE for the amplitude (eq. 2.20).
27 * RESULT 1 (eq. 4.11) and RESULT 2 (eq. 4.17) are the two-term approximations returned here.
28 *
29 * Scaling. Morrison writes K_j = N b_j and lambda_j = N r_j g_j with usage
30 * rho = sum_j b_j/g_j = 1 - a/sqrt(N). N is bookkeeping only and the approximation is invariant to
31 * it, so this routine fixes N = 1: b = N_pop, g = Z/S, r = 1/Z, a = 1 - rho. Accuracy is governed
32 * by the PHYSICAL regime -- large populations with rho near 1. rho > 1 is admissible, being the
33 * saturated regime of appendix A.
34 *
35 * Arithmetic. The W_m of eq. (3.23) need an erfc and an exp, so this requires transcendental
36 * arithmetic and cannot be instantiated at T = Rational.
37 */
38
39#include <cstddef>
40#include <vector>
41
43#include "line/num/number.h"
44#include "line/util/error.h"
45
46namespace line {
47namespace npfqn {
48
49/** Mean queue lengths, sojourn times and throughputs, with Morrison's intermediate constants. */
50template <class T>
52 std::vector<T> Q; ///< mean number of class-k jobs at the DPS station
53 std::vector<T> R; ///< mean class-k sojourn time per visit to the DPS station
54 std::vector<T> X; ///< per-class throughput
55 std::vector<T> Qlead; ///< leading-order (one-term) queue lengths
56 std::vector<T> Rlead; ///< leading-order (one-term) sojourn times
57 std::vector<T> sigma; ///< the vector sigma of eq. (4.12)
58 std::vector<T> W; ///< W_0..W_4 of eq. (3.23)
59 T rho; ///< usage sum_j b_j/g_j
60 T a; ///< heavy-usage parameter a = 1 - rho at the N = 1 scale
61 T cB, cC, cD, cH, cI, cJ, cK, cL, cM, cQ, delta, cR, cS, cU, cA, cV; ///< Morrison's constants
62};
63
64namespace detail {
65
66/**
67 * Scaled complementary error function exp(x^2) erfc(x), for either sign. The direct product
68 * overflows past x ~ 26, where the asymptotic series is already exact to double precision; for
69 * x < 0 the reflection erfcx(x) = 2 exp(x^2) - erfcx(-x) is used, which overflows below x ~ -26
70 * and is reported as such by the caller.
71 */
72template <class T>
73T num_erfcx(const T& x) {
74 const T zero = num_traits<T>::from_int(0);
75 if (x < zero) {
76 return num_traits<T>::from_int(2) * num_exp(T(x * x)) - num_erfcx(T(-x));
77 }
78 if (x < num_traits<T>::from_int(25)) {
79 return num_exp(T(x * x)) * num_erfc(x);
80 }
81 const T y = num_traits<T>::from_int(1) / (num_traits<T>::from_int(2) * x * x);
82 T term = num_traits<T>::from_int(1);
84 for (int k = 1; k <= 12; ++k) {
85 term *= -num_traits<T>::from_int(2 * k - 1) * y;
86 sum += term;
87 }
88 return sum / (x * num_sqrt(boost::math::constants::pi<T>()));
89}
90
91/** I_m(yh) = int_0^inf s^m exp(-s^2/2 - yh s) ds by composite Simpson, the recursion's fallback. */
92template <class T>
93T dps_quad_I(std::size_t m, const T& yh) {
94 const T zero = num_traits<T>::from_int(0);
95 const T twelve = num_traits<T>::from_int(12);
96 T hi = twelve;
97 if (yh >= num_traits<T>::from_int(1)) {
98 const T shrunk = num_traits<T>::from_int(40) / yh;
99 hi = (shrunk < twelve) ? shrunk : twelve;
100 } else if (yh < zero) {
101 hi = twelve - yh;
102 }
103 const int n = 4096;
104 const T h = hi / num_traits<T>::from_int(n);
105 T sum = zero;
106 for (int k = 0; k <= n; ++k) {
107 const T s = num_traits<T>::from_int(k) * h;
108 T f = num_exp(T(-s * s / num_traits<T>::from_int(2) - yh * s));
109 for (std::size_t j = 0; j < m; ++j) f *= s;
110 const T wgt = (k == 0 || k == n) ? num_traits<T>::from_int(1)
111 : num_traits<T>::from_int((k % 2 == 1) ? 4 : 2);
112 sum += wgt * f;
113 }
114 return sum * h / num_traits<T>::from_int(3);
115}
116
117/** Gaussian elimination with partial pivoting on a small dense square system. */
118template <class T>
119std::vector<T> dps_solve_square(std::vector<std::vector<T> > A, std::vector<T> rhs) {
120 const std::size_t n = rhs.size();
121 for (std::size_t i = 0; i < n; ++i) A[i].push_back(rhs[i]);
122 for (std::size_t c = 0; c < n; ++c) {
123 std::size_t piv = c;
124 for (std::size_t i = c + 1; i < n; ++i) {
125 T ai = A[i][c] < num_traits<T>::from_int(0) ? T(-A[i][c]) : A[i][c];
126 T ap = A[piv][c] < num_traits<T>::from_int(0) ? T(-A[piv][c]) : A[piv][c];
127 if (ai > ap) piv = i;
128 }
129 T ap = A[piv][c] < num_traits<T>::from_int(0) ? T(-A[piv][c]) : A[piv][c];
130 if (!(ap > num_traits<T>::from_double(1e-300))) {
131 throw InputError("npfqn_dps_morrison: singular system while solving Morrison's "
132 "sigma equations (4.12).");
133 }
134 A[c].swap(A[piv]);
135 for (std::size_t i = c + 1; i < n; ++i) {
136 const T f = A[i][c] / A[c][c];
137 for (std::size_t j = c; j <= n; ++j) A[i][j] -= f * A[c][j];
138 }
139 }
140 std::vector<T> x(n, num_traits<T>::from_int(0));
141 for (std::size_t ii = n; ii-- > 0;) {
142 T acc = A[ii][n];
143 for (std::size_t j = ii + 1; j < n; ++j) acc -= A[ii][j] * x[j];
144 x[ii] = acc / A[ii][ii];
145 }
146 return x;
147}
148
149/** W_m of eq. (3.23), m = 0..4; see the MATLAB local_W for the normalization used. */
150template <class T>
151std::vector<T> dps_W(const T& cB, const T& cC, const T& cD, const T& y) {
152 const std::size_t mmax = 4;
153 const T sig = num_sqrt(T(cD / (cB * cC)));
154 const T yh = y * num_sqrt(T(cB / (cC * cD)));
155 const T two = num_traits<T>::from_int(2);
156
157 std::vector<T> Iv(mmax + 1, num_traits<T>::from_int(0));
158 Iv[0] = num_sqrt(T(boost::math::constants::pi<T>() / two)) * num_erfcx(T(yh / num_sqrt(two)));
159 if (!num_isfinite(Iv[0])) {
160 throw InputError("npfqn_dps_morrison: the usage is so far above saturation that the "
161 "expansion overflows. This model is outside the moderately-heavy regime the "
162 "approximation is derived for; use SolverFLD, SolverMVA or SolverCTMC.");
163 }
164 Iv[1] = num_traits<T>::from_int(1) - yh * Iv[0];
165 for (std::size_t m = 2; m <= mmax; ++m) {
166 Iv[m] = num_traits<T>::from_int(static_cast<int>(m) - 1) * Iv[m - 2] - yh * Iv[m - 1];
167 }
168 bool positive = true;
169 for (std::size_t m = 0; m <= mmax; ++m) {
170 if (!(Iv[m] > num_traits<T>::from_int(0))) positive = false;
171 }
172 if (!positive) {
173 for (std::size_t m = 0; m <= mmax; ++m) Iv[m] = dps_quad_I(m, yh);
174 }
175 std::vector<T> W(mmax + 1, num_traits<T>::from_int(0));
176 T sp = sig;
177 for (std::size_t m = 0; m <= mmax; ++m) {
178 W[m] = (cB / cD) * (cB / cD) * sp * Iv[m];
179 sp *= sig;
180 }
181 return W;
182}
183
184} // namespace detail
185
186/**
187 * Evaluates Morrison's two-term approximation.
188 *
189 * @param N per-class populations, finite and positive
190 * @param Z per-class mean think times, finite and positive
191 * @param S per-class mean DPS service times, finite and positive
192 * @param w per-class DPS weights, finite and positive
193 * @return the mean performance measures and the intermediate constants
194 */
195template <class T>
196DpsMorrisonResult<T> npfqn_dps_morrison(const std::vector<T>& N, const std::vector<T>& Z,
197 const std::vector<T>& S, const std::vector<T>& w) {
199 "npfqn_dps_morrison requires transcendental arithmetic");
200 const std::size_t p = N.size();
201 if (Z.size() != p || S.size() != p || w.size() != p) {
202 throw InputError("npfqn_dps_morrison: N, Z, S and w must have the same number of classes.");
203 }
204 const T zero = num_traits<T>::from_int(0);
205 const T one = num_traits<T>::from_int(1);
206 const T two = num_traits<T>::from_int(2);
207 const T three = num_traits<T>::from_int(3);
208
209 for (std::size_t i = 0; i < p; ++i) {
210 if (!detail::num_isfinite(N[i]) || !(N[i] > zero)) {
211 throw InputError("npfqn_dps_morrison: the approximation requires finite positive class "
212 "populations.");
213 }
214 if (!detail::num_isfinite(Z[i]) || !(Z[i] > zero) || !detail::num_isfinite(S[i]) ||
215 !(S[i] > zero)) {
216 throw InputError("npfqn_dps_morrison: think times Z and DPS service times S must be "
217 "finite and positive.");
218 }
219 if (!detail::num_isfinite(w[i]) || !(w[i] > zero)) {
220 throw InputError("npfqn_dps_morrison: DPS weights must be finite and positive.");
221 }
222 }
223
224 // Morrison's parameters at the bookkeeping scale N = 1
225 std::vector<T> b(N), r(p, zero), g(p, zero);
226 T rho = zero;
227 for (std::size_t i = 0; i < p; ++i) {
228 r[i] = one / Z[i];
229 g[i] = Z[i] / S[i];
230 rho += b[i] / g[i];
231 }
232 const T a = one - rho;
233
234 // constants, eqs. (2.18), (2.19), (3.11), (3.13)-(3.15)
235 T cB = zero, cC = zero, cD = zero, cH = zero, cI = zero, cJ = zero;
236 T cK = zero, cL = zero, cM = zero, cQ = zero;
237 for (std::size_t i = 0; i < p; ++i) {
238 const T g2 = g[i] * g[i], g3 = g2 * g[i], w2 = w[i] * w[i], r2 = r[i] * r[i];
239 cB += b[i] / (r[i] * g2 * w[i]);
240 cC += b[i] / (g2 * w[i]);
241 cD += b[i] / (r[i] * g2);
242 cH += b[i] / (r2 * g3 * w[i]);
243 cI += b[i] / (r2 * g3 * w2);
244 cJ += b[i] / (r[i] * g3 * w2);
245 cK += b[i] / (g3 * w2);
246 cL += b[i] / (r[i] * g3 * w[i]);
247 cM += b[i] / (r2 * g3);
248 cQ += b[i] / g2;
249 }
250
251 // sigma: eq. (4.12) with the normalization (4.13). The p equations have rank p-1, so the last
252 // one -- implied by the others -- is REPLACED by (4.13), giving a square nonsingular system.
253 // All four codebases use this same scheme so their sigma agree.
254 std::vector<std::vector<T> > A(p, std::vector<T>(p, zero));
255 std::vector<T> rhs(p, zero);
256 for (std::size_t i = 0; i < p; ++i) {
257 A[i][i] += rho;
258 for (std::size_t j = 0; j < p; ++j) {
259 const T den = r[i] * g[i] * w[i] + r[j] * g[j] * w[j];
260 A[i][i] -= w[j] * b[j] * r[j] / den;
261 A[i][j] -= w[j] * b[i] * r[i] / den;
262 }
263 rhs[i] = rho * (b[i] / g[i]) * (cD / (cB * w[i]) - one);
264 }
265 for (std::size_t j = 0; j < p; ++j) A[p - 1][j] = one / (r[j] * g[j]);
266 rhs[p - 1] = zero;
267 const std::vector<T> sigma = detail::dps_solve_square(A, rhs);
268
269 // alpha from eq. (4.9), then delta of eq. (3.15)
270 T delta = zero;
271 for (std::size_t i = 0; i < p; ++i) {
272 const T alpha_i = sigma[i] - (b[i] / g[i]) * (cD / (cB * w[i]) - one);
273 delta += alpha_i / g[i];
274 }
275
276 // eqs. (3.19)-(3.21)
277 const T cR = three * (cB * cL - cD * cJ) / (cB * cD);
278 const T cS = (two * cB * (cD * cH - cB * cM) - cD * (cD * cI - cB * cH)) / (two * cB * cB * cD * cD);
279 const T cU = (cQ - cC * cD / cB - delta) / rho - cD * cR / cB + (a * a - cC * cD / cB) * cS;
280 const T cA = cS * cC * cC + cR * cC - cK;
281 const T cV = cR + two * cS * cC;
282
283 const std::vector<T> W = detail::dps_W(cB, cC, cD, a);
284
285 // RESULT 1 (4.11) and RESULT 2 (4.17), at sqrt(N) = 1. NOTE the numerator bracket carries
286 // U*W2: eq. (4.10) of the paper misprints it as U*W1, but (4.7), (4.11), (A6) and (B2) all
287 // agree on U*W2, and it is what the derivation from (4.4)-(4.9) gives.
288 const T eps = cB / cD;
289 const T num = W[1] - eps * (cA / three * W[4] + a / two * cV * W[3] + cU * W[2]);
290 const T den = W[0] - eps * (cA / three * W[3] + a / two * cV * W[2] + cU * W[1] + cS);
291 if (!detail::num_isfinite(den) || den == zero) {
292 throw InputError("npfqn_dps_morrison: the expansion is degenerate for this model (vanishing "
293 "denominator); the usage is too far from the moderately-heavy regime.");
294 }
295
297 res.Q.resize(p); res.R.resize(p); res.X.resize(p);
298 res.Qlead.resize(p); res.Rlead.resize(p); res.sigma = sigma; res.W = W;
299 for (std::size_t j = 0; j < p; ++j) {
300 const T gw = g[j] * w[j];
301 res.Qlead[j] = b[j] * W[1] / (gw * W[0]);
302 res.Q[j] = b[j] * num / (gw * den) - b[j] * W[2] / (g[j] * g[j] * w[j] * w[j] * W[0]) -
303 sigma[j] / rho;
304 res.Rlead[j] = W[1] / (r[j] * gw * W[0]);
305 res.R[j] = num / (r[j] * gw * den) +
306 ((W[1] / W[0]) * (W[1] / W[0]) - W[2] / W[0]) /
307 (r[j] * g[j] * g[j] * w[j] * w[j]) -
308 sigma[j] / (rho * r[j] * b[j]);
309 res.X[j] = r[j] * (b[j] - res.Q[j]);
310 }
311 res.rho = rho; res.a = a;
312 res.cB = cB; res.cC = cC; res.cD = cD; res.cH = cH; res.cI = cI; res.cJ = cJ;
313 res.cK = cK; res.cL = cL; res.cM = cM; res.cQ = cQ; res.delta = delta;
314 res.cR = cR; res.cS = cS; res.cU = cU; res.cA = cA; res.cV = cV;
315 return res;
316}
317
318} // namespace npfqn
319} // namespace line
320
321#endif // LINE_API_NPFQN_DPS_MORRISON_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
DpsMorrisonResult< T > npfqn_dps_morrison(const std::vector< T > &N, const std::vector< T > &Z, const std::vector< T > &S, const std::vector< T > &w)
Evaluates Morrison's two-term approximation.
Shared arithmetic helpers for the templated npfqn port.
Number-type abstraction for the templated API port.
Mean queue lengths, sojourn times and throughputs, with Morrison's intermediate constants.
std::vector< T > R
mean class-k sojourn time per visit to the DPS station
std::vector< T > Qlead
leading-order (one-term) queue lengths
std::vector< T > Q
mean number of class-k jobs at the DPS station
std::vector< T > sigma
the vector sigma of eq. (4.12)
std::vector< T > W
W_0..W_4 of eq. (3.23).
std::vector< T > Rlead
leading-order (one-term) sojourn times
std::vector< T > X
per-class throughput
T a
heavy-usage parameter a = 1 - rho at the N = 1 scale