LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_clust.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_CLUST_H
6#define LINE_API_PFQN_CLUST_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * de Souza e Silva-Lavenberg-Muntz Clustering Approximation (CA).
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_clust.m, cross-checked against
14 * jar/src/main/java/jline/api/pfqn/mva/Pfqn_clust.java. E. de Souza e Silva,
15 * S. S. Lavenberg, R. R. Muntz, "A clustering approximation technique for
16 * queueing network models with a large number of chains", IEEE Trans.
17 * Computers C-35(5), 1986. The network is covered by subnetworks whose union
18 * is the whole network but which need not be disjoint. Every class visiting a
19 * subnetwork S is either LOCAL to S, and is then solved inside it, or FOREIGN,
20 * and is then seen only through the utilization it leaves behind. Each
21 * subnetwork is solved by an ordinary approximate MVA algorithm with two
22 * replacements: the complement of S is collapsed into a per-class delay P_c
23 * and the foreign classes into a per-centre utilization U_k,
24 *
25 * X_c(N) = N_c / (sum_{k in S} R_ck(N) + Z_c + P_c),
26 * Q_k(N) = [sum_{c in LC(S)} R_ck(N) X_c(N) + U_k] / (1 - U_k).
27 *
28 * Choosing the PE algorithm for every subnetwork reproduces global PE exactly,
29 * so the useful setting is Linearizer inside, PE outside: the cost then sits
30 * between pfqn_bs and pfqn_linearizer, which is the point of the method.
31 *
32 * When no decomposition is supplied the criterion of the paper is applied
33 * automatically: the cheap PAMB estimate (pfqn_pam) of the centre utilizations
34 * is taken, every class is attached to the centre where it loads the most,
35 * classes sharing that centre form one cluster, and the subnetwork of a
36 * cluster is the set of centres its classes visit. The answer depends on the
37 * decomposition, which is why it is an input.
38 *
39 * The name avoids pfqn_ca, which is the exact convolution algorithm.
40 *
41 * Arithmetic: field operations only, so each iterate is EXACT in rational
42 * arithmetic; both the outer and the inner loop stop on a tolerance.
43 */
44
45#include <algorithm>
46#include <cmath>
47#include <cstddef>
48#include <vector>
49
53#include "line/num/number.h"
54#include "line/util/error.h"
55#include "line/util/matrix.h"
56
57namespace line {
58namespace pfqn {
59
60/** Which algorithm runs inside a subnetwork. */
62
63namespace detail {
64
65template <class T>
66struct SubnetSolution {
67 std::vector<T> X;
68 Matrix<T> Q;
69};
70
71/** One forward MVA sweep over the local classes, with the foreign share folded in. */
72template <class T>
73SubnetSolution<T> clust_forward(const Matrix<T>& L, const std::vector<T>& N,
74 const std::vector<T>& Z, const std::vector<T>& Uk,
75 const Matrix<T>& Q,
76 const std::vector<std::vector<std::vector<T> > >& Delta) {
77 const std::size_t M = L.rows(), R = L.cols();
78 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
79 SubnetSolution<T> out;
80 out.X.assign(R, zero);
81 out.Q = Matrix<T>(M, R, zero);
82 for (std::size_t r = 0; r < R; ++r) {
83 if (!(N[r] > zero)) continue;
84 std::vector<T> W(M, zero);
85 T wsum = zero;
86 for (std::size_t i = 0; i < M; ++i) {
87 // Linearizer estimate of the local queue at N - 1_r
88 T qm = zero;
89 for (std::size_t s = 0; s < R; ++s) {
90 const T ns = (s == r) ? T(N[s] - one) : N[s];
91 if (N[s] > zero && ns > zero) qm += ns * T(Q(i, s) / N[s] + Delta[i][s][r]);
92 }
93 if (qm < zero) qm = zero;
94 const T A = T(T(qm + Uk[i]) / T(one - Uk[i]));
95 W[i] = L(i, r) * T(one + A);
96 wsum += W[i];
97 }
98 const T den = T(Z[r] + wsum);
99 if (den == zero) throw NumericError("pfqn_clust: zero subnetwork cycle time");
100 out.X[r] = N[r] / den;
101 for (std::size_t i = 0; i < M; ++i) out.Q(i, r) = out.X[r] * W[i];
102 }
103 return out;
104}
105
106template <class T>
107Matrix<T> clust_core(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
108 const std::vector<T>& Uk, const Matrix<T>& Qin,
109 const std::vector<std::vector<std::vector<T> > >& Delta, double tol,
110 std::size_t maxiter) {
111 Matrix<T> Q = Qin;
112 for (std::size_t it = 0; it < maxiter; ++it) {
113 const Matrix<T> Qold = Q;
114 Q = clust_forward(L, N, Z, Uk, Q, Delta).Q;
115 double d = 0.0;
116 for (std::size_t i = 0; i < Q.rows(); ++i)
117 for (std::size_t j = 0; j < Q.cols(); ++j) {
118 const double v = std::fabs(num_traits<T>::to_double(T(Q(i, j) - Qold(i, j))));
119 if (v > d) d = v;
120 }
121 if (d < tol) break;
122 }
123 return Q;
124}
125
126/** Approximate MVA restricted to the local classes of one subnetwork. */
127template <class T>
128SubnetSolution<T> clust_subnet(const Matrix<T>& L, const std::vector<T>& N,
129 const std::vector<T>& Z, const std::vector<T>& Uk,
130 ClustInner inner, double tol, std::size_t maxiter) {
131 const std::size_t M = L.rows(), R = L.cols();
132 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
133 SubnetSolution<T> out;
134 out.X.assign(R, zero);
135 out.Q = Matrix<T>(M, R, zero);
136 if (M == 0 || R == 0) return out;
137 const T Mt = num_traits<T>::from_int(static_cast<long>(M));
138 for (std::size_t i = 0; i < M; ++i)
139 for (std::size_t r = 0; r < R; ++r) out.Q(i, r) = N[r] / Mt;
140
142 for (std::size_t it = 0; it < maxiter; ++it) {
143 const Matrix<T> Qold = out.Q;
144 for (std::size_t r = 0; r < R; ++r) {
145 if (!(N[r] > zero)) continue;
146 std::vector<T> W(M, zero);
147 T wsum = zero;
148 for (std::size_t i = 0; i < M; ++i) {
149 // PE arrival-instant local queue, inflated by the foreign share
150 T loc = zero;
151 for (std::size_t s = 0; s < R; ++s) loc += out.Q(i, s);
152 loc -= out.Q(i, r) / N[r];
153 const T A = T(T(loc + Uk[i]) / T(one - Uk[i]));
154 W[i] = L(i, r) * T(one + A);
155 wsum += W[i];
156 }
157 const T den = T(Z[r] + wsum);
158 if (den == zero) throw NumericError("pfqn_clust: zero subnetwork cycle time");
159 out.X[r] = N[r] / den;
160 for (std::size_t i = 0; i < M; ++i) out.Q(i, r) = out.X[r] * W[i];
161 }
162 double d = 0.0;
163 for (std::size_t i = 0; i < M; ++i)
164 for (std::size_t r = 0; r < R; ++r) {
165 const double v =
166 std::fabs(num_traits<T>::to_double(T(out.Q(i, r) - Qold(i, r))));
167 if (v > d) d = v;
168 }
169 if (d < tol) break;
170 }
171 return out;
172 }
173
174 std::vector<Matrix<T> > Qs(R + 1, out.Q);
175 std::vector<std::vector<std::vector<T> > > Delta(
176 M, std::vector<std::vector<T> >(R, std::vector<T>(R, zero)));
177 for (int pass = 0; pass < 3; ++pass) {
178 for (std::size_t s = 0; s <= R; ++s) {
179 std::vector<T> Ns(N);
180 if (s > 0) Ns[s - 1] = T(Ns[s - 1] - one);
181 Qs[s] = clust_core(L, Ns, Z, Uk, Qs[s], Delta, tol, maxiter);
182 }
183 for (std::size_t i = 0; i < M; ++i)
184 for (std::size_t r = 0; r < R; ++r)
185 for (std::size_t s = 1; s <= R; ++s) {
186 const T ns = (r == s - 1) ? T(N[r] - one) : N[r];
187 if (N[r] > zero && ns > zero)
188 Delta[i][r][s - 1] = Qs[s](i, r) / ns - Qs[0](i, r) / N[r];
189 else if (N[r] > zero)
190 Delta[i][r][s - 1] = zero - Qs[0](i, r) / N[r];
191 else
192 Delta[i][r][s - 1] = zero;
193 }
194 }
195 Qs[0] = clust_core(L, N, Z, Uk, Qs[0], Delta, tol, maxiter);
196 return clust_forward(L, N, Z, Uk, Qs[0], Delta);
197}
198
199} // namespace detail
200
201/**
202 * @brief de Souza e Silva-Lavenberg-Muntz Clustering Approximation (CA).
203 *
204 * @param L (M x R) demands, @param N (R) populations, @param Z (R) think times
205 * @param subnets per subnetwork, the 0-based station indices it contains;
206 * empty for the automatic decomposition described above
207 * @param localclasses per subnetwork, the 0-based classes local to it; empty
208 * for the automatic decomposition
209 * @param inner algorithm run inside a subnetwork
210 * @param tol convergence tolerance
211 * @param maxiter outer-iteration cap
212 */
213template <class T>
214AmvaResult<T> pfqn_clust(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
215 const std::vector<std::vector<std::size_t> >& subnets,
216 const std::vector<std::vector<std::size_t> >& localclasses,
217 ClustInner inner = ClustInner::Linearizer, double tol = 1e-6,
218 std::size_t maxiter = 1000) {
219 const std::size_t M = L.rows(), R = L.cols();
220 if (N.size() != R) throw InputError("pfqn_clust: L and N disagree on the class count");
221 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_clust: Z has the wrong length");
222 if (subnets.size() != localclasses.size())
223 throw InputError("pfqn_clust: subnets and localclasses disagree on the cluster count");
224
225 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
226 const std::vector<T> Zv = Z.empty() ? std::vector<T>(R, zero) : Z;
227
228 const AmvaResult<T> seed = pfqn_pam(L, N, Zv, PamVariant::Basic);
230 r.XN = seed.XN;
231 r.QN = seed.QN;
232 r.UN = Matrix<T>(M, R, zero);
233 r.RN = Matrix<T>(M, R, zero);
234 if (M == 0) return r;
235
236 std::vector<std::vector<std::size_t> > nets = subnets, locals = localclasses;
237 if (nets.empty() || locals.empty()) {
238 std::vector<std::size_t> bottleneck(R, 0);
239 for (std::size_t s = 0; s < R; ++s) {
240 bool any = false;
241 T best = zero;
242 for (std::size_t i = 0; i < M; ++i) {
243 if (!(L(i, s) > zero)) continue;
244 const T u = L(i, s) * r.XN[s];
245 if (!any || u > best) {
246 best = u;
247 bottleneck[s] = i;
248 any = true;
249 }
250 }
251 }
252 std::vector<std::size_t> centres;
253 for (std::size_t s = 0; s < R; ++s)
254 if (std::find(centres.begin(), centres.end(), bottleneck[s]) == centres.end())
255 centres.push_back(bottleneck[s]);
256 std::sort(centres.begin(), centres.end());
257 nets.clear();
258 locals.clear();
259 std::vector<bool> covered(M, false);
260 for (std::size_t g = 0; g < centres.size(); ++g) {
261 std::vector<std::size_t> cls;
262 for (std::size_t s = 0; s < R; ++s)
263 if (bottleneck[s] == centres[g]) cls.push_back(s);
264 std::vector<std::size_t> st;
265 for (std::size_t i = 0; i < M; ++i)
266 for (std::size_t a = 0; a < cls.size(); ++a)
267 if (L(i, cls[a]) > zero) {
268 st.push_back(i);
269 break;
270 }
271 if (st.empty()) st.push_back(centres[g]);
272 for (std::size_t a = 0; a < st.size(); ++a) covered[st[a]] = true;
273 nets.push_back(st);
274 locals.push_back(cls);
275 }
276 std::vector<std::size_t> missing;
277 for (std::size_t i = 0; i < M; ++i)
278 if (!covered[i]) missing.push_back(i);
279 if (!missing.empty()) {
280 nets.push_back(missing);
281 locals.push_back(std::vector<std::size_t>());
282 }
283 }
284 const std::size_t G = nets.size();
285 std::vector<int> owner(R, -1);
286 for (std::size_t g = 0; g < G; ++g)
287 for (std::size_t a = 0; a < locals[g].size(); ++a)
288 owner[locals[g][a]] = static_cast<int>(g);
289
290 for (std::size_t it = 1; it <= maxiter; ++it) {
291 r.iterations = it;
292 const Matrix<T> Qprev = r.QN;
293 std::vector<T> Qk(M, zero);
294 for (std::size_t i = 0; i < M; ++i)
295 for (std::size_t s = 0; s < R; ++s) Qk[i] += r.QN(i, s);
296
297 for (std::size_t g = 0; g < G; ++g) {
298 const std::vector<std::size_t>& S = nets[g];
299 const std::vector<std::size_t>& LC = locals[g];
300 if (LC.empty() || S.empty()) continue;
301 std::vector<bool> inS(M, false);
302 for (std::size_t a = 0; a < S.size(); ++a) inS[S[a]] = true;
303 std::vector<bool> isLocal(R, false);
304 for (std::size_t a = 0; a < LC.size(); ++a) isLocal[LC[a]] = true;
305 std::vector<bool> isForeign(R, false);
306 for (std::size_t s = 0; s < R; ++s) {
307 if (isLocal[s]) continue;
308 for (std::size_t a = 0; a < S.size(); ++a)
309 if (L(S[a], s) > zero) {
310 isForeign[s] = true;
311 break;
312 }
313 }
314 // per-class delay in the complement of S
315 std::vector<T> Zeff(LC.size(), zero);
316 for (std::size_t a = 0; a < LC.size(); ++a) {
317 const std::size_t c = LC[a];
318 T p = zero;
319 if (N[c] > zero)
320 for (std::size_t i = 0; i < M; ++i)
321 if (!inS[i])
322 p += L(i, c) * T(one + Qk[i]) / T(one + L(i, c) * r.XN[c] / N[c]);
323 Zeff[a] = T(Zv[c] + p);
324 }
325 // utilization left in S by the foreign classes
326 std::vector<T> Uk(S.size(), zero);
327 const T cap = T(one - num_traits<T>::from_int(1) / num_traits<T>::from_int(100000000));
328 for (std::size_t b = 0; b < S.size(); ++b) {
329 const std::size_t i = S[b];
330 T u = zero;
331 for (std::size_t s = 0; s < R; ++s)
332 if (isForeign[s] && N[s] > zero)
333 u += L(i, s) * r.XN[s] / T(one + L(i, s) * r.XN[s] / N[s]);
334 Uk[b] = (u < cap) ? u : cap;
335 }
336 Matrix<T> Lsub(S.size(), LC.size(), zero);
337 std::vector<T> Nsub(LC.size(), zero);
338 for (std::size_t b = 0; b < S.size(); ++b)
339 for (std::size_t a = 0; a < LC.size(); ++a) Lsub(b, a) = L(S[b], LC[a]);
340 for (std::size_t a = 0; a < LC.size(); ++a) Nsub[a] = N[LC[a]];
341
342 const detail::SubnetSolution<T> sub =
343 detail::clust_subnet(Lsub, Nsub, Zeff, Uk, inner, tol, maxiter);
344 for (std::size_t a = 0; a < LC.size(); ++a) {
345 r.XN[LC[a]] = sub.X[a];
346 for (std::size_t b = 0; b < S.size(); ++b) r.QN(S[b], LC[a]) = sub.Q(b, a);
347 }
348 // the local classes still hold jobs outside S
349 for (std::size_t a = 0; a < LC.size(); ++a) {
350 const std::size_t c = LC[a];
351 if (!(N[c] > zero)) continue;
352 for (std::size_t i = 0; i < M; ++i)
353 if (!inS[i])
354 r.QN(i, c) = r.XN[c] * L(i, c) * T(one + Qk[i]) /
355 T(one + L(i, c) * r.XN[c] / N[c]);
356 }
357 }
358 // a class owned by no subnetwork keeps the seed throughput
359 for (std::size_t s = 0; s < R; ++s)
360 if (owner[s] < 0)
361 for (std::size_t i = 0; i < M; ++i) r.QN(i, s) = r.XN[s] * L(i, s);
362
363 double d = 0.0;
364 for (std::size_t i = 0; i < M; ++i)
365 for (std::size_t s = 0; s < R; ++s) {
366 const double v = std::fabs(num_traits<T>::to_double(T(r.QN(i, s) - Qprev(i, s))));
367 if (v > d) d = v;
368 }
369 if (d < tol) {
370 r.converged = true;
371 break;
372 }
373 }
374
375 for (std::size_t i = 0; i < M; ++i)
376 for (std::size_t s = 0; s < R; ++s) {
377 r.UN(i, s) = r.XN[s] * L(i, s);
378 r.RN(i, s) = (N[s] == zero || r.XN[s] == zero) ? zero : T(r.QN(i, s) / r.XN[s]);
379 }
380 return r;
381}
382
383template <class T>
384AmvaResult<T> pfqn_clust(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z) {
385 return pfqn_clust(L, N, Z, std::vector<std::vector<std::size_t> >(),
386 std::vector<std::vector<std::size_t> >());
387}
388
389template <class T>
390AmvaResult<T> pfqn_clust(const Matrix<T>& L, const std::vector<T>& N) {
391 return pfqn_clust(L, N, std::vector<T>());
392}
393
394} // namespace pfqn
395} // namespace line
396
397#endif // LINE_API_PFQN_CLUST_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.
AmvaResult< T > pfqn_clust(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< std::vector< std::size_t > > &subnets, const std::vector< std::vector< std::size_t > > &localclasses, ClustInner inner=ClustInner::Linearizer, double tol=1e-6, std::size_t maxiter=1000)
de Souza e Silva-Lavenberg-Muntz Clustering Approximation (CA).
Definition pfqn_clust.h:214
AmvaResult< T > pfqn_pam(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, PamVariant variant=PamVariant::Basic)
Hsieh-Lam Proportional Approximation Methods (PAMB / PAMI / PAMT).
Definition pfqn_pam.h:55
ClustInner
Which algorithm runs inside a subnetwork.
Definition pfqn_clust.h:61
Number-type abstraction for the templated API port.
Scaffolding shared by the approximate-MVA family.
Bard-Schweitzer approximate MVA.
Hsieh-Lam Proportional Approximation Methods (PAMB / PAMI / PAMT).
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