LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_tay.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_TAY_H
6#define LINE_API_PFQN_TAY_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Tay's arrival-instant approximate MVA.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_tay.m, cross-checked against
14 * jar/src/main/java/jline/api/pfqn/mva/Pfqn_tay.java.
15 *
16 * WHAT MAKES IT DIFFERENT. Every other AMVA in this directory estimates the
17 * arrival-instant queue length by shifting the population: Bard-Schweitzer
18 * scales Q by (N-1)/N, Linearizer solves R auxiliary networks, AQL carries an
19 * aggregate correction. Tay estimates it from the THROUGHPUT ELASTICITIES
20 * instead. With E_mkc = (D_mk/X_c) dX_c/dD_mk the elasticity of the class-c
21 * throughput in the class-k demand at station m, and B_ir = 1/(1 + D_ir X_r/N_r),
22 * the elasticities satisfy R linear equations
23 *
24 * E_mkj sum_t B_tj Q_jt (1+Q_jt)
25 * = -[(delta_jk + Q_jm) B_mk Q_km + sum_{c/=j} E_mkc sum_t B_tc Q_jt Q_ct]
26 *
27 * and the arrival-instant queue length is then simply Q_km^(r) = Q_km + E_mkr,
28 * which closes the recursion R_rm = D_rm (1 + sum_k Q_km^(r)).
29 *
30 * COST. One R x R solve per (station, class) pair per sweep, so O(M R (R^3 + M R^2))
31 * per iteration: more than Bard-Schweitzer, less than Linearizer's R+1 auxiliary
32 * networks.
33 *
34 * DELAY STATIONS enter through Z only. They are "AS" servers in the survey's
35 * notation (d_t = 0), contributing Z_j X_j to the DENOMINATOR of the elasticity
36 * equations and nothing to the numerator.
37 *
38 * EMPTY CLASSES are solved out and re-expanded, exactly as pfqn_bs does. Their
39 * elasticity denominator is identically zero, so leaving them in makes the R x R
40 * system singular rather than merely redundant.
41 *
42 * Reference: Y. C. Tay and R. Suri, "Error bounds for performance prediction in
43 * queueing networks", ACM TOCS 3(4), 1985; Y. C. Tay, "An approach to analyzing
44 * the behavior of some queueing networks", Operations Research 40(S2), 1992;
45 * P. J. Schweitzer, G. Serazzi and M. Broglia, "A survey of bottleneck analysis
46 * in closed queueing networks", Sec. 4.8.2, eqs. 4.8.2-1..3.
47 *
48 * Iterates to an absolute tolerance on the queue lengths, so exact arithmetic
49 * buys nothing and the static_assert records that, as in pfqn_aql.h.
50 */
51
52#include <cmath>
53#include <cstddef>
54#include <vector>
55
57#include "line/num/number.h"
58#include "line/util/error.h"
59#include "line/util/lu.h"
60#include "line/util/matrix.h"
61
62namespace line {
63namespace pfqn {
64
65/**
66 * @brief Tay's arrival-instant approximate MVA.
67 *
68 * @param L (M x R) demands
69 * @param N (R) populations
70 * @param Z (R) think times, empty for none
71 * @param tol absolute tolerance on the queue lengths
72 * @param maxiter iteration cap
73 * @param QN0 (M x R) initial queue lengths, empty for uniform
74 *
75 * `AmvaResult::RN` holds the residence times. The arrival-instant queue lengths
76 * QNarr(m,k,r) that the method is tabulated on are NOT returned: the reference
77 * exposes them as a sixth output for diagnostics only, and no caller in this
78 * port reads them. They are the auxiliary quantities of the approximation, not
79 * the model re-solved at N - e_r, which is the same object only for an exact
80 * solution.
81 */
82template <class T>
83AmvaResult<T> pfqn_tay(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
84 double tol = 1e-6, std::size_t maxiter = 1000,
85 const Matrix<T>& QN0 = Matrix<T>()) {
87 "pfqn_tay requires transcendental arithmetic: it iterates to a tolerance, so "
88 "its answer is a fixed point only to within tol");
89 const std::size_t M = L.rows(), R = L.cols();
90 if (N.size() != R) throw InputError("pfqn_tay: L and N disagree on the class count");
91 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_tay: Z has the wrong length");
92
93 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
94 AmvaResult<T> out;
95 out.XN.assign(R, zero);
96 out.QN = Matrix<T>(M, R, zero);
97 out.UN = Matrix<T>(M, R, zero);
98 out.RN = Matrix<T>(M, R, zero);
99 if (M == 0) return out;
100
101 // Empty classes make the elasticity system singular, not merely redundant:
102 // their denominator is identically zero. Solve without them and re-expand.
103 std::vector<std::size_t> act;
104 for (std::size_t r = 0; r < R; ++r)
105 if (N[r] > zero) act.push_back(r);
106 if (act.empty()) return out;
107 if (act.size() < R) {
108 Matrix<T> La(M, act.size(), zero);
109 std::vector<T> Na(act.size(), zero), Za(act.size(), zero);
110 for (std::size_t a = 0; a < act.size(); ++a) {
111 for (std::size_t m = 0; m < M; ++m) La(m, a) = L(m, act[a]);
112 Na[a] = N[act[a]];
113 Za[a] = Z.empty() ? zero : Z[act[a]];
114 }
115 const AmvaResult<T> sub = pfqn_tay(La, Na, Za, tol, maxiter);
116 for (std::size_t a = 0; a < act.size(); ++a) {
117 out.XN[act[a]] = sub.XN[a];
118 for (std::size_t m = 0; m < M; ++m) {
119 out.QN(m, act[a]) = sub.QN(m, a);
120 out.UN(m, act[a]) = sub.UN(m, a);
121 out.RN(m, act[a]) = sub.RN(m, a);
122 }
123 }
124 out.iterations = sub.iterations;
125 out.converged = sub.converged;
126 return out;
127 }
128
129 Matrix<T> QN(M, R, zero);
130 if (QN0.empty()) {
131 const T Md = num_traits<T>::from_int(static_cast<long>(M));
132 for (std::size_t m = 0; m < M; ++m)
133 for (std::size_t r = 0; r < R; ++r) QN(m, r) = N[r] / Md;
134 } else {
135 if (QN0.rows() != M || QN0.cols() != R)
136 throw InputError("pfqn_tay: QN0 has the wrong shape");
137 QN = QN0;
138 }
139 // XN = N ./ (Z + sum(L,1).*(1+sum(QN,1)))
140 for (std::size_t r = 0; r < R; ++r) {
141 T Lsum = zero, Qsum = zero;
142 for (std::size_t m = 0; m < M; ++m) {
143 Lsum += L(m, r);
144 Qsum += QN(m, r);
145 }
146 const T den = (Z.empty() ? zero : Z[r]) + Lsum * (one + Qsum);
147 out.XN[r] = (den == zero) ? zero : N[r] / den;
148 }
149
150 Matrix<T> Qarr(M, R * R, zero); // Qarr(m, k*R + r) = Q_km seen by class r
151 for (std::size_t it = 1; it <= maxiter; ++it) {
152 out.iterations = it;
153 const Matrix<T> QN_1 = QN;
154
155 // B(i,r) = 1/(1 + L(i,r) X_r/N_r)
156 Matrix<T> B(M, R, zero);
157 for (std::size_t m = 0; m < M; ++m)
158 for (std::size_t r = 0; r < R; ++r)
159 B(m, r) = one / (one + L(m, r) * out.XN[r] / N[r]);
160
161 // den(j): the AS-server term Z_j X_j is the delay contribution, which
162 // leaves B = 1 because a delay has d_t = 0.
163 std::vector<T> den(R, zero);
164 for (std::size_t j = 0; j < R; ++j) {
165 T s = zero;
166 for (std::size_t m = 0; m < M; ++m) s += B(m, j) * QN(m, j) * (one + QN(m, j));
167 den[j] = s + (Z.empty() ? zero : Z[j]) * out.XN[j];
168 }
169
170 // C(j,c) = sum_t B_tc Q_jt Q_ct
171 Matrix<T> C(R, R, zero);
172 for (std::size_t j = 0; j < R; ++j)
173 for (std::size_t c = 0; c < R; ++c) {
174 T s = zero;
175 for (std::size_t m = 0; m < M; ++m) s += B(m, c) * QN(m, j) * QN(m, c);
176 C(j, c) = s;
177 }
178
179 for (std::size_t m = 0; m < M; ++m)
180 for (std::size_t k = 0; k < R; ++k) {
181 Matrix<T> A(R, R, zero);
182 std::vector<T> b(R, zero);
183 for (std::size_t j = 0; j < R; ++j) {
184 A(j, j) = one;
185 if (den[j] == zero)
186 throw NumericError(
187 "pfqn_tay: the elasticity system is singular at station " +
188 std::to_string(m + 1) + ", class " + std::to_string(j + 1) +
189 ": the class carries no queue anywhere and no think time");
190 for (std::size_t c = 0; c < R; ++c)
191 if (c != j) A(j, c) = C(j, c) / den[j];
192 const T delta = (j == k) ? one : zero;
193 b[j] = -((delta + QN(m, j)) * B(m, k) * QN(m, k) / den[j]);
194 }
195 const std::vector<T> E = line::solve(A, b);
196 for (std::size_t r = 0; r < R; ++r) Qarr(m, k * R + r) = QN(m, k) + E[r];
197 }
198
199 for (std::size_t r = 0; r < R; ++r)
200 for (std::size_t m = 0; m < M; ++m) {
201 T s = zero;
202 for (std::size_t k = 0; k < R; ++k) s += Qarr(m, k * R + r);
203 out.RN(m, r) = L(m, r) * (one + s);
204 }
205 for (std::size_t r = 0; r < R; ++r) {
206 T s = (Z.empty() ? zero : Z[r]);
207 for (std::size_t m = 0; m < M; ++m) s += out.RN(m, r);
208 out.XN[r] = (s == zero) ? zero : N[r] / s;
209 }
210 for (std::size_t m = 0; m < M; ++m)
211 for (std::size_t r = 0; r < R; ++r) QN(m, r) = out.RN(m, r) * out.XN[r];
212
213 double delta = 0.0;
214 for (std::size_t m = 0; m < M; ++m)
215 for (std::size_t r = 0; r < R; ++r) {
216 const double d =
217 std::fabs(num_traits<T>::to_double(T(QN(m, r) - QN_1(m, r))));
218 if (d > delta) delta = d;
219 }
220 if (delta < tol) {
221 out.converged = true;
222 break;
223 }
224 }
225
226 out.QN = QN;
227 for (std::size_t m = 0; m < M; ++m)
228 for (std::size_t r = 0; r < R; ++r) out.UN(m, r) = L(m, r) * out.XN[r];
229 return out;
230}
231
232template <class T>
233AmvaResult<T> pfqn_tay(const Matrix<T>& L, const std::vector<T>& N) {
234 return pfqn_tay(L, N, std::vector<T>());
235}
236
237} // namespace pfqn
238} // namespace line
239
240#endif // LINE_API_PFQN_TAY_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.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
AmvaResult< T > pfqn_tay(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >())
Tay's arrival-instant approximate MVA.
Definition pfqn_tay.h:83
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
Bard-Schweitzer 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