LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_bs.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_BS_H
6#define LINE_API_PFQN_BS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Bard-Schweitzer approximate MVA.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_bs.m. The exact arrival theorem
14 * Q(i|n - e_r) is replaced by the proportional estimate
15 * Q(i,r|n - e_r) = Q(i,r|n) (N_r - 1)/N_r, Q(i,s|n - e_r) = Q(i,s|n),
16 * and the resulting fixed point is iterated to a relative tolerance on Q.
17 *
18 * FCFS stations use the other class's own demand in the queueing term
19 * (L(i,s) Q(i,s)), the PS family uses the arriving class's demand
20 * (L(i,r) Q(i,s)); that distinction is what makes the FCFS variant sensitive
21 * to demand heterogeneity, and it is easy to lose when transcribing.
22 *
23 * The iteration stops on a tolerance, so the result is a fixed point only to
24 * within tol whatever the arithmetic. That is a caveat on what the answer
25 * MEANS, not a reason to deny the exact backend: an exact run returns the
26 * iterate the stopping rule selected, without rounding error, which is what
27 * one wants when separating arithmetic error from algorithmic error.
28 */
29
30#include <cmath>
31#include <cstddef>
32#include <limits>
33#include <vector>
34
36#include "line/num/number.h"
37#include "line/util/error.h"
38#include "line/util/matrix.h"
39
40namespace line {
41namespace pfqn {
42
43/** Station scheduling as far as the AMVA formulas distinguish it. */
44/// INF marks a delay centre; pfqn_bs only distinguishes FCFS, pfqn_qsa needs it.
45enum class AmvaSched { PS, FCFS, INF };
46
47template <class T>
48struct AmvaResult {
49 std::vector<T> XN; ///< (R) throughput
50 Matrix<T> QN; ///< (M x R) queue length
51 Matrix<T> UN; ///< (M x R) utilization
52 Matrix<T> RN; ///< (M x R) residence time
53 std::size_t iterations = 0;
54 bool converged = false;
55};
56
57/**
58 * @brief Bard-Schweitzer approximate MVA.
59 *
60 * @param L (M x R) demands
61 * @param N (R) populations
62 * @param Z (R) think times, empty for none
63 * @param type (M) per-station scheduling, empty for all PS
64 * @param tol convergence tolerance; NaN selects the published Linearizer termination
65 * test of Chandy and Neuse, Commun. ACM 25(2), 1982, i.e. the cutoff
66 * pfqn_cntol(N) applied to max_{i,r}|dQ(i,r)|/N_r instead of the
67 * relative-change metric used by default. This is the test LQNS runs,
68 * since it sets it in SchweitzerCommon.
69 * @param maxiter iteration cap
70 * @param QN0 queue lengths that warm-start the iteration; empty for a cold start
71 */
72template <class T>
73AmvaResult<T> pfqn_bs(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
74 const std::vector<AmvaSched>& type, double tol = 1e-6,
75 std::size_t maxiter = 1000, const Matrix<T>& QN0 = Matrix<T>()) {
76 // field-arithmetic exactness caveat: see _kb/03-api-layer.md (cpp port notes: pfqn)
77 const std::size_t M = L.rows(), R = L.cols();
78 if (N.size() != R) throw InputError("pfqn_bs: L and N disagree on the class count");
79 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_bs: Z has the wrong length");
80 if (!type.empty() && type.size() != M) throw InputError("pfqn_bs: type has the wrong length");
81
82 const bool cntest = is_cntol(tol);
83 if (cntest) tol = pfqn_cntol(N);
84
85 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
87 r.XN.assign(R, zero);
88 r.QN = Matrix<T>(M, R, zero);
89 r.UN = Matrix<T>(M, R, zero);
90 r.RN = Matrix<T>(M, R, zero);
91 Matrix<T> CN(M, R, zero);
92
93 // QN0 warm-start rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
94 if (M == 0) return r;
95 if (!QN0.empty()) {
96 if (QN0.rows() != M || QN0.cols() != R)
97 throw InputError("pfqn_bs: QN0 has the wrong shape");
98 r.QN = QN0;
99 } else {
100 for (std::size_t i = 0; i < M; ++i)
101 for (std::size_t s = 0; s < R; ++s)
102 r.QN(i, s) = N[s] / num_traits<T>::from_int(static_cast<long>(M));
103 }
104
105 for (std::size_t it = 1; it <= maxiter; ++it) {
106 r.iterations = it;
107 const Matrix<T> Qprev = r.QN;
108
109 for (std::size_t cls = 0; cls < R; ++cls) {
110 if (N[cls] == zero) {
111 r.XN[cls] = zero;
112 for (std::size_t i = 0; i < M; ++i) {
113 CN(i, cls) = zero;
114 r.QN(i, cls) = zero;
115 r.UN(i, cls) = zero;
116 }
117 continue;
118 }
119 T ctot = Z.empty() ? zero : Z[cls];
120 for (std::size_t i = 0; i < M; ++i) {
121 CN(i, cls) = L(i, cls);
122 if (L(i, cls) == zero) {
123 continue;
124 }
125 const bool fcfs = !type.empty() && type[i] == AmvaSched::FCFS;
126 for (std::size_t s = 0; s < R; ++s) {
127 if (s != cls)
128 CN(i, cls) += fcfs ? L(i, s) * r.QN(i, s) : L(i, cls) * r.QN(i, s);
129 else
130 CN(i, cls) += L(i, cls) * r.QN(i, cls) * (N[cls] - one) / N[cls];
131 }
132 ctot += CN(i, cls);
133 }
134 if (ctot == zero) throw NumericError("pfqn_bs: zero total residence time");
135 r.XN[cls] = N[cls] / ctot;
136 }
137 for (std::size_t cls = 0; cls < R; ++cls)
138 for (std::size_t i = 0; i < M; ++i) {
139 r.QN(i, cls) = r.XN[cls] * CN(i, cls);
140 r.UN(i, cls) = r.XN[cls] * L(i, cls);
141 }
142
143 // 0/0 vs x/0 convergence rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
144 double delta = 0.0;
145 for (std::size_t cls = 0; cls < R; ++cls) {
146 if (N[cls] == zero) continue;
147 for (std::size_t i = 0; i < M; ++i) {
148 if (cntest) {
149 // Chandy and Neuse (1982), p.129: absolute queue-length change scaled by
150 // the class population, not the relative change.
151 const double d = std::fabs(num_traits<T>::to_double(
152 T((r.QN(i, cls) - Qprev(i, cls)) / N[cls])));
153 if (d > delta) delta = d;
154 continue;
155 }
156 if (Qprev(i, cls) == zero) {
157 if (r.QN(i, cls) == zero) continue; // 0/0, omitted by max
158 delta = std::numeric_limits<double>::infinity();
159 continue;
160 }
161 const double d =
162 std::fabs(num_traits<T>::to_double(T(one - r.QN(i, cls) / Qprev(i, cls))));
163 if (d > delta) delta = d;
164 }
165 }
166 if (delta < tol) {
167 r.converged = true;
168 break;
169 }
170 }
171
172 for (std::size_t cls = 0; cls < R; ++cls)
173 for (std::size_t i = 0; i < M; ++i)
174 r.RN(i, cls) = (N[cls] == zero) ? zero : r.QN(i, cls) / r.XN[cls];
175 return r;
176}
177
178template <class T>
179AmvaResult<T> pfqn_bs(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z) {
180 return pfqn_bs(L, N, Z, std::vector<AmvaSched>());
181}
182
183template <class T>
184AmvaResult<T> pfqn_bs(const Matrix<T>& L, const std::vector<T>& N) {
185 return pfqn_bs(L, N, std::vector<T>(), std::vector<AmvaSched>());
186}
187
188} // namespace pfqn
189} // namespace line
190
191#endif // LINE_API_PFQN_BS_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.
AmvaSched
Station scheduling as far as the AMVA formulas distinguish it.
Definition pfqn_bs.h:45
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
bool is_cntol(double tol)
True when tol is the sentinel requesting the Chandy-Neuse test.
Definition pfqn_cntol.h:72
double pfqn_cntol(const std::vector< T > &N)
Termination cutoff at the given population vector.
Definition pfqn_cntol.h:58
Number-type abstraction for the templated API port.
Chandy-Neuse population-scaled termination cutoff for 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