LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_ab_amva.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_AB_AMVA_H
6#define LINE_API_PFQN_AB_AMVA_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Akyildiz-Bolch approximate MVA for multi-server BCMP networks.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_ab_amva.m, cross-checked against
14 * jar/src/main/java/jline/api/pfqn/mva/Pfqn_ab_amva.java.
15 *
16 * The driver is a Linearizer-shaped three-pass scheme around a fixed-point
17 * core:
18 *
19 * step 1 run the core at the full population N, from the flat start
20 * L(i,r) = N_r / M;
21 * step 2 run the core once per class at N - e_r, seeding it with the
22 * per-class queue lengths of step 1;
23 * step 3 form the fractional-change tensor
24 * Delta(i,r,t) = Q(i,t | N - e_r)/den - Q(i,r | N)/N_r,
25 * with den = N_r - 1 when r == t and N_r otherwise;
26 * step 4 rerun the core at N with those Delta held fixed.
27 *
28 * Inside the core the arrival-theorem queue length seen by a class-r job is
29 * L(i,c | N - e_r) = scalar (F(i,c) + Delta(i,c,r)), and the residence time is
30 * formed per station kind:
31 *
32 * INF W = S(i,r);
33 * single server W = S(i,r) (1 + sum_c L(i,c | N - e_r));
34 * multiserver W = S(i,r)/c (1 + Qtot + sum_{j<c} (c-j) Pr(j)),
35 * with Pr(j) the Akyildiz-Bolch marginal weight (or the
36 * two-point 'scat' scatter) of the queue length;
37 * FCFS, fcfsSchmidt W = sum_{n <= N, n_r > 0} B_r(n) Pr(n - e_r),
38 * with Pr a per-class binomial product and B_r the
39 * queue-composition-weighted mean service time.
40 *
41 * Reference behaviour preserved verbatim, including the parts that look like
42 * defects but define the numbers the reference produces:
43 *
44 * - the throughput is read off STATION 1, XN(r) = Q(1,r)/W(1,r), so it is the
45 * class-r throughput AT that station, i.e. v(1,r) times the system
46 * throughput, not the system throughput itself;
47 * - the convergence tolerance is the reference's 1/(4000 + 16 sum(N)) and the
48 * iteration cap is 100 passes, with no error on non-convergence;
49 * - a Schmidt-FCFS wait below 1e-3 is snapped to zero;
50 * - the AB marginal weights use the reference's ALPHA = 45, BETA = 0.7 and
51 * its distance cutoff of 25.
52 *
53 * One sizing divergence, deliberate. weightFun builds its weight table with
54 * max(N) + 1 rows, but the row index used later is floor(Qtot) where Qtot is
55 * the TOTAL queue length over all classes, which exceeds max(N) as soon as two
56 * classes are both loaded at a multiserver station; MATLAB then raises an
57 * index-out-of-bounds error. The recursion defining the table is index-generic
58 * (row l is built from row l-1 and the geometric scaling sequence), so the port
59 * sizes the table by the largest row actually required. That evaluates the SAME
60 * function at a larger argument; it changes no in-range entry and it removes an
61 * error the reference cannot otherwise avoid.
62 *
63 * Arithmetic: INEXACT BY CONSTRUCTION. The core is a tolerance-stopped fixed
64 * point, so the answer depends on where the iteration is cut; the marginal
65 * weights additionally need floor and integer powers of a non-integer
66 * fraction. It is gated on has_transcendental accordingly.
67 */
68
69#include <cmath>
70#include <cstddef>
71#include <map>
72#include <vector>
73
75#include "line/num/number.h"
76#include "line/util/error.h"
77#include "line/util/matrix.h"
79
80namespace line {
81namespace pfqn {
82
83/** Which marginal-probability rule the multiserver correction uses. */
84enum class AbMarginalMethod {
85 Ab, ///< the Akyildiz-Bolch weight function
86 Scat ///< two-point scatter around floor(Qtot)
87};
88
89/** Return value of pfqn_ab_amva, mirroring [QN,UN,RN,CN,XN,totiter]. */
90template <class T>
92 Matrix<T> QN; ///< (M x R) mean queue length
93 Matrix<T> UN; ///< (M x R) utilization
94 Matrix<T> RN; ///< (M x R) residence (wait) time per visit
95 std::vector<T> CN; ///< (R) cycle time
96 std::vector<T> XN; ///< (R) class throughput AT STATION 1
97 std::size_t totiter; ///< iterations of the final core pass
98};
99
100namespace detail {
101
102/** Akyildiz-Bolch weight table, rows 0..lmax, w(l,j) defined for j <= l. */
103template <class T>
104Matrix<T> ab_weight_fun(const std::vector<int>& population, int lmax, double alpha, double beta) {
105 int maxPop = 0;
106 for (int n : population)
107 if (n > maxPop) maxPop = n;
108 if (lmax > maxPop) maxPop = lmax;
109 if (maxPop < 0) maxPop = 0;
110 const T zero = num_traits<T>::from_int(0);
111 const T one = num_traits<T>::from_int(1);
112 const std::size_t n1 = static_cast<std::size_t>(maxPop) + 1;
113
114 std::vector<T> scaling(n1, zero);
115 if (maxPop >= 1) {
116 scaling[1] = num_traits<T>::from_double(alpha);
117 for (int n = 2; n <= maxPop; ++n)
118 scaling[static_cast<std::size_t>(n)] =
119 num_traits<T>::from_double(beta) * scaling[static_cast<std::size_t>(n) - 1];
120 }
121 Matrix<T> w(n1, n1, zero);
122 w(0, 0) = one;
123 const T hundred = num_traits<T>::from_int(100);
124 for (int l = 1; l <= maxPop; ++l) {
125 const std::size_t lu = static_cast<std::size_t>(l);
126 T sum = zero;
127 for (int j = 0; j <= l - 1; ++j) {
128 const std::size_t ju = static_cast<std::size_t>(j);
129 w(lu, ju) = w(lu - 1, ju) - w(lu - 1, ju) * scaling[lu] / hundred;
130 sum += w(lu, ju);
131 }
132 w(lu, lu) = one - sum;
133 }
134 return w;
135}
136
137/** matlab findMarginalProbs, as an integer-keyed map (keys may be negative). */
138template <class T>
139std::map<int, T> ab_marginal_probs(const T& avgJobs, int numServers,
140 const std::vector<int>& population, std::size_t classIdx,
141 AbMarginalMethod method) {
142 const T zero = num_traits<T>::from_int(0);
143 std::map<int, T> mp;
144 const double aj = num_traits<T>::to_double(avgJobs);
145 const int floorVal = static_cast<int>(std::floor(aj));
146
147 if (method == AbMarginalMethod::Scat) {
148 const int ceilVal = floorVal + 1;
149 mp[floorVal] = num_traits<T>::from_int(ceilVal) - avgJobs;
150 mp[ceilVal] = avgJobs - num_traits<T>::from_int(floorVal);
151 return mp;
152 }
153
154 const int ceiling = floorVal + 1;
155 const int maxVal = (2 * floorVal + 1) < (numServers - 2) ? (2 * floorVal + 1) : (numServers - 2);
156 const Matrix<T> w = ab_weight_fun<T>(population, floorVal < 0 ? 0 : floorVal, 45.0, 0.7);
157 const std::size_t wn = w.rows();
158 const auto wat = [&](int l, int j) -> T {
159 if (l < 0 || j < 0 || static_cast<std::size_t>(l) >= wn ||
160 static_cast<std::size_t>(j) >= wn)
161 return num_traits<T>::from_int(0);
162 return w(static_cast<std::size_t>(l), static_cast<std::size_t>(j));
163 };
164 const int popc = population[classIdx];
165
166 for (int j = 0; j <= maxVal; ++j) {
167 if (j <= floorVal) {
168 const int lDist = floorVal - j;
169 const int lowerVal = floorVal - lDist;
170 const int upperVal = ceiling + lDist;
171 T prob = zero;
172 if (lDist <= 25 && floorVal < popc && upperVal != lowerVal)
173 prob = wat(floorVal, lDist) *
174 ((num_traits<T>::from_int(upperVal) - avgJobs) /
175 num_traits<T>::from_int(upperVal - lowerVal));
176 mp[j] = prob;
177 } else {
178 const int uDist = j - ceiling;
179 if (uDist > 25) {
180 mp[j] = zero;
181 } else if (j > popc - 1 && uDist < 25) {
182 // uDist == 25 falls through to the plain branch in the
183 // reference, which keys the entry on j rather than on popc-1
184 const auto ite = mp.find(popc - 1);
185 const T existing = ite == mp.end() ? zero : ite->second;
186 const auto itf = mp.find(floorVal - uDist);
187 const T mfu = itf == mp.end() ? zero : itf->second;
188 mp[popc - 1] = existing + (wat(floorVal, uDist) - mfu);
189 } else {
190 const auto itf = mp.find(floorVal - uDist);
191 const T mfu = itf == mp.end() ? zero : itf->second;
192 mp[j] = wat(floorVal, uDist) - mfu;
193 }
194 }
195 }
196 return mp;
197}
198
199/** matlab getBcnForAB: queue-composition-weighted mean service time. */
200template <class T>
201T ab_bcn(const Matrix<T>& S, std::size_t i, std::size_t c, const std::vector<int>& nvec, int ns) {
202 T bcn = S(i, c);
203 long nsum = 0;
204 for (int t : nvec) nsum += t;
205 if (nsum > 1) {
206 const T eps = num_traits<T>::from_double(1e-12);
207 T sumVal = num_traits<T>::from_int(0);
208 for (std::size_t t = 0; t < nvec.size(); ++t)
209 sumVal += num_traits<T>::from_int(nvec[t]) * S(i, t);
210 const T num = num_traits<T>::from_int(nsum - ns > 0 ? nsum - ns : 0);
211 const T den0 = num_traits<T>::from_int(ns * (nsum - 1));
212 const T den = den0 > eps ? den0 : eps;
213 bcn += num / den * (sumVal - S(i, c));
214 }
215 return bcn;
216}
217
218/** matlab getMarginalProb: per-class binomial product. */
219template <class T>
220T ab_binomial_prob(const std::vector<int>& n, const std::vector<int>& Kpop, const T& Ljr) {
221 const T zero = num_traits<T>::from_int(0);
222 const T one = num_traits<T>::from_int(1);
223 T prob = one;
224 for (std::size_t r = 0; r < Kpop.size(); ++r) {
225 if (Kpop[r] <= 0) continue;
226 const T frac = Ljr / num_traits<T>::from_int(Kpop[r]);
227 if (frac == zero) continue;
228 if (n[r] < 0 || n[r] > Kpop[r]) continue;
229 const T t1 = num_nck<T>(Kpop[r], n[r]);
230 const T t2 = num_pow_int(frac, static_cast<unsigned>(n[r]));
231 const T t3 = num_pow_int(T(one - frac), static_cast<unsigned>(Kpop[r] - n[r]));
232 prob *= t1 * t2 * t3;
233 }
234 return prob;
235}
236
237/** The Akyildiz-Bolch fixed-point core, matlab pfqn_ab_core. */
238template <class T>
239AbAmvaResult<T> ab_core(const std::vector<int>& population, const std::vector<int>& nservers,
240 const std::vector<SchedStrategy>& type, const Matrix<T>& v,
241 const Matrix<T>& S, std::size_t maxiter, const std::vector<T>& Delta,
242 const Matrix<T>& lIn, bool fcfsSchmidt, AbMarginalMethod method) {
243 const std::size_t M = S.rows(), K = S.cols();
244 const T zero = num_traits<T>::from_int(0);
245 const T one = num_traits<T>::from_int(1);
246 const auto dat = [&](std::size_t i, std::size_t r, std::size_t t) -> const T& {
247 return Delta[(i * K + r) * K + t];
248 };
249
250 long Npop = 0;
251 for (int n : population) Npop += n;
252 const T tol = one / num_traits<T>::from_int(4000 + 16 * Npop);
253
254 AbAmvaResult<T> res;
255 res.QN = lIn;
256 res.RN = Matrix<T>(M, K, zero);
257 res.UN = Matrix<T>(M, K, zero);
258 res.CN.assign(K, zero);
259 res.XN.assign(K, zero);
260 res.totiter = 0;
261
262 Matrix<T>& L = res.QN;
263 Matrix<T>& W = res.RN;
264 Matrix<T> F(M, K, zero);
265 std::vector<T> lWJ(M * K * K, zero);
266 const auto lwj = [&](std::size_t i, std::size_t r, std::size_t t) -> T& {
267 return lWJ[(i * K + r) * K + t];
268 };
269 const T milli = num_traits<T>::from_double(1e-3);
270
271 while (res.totiter < maxiter) {
272 for (std::size_t i = 0; i < M; ++i)
273 for (std::size_t r = 0; r < K; ++r)
274 F(i, r) = population[r] > 0 ? T(L(i, r) / num_traits<T>::from_int(population[r]))
275 : zero;
276 for (std::size_t i = 0; i < M; ++i)
277 for (std::size_t r = 0; r < K; ++r)
278 for (std::size_t t = 0; t < K; ++t) {
279 const long scalar = r == t ? population[r] - 1 : population[r];
280 lwj(i, r, t) = num_traits<T>::from_int(scalar) * (F(i, r) + dat(i, r, t));
281 }
282
283 for (std::size_t i = 0; i < M; ++i)
284 for (std::size_t r = 0; r < K; ++r) {
285 if (type[i] == SchedStrategy::INF) {
286 W(i, r) = S(i, r);
287 } else if (nservers[i] == 1) {
288 T qtot = zero;
289 for (std::size_t c = 0; c < K; ++c) qtot += lwj(i, c, r);
290 W(i, r) = S(i, r) * (one + qtot);
291 } else if (fcfsSchmidt && type[i] == SchedStrategy::FCFS) {
292 T wait = zero;
293 std::vector<int> nvec(K, 0);
294 bool more = true;
295 while (more) {
296 if (nvec[r] > 0) {
297 const T bcn = ab_bcn(S, i, r, nvec, nservers[i]);
298 std::vector<int> nm = nvec;
299 nm[r] -= 1;
300 // seeded-queue-length binomial rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
301 wait += bcn * ab_binomial_prob<T>(nm, population, lIn(i, r));
302 }
303 more = next_pop(nvec, population);
304 }
305 W(i, r) = wait <= milli ? zero : wait;
306 } else {
307 T qtot = zero;
308 for (std::size_t j = 0; j < K; ++j) qtot += lwj(i, j, r);
309 const int c = nservers[i];
310 T corr = zero;
311 if (c > 1) {
312 std::vector<int> popWithoutR = population;
313 popWithoutR[r] -= 1;
314 const std::map<int, T> mp =
315 ab_marginal_probs<T>(qtot, c, popWithoutR, r, method);
316 for (int j = 1; j <= c - 1; ++j) {
317 const auto it = mp.find(j);
318 if (it != mp.end())
319 corr += it->second * num_traits<T>::from_int(c - j);
320 }
321 }
322 W(i, r) = S(i, r) / num_traits<T>::from_int(c) * (one + qtot + corr);
323 }
324 }
325
326 for (std::size_t r = 0; r < K; ++r) {
327 T cyc = zero;
328 for (std::size_t i = 0; i < M; ++i) cyc += v(i, r) * W(i, r);
329 res.CN[r] = cyc;
330 }
331
332 Matrix<T> itQ(M, K, zero);
333 for (std::size_t i = 0; i < M; ++i)
334 for (std::size_t r = 0; r < K; ++r)
335 itQ(i, r) = res.CN[r] > zero
336 ? T(num_traits<T>::from_int(population[r]) *
337 (v(i, r) * W(i, r) / res.CN[r]))
338 : zero;
339
340 T maxDiff = zero;
341 for (std::size_t i = 0; i < M; ++i)
342 for (std::size_t r = 0; r < K; ++r) {
343 if (population[r] <= 0) continue;
344 const T diff =
345 num_abs(T(L(i, r) - itQ(i, r))) / num_traits<T>::from_int(population[r]);
346 if (diff > maxDiff) maxDiff = diff;
347 }
348
349 res.totiter += 1;
350 L = itQ;
351 if (maxDiff < tol) break;
352 }
353
354 for (std::size_t r = 0; r < K; ++r)
355 res.XN[r] = W(0, r) > zero ? T(L(0, r) / W(0, r)) : zero;
356 for (std::size_t i = 0; i < M; ++i)
357 for (std::size_t r = 0; r < K; ++r) {
358 if (!(S(i, r) > zero)) continue;
359 res.UN(i, r) = type[i] == SchedStrategy::INF
360 ? T(res.XN[r] * S(i, r))
361 : T(res.XN[r] * S(i, r) / num_traits<T>::from_int(nservers[i]));
362 }
363 return res;
364}
365
366} // namespace detail
367
368/**
369 * @brief Akyildiz-Bolch approximate MVA for multi-server BCMP networks.
370 *
371 * @param S (M x R) service demands
372 * @param N (R) population per class
373 * @param v (M x R) visit ratios
374 * @param nservers (M) server counts
375 * @param sched (M) scheduling discipline
376 * @param fcfsSchmidt use the Schmidt state-sum wait at FCFS stations
377 * @param method marginal-probability rule for the multiserver correction
378 */
379template <class T>
380AbAmvaResult<T> pfqn_ab_amva(const Matrix<T>& S, const std::vector<int>& N, const Matrix<T>& v,
381 const std::vector<int>& nservers,
382 const std::vector<SchedStrategy>& sched, bool fcfsSchmidt,
383 AbMarginalMethod method) {
385 "pfqn_ab_amva requires transcendental arithmetic: its core is a "
386 "tolerance-stopped fixed point, so the answer depends on where the iteration "
387 "stops, and its marginal weights use floor and non-integer fractions");
388
389 const std::size_t M = S.rows(), K = S.cols();
390 if (N.size() != K) throw InputError("pfqn_ab_amva: S and N disagree on the class count");
391 if (v.rows() != M || v.cols() != K)
392 throw InputError("pfqn_ab_amva: visit-ratio matrix has the wrong shape");
393 if (nservers.size() != M) throw InputError("pfqn_ab_amva: nservers has the wrong length");
394 if (sched.size() != M) throw InputError("pfqn_ab_amva: sched has the wrong length");
395 for (std::size_t i = 0; i < M; ++i)
396 if (nservers[i] < 1) throw InputError("pfqn_ab_amva: server count below one");
397 for (int n : N)
398 if (n < 0) throw InputError("pfqn_ab_amva: negative population");
399
400 const T zero = num_traits<T>::from_int(0);
401 const std::size_t maxiter = 100;
402
403 // Flat start L(i,r) = N_r / M, and the per-class seeds of step 2.
404 Matrix<T> L(M, K, zero);
405 for (std::size_t i = 0; i < M; ++i)
406 for (std::size_t r = 0; r < K; ++r)
407 L(i, r) = num_traits<T>::from_int(N[r]) / num_traits<T>::from_int(static_cast<long>(M));
408
409 std::vector<Matrix<T>> lWithoutR(K, Matrix<T>(M, K, zero));
410 for (std::size_t r = 0; r < K; ++r)
411 for (std::size_t i = 0; i < M; ++i)
412 for (std::size_t t = 0; t < K; ++t)
413 lWithoutR[r](i, t) =
414 r == t ? T(num_traits<T>::from_int(N[r] - 1) /
415 num_traits<T>::from_int(static_cast<long>(M)))
416 : L(i, r);
417
418 std::vector<T> Delta(M * K * K, zero);
419
420 // STEP 1: the core at the full population.
421 const AbAmvaResult<T> step1 =
422 detail::ab_core(N, nservers, sched, v, S, maxiter, Delta, L, fcfsSchmidt, method);
423 const Matrix<T> LUpdated = step1.QN;
424
425 // STEP 2: the core at N - e_r, one class at a time.
426 for (std::size_t r = 0; r < K; ++r) {
427 std::vector<int> popWithout = N;
428 popWithout[r] -= 1;
429 Matrix<T> lWithoutC(M, K, zero);
430 for (std::size_t j = 0; j < M; ++j)
431 for (std::size_t c = 0; c < K; ++c) lWithoutC(j, c) = lWithoutR[c](j, c);
432 const AbAmvaResult<T> ret = detail::ab_core(popWithout, nservers, sched, v, S, maxiter,
433 Delta, lWithoutC, fcfsSchmidt, method);
434 for (std::size_t j = 0; j < M; ++j)
435 for (std::size_t c = 0; c < K; ++c) lWithoutR[c](j, r) = ret.QN(j, c);
436 }
437
438 // STEP 3: the fractional-change tensor.
439 for (std::size_t i = 0; i < M; ++i)
440 for (std::size_t r = 0; r < K; ++r) {
441 const T F_ir = N[r] != 0 ? T(LUpdated(i, r) / num_traits<T>::from_int(N[r])) : zero;
442 for (std::size_t t = 0; t < K; ++t) {
443 const long divisor = r == t ? N[r] - 1 : N[r];
444 const T F_irt =
445 divisor != 0 ? T(lWithoutR[r](i, t) / num_traits<T>::from_int(divisor)) : zero;
446 Delta[(i * K + r) * K + t] = F_irt - F_ir;
447 }
448 }
449
450 // STEP 4: the core again at N, with the step-1 queue lengths and step-3
451 // fractional changes.
452 return detail::ab_core(N, nservers, sched, v, S, maxiter, Delta, LUpdated, fcfsSchmidt, method);
453}
454
455/** Reference defaults: no Schmidt FCFS wait, the AB marginal rule. */
456template <class T>
457AbAmvaResult<T> pfqn_ab_amva(const Matrix<T>& S, const std::vector<int>& N, const Matrix<T>& v,
458 const std::vector<int>& nservers,
459 const std::vector<SchedStrategy>& sched) {
460 return pfqn_ab_amva(S, N, v, nservers, sched, false, AbMarginalMethod::Ab);
461}
462
463} // namespace pfqn
464} // namespace line
465
466#endif // LINE_API_PFQN_AB_AMVA_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.
Dense matrix and non-owning view.
AbAmvaResult< T > pfqn_ab_amva(const Matrix< T > &S, const std::vector< int > &N, const Matrix< T > &v, const std::vector< int > &nservers, const std::vector< SchedStrategy > &sched, bool fcfsSchmidt, AbMarginalMethod method)
Akyildiz-Bolch approximate MVA for multi-server BCMP networks.
AbMarginalMethod
Which marginal-probability rule the multiserver correction uses.
@ Ab
the Akyildiz-Bolch weight function
@ Scat
two-point scatter around floor(Qtot)
T num_abs(const T &v)
Definition number.h:172
bool next_pop(std::vector< int > &n, const std::vector< int > &N)
Advance n to the next population vector in the lattice 0 <= n <= N, odometer order with the last clas...
Definition population.h:56
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
T num_nck(int n, int k)
Binomial coefficient as a value of T, by the Pascal recurrence.
Definition population.h:87
Number-type abstraction for the templated API port.
Scaffolding shared by the approximate-MVA family.
Population-vector enumeration and combinatorics.
Return value of pfqn_ab_amva, mirroring [QN,UN,RN,CN,XN,totiter].
Matrix< T > UN
(M x R) utilization
Matrix< T > RN
(M x R) residence (wait) time per visit
std::size_t totiter
iterations of the final core pass
std::vector< T > XN
(R) class throughput AT STATION 1
std::vector< T > CN
(R) cycle time
Matrix< T > QN
(M x R) mean queue length