LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_sens_respt.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_SENS_RESPT_H
6#define LINE_API_PFQN_SENS_RESPT_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact raw moments E[W^t], t = 1..3, of the sojourn time of a job at an FCFS
12 * b-server center of a closed product-form network.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_sens_respt.m, Theorem 4.1 of
15 * Strelen (Performance Evaluation 11:127-142, 1990). By the arrival theorem a
16 * class-l job finds j jobs at center i with probability p_i(j, N - e_l);
17 * conditioning on j and inverting the Laplace transform of the conditional
18 * density gives equation (4.5),
19 *
20 * E[W_(i,l)^t] = t!/mu^t + sum_tau a_(t,tau)(0) E[Qt_i^tau]
21 * - sum_{j<b_i} p_i(j,N-e_l) sum_tau a_(t,tau)(0) j^tau
22 *
23 * with mu = 1/S(i), Qt_i the total queue at center i at population N - e_l and
24 * the coefficients a_(t,tau)(0) of Remark 4.3 depending only on b and mu. The
25 * moments E[Qt_i^tau] up to tau = 3 need the second derivative of the
26 * b-server recursion (4.1)-(4.2), which is carried here by a second-order
27 * forward-mode pass along the population lattice, exactly as pfqn_sens_mom
28 * does for the single-server recursion.
29 *
30 * Only FCFS centers are covered: the reference is explicit that the
31 * sojourn-time distribution at PS and LCFS centers is in general not known.
32 * FCFS in a BCMP network requires a class-independent exponential service
33 * time, which is why the input is a per-station service time S and a separate
34 * visit matrix V rather than a demand matrix: the sojourn time is per visit, so
35 * mu = 1/S(i) must be known and cannot be recovered from L(i,l) = S(i) V(i,l).
36 *
37 * Arithmetic. Every step is a field operation, so the moments instantiate at
38 * line::Rational and are exact rationals. The two skewnesses are the only
39 * derived quantities that leave the field (they divide by a variance to the
40 * power 1.5), so they are returned as doubles and the header carries no
41 * transcendental gate.
42 */
43
44#include <cmath>
45#include <cstddef>
46#include <vector>
47
49#include "line/num/number.h"
50#include "line/util/error.h"
51#include "line/util/matrix.h"
53
54namespace line {
55namespace pfqn {
56
57template <class T>
59 std::vector<T> XN; ///< (R) throughput at population N
60 Matrix<T> QN; ///< (M x R) mean queue length
61 Matrix<T> UN; ///< (M x R) utilization
62
63 Matrix<T> W; ///< (M x R) E[W_(i,l)], the mean sojourn time per visit
64 std::vector<Matrix<T>> WM; ///< (tmax) matrices M x R, WM[t-1](i,l) = E[W^t]
65 Matrix<T> WVar; ///< (M x R) Var[W], zero unless tmax >= 2
66 Matrix<double> WSkew; ///< (M x R) skewness of W, zero unless tmax >= 3
67
68 std::vector<T> m; ///< (M) E[Q_i] at population N
69 std::vector<T> Var; ///< (M) Var[Q_i] at population N
70 /// (M x max(b)) p(i,j) = P[Q_i = j] for j = 0..b_i-1. RAGGED: row i is
71 /// meaningful only up to column b_i-1 and is zero-padded out to max(b).
73 Matrix<T> Wresid; ///< (M x R) the residence time w_i(l) of the recursion
74};
75
76/**
77 * @brief Exact raw moments E[W^t], t = 1..3, of the sojourn time of a job at
78 * an FCFS b-server center of a closed product-form network.
79 *
80 * @param S (M) service time of each station, common to all classes
81 * @param V (M x R) visit ratios; the demand is L(i,r) = S(i) V(i,r)
82 * @param N (R) population per class, closed only
83 * @param Z (R) think times, empty for none
84 * @param b (M) servers per station, empty for all ones
85 * @param tmax highest sojourn-time moment, 1..3
86 */
87template <class T>
88SensResptResult<T> pfqn_sens_respt(const std::vector<T>& S, const Matrix<T>& V,
89 const std::vector<int>& N, const std::vector<T>& Z,
90 const std::vector<int>& b, int tmax) {
91 const std::size_t M = V.rows();
92 const std::size_t R = N.size();
93 if (V.cols() != R)
94 throw InputError("pfqn_sens_respt: visit matrix and population vector disagree on the class count");
95 if (S.size() != M) throw InputError("pfqn_sens_respt: service-time vector has the wrong length");
96 if (!Z.empty() && Z.size() != R)
97 throw InputError("pfqn_sens_respt: think-time vector has the wrong length");
98 if (!b.empty() && b.size() != M)
99 throw InputError("pfqn_sens_respt: server-count vector has the wrong length");
100 if (tmax < 1 || tmax > 3)
101 throw InputError(
102 "pfqn_sens_respt: tmax must be 1, 2 or 3, the orders at which the coefficients "
103 "a_{t,tau}(0) are tabulated in the reference");
104
105 const T zero = num_traits<T>::from_int(0);
106 const T one = num_traits<T>::from_int(1);
107
108 std::vector<int> bv = b.empty() ? std::vector<int>(M, 1) : b;
109 std::size_t bmax = 1;
110 for (std::size_t i = 0; i < M; ++i) {
111 if (bv[i] < 1)
112 throw InputError("pfqn_sens_respt: every station must have at least one server");
113 if (static_cast<std::size_t>(bv[i]) > bmax) bmax = static_cast<std::size_t>(bv[i]);
114 if (S[i] <= zero)
115 throw InputError("pfqn_sens_respt: every FCFS station needs a strictly positive service time");
116 }
117
119 res.XN.assign(R, zero);
120 res.QN = Matrix<T>(M, R, zero);
121 res.UN = Matrix<T>(M, R, zero);
122 res.W = Matrix<T>(M, R, zero);
123 res.WM.assign(static_cast<std::size_t>(tmax), Matrix<T>(M, R, zero));
124 res.WVar = Matrix<T>(M, R, zero);
125 res.WSkew = Matrix<double>(M, R, 0.0);
126 res.m.assign(M, zero);
127 res.Var.assign(M, zero);
128 res.p = Matrix<T>(M, bmax, zero);
129 res.Wresid = Matrix<T>(M, R, zero);
130
131 bool anyPositive = false;
132 for (int v : N) {
133 if (v < 0) throw InputError("pfqn_sens_respt: negative population");
134 if (v > 0) anyPositive = true;
135 }
136 if (!anyPositive || M == 0 || R == 0) return res;
137
138 Matrix<T> rho(M, R, zero);
139 for (std::size_t i = 0; i < M; ++i)
140 for (std::size_t l = 0; l < R; ++l) rho(i, l) = S[i] * V(i, l);
141 std::vector<T> mu(M, zero);
142 for (std::size_t i = 0; i < M; ++i) mu[i] = one / S[i];
143
144 const auto Zr = [&](std::size_t r) -> T { return Z.empty() ? zero : Z[r]; };
145
146 const std::vector<std::size_t> radix = sens_lattice_radix(N);
147 const std::size_t totpop = population_count(N);
148
149 // lattice-state rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
150 Matrix<T> Mrow(totpop, M, zero);
151 std::vector<Matrix<T>> D1m(totpop, Matrix<T>(M, M, zero));
152 std::vector<Matrix<T>> D2m(totpop, Matrix<T>(M, M, zero));
153 std::vector<Matrix<T>> Prow(totpop, Matrix<T>(M, bmax, zero));
154 std::vector<std::vector<Matrix<T>>> D1p(totpop, std::vector<Matrix<T>>(M, Matrix<T>(bmax, M, zero)));
155 std::vector<std::vector<Matrix<T>>> D2p(totpop, std::vector<Matrix<T>>(M, Matrix<T>(bmax, M, zero)));
156 for (std::size_t i = 0; i < M; ++i) Prow[0](i, 0) = one; // empty population
157
158 std::vector<std::size_t> rows(R, 0);
159 Matrix<T> wv(M, R, zero);
160 std::vector<Matrix<T>> d1w(M, Matrix<T>(R, M, zero)), d2w(M, Matrix<T>(R, M, zero));
161 std::vector<T> lam(R, zero);
162 Matrix<T> d1lam(R, M, zero), d2lam(R, M, zero);
163
164 for (std::size_t k = 1; k < totpop; ++k) {
165 const std::vector<int> n = sens_lattice_decode(k, N, radix);
166 wv.fill(zero);
167 for (std::size_t i = 0; i < M; ++i) {
168 d1w[i].fill(zero);
169 d2w[i].fill(zero);
170 }
171 // ---- residence times (4.1) and their first two derivatives --------
172 for (std::size_t s = 0; s < R; ++s) {
173 const std::size_t row = n[s] > 0 ? k - radix[s] : 0;
174 rows[s] = row;
175 if (n[s] == 0) continue; // w and every derivative stay zero, as does X(s)
176 for (std::size_t i = 0; i < M; ++i) {
177 const T bi = num_traits<T>::from_int(bv[i]);
178 T brk = one + Mrow(row, i);
179 for (int j = 0; j + 2 <= bv[i]; ++j)
180 brk += num_traits<T>::from_int(bv[i] - 1 - j) * Prow[row](i, static_cast<std::size_t>(j));
181 wv(i, s) = rho(i, s) / bi * brk;
182 for (std::size_t h = 0; h < M; ++h) {
183 T dbrk = D1m[row](i, h);
184 T d2brk = D2m[row](i, h);
185 for (int j = 0; j + 2 <= bv[i]; ++j) {
186 const T c = num_traits<T>::from_int(bv[i] - 1 - j);
187 dbrk += c * D1p[row][i](static_cast<std::size_t>(j), h);
188 d2brk += c * D2p[row][i](static_cast<std::size_t>(j), h);
189 }
190 if (i == h) {
191 d1w[i](s, h) = rho(i, s) / bi * (brk + dbrk);
192 d2w[i](s, h) = rho(i, s) / bi * (num_traits<T>::from_int(2) * dbrk + d2brk);
193 } else {
194 d1w[i](s, h) = rho(i, s) / bi * dbrk;
195 d2w[i](s, h) = rho(i, s) / bi * d2brk;
196 }
197 }
198 }
199 }
200
201 // ---- throughputs and their derivatives ------------------------------
202 for (std::size_t s = 0; s < R; ++s) {
203 lam[s] = zero;
204 for (std::size_t h = 0; h < M; ++h) {
205 d1lam(s, h) = zero;
206 d2lam(s, h) = zero;
207 }
208 if (n[s] == 0) continue;
209 T sw = zero;
210 for (std::size_t i = 0; i < M; ++i) sw += wv(i, s);
211 const T den = Zr(s) + sw;
212 const T nsT = num_traits<T>::from_int(n[s]);
213 lam[s] = nsT / den;
214 const T den2 = den * den;
215 const T den3 = den2 * den;
216 for (std::size_t h = 0; h < M; ++h) {
217 T dden = zero, d2den = zero;
218 for (std::size_t i = 0; i < M; ++i) {
219 dden += d1w[i](s, h);
220 d2den += d2w[i](s, h);
221 }
222 d1lam(s, h) = -nsT * dden / den2;
223 d2lam(s, h) = -nsT * d2den / den2 +
224 num_traits<T>::from_int(2) * nsT * dden * dden / den3;
225 }
226 }
227
228 // ---- mean queue lengths ----------------------------------------------
229 for (std::size_t i = 0; i < M; ++i) {
230 T acc = zero;
231 for (std::size_t s = 0; s < R; ++s)
232 if (n[s] > 0) acc += lam[s] * wv(i, s);
233 Mrow(k, i) = acc;
234 for (std::size_t h = 0; h < M; ++h) {
235 T d1acc = zero, d2acc = zero;
236 for (std::size_t s = 0; s < R; ++s) {
237 if (n[s] == 0) continue;
238 d1acc += d1lam(s, h) * wv(i, s) + lam[s] * d1w[i](s, h);
239 d2acc += d2lam(s, h) * wv(i, s) +
240 num_traits<T>::from_int(2) * d1lam(s, h) * d1w[i](s, h) +
241 lam[s] * d2w[i](s, h);
242 }
243 D1m[k](i, h) = d1acc;
244 D2m[k](i, h) = d2acc;
245 }
246 }
247
248 // ---- marginal probabilities (4.2) and their derivatives ---------------
249 int nc = 0;
250 for (std::size_t s = 0; s < R; ++s) nc += n[s];
251 for (std::size_t i = 0; i < M; ++i) {
252 for (int j = 1; j <= bv[i] - 1; ++j) {
253 const std::size_t ju = static_cast<std::size_t>(j);
254 if (j > nc) continue; // stays zero
255 const T jT = num_traits<T>::from_int(j);
256 T acc = zero;
257 for (std::size_t l = 0; l < R; ++l) {
258 if (n[l] == 0) continue;
259 acc += lam[l] * rho(i, l) * Prow[rows[l]](i, ju - 1);
260 }
261 Prow[k](i, ju) = acc / jT;
262 for (std::size_t h = 0; h < M; ++h) {
263 T d1acc = zero, d2acc = zero;
264 for (std::size_t l = 0; l < R; ++l) {
265 if (n[l] == 0) continue;
266 const T pprev = Prow[rows[l]](i, ju - 1);
267 const T d1prev = D1p[rows[l]][i](ju - 1, h);
268 const T d2prev = D2p[rows[l]][i](ju - 1, h);
269 T v1, v2;
270 if (i == h) {
271 v1 = pprev + d1prev;
272 v2 = num_traits<T>::from_int(2) * d1prev + d2prev;
273 } else {
274 v1 = d1prev;
275 v2 = d2prev;
276 }
277 d1acc += rho(i, l) * (d1lam(l, h) * pprev + lam[l] * v1);
278 d2acc += rho(i, l) * (d2lam(l, h) * pprev +
279 num_traits<T>::from_int(2) * d1lam(l, h) * v1 +
280 lam[l] * v2);
281 }
282 D1p[k][i](ju, h) = d1acc / jT;
283 D2p[k][i](ju, h) = d2acc / jT;
284 }
285 }
286 // mean number of busy servers
287 T ui = zero;
288 std::vector<T> d1ui(M, zero), d2ui(M, zero);
289 for (std::size_t l = 0; l < R; ++l) {
290 if (n[l] == 0) continue;
291 ui += lam[l] * rho(i, l);
292 for (std::size_t h = 0; h < M; ++h) {
293 if (i == h) {
294 d1ui[h] += rho(i, l) * (d1lam(l, h) + lam[l]);
295 d2ui[h] += rho(i, l) * (d2lam(l, h) + num_traits<T>::from_int(2) * d1lam(l, h));
296 } else {
297 d1ui[h] += rho(i, l) * d1lam(l, h);
298 d2ui[h] += rho(i, l) * d2lam(l, h);
299 }
300 }
301 }
302 const T biT = num_traits<T>::from_int(bv[i]);
303 T acc0 = ui;
304 for (int j = 1; j <= bv[i] - 1; ++j)
305 acc0 += num_traits<T>::from_int(bv[i] - j) * Prow[k](i, static_cast<std::size_t>(j));
306 Prow[k](i, 0) = one - acc0 / biT;
307 for (std::size_t h = 0; h < M; ++h) {
308 T d1acc0 = d1ui[h], d2acc0 = d2ui[h];
309 for (int j = 1; j <= bv[i] - 1; ++j) {
310 const T c = num_traits<T>::from_int(bv[i] - j);
311 d1acc0 += c * D1p[k][i](static_cast<std::size_t>(j), h);
312 d2acc0 += c * D2p[k][i](static_cast<std::size_t>(j), h);
313 }
314 D1p[k][i](0, h) = -d1acc0 / biT;
315 D2p[k][i](0, h) = -d2acc0 / biT;
316 }
317 }
318
319 // keep the measures of the last (full) population
320 for (std::size_t s = 0; s < R; ++s) res.XN[s] = lam[s];
321 for (std::size_t i = 0; i < M; ++i)
322 for (std::size_t s = 0; s < R; ++s) {
323 res.Wresid(i, s) = wv(i, s);
324 res.QN(i, s) = lam[s] * wv(i, s);
325 res.UN(i, s) = lam[s] * rho(i, s);
326 }
327 }
328
329 const std::size_t last = totpop - 1;
330 for (std::size_t i = 0; i < M; ++i) {
331 res.m[i] = Mrow(last, i);
332 for (std::size_t j = 0; j < bmax; ++j) res.p(i, j) = Prow[last](i, j);
333 res.Var[i] = D1m[last](i, i);
334 }
335
336 // ---- sojourn-time moments (4.5) ---------------------------------------
337 std::vector<std::size_t> rowsN(R, 0);
338 for (std::size_t l = 0; l < R; ++l)
339 if (N[l] > 0) rowsN[l] = last - radix[l];
340
341 for (std::size_t i = 0; i < M; ++i) {
342 // a_{t,tau}(0) of Remark 4.3: they depend only on b and mu.
343 const T bT = num_traits<T>::from_int(bv[i]);
344 const T mT = mu[i];
345 Matrix<T> a(3, 4, zero);
346 a(0, 0) = (one - bT) / (bT * mT);
347 a(0, 1) = one / (bT * mT);
348 if (tmax >= 2) {
349 const T d = bT * bT * mT * mT;
350 a(1, 0) = (num_traits<T>::from_int(2) - bT - bT * bT) / d;
351 a(1, 1) = num_traits<T>::from_int(3) / d;
352 a(1, 2) = one / d;
353 }
354 if (tmax >= 3) {
355 const T d = bT * bT * bT * mT * mT * mT;
356 a(2, 0) = (num_traits<T>::from_int(6) - num_traits<T>::from_int(5) * bT +
357 num_traits<T>::from_int(3) * bT * bT - num_traits<T>::from_int(4) * bT * bT * bT) / d;
358 a(2, 1) = (num_traits<T>::from_int(11) - num_traits<T>::from_int(3) * bT +
359 num_traits<T>::from_int(3) * bT * bT) / d;
360 a(2, 2) = num_traits<T>::from_int(6) / d;
361 a(2, 3) = one / d;
362 }
363 for (std::size_t l = 0; l < R; ++l) {
364 if (N[l] == 0 || V(i, l) <= zero) continue;
365 const std::size_t rl = rowsN[l];
366 const T mt = Mrow(rl, i);
367 const T d1t = D1m[rl](i, i);
368 const T d2t = D2m[rl](i, i);
369 T EQ[4];
370 EQ[0] = one;
371 EQ[1] = mt;
372 EQ[2] = d1t + mt * mt;
373 EQ[3] = d2t + (one + num_traits<T>::from_int(3) * mt) * d1t + mt * mt * mt;
374 for (int t = 1; t <= tmax; ++t) {
375 T val = num_factorial<T>(static_cast<unsigned>(t)) /
376 num_pow_int(mu[i], static_cast<unsigned>(t));
377 for (int tau = 0; tau <= t; ++tau)
378 val += a(static_cast<std::size_t>(t - 1), static_cast<std::size_t>(tau)) *
379 EQ[tau];
380 // correction over the states in which a server is idle
381 for (int j = 0; j <= bv[i] - 1; ++j) {
382 T inner = zero;
383 for (int tau = 0; tau <= t; ++tau) {
384 // j^tau with the convention 0^0 = 1 of the reference
385 const T jp = tau == 0 ? one
387 static_cast<unsigned>(tau));
388 inner += a(static_cast<std::size_t>(t - 1), static_cast<std::size_t>(tau)) * jp;
389 }
390 val -= Prow[rl](i, static_cast<std::size_t>(j)) * inner;
391 }
392 res.WM[static_cast<std::size_t>(t - 1)](i, l) = val;
393 }
394 res.W(i, l) = res.WM[0](i, l);
395 }
396 }
397
398 if (tmax >= 2)
399 for (std::size_t i = 0; i < M; ++i)
400 for (std::size_t l = 0; l < R; ++l)
401 res.WVar(i, l) = res.WM[1](i, l) - res.WM[0](i, l) * res.WM[0](i, l);
402 if (tmax >= 3)
403 for (std::size_t i = 0; i < M; ++i)
404 for (std::size_t l = 0; l < R; ++l) {
405 const T mu3 = res.WM[2](i, l) -
406 num_traits<T>::from_int(3) * res.WM[0](i, l) * res.WM[1](i, l) +
407 num_traits<T>::from_int(2) * res.WM[0](i, l) * res.WM[0](i, l) *
408 res.WM[0](i, l);
409 const double var = num_traits<T>::to_double(res.WVar(i, l));
410 res.WSkew(i, l) = var > 0.0 ? num_traits<T>::to_double(mu3) / std::pow(var, 1.5)
411 : std::nan("");
412 }
413
414 return res;
415}
416
417/** pfqn_sens_respt with single servers and moments up to order three. */
418template <class T>
419SensResptResult<T> pfqn_sens_respt(const std::vector<T>& S, const Matrix<T>& V,
420 const std::vector<int>& N, const std::vector<T>& Z) {
421 return pfqn_sens_respt(S, V, N, Z, std::vector<int>(), 3);
422}
423
424} // namespace pfqn
425} // namespace line
426
427#endif // LINE_API_PFQN_SENS_RESPT_H
InputError(const std::string &what)
Definition error.h:39
void fill(const T &x)
Definition matrix.h:108
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Dense matrix and non-owning view.
std::vector< int > sens_lattice_decode(std::size_t k, const std::vector< int > &N, const std::vector< std::size_t > &radix)
Decode a lattice index back into a population vector.
std::vector< std::size_t > sens_lattice_radix(const std::vector< int > &N)
Radix weights of the MVA population lattice, class R-1 varying fastest.
SensResptResult< T > pfqn_sens_respt(const std::vector< T > &S, const Matrix< T > &V, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< int > &b, int tmax)
Exact raw moments E[W^t], t = 1..3, of the sojourn time of a job at an FCFS b-server center of a clos...
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
std::size_t population_count(const std::vector< int > &N)
Number of population vectors n with 0 <= n <= N.
Definition population.h:38
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
Number-type abstraction for the templated API port.
Exact per-station queue-length variances and covariances of a closed product-form network,...
Population-vector enumeration and combinatorics.
Matrix< T > p
(M x max(b)) p(i,j) = P[Q_i = j] for j = 0..b_i-1.
Matrix< double > WSkew
(M x R) skewness of W, zero unless tmax >= 3
std::vector< Matrix< T > > WM
(tmax) matrices M x R, WM[t-1](i,l) = E[W^t]
Matrix< T > WVar
(M x R) Var[W], zero unless tmax >= 2
Matrix< T > QN
(M x R) mean queue length
std::vector< T > Var
(M) Var[Q_i] at population N
std::vector< T > XN
(R) throughput at population N
Matrix< T > UN
(M x R) utilization
Matrix< T > Wresid
(M x R) the residence time w_i(l) of the recursion
Matrix< T > W
(M x R) E[W_(i,l)], the mean sojourn time per visit
std::vector< T > m
(M) E[Q_i] at population N