LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_qsa.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_QSA_H
6#define LINE_API_PFQN_QSA_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Queue-Shift Approximation (QSA) for closed product-form networks.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_qsa.m. Schweitzer, Serazzi and
14 * Broglia, "A Queue-Shift Approximation Technique for Product-Form Queueing
15 * Networks", Tools'98, LNCS 1469, pp. 267-279.
16 *
17 * Where Linearizer extrapolates the fractional deviation D_rit, QSA
18 * extrapolates the ABSOLUTE shift of the aggregate queue length,
19 * Y_ri(K) = 1 + Q_i(K - e_r) - Q_i(K) i in QC
20 * so the unknowns are one per station rather than one per station-class. The
21 * core equation (13a),
22 * Q_i(K) = sum_r K_r L_ri [Q_i(K) + Y_ri(K)] / C_r(K)
23 * is imposed at K, at every K - e_s and, in the three-level variant of eq. (16),
24 * at every K - e_s - e_t through the affine extrapolation of eq. (15).
25 *
26 * The quintuple (16) is solved as ONE system by damped Newton, as Sect. 4 of
27 * the paper prescribes. The decomposed successive substitution that works for
28 * Linearizer must NOT be used here: it drifts to the degenerate root in which
29 * the bottleneck absorbs the whole population, and does so even when seeded at
30 * the exact solution, because the instability is a positive real eigenvalue
31 * rather than an oscillation that under-relaxation could damp.
32 *
33 * Iterates to a residual tolerance, so exact arithmetic buys nothing: the
34 * static_assert records that.
35 */
36
37#include <cmath>
38#include <cstddef>
39#include <vector>
40
42#include "line/num/number.h"
43#include "line/util/error.h"
44#include "line/util/lu.h"
45#include "line/util/matrix.h"
46
47namespace line {
48namespace pfqn {
49
50namespace detail {
51
52/// Shift matrix (M x R) of (16d)-(16e), or (15) when both s and t are set.
53template <class T>
54Matrix<T> qsa_shift(const Matrix<T>& q, const Matrix<T>& pops, const std::vector<long>& sIdx,
55 const Matrix<long>& pIdx, long s, long t, int levels) {
56 const std::size_t M = q.rows(), R = pops.cols();
57 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
58 Matrix<T> Y(M, R, zero);
59 if (s < 0) {
60 for (std::size_t r = 0; r < R; ++r)
61 if (sIdx[r] >= 0)
62 for (std::size_t i = 0; i < M; ++i)
63 Y(i, r) = one + q(i, static_cast<std::size_t>(sIdx[r])) - q(i, 0);
64 } else if (t < 0) {
65 if (levels < 3) return qsa_shift(q, pops, sIdx, pIdx, -1, -1, levels); // (14)
66 const std::size_t ps = static_cast<std::size_t>(sIdx[static_cast<std::size_t>(s)]);
67 for (std::size_t r = 0; r < R; ++r) {
68 const long pr = pIdx(static_cast<std::size_t>(s), r);
69 if (pr >= 0 && pops(ps, r) >= one)
70 for (std::size_t i = 0; i < M; ++i)
71 Y(i, r) = one + q(i, static_cast<std::size_t>(pr)) - q(i, ps);
72 }
73 } else {
74 const Matrix<T> Ys = qsa_shift(q, pops, sIdx, pIdx, s, -1, levels);
75 const Matrix<T> Yt = qsa_shift(q, pops, sIdx, pIdx, t, -1, levels);
76 const Matrix<T> Y0 = qsa_shift(q, pops, sIdx, pIdx, -1, -1, levels);
77 for (std::size_t i = 0; i < M; ++i)
78 for (std::size_t r = 0; r < R; ++r) Y(i, r) = Ys(i, r) + Yt(i, r) - Y0(i, r);
79 }
80 return Y;
81}
82
83/// Decode a population index into the removed classes.
84inline void qsa_which(std::size_t p, const std::vector<long>& sIdx, const Matrix<long>& pIdx,
85 long& s, long& t) {
86 s = -1;
87 t = -1;
88 if (p == 0) return;
89 for (std::size_t k = 0; k < sIdx.size(); ++k)
90 if (sIdx[k] == static_cast<long>(p)) {
91 s = static_cast<long>(k);
92 return;
93 }
94 for (std::size_t a = 0; a < pIdx.rows(); ++a)
95 for (std::size_t b = 0; b < pIdx.cols(); ++b)
96 if (pIdx(a, b) == static_cast<long>(p)) {
97 s = static_cast<long>(a);
98 t = static_cast<long>(b);
99 return;
100 }
101}
102
103/**
104 * Residual of (13) imposed simultaneously at every population of (16). adm
105 * carries the side conditions of Remark 2: non-negative queue lengths and
106 * positive cycle times.
107 */
108template <class T>
109std::vector<T> qsa_resid(const std::vector<T>& x, const Matrix<T>& L, const std::vector<T>& Z,
110 const Matrix<T>& pops, const std::vector<long>& sIdx,
111 const Matrix<long>& pIdx, const std::vector<std::size_t>& qc,
112 const std::vector<T>& Ldc, int levels, bool& adm) {
113 const std::size_t M = L.rows(), R = L.cols(), nP = pops.rows(), mq = qc.size();
114 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
115 adm = true;
116 Matrix<T> q(M, nP, zero);
117 for (std::size_t a = 0; a < mq; ++a)
118 for (std::size_t p = 0; p < nP; ++p) {
119 q(qc[a], p) = x[a * nP + p];
120 if (x[a * nP + p] < zero) adm = false;
121 }
122 std::vector<T> F(mq * nP, zero);
123 for (std::size_t p = 0; p < nP; ++p) {
124 long s = 0, t = 0;
125 qsa_which(p, sIdx, pIdx, s, t);
126 const Matrix<T> Y = qsa_shift(q, pops, sIdx, pIdx, s, t, levels);
127 std::vector<T> acc(mq, zero);
128 for (std::size_t r = 0; r < R; ++r) {
129 if (pops(p, r) < one) continue;
130 T c = Z.empty() ? Ldc[r] : T(Z[r] + Ldc[r]);
131 for (std::size_t a = 0; a < mq; ++a) c += L(qc[a], r) * (q(qc[a], p) + Y(qc[a], r));
132 if (!(c > zero) || !std::isfinite(num_traits<T>::to_double(c))) {
133 adm = false;
134 c = num_traits<T>::from_double(1e-300);
135 }
136 const T xr = pops(p, r) / c;
137 for (std::size_t a = 0; a < mq; ++a)
138 acc[a] += xr * L(qc[a], r) * (q(qc[a], p) + Y(qc[a], r));
139 }
140 for (std::size_t a = 0; a < mq; ++a) F[a * nP + p] = q(qc[a], p) - acc[a];
141 }
142 return F;
143}
144
145/**
146 * Aggregate Bard-Schweitzer queue lengths at population n, with the
147 * delay-centre demands folded into the think time.
148 */
149template <class T>
150std::vector<T> qsa_aggbs(const Matrix<T>& L, const std::vector<T>& n, const std::vector<T>& Z,
151 const std::vector<bool>& isQC) {
152 const std::size_t M = L.rows(), R = L.cols();
153 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
154 std::vector<T> q(M, zero);
155 std::vector<T> nn(R, zero);
156 bool empty = true;
157 for (std::size_t r = 0; r < R; ++r) {
158 nn[r] = (n[r] > zero) ? n[r] : zero;
159 if (nn[r] > zero) empty = false;
160 }
161 if (empty) return q;
162
163 std::vector<T> Zeff(R, zero);
164 for (std::size_t r = 0; r < R; ++r) {
165 Zeff[r] = Z.empty() ? zero : Z[r];
166 for (std::size_t i = 0; i < M; ++i)
167 if (!isQC[i]) Zeff[r] += L(i, r);
168 }
169 std::size_t mq = 0;
170 for (std::size_t i = 0; i < M; ++i)
171 if (isQC[i]) ++mq;
172
173 std::vector<T> X(R, zero);
174 if (mq > 0) {
175 Matrix<T> Lq(mq, R, zero);
176 std::size_t a = 0;
177 for (std::size_t i = 0; i < M; ++i)
178 if (isQC[i]) {
179 for (std::size_t r = 0; r < R; ++r) Lq(a, r) = L(i, r);
180 ++a;
181 }
182 const AmvaResult<T> bs = pfqn_bs(Lq, nn, Zeff, std::vector<AmvaSched>());
183 a = 0;
184 for (std::size_t i = 0; i < M; ++i)
185 if (isQC[i]) {
186 T s = zero;
187 for (std::size_t r = 0; r < R; ++r) s += bs.QN(a, r);
188 q[i] = s;
189 ++a;
190 }
191 X = bs.XN;
192 } else {
193 for (std::size_t r = 0; r < R; ++r)
194 if (nn[r] >= one && Zeff[r] > zero) X[r] = nn[r] / Zeff[r];
195 }
196 for (std::size_t i = 0; i < M; ++i)
197 if (!isQC[i]) {
198 T s = zero;
199 for (std::size_t r = 0; r < R; ++r) s += X[r] * L(i, r);
200 q[i] = s;
201 }
202 return q;
203}
204
205} // namespace detail
206
207/**
208 * @brief Queue-Shift Approximation (QSA) for closed product-form networks.
209 *
210 * @param L (M x R) demands
211 * @param N (R) populations
212 * @param Z (R) think times, empty for none
213 * @param type (M) per-station scheduling; AmvaSched::INF marks a delay centre,
214 * whose demand enters the cycle time without a queueing term (the
215 * paper's DC set). Empty means every station queues.
216 * @param tol residual tolerance of the Newton iteration
217 * @param maxiter maximum Newton iterations
218 * @param levels 2 for the two-level QSA of eq. (14), 3 for eq. (16)
219 */
220template <class T>
221AmvaResult<T> pfqn_qsa(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
222 const std::vector<AmvaSched>& type, double tol = 1e-10,
223 std::size_t maxiter = 100, int levels = 3) {
225 "pfqn_qsa requires transcendental arithmetic: the Newton iteration stops on a "
226 "residual tolerance, so its answer is a fixed point only to within tol");
227 const std::size_t M = L.rows(), R = L.cols();
228 if (N.size() != R) throw InputError("pfqn_qsa: L and N disagree on the class count");
229 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_qsa: Z has the wrong length");
230 if (!type.empty() && type.size() != M) throw InputError("pfqn_qsa: type has the wrong length");
231 if (levels != 2 && levels != 3) throw InputError("pfqn_qsa: levels must be 2 or 3");
232
233 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
234 std::vector<bool> isQC(M, true);
235 for (std::size_t i = 0; i < M && !type.empty(); ++i) isQC[i] = type[i] != AmvaSched::INF;
236
237 AmvaResult<T> out;
238 out.XN.assign(R, zero);
239 out.QN = Matrix<T>(M, R, zero);
240 out.UN = Matrix<T>(M, R, zero);
241 out.RN = Matrix<T>(M, R, zero);
242
243 bool emptyDemands = true;
244 for (std::size_t i = 0; i < M && emptyDemands; ++i)
245 for (std::size_t r = 0; r < R; ++r)
246 if (L(i, r) != zero) {
247 emptyDemands = false;
248 break;
249 }
250 bool emptyPop = true;
251 for (std::size_t r = 0; r < R; ++r)
252 if (N[r] > zero) emptyPop = false;
253 if (M == 0 || emptyDemands || emptyPop) {
254 for (std::size_t r = 0; r < R; ++r) {
255 if (N[r] > zero && !Z.empty() && Z[r] > zero) out.XN[r] = N[r] / Z[r];
256 for (std::size_t i = 0; i < M; ++i) out.UN(i, r) = out.XN[r] * L(i, r);
257 }
258 return out;
259 }
260
261 // Populations touched by (16): K, every K - e_s, every K - e_s - e_t.
262 std::vector<std::vector<T>> popList;
263 popList.push_back(N);
264 std::vector<long> sIdx(R, -1);
265 Matrix<long> pIdx(R, R, -1);
266 for (std::size_t s = 0; s < R; ++s) {
267 std::vector<T> n = N;
268 n[s] -= one;
269 bool ok = true;
270 for (std::size_t r = 0; r < R; ++r)
271 if (n[r] < zero) ok = false;
272 if (ok) {
273 popList.push_back(n);
274 sIdx[s] = static_cast<long>(popList.size()) - 1;
275 }
276 }
277 if (levels >= 3) {
278 for (std::size_t s = 0; s < R; ++s)
279 for (std::size_t t = s; t < R; ++t) {
280 std::vector<T> n = N;
281 n[s] -= one;
282 n[t] -= one;
283 bool ok = true;
284 for (std::size_t r = 0; r < R; ++r)
285 if (n[r] < zero) ok = false;
286 if (ok) {
287 popList.push_back(n);
288 pIdx(s, t) = static_cast<long>(popList.size()) - 1;
289 pIdx(t, s) = pIdx(s, t);
290 }
291 }
292 }
293 const std::size_t nP = popList.size();
294 Matrix<T> pops(nP, R, zero);
295 for (std::size_t p = 0; p < nP; ++p)
296 for (std::size_t r = 0; r < R; ++r) pops(p, r) = popList[p][r];
297
298 std::vector<std::size_t> qc;
299 for (std::size_t i = 0; i < M; ++i)
300 if (isQC[i]) qc.push_back(i);
301 const std::size_t mq = qc.size();
302 std::vector<T> Ldc(R, zero);
303 for (std::size_t r = 0; r < R; ++r)
304 for (std::size_t i = 0; i < M; ++i)
305 if (!isQC[i]) Ldc[r] += L(i, r);
306
307 // Bard-Schweitzer at every population supplies the Newton starting point.
308 Matrix<T> q(M, nP, zero);
309 for (std::size_t p = 0; p < nP; ++p) {
310 const std::vector<T> qp = detail::qsa_aggbs(L, popList[p], Z, isQC);
311 for (std::size_t i = 0; i < M; ++i) q(i, p) = qp[i];
312 }
313
314 const std::size_t nUnk = mq * nP;
315 std::vector<T> x(nUnk, zero);
316 for (std::size_t a = 0; a < mq; ++a)
317 for (std::size_t p = 0; p < nP; ++p) x[a * nP + p] = q(qc[a], p);
318
319 bool adm = true;
320 std::vector<T> F = detail::qsa_resid(x, L, Z, pops, sIdx, pIdx, qc, Ldc, levels, adm);
321 auto nrm = [](const std::vector<T>& v) {
322 double s = 0.0;
323 for (std::size_t i = 0; i < v.size(); ++i) {
324 const double d = num_traits<T>::to_double(v[i]);
325 s += d * d;
326 }
327 return std::sqrt(s);
328 };
329 double fnrm = nrm(F);
330 for (std::size_t it = 1; it <= maxiter; ++it) {
331 if (fnrm < tol) break;
332 out.iterations = it;
333 Matrix<T> J(nUnk, nUnk, zero);
334 for (std::size_t col = 0; col < nUnk; ++col) {
335 const double xc = num_traits<T>::to_double(x[col]);
336 const double hd = 1e-7 * std::max(1.0, std::fabs(xc));
337 const T h = num_traits<T>::from_double(hd);
338 std::vector<T> xp = x;
339 xp[col] += h;
340 bool dummy = true;
341 const std::vector<T> Fp =
342 detail::qsa_resid(xp, L, Z, pops, sIdx, pIdx, qc, Ldc, levels, dummy);
343 for (std::size_t row = 0; row < nUnk; ++row) J(row, col) = (Fp[row] - F[row]) / h;
344 }
345 std::vector<T> rhs(nUnk, zero);
346 for (std::size_t row = 0; row < nUnk; ++row) rhs[row] = zero - F[row];
347 std::vector<T> step;
348 try {
349 step = solve(J, rhs);
350 } catch (const std::exception&) {
351 break; // singular Jacobian: keep the best iterate rather than guessing
352 }
353 bool accepted = false;
354 double lambda = 1.0;
355 for (int ls = 0; ls < 40; ++ls) {
356 std::vector<T> xn(nUnk, zero);
357 bool finite = true;
358 const T lam = num_traits<T>::from_double(lambda);
359 for (std::size_t row = 0; row < nUnk; ++row) {
360 xn[row] = x[row] + lam * step[row];
361 if (!std::isfinite(num_traits<T>::to_double(xn[row]))) finite = false;
362 }
363 if (finite) {
364 bool admn = true;
365 const std::vector<T> Fn =
366 detail::qsa_resid(xn, L, Z, pops, sIdx, pIdx, qc, Ldc, levels, admn);
367 const double nn = nrm(Fn);
368 if (admn && nn < fnrm) {
369 x = xn;
370 F = Fn;
371 fnrm = nn;
372 adm = admn;
373 accepted = true;
374 break;
375 }
376 }
377 lambda /= 2.0;
378 }
379 if (!accepted) break;
380 }
381 out.converged = adm && fnrm < tol;
382
383 // Disaggregate (13) at K into the per-class measures
384 for (std::size_t a = 0; a < mq; ++a)
385 for (std::size_t p = 0; p < nP; ++p) q(qc[a], p) = x[a * nP + p];
386 const Matrix<T> Y0 = detail::qsa_shift(q, pops, sIdx, pIdx, -1, -1, levels);
387 for (std::size_t r = 0; r < R; ++r) {
388 if (N[r] < one) continue;
389 T sumW = zero;
390 for (std::size_t i = 0; i < M; ++i) {
391 out.RN(i, r) = isQC[i] ? T(L(i, r) * (q(i, 0) + Y0(i, r))) : L(i, r);
392 sumW += out.RN(i, r);
393 }
394 const T denom = (Z.empty() ? zero : Z[r]) + sumW;
395 out.XN[r] = N[r] / denom;
396 for (std::size_t i = 0; i < M; ++i) {
397 out.QN(i, r) = out.XN[r] * out.RN(i, r);
398 out.UN(i, r) = out.XN[r] * L(i, r);
399 }
400 }
401 return out;
402}
403
404template <class T>
405AmvaResult<T> pfqn_qsa(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z) {
406 return pfqn_qsa(L, N, Z, std::vector<AmvaSched>());
407}
408
409template <class T>
410AmvaResult<T> pfqn_qsa(const Matrix<T>& L, const std::vector<T>& N) {
411 return pfqn_qsa(L, N, std::vector<T>(), std::vector<AmvaSched>());
412}
413
414} // namespace pfqn
415} // namespace line
416
417#endif // LINE_API_PFQN_QSA_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
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
AmvaResult< T > pfqn_qsa(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-10, std::size_t maxiter=100, int levels=3)
Queue-Shift Approximation (QSA) for closed product-form networks.
Definition pfqn_qsa.h:221
AmvaResult< T > pfqn_bs(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >())
Bard-Schweitzer approximate MVA.
Definition pfqn_bs.h:73
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.
Bard-Schweitzer approximate MVA.
Matrix< T > RN
(M x R) residence time
Definition pfqn_bs.h:52
std::vector< T > XN
(R) throughput
Definition pfqn_bs.h:49
Matrix< T > UN
(M x R) utilization
Definition pfqn_bs.h:51
std::size_t iterations
Definition pfqn_bs.h:53
Matrix< T > QN
(M x R) queue length
Definition pfqn_bs.h:50