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. Do not refine
35 * through Zahorjan's (1980) convolution identity Q_jk(N - 1_c) =
36 * [X_j(N - 1_c)/X_j(N)] Q_jk(N) with one class-level ratio at every station:
37 * that is not a per-station under-estimate, so the bounds are lost. The
38 * level-0 multiple-class PBH bounds on the mean response time are
39 *
40 * J_j(n) = sum_k D_jk, B_j(n) = sum_k D_jk + (sum(n) - 1) max_k D_jk,
41 *
42 * i.e. an arriving customer queues behind nobody, respectively behind every
43 * other customer in the network at its own worst centre.
44 *
45 * Arithmetic: sums, products, divisions, maxima and minima only, so each
46 * iterate is EXACT in rational arithmetic; the stopping rule selects which
47 * iterate is returned.
48 */
49
50#include <cmath>
51#include <cstddef>
52#include <vector>
53
54#include "line/num/number.h"
55#include "line/util/error.h"
56#include "line/util/matrix.h"
57
58namespace line {
59namespace pfqn {
60
61/** Return value of pfqn_looping: the throughput bracket plus its queue lengths. */
62template <class T>
64 std::vector<T> Xlo; ///< (R) pessimistic (lower) throughput bound
65 std::vector<T> Xup; ///< (R) optimistic (upper) throughput bound
66 Matrix<T> Q; ///< (M x R) queue lengths on the pessimistic side
67 Matrix<T> R; ///< (M x R) residence times
68 std::size_t iterations = 0;
69 bool converged = false;
70};
71
72/**
73 * @brief Eager Looping bounds for closed multiclass product-form networks.
74 *
75 * @param L (M x R) demands, @param N (R) populations,
76 * @param Z (R) think times (empty for none)
77 * @param tol convergence tolerance on the queue lengths
78 * @param maxiter iteration cap
79 */
80template <class T>
81LoopingBounds<T> pfqn_looping(const Matrix<T>& L, const std::vector<T>& N,
82 const std::vector<T>& Z, double tol = 1e-6,
83 std::size_t maxiter = 1000) {
84 const std::size_t M = L.rows(), R = L.cols();
85 if (N.size() != R) throw InputError("pfqn_looping: L and N disagree on the class count");
86 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_looping: Z has the wrong length");
87
88 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
89 const T two = num_traits<T>::from_int(2);
91 r.Xlo.assign(R, zero);
92 r.Xup.assign(R, zero);
93 r.Q = Matrix<T>(M, R, zero);
94 r.R = Matrix<T>(M, R, zero);
95 if (M == 0) return r;
96
97 std::vector<T> Dtot(R, zero), Vpess(R, zero), Lopt(R, zero);
98 T Ntot = zero;
99 for (std::size_t c = 0; c < R; ++c) {
100 T mx = L(0, c), mn = L(0, c);
101 for (std::size_t k = 0; k < M; ++k) {
102 Dtot[c] += L(k, c);
103 if (L(k, c) > mx) mx = L(k, c);
104 if (L(k, c) < mn) mn = L(k, c);
105 }
106 Vpess[c] = mx;
107 Lopt[c] = mn;
108 Ntot += N[c];
109 }
110 // level-0 multiple-class PBH bounds on R_j, at population N - 1_c
111 std::vector<T> Jbnd(R, zero), Bm(R, zero);
112 for (std::size_t c = 0; c < R; ++c) {
113 Jbnd[c] = Dtot[c];
114 const T span = (Ntot > two) ? T(Ntot - two) : zero;
115 Bm[c] = T(Dtot[c] + span * Vpess[c]);
116 }
117
118 // Population of class j at the stations when one class-c job is removed, at
119 // its optimistic and pessimistic extremes. These cap what the queue-length
120 // lower bounds may account for; the remainder is the heap.
121 Matrix<T> popt(R, R, zero), ppess(R, R, zero); // (j, c)
122 for (std::size_t c = 0; c < R; ++c)
123 for (std::size_t j = 0; j < R; ++j) {
124 const T nj = (c == j) ? T(N[j] - one) : N[j];
125 if (nj <= zero) continue;
126 const T zj = Z.empty() ? zero : Z[j];
127 const T dj = T(zj + Jbnd[j]);
128 if (dj > zero) popt(j, c) = T(T(Jbnd[j] / dj) * nj);
129 const T db = T(zj + Bm[j]);
130 if (db > zero) ppess(j, c) = T(T(Bm[j] / db) * nj);
131 }
132
133 // Zero is the one queue-length lower bound available before any iterate,
134 // and the heaps then carry the whole population. The even split N/M this
135 // once used is not a bound: it exceeds the true queue at every
136 // below-average station, so the first residence is already not a bound.
137 // Qm[c][j][k] = Q_jk(N - 1_c), a LOWER bound
138 std::vector<std::vector<std::vector<T> > > Qm(
139 R, std::vector<std::vector<T> >(R, std::vector<T>(M, zero)));
140 Matrix<T> Hopt(R, R, zero), Hpess(R, R, zero); // (j, c)
141 for (std::size_t c = 0; c < R; ++c)
142 for (std::size_t j = 0; j < R; ++j) {
143 Hopt(j, c) = popt(j, c);
144 Hpess(j, c) = ppess(j, c);
145 }
146 std::vector<T> Ub(Bm); // upper bound on R_j(N - 1_c)
147
148 std::vector<T> Rc(R, zero), Rpess(R, zero), Ropt(R, zero);
149 for (std::size_t it = 1; it <= maxiter; ++it) {
150 r.iterations = it;
151 const Matrix<T> Qprev = r.Q;
152
153 for (std::size_t c = 0; c < R; ++c) {
154 if (N[c] == zero) {
155 for (std::size_t k = 0; k < M; ++k) r.R(k, c) = zero;
156 r.Xlo[c] = zero;
157 Rc[c] = zero;
158 Rpess[c] = zero;
159 continue;
160 }
161 T rsum = zero;
162 for (std::size_t k = 0; k < M; ++k) {
163 T qk = zero;
164 for (std::size_t j = 0; j < R; ++j) qk += Qm[c][j][k];
165 r.R(k, c) = L(k, c) * T(one + qk);
166 rsum += r.R(k, c);
167 }
168 Rc[c] = rsum;
169 T hp = zero;
170 for (std::size_t j = 0; j < R; ++j) hp += Hpess(j, c);
171 const T zc = Z.empty() ? zero : Z[c];
172 Rpess[c] = T(Rc[c] + Vpess[c] * hp);
173 const T den = T(zc + Rpess[c]);
174 if (den == zero) throw NumericError(ErrorKind{"Degenerate"}, "pfqn_looping: zero pessimistic cycle time");
175 r.Xlo[c] = N[c] / den;
176 }
177 for (std::size_t c = 0; c < R; ++c) {
178 if (N[c] == zero) {
179 Ropt[c] = zero;
180 continue;
181 }
182 const T zc = Z.empty() ? zero : Z[c];
183 bool anySat = false;
184 T sat = zero;
185 for (std::size_t k = 0; k < M; ++k) {
186 T used = zero;
187 for (std::size_t j = 0; j < R; ++j)
188 if (j != c) used += r.Xlo[j] * L(k, j);
189 const T den = T(one - used);
190 if (den > zero) {
191 const T v = T(L(k, c) * N[c] / den - zc);
192 if (!anySat || v > sat) {
193 sat = v;
194 anySat = true;
195 }
196 }
197 }
198 T ho = zero;
199 for (std::size_t j = 0; j < R; ++j) ho += Hopt(j, c);
200 T best = T(Rc[c] + Lopt[c] * ho);
201 if (anySat && sat > best) best = sat;
202 if (Dtot[c] > best) best = Dtot[c];
203 // an optimistic bound can never exceed the pessimistic one
204 Ropt[c] = (best < Rpess[c]) ? best : Rpess[c];
205 }
206 for (std::size_t c = 0; c < R; ++c)
207 for (std::size_t k = 0; k < M; ++k) r.Q(k, c) = r.Xlo[c] * r.R(k, c);
208
209 // R_j(N - 1_c) <= R_j(N) <= R_j^(pess): the pessimistic iterate tightens
210 // the upper bound the seed divides by, and never loosens it.
211 for (std::size_t j = 0; j < R; ++j)
212 if (N[j] > zero && Rpess[j] > zero && Rpess[j] < Ub[j]) Ub[j] = Rpess[j];
213 for (std::size_t c = 0; c < R; ++c)
214 for (std::size_t j = 0; j < R; ++j) {
215 const T nj = (c == j) ? T(N[j] - one) : N[j];
216 const T zj = Z.empty() ? zero : Z[j];
217 const T du = T(zj + Ub[j]);
218 T qsum = zero;
219 if (N[j] <= zero || nj <= zero || du <= zero) {
220 for (std::size_t k = 0; k < M; ++k) Qm[c][j][k] = zero;
221 } else {
222 const T f = T(nj / du);
223 for (std::size_t k = 0; k < M; ++k) {
224 Qm[c][j][k] = f * L(k, j);
225 qsum += Qm[c][j][k];
226 }
227 }
228 // The clamp cannot fire: Ub >= J = Dtot, so qsum <= popt by
229 // construction. It states the invariant the heaps rest on.
230 const T ho = T(popt(j, c) - qsum);
231 const T hp = T(ppess(j, c) - qsum);
232 Hopt(j, c) = (ho > zero) ? ho : zero;
233 Hpess(j, c) = (hp > zero) ? hp : zero;
234 }
235
236 bool anyNonEmpty = false;
237 double maxdiff = 0.0;
238 for (std::size_t c = 0; c < R; ++c) {
239 if (N[c] <= zero) continue;
240 anyNonEmpty = true;
241 for (std::size_t k = 0; k < M; ++k) {
242 const double d =
243 std::fabs(num_traits<T>::to_double(T(r.Q(k, c) - Qprev(k, c))));
244 if (d > maxdiff) maxdiff = d;
245 }
246 }
247 if (!anyNonEmpty || (it > 1 && maxdiff < tol)) {
248 r.converged = true;
249 break;
250 }
251 }
252
253 for (std::size_t c = 0; c < R; ++c) {
254 if (N[c] <= zero) continue;
255 const T zc = Z.empty() ? zero : Z[c];
256 const T den = T(zc + Ropt[c]);
257 if (den == zero) throw NumericError(ErrorKind{"Degenerate"}, "pfqn_looping: zero optimistic cycle time");
258 r.Xup[c] = N[c] / den;
259 }
260 return r;
261}
262
263template <class T>
264LoopingBounds<T> pfqn_looping(const Matrix<T>& L, const std::vector<T>& N) {
265 return pfqn_looping(L, N, std::vector<T>());
266}
267
268} // namespace pfqn
269} // namespace line
270
271#endif // LINE_API_PFQN_LOOPING_H
InputError(const std::string &what)
Definition error.h:105
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:112
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.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
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