LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_respt_ps_moments.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_PFQN_RESPT_PS_MOMENTS_H
6#define LINE_API_PFQN_RESPT_PS_MOMENTS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Sojourn-time moments at the processor-sharing station of a closed
12 * terminal-driven system (Mitra and Morrison 1983).
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_respt_ps_moments.m, cross-checked
15 * against jar/src/main/java/jline/api/pfqn/Pfqn_respt_ps_moments.java.
16 *
17 * The model is a bank of terminals in series with a single processor-sharing
18 * CPU, with class-dependent exponential think times (mean Z_r) and
19 * class-dependent exponential service times (mean S_r), and N_r jobs of class r
20 * cycling between the two. Two routes to the moments are implemented, both from
21 * that paper:
22 *
23 * Exact solves the linear system c'[A - q_J I] = -pi'B of Proposition 3
24 * on the state space {n : 0 <= n <= K}, K being the population
25 * vector with the tagged class decremented by one. The moments are
26 * then E[W_J] = sum_n c(n) and (q_J/2) E[W_J^2] =
27 * sum_n (n'1 + 1) c(n). Exact to solver precision, at the cost of
28 * a linear solve of dimension prod_r (K_r + 1).
29 *
30 * Asymptotic evaluates the two leading terms of the expansion in inverse
31 * powers of the large parameter Nexp = max_r Z_r / S_r,
32 * E[W_J^2] ~ c0 + c1/Nexp, of Proposition 6. The cost is a linear
33 * system of dimension R and is therefore independent of the
34 * populations. NOTE the expansion parameter is the
35 * THINK-TO-SERVICE RATIO and NOT the population, so a model with
36 * short think times is expanded in a small parameter no matter how
37 * many jobs it holds.
38 *
39 * Auto (default) takes the exact route when the state space has at most
40 * 4096 states and the asymptotic route otherwise.
41 *
42 * The asymptotic route requires the normal-usage condition alpha > 0, where
43 * alpha = 1 - sum_r lambda_r / q_r with lambda_r = K_r / Z_r and q_r = 1 / S_r,
44 * is the unutilized fraction of the CPU in the corresponding open system. Where
45 * it fails and the exact route is not affordable, the entry of W and W2 is NaN
46 * and the per-class method records Unavailable; asking for Asymptotic explicitly
47 * in that regime is an error rather than a blank.
48 *
49 * A class with N_r = 0 has no sojourn time and its entries are NaN.
50 *
51 * Reference: D. Mitra, J. A. Morrison, "Asymptotic Expansions of Moments of the
52 * Waiting Time in Closed and Open Processor-Sharing Systems with Multiple Job
53 * Classes", Adv. Appl. Prob. 15(4), 1983, Propositions 3 and 6.
54 *
55 * Arithmetic: TRANSCENDENTAL. The exact route builds its stationary law in the
56 * log domain so that large populations do not overflow.
57 */
58
59#include <algorithm>
60#include <cmath>
61#include <cstddef>
62#include <limits>
63#include <vector>
64
66#include "line/num/number.h"
67#include "line/util/error.h"
68#include "line/util/lu.h"
69#include "line/util/matrix.h"
70
71namespace line {
72namespace pfqn {
73
74/** Which route produced the moments of a given class. */
75enum class ResptPsMethod {
76 None, ///< the class is unpopulated
77 Exact, ///< Proposition 3, the linear solve
78 Asymptotic, ///< Proposition 6, the two-term expansion
79 Unavailable ///< normal usage fails and the exact route was not affordable
80};
81
82/** Requested route. */
83enum class ResptPsRoute { Auto, Exact, Asymptotic };
84
85/** Sojourn-time moments at the PS station, per class. */
86template <class T>
88 std::vector<T> W; ///< (R) mean sojourn times; NaN for an unpopulated class
89 std::vector<T> W2; ///< (R) second moments
90 std::vector<ResptPsMethod> method;
91 std::vector<T> c0; ///< (R) leading asymptotic term, NaN off that route
92 std::vector<T> c1; ///< (R) first correction, NaN off that route
93 std::vector<T> alpha; ///< (R) unutilized CPU fraction of the open system
94 std::vector<double> nstates; ///< (R) size of the exact state space
95 T expansionParam; ///< Nexp = max_r q_r / p_r
96};
97
98namespace detail {
99
100/** State-space size below which Auto goes exact. */
101constexpr double RESPT_PS_AUTO_MAX = 4096.0;
102/** Hard bound on an explicitly requested exact solve. */
103constexpr double RESPT_PS_EXACT_MAX = 65536.0;
104
105/**
106 * Proposition 3: the moments follow from c, the solution of
107 * c'[A - q_J I] = -pi'B, with A the generator-like operator of equation (26) and
108 * B the diagonal operator B(n,n) = n'1 + 1.
109 */
110template <class T>
111void respt_ps_exact(const std::vector<T>& p, const std::vector<T>& q, const std::vector<long>& K,
112 std::size_t J, T& W, T& W2) {
113 using std::exp;
114 using std::log;
115 const std::size_t R = K.size();
116 const T zero = num_traits<T>::from_int(0);
117 std::vector<std::size_t> dims(R);
118 std::size_t ns = 1;
119 for (std::size_t j = 0; j < R; ++j) {
120 dims[j] = static_cast<std::size_t>(K[j]) + 1;
121 ns *= dims[j];
122 }
123 std::vector<std::size_t> stride(R, 1);
124 for (std::size_t j = 1; j < R; ++j) stride[j] = stride[j - 1] * dims[j - 1];
125
126 Matrix<long> states(ns, R);
127 std::vector<long> tot(ns, 0);
128 for (std::size_t lin = 0; lin < ns; ++lin) {
129 std::size_t res = lin;
130 for (std::size_t j = 0; j < R; ++j) {
131 states(lin, j) = static_cast<long>(res % dims[j]);
132 res /= dims[j];
133 tot[lin] += states(lin, j);
134 }
135 }
136
137 // stationary law (15), in logs so that large populations do not overflow
138 std::vector<T> r(R);
139 for (std::size_t j = 0; j < R; ++j) r[j] = T(p[j] / q[j]);
140 const double ninf = -std::numeric_limits<double>::infinity();
141 std::vector<double> logpi(ns, 0.0);
142 for (std::size_t lin = 0; lin < ns; ++lin) {
143 double v = num_traits<T>::to_double(
144 num_lgamma<T>(num_traits<T>::from_int(tot[lin] + 1)));
145 for (std::size_t j = 0; j < R; ++j) {
146 const long nj = states(lin, j);
147 v += num_traits<T>::to_double(num_lgamma<T>(num_traits<T>::from_int(K[j] + 1))) -
148 num_traits<T>::to_double(num_lgamma<T>(num_traits<T>::from_int(nj + 1))) -
149 num_traits<T>::to_double(num_lgamma<T>(num_traits<T>::from_int(K[j] - nj + 1)));
150 if (r[j] > num_traits<T>::from_int(0)) {
151 v += static_cast<double>(nj) * num_traits<T>::log_as_double(r[j]);
152 } else if (nj > 0) {
153 v = ninf;
154 }
155 }
156 logpi[lin] = v;
157 }
158 double mx = ninf;
159 for (std::size_t lin = 0; lin < ns; ++lin) mx = std::max(mx, logpi[lin]);
160 std::vector<T> pin(ns, zero);
161 T psum = zero;
162 for (std::size_t lin = 0; lin < ns; ++lin) {
163 pin[lin] = num_traits<T>::from_double(std::exp(logpi[lin] - mx));
164 psum += pin[lin];
165 }
166 for (std::size_t lin = 0; lin < ns; ++lin) pin[lin] = T(pin[lin] / psum);
167
168 Matrix<T> A(ns, ns, zero);
169 for (std::size_t lin = 0; lin < ns; ++lin) {
170 T diagv = zero;
171 for (std::size_t j = 0; j < R; ++j) {
172 const long nj = states(lin, j);
173 if (nj >= 1)
174 A(lin - stride[j], lin) +=
175 p[j] * num_traits<T>::from_int(K[j] - nj + 1) *
176 num_traits<T>::from_int(tot[lin]);
177 if (nj <= K[j] - 1)
178 A(lin + stride[j], lin) += num_traits<T>::from_int(nj + 1) * q[j];
179 diagv -= p[j] * num_traits<T>::from_int(K[j] - nj) *
180 num_traits<T>::from_int(tot[lin] + 1) +
181 num_traits<T>::from_int(nj) * q[j];
182 }
183 A(lin, lin) += diagv;
184 }
185
186 // c = ((A - q_J I)')^{-1} (-(tot+1) pi)
187 Matrix<T> Mt(ns, ns, zero);
188 for (std::size_t i = 0; i < ns; ++i)
189 for (std::size_t k = 0; k < ns; ++k) Mt(i, k) = A(k, i);
190 for (std::size_t i = 0; i < ns; ++i) Mt(i, i) -= q[J];
191 std::vector<T> rhs(ns, zero);
192 for (std::size_t lin = 0; lin < ns; ++lin)
193 rhs[lin] = T(-num_traits<T>::from_int(tot[lin] + 1) * pin[lin]);
194 const std::vector<T> c = solve(Mt, rhs);
195
196 W = zero;
197 T acc = zero;
198 for (std::size_t lin = 0; lin < ns; ++lin) {
199 W += c[lin];
200 acc += num_traits<T>::from_int(tot[lin] + 1) * c[lin];
201 }
202 W2 = T(T(num_traits<T>::from_int(2) / q[J]) * acc);
203}
204
205/**
206 * Proposition 6: the two leading terms of the expansion in 1/Nexp. Equation
207 * numbers are those of Mitra and Morrison (1983).
208 */
209template <class T>
210void respt_ps_asymptotic(const std::vector<T>& p, const std::vector<T>& q,
211 const std::vector<long>& K, std::size_t J, T& W, T& W2, T& c0, T& c1) {
212 const std::size_t R = K.size();
213 const T zero = num_traits<T>::from_int(0);
214 const T one = num_traits<T>::from_int(1);
215 std::vector<T> lambda(R);
216 for (std::size_t j = 0; j < R; ++j) lambda[j] = T(p[j] * num_traits<T>::from_int(K[j]));
217 T alpha = one;
218 for (std::size_t j = 0; j < R; ++j) alpha -= T(lambda[j] / q[j]);
219 T Nexp = T(q[0] / p[0]); // (50)
220 for (std::size_t j = 1; j < R; ++j) {
221 const T v = T(q[j] / p[j]);
222 if (v > Nexp) Nexp = v;
223 }
224 std::vector<T> Gam(R), beta(R);
225 for (std::size_t j = 0; j < R; ++j) {
226 Gam[j] = T(Nexp * p[j] / q[j]); // (51)
227 beta[j] = T(num_traits<T>::from_int(K[j]) / Nexp); // (51)
228 }
229 const T qJ = q[J];
230
231 T den = one;
232 for (std::size_t j = 0; j < R; ++j) den -= T(lambda[j] / T(q[j] + qJ));
233 T num = one;
234 for (std::size_t j = 0; j < R; ++j)
235 num -= T(lambda[j] * T(q[j] - qJ) / T(q[j] * T(q[j] + qJ)));
236 const T alpha2 = T(alpha * alpha);
237 const T F10 = T(T(-one / T(alpha2 * qJ)) * num / den); // (110)
238 c0 = T(T(-num_traits<T>::from_int(2) / qJ) * F10);
239
240 T bg2 = zero;
241 for (std::size_t j = 0; j < R; ++j) bg2 += beta[j] * Gam[j] * Gam[j];
242 std::vector<T> f1(R);
243 for (std::size_t j = 0; j < R; ++j) // (113iii)
244 f1[j] = T(T(lambda[j] / T(q[j] + qJ)) *
245 T(F10 - T(num_traits<T>::from_int(2) / T(alpha2 * q[j]))));
246 const T alpha3 = T(alpha2 * alpha);
247 const T alpha4 = T(alpha3 * alpha);
248 std::vector<T> S2j(R);
249 for (std::size_t j = 0; j < R; ++j) // (113i)
250 S2j[j] = T(T(num_traits<T>::from_int(6) / alpha4) *
251 T(alpha * beta[j] * Gam[j] * Gam[j] +
252 num_traits<T>::from_int(2) * bg2 * beta[j] * Gam[j]));
253 Matrix<T> S2js(R, R, zero); // (113ii)
254 for (std::size_t j = 0; j < R; ++j)
255 for (std::size_t s = 0; s < R; ++s)
256 S2js(j, s) = T(T(num_traits<T>::from_int(3) / alpha3) * beta[j] * Gam[j] * beta[s] *
257 Gam[s]);
258
259 Matrix<T> Amat(R, R, zero);
260 for (std::size_t j = 0; j < R; ++j) Amat(j, j) = one;
261 std::vector<T> rhs(R, zero);
262 for (std::size_t j = 0; j < R; ++j) {
263 for (std::size_t s = 0; s < R; ++s) {
264 const T d = T(q[j] + q[s] + qJ);
265 Amat(j, s) -= T(lambda[j] / d);
266 Amat(j, j) -= T(lambda[s] / d);
267 rhs[j] += T(S2js(j, s) / d);
268 }
269 rhs[j] -= f1[j];
270 }
271 const std::vector<T> F2 = solve(Amat, rhs); // (112)
272
273 const T f10 = T(T(-num_traits<T>::from_int(3) / T(alpha3 * qJ)) * bg2); // (98)
274 T acc = zero;
275 for (std::size_t j = 0; j < R; ++j)
276 acc += T(T(num_traits<T>::from_int(2) * Gam[j] * q[j] * F2[j] + S2j[j]) / T(q[j] + qJ));
277 const T F20 = T(T(acc - f10) / den); // (111)
278 c1 = T(T(T(-num_traits<T>::from_int(2) / qJ) * F20) + T(c0 / alpha2 * bg2)); // (114ii)
279
280 W = T(T(one / T(alpha * qJ)) *
281 T(one - T(num_traits<T>::from_int(2) / Nexp * bg2 / alpha2))); // (68)
282 W2 = T(c0 + T(c1 / Nexp)); // (114i)
283}
284
285} // namespace detail
286
287/**
288 * @brief Sojourn-time moments at the processor-sharing station of a closed
289 * terminal-driven system (Mitra and Morrison 1983).
290 *
291 * @param S (R) mean service times at the PS station, positive
292 * @param N (R) populations, non-negative integers
293 * @param Z (R) mean think times, positive where N > 0
294 * @param route which route to take
295 */
296template <class T>
297ResptPsMomentsResult<T> pfqn_respt_ps_moments(const std::vector<T>& S, const std::vector<long>& N,
298 const std::vector<T>& Z, ResptPsRoute route) {
300 "pfqn_respt_ps_moments builds its stationary law in the log domain and needs "
301 "transcendental arithmetic");
302 const std::size_t R = S.size();
303 const T zero = num_traits<T>::from_int(0);
304 if (N.size() != R || Z.size() != R)
305 throw InputError("pfqn_respt_ps_moments: S, N and Z must have the same number of classes");
306 for (std::size_t r = 0; r < R; ++r)
307 if (S[r] <= zero) throw InputError("pfqn_respt_ps_moments: S must be finite and positive");
308 for (std::size_t r = 0; r < R; ++r)
309 if (N[r] < 0)
310 throw InputError("pfqn_respt_ps_moments: N must contain non-negative integers");
311
312 std::vector<std::size_t> act;
313 for (std::size_t r = 0; r < R; ++r)
314 if (N[r] > 0) act.push_back(r);
315 for (std::size_t k = 0; k < act.size(); ++k)
316 if (Z[act[k]] <= zero)
317 throw InputError(
318 "pfqn_respt_ps_moments: Z must be finite and positive for every populated class");
319
320 const T nan = std::numeric_limits<T>::quiet_NaN();
322 res.W.assign(R, nan);
323 res.W2.assign(R, nan);
324 res.method.assign(R, ResptPsMethod::None);
325 res.c0.assign(R, nan);
326 res.c1.assign(R, nan);
327 res.alpha.assign(R, nan);
328 res.nstates.assign(R, std::numeric_limits<double>::quiet_NaN());
329 res.expansionParam = nan;
330 if (act.empty()) return res;
331
332 const std::size_t A = act.size();
333 std::vector<T> qa(A), pa(A);
334 for (std::size_t k = 0; k < A; ++k) {
335 qa[k] = T(num_traits<T>::from_int(1) / S[act[k]]);
336 pa[k] = T(num_traits<T>::from_int(1) / Z[act[k]]);
337 }
338 res.expansionParam = T(qa[0] / pa[0]);
339 for (std::size_t k = 1; k < A; ++k) {
340 const T v = T(qa[k] / pa[k]);
341 if (v > res.expansionParam) res.expansionParam = v;
342 }
343
344 for (std::size_t jj = 0; jj < A; ++jj) {
345 const std::size_t J = act[jj];
346 std::vector<long> K(A);
347 for (std::size_t k = 0; k < A; ++k) K[k] = N[act[k]];
348 K[jj] -= 1;
349 double ns = 1.0;
350 for (std::size_t k = 0; k < A; ++k) ns *= static_cast<double>(K[k] + 1);
351 T alpha = num_traits<T>::from_int(1);
352 for (std::size_t k = 0; k < A; ++k)
353 alpha -= T(T(pa[k] * num_traits<T>::from_int(K[k])) / qa[k]);
354 res.alpha[J] = alpha;
355 res.nstates[J] = ns;
356
357 const bool useExact =
358 route == ResptPsRoute::Exact ||
359 (route == ResptPsRoute::Auto && ns <= detail::RESPT_PS_AUTO_MAX);
360 if (useExact) {
361 if (ns > detail::RESPT_PS_EXACT_MAX)
362 throw InputError(
363 "pfqn_respt_ps_moments: the exact route needs a linear solve above the "
364 "supported dimension; use the asymptotic route");
365 detail::respt_ps_exact(pa, qa, K, jj, res.W[J], res.W2[J]);
367 continue;
368 }
369 if (alpha <= zero) {
370 if (route == ResptPsRoute::Asymptotic)
371 throw InputError(
372 "pfqn_respt_ps_moments: the asymptotic expansion needs normal usage alpha > 0, "
373 "which this model violates");
375 continue;
376 }
377 detail::respt_ps_asymptotic(pa, qa, K, jj, res.W[J], res.W2[J], res.c0[J], res.c1[J]);
379 }
380 return res;
381}
382
383/** MATLAB default: the automatic route. */
384template <class T>
385ResptPsMomentsResult<T> pfqn_respt_ps_moments(const std::vector<T>& S, const std::vector<long>& N,
386 const std::vector<T>& Z) {
388}
389
390} // namespace pfqn
391} // namespace line
392
393#endif // LINE_API_PFQN_RESPT_PS_MOMENTS_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
ResptPsMethod
Which route produced the moments of a given class.
@ Exact
Proposition 3, the linear solve.
@ Unavailable
normal usage fails and the exact route was not affordable
@ Asymptotic
Proposition 6, the two-term expansion.
@ None
the class is unpopulated
@ Auto
tail for a single class, pmf otherwise
ResptPsMomentsResult< T > pfqn_respt_ps_moments(const std::vector< T > &S, const std::vector< long > &N, const std::vector< T > &Z, ResptPsRoute route)
Sojourn-time moments at the processor-sharing station of a closed terminal-driven system (Mitra and M...
ResptPsRoute
Requested route.
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
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Sojourn-time moments at the PS station, per class.
std::vector< T > c1
(R) first correction, NaN off that route
std::vector< T > W
(R) mean sojourn times; NaN for an unpopulated class
T expansionParam
Nexp = max_r q_r / p_r.
std::vector< T > c0
(R) leading asymptotic term, NaN off that route
std::vector< double > nstates
(R) size of the exact state space
std::vector< ResptPsMethod > method
std::vector< T > W2
(R) second moments
std::vector< T > alpha
(R) unutilized CPU fraction of the open system