LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_wangsevcik.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_PFQN_WANGSEVCIK_H
6#define LINE_API_PFQN_PFQN_WANGSEVCIK_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Two approximate MVA schemes: Queue-Line and Fraction-Line.
12 *
13 * Port of `pfqn_qli` and `pfqn_fli` from python/line_solver/api/pfqn/mva.py.
14 * PYTHON-ONLY: neither MATLAB nor the JAR carries them as standalone entry
15 * points, so native Python is the reference.
16 *
17 * Reference: W. Wang and K. C. Sevcik, "Performance Models for Multiprogrammed
18 * Systems", IBM Research Report RC 5925, 1976.
19 *
20 * `pfqn_qdlin` USED TO LIVE HERE AND NO LONGER DOES. Its Wang-Sevcik arm scaled
21 * the job's own-class contribution by (N_r - 1)/N_r and left the other classes
22 * alone, which is Bard-Schweitzer written out, so the function reproduced
23 * `pfqn_bs` to iteration tolerance and was neither a Linearizer nor
24 * queue-dependent. It is now `line/api/pfqn/pfqn_qdlin.h`, the array-level twin
25 * of what SolverMVA computes for method='qdlin'.
26 *
27 * BOTH ANSWER ONE QUESTION: what queue does an ARRIVING class-r job see?
28 * Exact MVA says it sees the queue at population N - e_r, which is why the
29 * exact recursion has to walk the whole population lattice. Every approximation
30 * here replaces that by a correction applied to the queue at the FULL
31 * population, and the two differ only in the correction:
32 *
33 * - QLI subtracts a `1/(N_r - 1)` weighted discrepancy between the own-class
34 * queue and its DEMAND-PROPORTIONAL share, so a station that holds more of
35 * the class than its demand warrants is discounted more.
36 * - FLI uses the same proportional share but combines it differently, with a
37 * `2/N_r` coefficient and the share ADDED rather than subtracted.
38 *
39 * THE `Q_seen` FLOOR AT ZERO IS LOAD-BEARING, not defensive. Both Wang-Sevcik
40 * corrections are differences of estimates and can go negative at a lightly
41 * loaded station; a negative queue would make the residence time SHORTER than
42 * the service demand, which is impossible, and the iteration then diverges away
43 * from the fixed point rather than toward it. The reference clamps and so does
44 * this.
45 *
46 * THE FALLBACK ARM IS NOT THE SAME FORMULA. When the denominator vanishes, or
47 * when the population is too small for the correction's own divisor (`N_r > 1`
48 * for QLI), the reference falls back to `Q_total - Q_own`, i.e. the other
49 * classes only. That is the Bard-Schweitzer arrival estimate, and it is a
50 * DIFFERENT approximation, so a caller comparing two runs across that boundary
51 * is comparing two schemes.
52 *
53 * ARITHMETIC: field.
54 */
55
56#include <algorithm>
57#include <cmath>
58#include <cstddef>
59#include <vector>
60
61#include "line/num/number.h"
62#include "line/util/error.h"
63#include "line/util/matrix.h"
64
65namespace line {
66namespace pfqn {
67
68/** What an approximate MVA sweep reports. */
69template <class T>
70struct WsResult {
71 Matrix<T> Q; ///< (M x R) mean queue lengths
72 Matrix<T> U; ///< (M x R) utilizations
73 Matrix<T> R; ///< (M x R) residence times
74 std::vector<T> X; ///< (R) class throughputs
75 std::vector<T> C; ///< (R) cycle times
76 std::size_t iterations = 0;
77};
78
79/** Which arrival-queue correction the sweep applies. */
80enum class WsScheme { Qli = 0, Fli };
81
82namespace wsdetail {
83
84/** The shared initial guess: each class spread over the stations by demand. */
85template <class T>
86Matrix<T> proportional_start(const Matrix<T>& L, const std::vector<T>& N) {
87 const std::size_t M = L.rows(), R = L.cols();
88 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
89 Matrix<T> Q(M, R, zero);
90 for (std::size_t r = 0; r < R; ++r) {
91 T s = zero;
92 for (std::size_t k = 0; k < M; ++k) s += L(k, r);
93 if (s == zero) s = one; // a class with no demand anywhere
94 for (std::size_t k = 0; k < M; ++k) Q(k, r) = L(k, r) / s * N[r];
95 }
96 return Q;
97}
98
99} // namespace wsdetail
100
101/**
102 * One approximate MVA sweep, by the chosen arrival-queue correction.
103 *
104 * @param L (M x R) service demands
105 * @param N (R) class populations
106 * @param Z (R) think times; empty means none
107 * @param tol convergence tolerance on the queue lengths
108 */
109template <class T>
110WsResult<T> pfqn_wangsevcik(const Matrix<T>& L, const std::vector<T>& N,
111 const std::vector<T>& Z, WsScheme scheme, double tol = 1e-6,
112 std::size_t max_iter = 1000) {
113 const std::size_t M = L.rows(), R = L.cols();
114 if (M == 0 || R == 0) throw InputError("pfqn_wangsevcik: the demand matrix is empty");
115 if (N.size() != R) throw InputError("pfqn_wangsevcik: N has the wrong length");
116 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_wangsevcik: Z has the wrong length");
117 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
118
119 WsResult<T> out;
120 out.Q = Matrix<T>(M, R, zero);
121 out.U = Matrix<T>(M, R, zero);
122 out.R = Matrix<T>(M, R, zero);
123 out.X.assign(R, zero);
124 out.C.assign(R, zero);
125
126 T Ntot = zero;
127 for (std::size_t r = 0; r < R; ++r) Ntot += N[r];
128 if (!(num_traits<T>::to_double(Ntot) > 0.0)) return out; // an empty network is all zeros
129
130 out.Q = wsdetail::proportional_start(L, N);
131 Matrix<T> Qprev(M, R, zero);
132 for (std::size_t k = 0; k < M; ++k)
133 for (std::size_t r = 0; r < R; ++r)
134 Qprev(k, r) = out.Q(k, r) * num_traits<T>::from_int(10);
135
136 while (out.iterations < max_iter) {
137 double gap = 0.0;
138 for (std::size_t k = 0; k < M; ++k)
139 for (std::size_t r = 0; r < R; ++r)
140 gap = std::max(gap, std::fabs(num_traits<T>::to_double(out.Q(k, r) - Qprev(k, r))));
141 if (gap <= tol) break;
142 ++out.iterations;
143 Qprev = out.Q;
144
145 // The per-station totals of the PREVIOUS iterate, which every
146 // correction reads; computing them inside the station loop would make
147 // the sweep depend on the update order.
148 std::vector<T> Qtot(M, zero);
149 for (std::size_t k = 0; k < M; ++k)
150 for (std::size_t s = 0; s < R; ++s) Qtot[k] += Qprev(k, s);
151
152 for (std::size_t r = 0; r < R; ++r) {
153 if (!(num_traits<T>::to_double(N[r]) > 0.0)) continue;
154
155 // The demand-proportional share both Wang-Sevcik schemes use.
156 T qliden = zero;
157 for (std::size_t m = 0; m < M; ++m)
158 if (num_traits<T>::to_double(L(m, r)) > 0.0)
159 qliden += L(m, r) * (one + Qtot[m] - Qprev(m, r));
160
161 for (std::size_t k = 0; k < M; ++k) {
162 T qseen = zero;
163 {
164 const T qlinum = L(k, r) * (one + Qtot[k] - Qprev(k, r));
165 const bool usable = num_traits<T>::to_double(qliden) > 0.0 &&
166 (scheme == WsScheme::Qli
167 ? num_traits<T>::to_double(N[r]) > 1.0
168 : num_traits<T>::to_double(N[r]) > 0.0);
169 if (!usable) {
170 // Bard-Schweitzer: the other classes only. A DIFFERENT
171 // approximation, not a degenerate case of the same one.
172 qseen = T(Qtot[k] - Qprev(k, r));
173 } else if (scheme == WsScheme::Qli) {
174 qseen = T(Qtot[k] - (one / (N[r] - one)) * (Qprev(k, r) - qlinum / qliden));
175 } else {
176 qseen = T(Qtot[k] - (num_traits<T>::from_int(2) / N[r]) * Qprev(k, r) +
177 qlinum / qliden);
178 }
179 }
180 // A negative arrival queue would make the residence time
181 // shorter than the demand, which is impossible, and the
182 // iteration then walks away from the fixed point.
183 if (num_traits<T>::to_double(qseen) < 0.0) qseen = zero;
184 out.R(k, r) = L(k, r) * (one + qseen);
185 }
186
187 T Rtot = zero;
188 for (std::size_t k = 0; k < M; ++k) Rtot += out.R(k, r);
189 const T zr = Z.empty() ? zero : Z[r];
190 out.X[r] = (num_traits<T>::to_double(zr + Rtot) > 0.0) ? T(N[r] / (zr + Rtot)) : zero;
191 for (std::size_t k = 0; k < M; ++k) {
192 out.Q(k, r) = out.X[r] * out.R(k, r);
193 out.U(k, r) = out.X[r] * L(k, r);
194 }
195 out.C[r] = Rtot;
196 }
197 }
198 return out;
199}
200
201/** Wang-Sevcik Queue-Line. */
202template <class T>
203WsResult<T> pfqn_qli(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
204 double tol = 1e-6, std::size_t max_iter = 1000) {
205 return pfqn_wangsevcik(L, N, Z, WsScheme::Qli, tol, max_iter);
206}
207
208/** Wang-Sevcik Fraction-Line. */
209template <class T>
210WsResult<T> pfqn_fli(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
211 double tol = 1e-6, std::size_t max_iter = 1000) {
212 return pfqn_wangsevcik(L, N, Z, WsScheme::Fli, tol, max_iter);
213}
214
215} // namespace pfqn
216} // namespace line
217
218#endif // LINE_API_PFQN_PFQN_WANGSEVCIK_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.
WsResult< T > pfqn_wangsevcik(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, WsScheme scheme, double tol=1e-6, std::size_t max_iter=1000)
One approximate MVA sweep, by the chosen arrival-queue correction.
WsScheme
Which arrival-queue correction the sweep applies.
WsResult< T > pfqn_fli(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, double tol=1e-6, std::size_t max_iter=1000)
Wang-Sevcik Fraction-Line.
WsResult< T > pfqn_qli(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, double tol=1e-6, std::size_t max_iter=1000)
Wang-Sevcik Queue-Line.
Number-type abstraction for the templated API port.
What an approximate MVA sweep reports.
Matrix< T > U
(M x R) utilizations
Matrix< T > R
(M x R) residence times
std::vector< T > X
(R) class throughputs
Matrix< T > Q
(M x R) mean queue lengths
std::vector< T > C
(R) cycle times