LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_looping.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_LOOPING_H
6#define LINE_API_PFQN_LOOPING_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Eager Looping bounds for closed multiclass product-form networks.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_looping.m, cross-checked against
14 * jar/src/main/java/jline/api/pfqn/mva/Pfqn_looping.java. D. L. Eager,
15 * "Bounding Algorithms for Queueing Network Models of Computer Systems",
16 * Ph.D. thesis, Tech. Rept. CSRG-156, University of Toronto, 1984. Looping
17 * supplies the initial pessimistic and optimistic estimates that the
18 * multiple-class performance bound hierarchy starts from, so it carries a pair
19 * of bounds rather than a single fixed point.
20 *
21 * A HEAP H_j is the class-j congestion that the current queue-length LOWER
22 * BOUNDS have not yet accounted for; it is charged back at the pessimistic
23 * inflation factor V_c = max_k D_ck or the optimistic one L_c = min_k D_ck,
24 * which are the largest and smallest delays one customer can inflict. The
25 * whole bracket rests on Q_jk(N - 1_c) being a lower bound, which is why the
26 * queue lengths are seeded from Little's law at the station,
27 *
28 * Q_jk(N - 1_c) = X_j(N - 1_c) R_jk(N - 1_c) >= n_j D_jk / (Z_j + U_j),
29 *
30 * with R_jk >= D_jk and U_j any UPPER bound on R_j(N - 1_c): the level-0 PBH
31 * bound B_j, or the pessimistic R_j(N) of the current iterate, whichever is
32 * smaller, since response time is nondecreasing in the population. Each
33 * iterate lowers R^(pess), which raises the seed, which lowers R^(pess) again,
34 * so the refinement is monotone and every iterate is a bound. It previously
35 * refined the queue lengths through the convolution identity of Zahorjan
36 * (1980), Q_jk(N - 1_c) = [X_j(N - 1_c)/X_j(N)] Q_jk(N), applied with one
37 * class-level ratio at every station; that is not a per-station under-estimate,
38 * so the queue lengths stopped being lower bounds, the heaps clamped to zero
39 * and R^(opt) collapsed onto R^(pess). The level-0 multiple-class PBH bounds on
40 * the mean response time are
41 *
42 * J_j(n) = sum_k D_jk, B_j(n) = sum_k D_jk + (sum(n) - 1) max_k D_jk,
43 *
44 * i.e. an arriving customer queues behind nobody, respectively behind every
45 * other customer in the network at its own worst centre.
46 *
47 * Arithmetic: sums, products, divisions, maxima and minima only, so each
48 * iterate is EXACT in rational arithmetic; the stopping rule selects which
49 * iterate is returned.
50 */
51
52#include <cmath>
53#include <cstddef>
54#include <vector>
55
56#include "line/num/number.h"
57#include "line/util/error.h"
58#include "line/util/matrix.h"
59
60namespace line {
61namespace pfqn {
62
63/** Return value of pfqn_looping: the throughput bracket plus its queue lengths. */
64template <class T>
66 std::vector<T> Xlo; ///< (R) pessimistic (lower) throughput bound
67 std::vector<T> Xup; ///< (R) optimistic (upper) throughput bound
68 Matrix<T> Q; ///< (M x R) queue lengths on the pessimistic side
69 Matrix<T> R; ///< (M x R) residence times
70 std::size_t iterations = 0;
71 bool converged = false;
72};
73
74/**
75 * @brief Eager Looping bounds for closed multiclass product-form networks.
76 *
77 * @param L (M x R) demands, @param N (R) populations,
78 * @param Z (R) think times (empty for none)
79 * @param tol convergence tolerance on the queue lengths
80 * @param maxiter iteration cap
81 */
82template <class T>
83LoopingBounds<T> pfqn_looping(const Matrix<T>& L, const std::vector<T>& N,
84 const std::vector<T>& Z, double tol = 1e-6,
85 std::size_t maxiter = 1000) {
86 const std::size_t M = L.rows(), R = L.cols();
87 if (N.size() != R) throw InputError("pfqn_looping: L and N disagree on the class count");
88 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_looping: Z has the wrong length");
89
90 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
91 const T two = num_traits<T>::from_int(2);
93 r.Xlo.assign(R, zero);
94 r.Xup.assign(R, zero);
95 r.Q = Matrix<T>(M, R, zero);
96 r.R = Matrix<T>(M, R, zero);
97 if (M == 0) return r;
98
99 std::vector<T> Dtot(R, zero), Vpess(R, zero), Lopt(R, zero);
100 T Ntot = zero;
101 for (std::size_t c = 0; c < R; ++c) {
102 T mx = L(0, c), mn = L(0, c);
103 for (std::size_t k = 0; k < M; ++k) {
104 Dtot[c] += L(k, c);
105 if (L(k, c) > mx) mx = L(k, c);
106 if (L(k, c) < mn) mn = L(k, c);
107 }
108 Vpess[c] = mx;
109 Lopt[c] = mn;
110 Ntot += N[c];
111 }
112 // level-0 multiple-class PBH bounds on R_j, at population N - 1_c
113 std::vector<T> Jbnd(R, zero), Bm(R, zero);
114 for (std::size_t c = 0; c < R; ++c) {
115 Jbnd[c] = Dtot[c];
116 const T span = (Ntot > two) ? T(Ntot - two) : zero;
117 Bm[c] = T(Dtot[c] + span * Vpess[c]);
118 }
119
120 // Population of class j at the stations when one class-c job is removed, at
121 // its optimistic and pessimistic extremes. These cap what the queue-length
122 // lower bounds may account for; the remainder is the heap.
123 Matrix<T> popt(R, R, zero), ppess(R, R, zero); // (j, c)
124 for (std::size_t c = 0; c < R; ++c)
125 for (std::size_t j = 0; j < R; ++j) {
126 const T nj = (c == j) ? T(N[j] - one) : N[j];
127 if (nj <= zero) continue;
128 const T zj = Z.empty() ? zero : Z[j];
129 const T dj = T(zj + Jbnd[j]);
130 if (dj > zero) popt(j, c) = T(T(Jbnd[j] / dj) * nj);
131 const T db = T(zj + Bm[j]);
132 if (db > zero) ppess(j, c) = T(T(Bm[j] / db) * nj);
133 }
134
135 // Zero is the one queue-length lower bound available before any iterate,
136 // and the heaps then carry the whole population. The even split N/M this
137 // once used is not a bound: it exceeds the true queue at every
138 // below-average station, so the first residence is already not a bound.
139 // Qm[c][j][k] = Q_jk(N - 1_c), a LOWER bound
140 std::vector<std::vector<std::vector<T> > > Qm(
141 R, std::vector<std::vector<T> >(R, std::vector<T>(M, zero)));
142 Matrix<T> Hopt(R, R, zero), Hpess(R, R, zero); // (j, c)
143 for (std::size_t c = 0; c < R; ++c)
144 for (std::size_t j = 0; j < R; ++j) {
145 Hopt(j, c) = popt(j, c);
146 Hpess(j, c) = ppess(j, c);
147 }
148 std::vector<T> Ub(Bm); // upper bound on R_j(N - 1_c)
149
150 std::vector<T> Rc(R, zero), Rpess(R, zero), Ropt(R, zero);
151 for (std::size_t it = 1; it <= maxiter; ++it) {
152 r.iterations = it;
153 const Matrix<T> Qprev = r.Q;
154
155 for (std::size_t c = 0; c < R; ++c) {
156 if (N[c] == zero) {
157 for (std::size_t k = 0; k < M; ++k) r.R(k, c) = zero;
158 r.Xlo[c] = zero;
159 Rc[c] = zero;
160 Rpess[c] = zero;
161 continue;
162 }
163 T rsum = zero;
164 for (std::size_t k = 0; k < M; ++k) {
165 T qk = zero;
166 for (std::size_t j = 0; j < R; ++j) qk += Qm[c][j][k];
167 r.R(k, c) = L(k, c) * T(one + qk);
168 rsum += r.R(k, c);
169 }
170 Rc[c] = rsum;
171 T hp = zero;
172 for (std::size_t j = 0; j < R; ++j) hp += Hpess(j, c);
173 const T zc = Z.empty() ? zero : Z[c];
174 Rpess[c] = T(Rc[c] + Vpess[c] * hp);
175 const T den = T(zc + Rpess[c]);
176 if (den == zero) throw NumericError("pfqn_looping: zero pessimistic cycle time");
177 r.Xlo[c] = N[c] / den;
178 }
179 for (std::size_t c = 0; c < R; ++c) {
180 if (N[c] == zero) {
181 Ropt[c] = zero;
182 continue;
183 }
184 const T zc = Z.empty() ? zero : Z[c];
185 bool anySat = false;
186 T sat = zero;
187 for (std::size_t k = 0; k < M; ++k) {
188 T used = zero;
189 for (std::size_t j = 0; j < R; ++j)
190 if (j != c) used += r.Xlo[j] * L(k, j);
191 const T den = T(one - used);
192 if (den > zero) {
193 const T v = T(L(k, c) * N[c] / den - zc);
194 if (!anySat || v > sat) {
195 sat = v;
196 anySat = true;
197 }
198 }
199 }
200 T ho = zero;
201 for (std::size_t j = 0; j < R; ++j) ho += Hopt(j, c);
202 T best = T(Rc[c] + Lopt[c] * ho);
203 if (anySat && sat > best) best = sat;
204 if (Dtot[c] > best) best = Dtot[c];
205 // an optimistic bound can never exceed the pessimistic one
206 Ropt[c] = (best < Rpess[c]) ? best : Rpess[c];
207 }
208 for (std::size_t c = 0; c < R; ++c)
209 for (std::size_t k = 0; k < M; ++k) r.Q(k, c) = r.Xlo[c] * r.R(k, c);
210
211 // R_j(N - 1_c) <= R_j(N) <= R_j^(pess): the pessimistic iterate tightens
212 // the upper bound the seed divides by, and never loosens it.
213 for (std::size_t j = 0; j < R; ++j)
214 if (N[j] > zero && Rpess[j] > zero && Rpess[j] < Ub[j]) Ub[j] = Rpess[j];
215 for (std::size_t c = 0; c < R; ++c)
216 for (std::size_t j = 0; j < R; ++j) {
217 const T nj = (c == j) ? T(N[j] - one) : N[j];
218 const T zj = Z.empty() ? zero : Z[j];
219 const T du = T(zj + Ub[j]);
220 T qsum = zero;
221 if (N[j] <= zero || nj <= zero || du <= zero) {
222 for (std::size_t k = 0; k < M; ++k) Qm[c][j][k] = zero;
223 } else {
224 const T f = T(nj / du);
225 for (std::size_t k = 0; k < M; ++k) {
226 Qm[c][j][k] = f * L(k, j);
227 qsum += Qm[c][j][k];
228 }
229 }
230 // The clamp cannot fire: Ub >= J = Dtot, so qsum <= popt by
231 // construction. It states the invariant the heaps rest on.
232 const T ho = T(popt(j, c) - qsum);
233 const T hp = T(ppess(j, c) - qsum);
234 Hopt(j, c) = (ho > zero) ? ho : zero;
235 Hpess(j, c) = (hp > zero) ? hp : zero;
236 }
237
238 bool anyNonEmpty = false;
239 double maxdiff = 0.0;
240 for (std::size_t c = 0; c < R; ++c) {
241 if (N[c] <= zero) continue;
242 anyNonEmpty = true;
243 for (std::size_t k = 0; k < M; ++k) {
244 const double d =
245 std::fabs(num_traits<T>::to_double(T(r.Q(k, c) - Qprev(k, c))));
246 if (d > maxdiff) maxdiff = d;
247 }
248 }
249 if (!anyNonEmpty || (it > 1 && maxdiff < tol)) {
250 r.converged = true;
251 break;
252 }
253 }
254
255 for (std::size_t c = 0; c < R; ++c) {
256 if (N[c] <= zero) continue;
257 const T zc = Z.empty() ? zero : Z[c];
258 const T den = T(zc + Ropt[c]);
259 if (den == zero) throw NumericError("pfqn_looping: zero optimistic cycle time");
260 r.Xup[c] = N[c] / den;
261 }
262 return r;
263}
264
265template <class T>
266LoopingBounds<T> pfqn_looping(const Matrix<T>& L, const std::vector<T>& N) {
267 return pfqn_looping(L, N, std::vector<T>());
268}
269
270} // namespace pfqn
271} // namespace line
272
273#endif // LINE_API_PFQN_LOOPING_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
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
LoopingBounds< T > pfqn_looping(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, double tol=1e-6, std::size_t maxiter=1000)
Eager Looping bounds for closed multiclass product-form networks.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
Return value of pfqn_looping: the throughput bracket plus its queue lengths.
std::vector< T > Xup
(R) optimistic (upper) throughput bound
Matrix< T > Q
(M x R) queue lengths on the pessimistic side
std::vector< T > Xlo
(R) pessimistic (lower) throughput bound
Matrix< T > R
(M x R) residence times